Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-693 | 🔵 active | **[`gate run` 的 context 階段在新 worktree 每次耗滿 90 秒上限且不保留進度(issue #780)]** 根因:`pmctl_context_workflow_refresh_bounded` 只用 `timeout 90` 從外面殺掉,`workflow-refresh` 沒傳預算給索引,索引在最後才 COMMIT,被殺就全丟。修法:`workflow-refresh` 新增 `--budget-ms`,wrapper 傳上限的三分之二,索引自己停下並保留進度,下一次接續(重用 #760 的機制)。 | ops/gate | 2026-11-05 | feedback:2026-11-05 pr:#784 | P2 | hygiene |
| CC-694 | 🔵 active | **[`PM_DISPATCH_STATE_ROOT` 用盘符写法时 `gate run` 先报告 detached 启动成功,监督进程不到 1 秒后才以 exit 2 失败(issue #779)]** `gate run` 在父程序、context 刷新與 operation record 之前檢查狀態根的寫法,拒絕時 exit 2、訊息指名變數(`PM_DISPATCH_STATE_ROOT` 或 `XDG_DATA_HOME`),不印 gate id;`docs/platform-support.md` 補一條。 | ops/gate | 2026-11-05 | feedback:2026-11-05 pr:#785 | P2 | hygiene |
| CC-695 | 🔵 active | **[Windows 的 state root ACL 守衛:拒絕訊息沒有說是哪個主體(issue #778),測試的 `%TEMP%` 繼承 `CodexSandboxUsers` 讓 7 個 telemetry 案例失敗(issue #782)]** 守衛本身是對的,不放寬:訊息指名第一個違規主體與是否繼承,附上 `PM_DISPATCH_STATE_ROOT` 補救與不要隨手改父目錄 ACL 的提醒,`docs/platform-support.md` 補一條;`test-pmctl-context.sh` 用 `th_secure_state_root` 給 tmp_root 一個 owner-only ACL。 | ops/portability | 2026-11-05 | feedback:2026-11-05 pr:#786 | P2 | hygiene |
| CC-696 | 🔵 active | **[原生 Windows 上 `memory-usage/sqlite-*` 三個案例穩定失敗:測試把含空白的 sqlite3 路徑不加引號、又不處理原生 sqlite3 的 CRLF(issue #783)]** 這三個案例的 `$real_sqlite "$db" ...` 沒有加引號,使用者目錄含空白(`/c/Users/Lien Chen/...`)時指令被拆開,查詢結果為空而讀成 `missing`;且原生 `sqlite3` 輸出 CRLF,bash 的命令替換只去掉結尾的換行字元,留下 CR。測試改為加引號並去掉 CR,產品程式沒有問題(資料庫內容正確)。並行兩個案例在 main 上現在通過,沒有重現。 | ops/test | 2026-11-05 | feedback:2026-11-05 | P2 | hygiene |
| CC-696 | 🔵 active | **[原生 Windows 上 `memory-usage/sqlite-*` 三個案例穩定失敗:測試把含空白的 sqlite3 路徑不加引號、又不處理原生 sqlite3 的 CRLF(issue #783)]** 這三個案例的 `$real_sqlite "$db" ...` 沒有加引號,使用者目錄含空白(`/c/Users/Lien Chen/...`)時指令被拆開,查詢結果為空而讀成 `missing`;且原生 `sqlite3` 輸出 CRLF,bash 的命令替換只去掉結尾的換行字元,留下 CR。測試改為加引號並去掉 CR,產品程式沒有問題(資料庫內容正確)。並行兩個案例在 main 上現在通過,沒有重現。 | ops/test | 2026-11-05 | feedback:2026-11-05 pr:#787 | P2 | hygiene |
| CC-697 | 🔵 active | **[`gate` 的 scope manifest 超預算時仍掃完所有來源(issue #781)]** 127 個變更路徑的 full-tier gate 在原生 Windows 建 manifest 花 495 秒才以 INCOMPLETE 結束。省略計數只增不減,只要來源、符號、搜尋匹配或契約消費者任一項已超預算,結果就是 INCOMPLETE,其後的掃描只是白花時間。修法:不接受截斷時,在來源之間一發現超預算就停止並註明省略數是下限;接受截斷時照舊掃完。 | ops/gate | 2026-11-05 | feedback:2026-11-05 | P2 | hygiene | |

---

Expand Down Expand Up @@ -2623,3 +2624,15 @@ symlink 或改名的 adapter 仍被拒絕。
**未重現**:issue 列的兩個並行案例(`concurrent-no-lost-updates` 8 個 writer 逾時 20 秒、`contention-matrix-flock-and-mkdir-fallback`)在這台機器上 main 就通過(分別約 12 秒與 34 到 46 秒,連跑三次都過),所以它們是負載相關的逾時,不是穩定失敗;#738 要求的 50 個並行 writer 的 exactly-once 驗收在原生 Windows 上仍未驗證。

**See**: issue #783、#738。

---

## CC-697 — scope manifest 超預算時提早停止 🔵 active

**Problem**:issue #781。127 個變更路徑的 full-tier gate 在原生 Windows 建 scope manifest 花 495 秒(512 個 expansion,每個來源約 1 秒)才以 INCOMPLETE 結束,使用者要等 8 分鐘才知道需要接受截斷。

**修法(第一步,只做估算/提早中止)**:`_gate_scope_expansions_collect_into` 在處理下一個來源之前檢查來源、符號、搜尋匹配、契約消費者四個省略計數;這些計數只增不減,任一已大於 0 就已決定是 INCOMPLETE。沒有 `--accept-scope-truncation` 時停止掃描,manifest 的 `truncation` 記 `scan_stopped_early: true`(只在提早停止時出現,schema 與產生的 bundle 一併加上),`INCOMPLETE` 回報據此說明省略數是下限、expansion 清單不完整,接受旗標會涵蓋完整掃描的所有省略;有接受旗標時照舊掃完。只在來源之間停,所以單一來源的計數仍然完整。

**未做**:expansion-entry-budget 要等去重後才知道,不能提早判斷;同一 subject 的 manifest 快取供 `--accept-scope-truncation` 重用(issue 建議 2)、時間上限參數(建議 3)、`docs/platform-support.md` 的路徑數提醒(建議 4)、退出碼措辭(項目 4)。

**See**: issue #781、#621、#726、#754。
4 changes: 4 additions & 0 deletions core/schema/gate-scope-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@
}
},
"additionalProperties": false
},
"scan_stopped_early": {
"const": true,
"description": "Present only when the expansion scan stopped at the first exceeded budget, so the omitted counts and the expansion list are partial (lower bounds). A run that accepts truncation never stops early."
}
},
"additionalProperties": false
Expand Down
4 changes: 4 additions & 0 deletions runtime/bin/pr-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,10 @@ if [[ "$SCOPE_MANIFEST_STATUS" == incomplete ]]; then
"$(jq -c '.truncation.omitted' "$SCOPE_MANIFEST_PATH")"
printf ' reasons: %s\n' \
"$(jq -c '.truncation.reasons' "$SCOPE_MANIFEST_PATH")"
if jq -e '.truncation.scan_stopped_early == true' "$SCOPE_MANIFEST_PATH" >/dev/null 2>&1; then
printf ' The scan stopped at the first omission: the omitted counts are lower bounds and the expansion list is partial (truncation.scan_stopped_early).\n'
printf ' --accept-scope-truncation scans every source, which takes longer, and accepts whatever that full scan omits, not only the omissions listed here.\n'
fi
printf ' Inspect the manifest, then rerun with --accept-scope-truncation only if those omissions are acceptable.\n'
} >&2
if [[ "$GATE_OUTPUT_EXISTED" != true ]]; then
Expand Down
19 changes: 17 additions & 2 deletions runtime/lib/gate-scope.sh
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ _gate_scope_expansions_collect_into() {
# more than every other collector in this file combined. The set is small and
# fixed for the whole call, so resolve it once here and answer from memory.
local -A changed_seen=()
local stopped_early=false
: > "$candidates"
: > "$sources"

Expand All @@ -785,6 +786,18 @@ _gate_scope_expansions_collect_into() {
done < <(jq -j '.[] | ., "\u0000"' <<<"$changed_paths_json")

while IFS= read -r -d '' source; do
# A run that will end INCOMPLETE gains nothing from scanning the rest: the
# omitted counters only ever grow, so one already above zero fixes the
# verdict, and each further source costs about a second on native Windows
# (#781: 495 s for 127 paths). Stop between sources, so a source is always
# counted whole, and let the counters read as lower bounds. A run that
# accepts truncation still builds the full bounded manifest.
if [[ "${ACCEPT_SCOPE_TRUNCATION:-false}" != true \
&& $((omitted_sources + omitted_symbols + omitted_matches \
+ omitted_contract_consumers)) -gt 0 ]]; then
stopped_early=true
break
fi
source_count=$((source_count + 1))
if [[ "$source_count" -gt "$source_limit" ]]; then
omitted_sources=$((omitted_sources + 1))
Expand Down Expand Up @@ -950,6 +963,7 @@ _gate_scope_expansions_collect_into() {
GATE_SCOPE_OMITTED_CONTRACT_CONSUMERS="$omitted_contract_consumers"
GATE_SCOPE_OMITTED_EXPANSION_ENTRIES="$omitted_entries"
GATE_SCOPE_OMITTED_SEARCH_FAILURES="$search_failures"
GATE_SCOPE_SCAN_STOPPED_EARLY="$stopped_early"
}

_gate_scope_flags_resolve() {
Expand Down Expand Up @@ -1266,6 +1280,7 @@ _gate_scope_manifest_write() {
--argjson omitted_contract_consumers "$GATE_SCOPE_OMITTED_CONTRACT_CONSUMERS" \
--argjson omitted_entries "$GATE_SCOPE_OMITTED_EXPANSION_ENTRIES" \
--argjson omitted_search_failures "$GATE_SCOPE_OMITTED_SEARCH_FAILURES" \
--argjson scan_stopped_early "${GATE_SCOPE_SCAN_STOPPED_EARLY:-false}" \
--argjson budget_hunks "$GATE_SCOPE_MAX_DIFF_HUNKS" \
--argjson budget_sources "$GATE_SCOPE_MAX_EXPANSION_SOURCES" \
--argjson budget_symbols "$GATE_SCOPE_MAX_SYMBOLS_PER_SOURCE" \
Expand Down Expand Up @@ -1311,7 +1326,7 @@ _gate_scope_manifest_write() {
claim:"declared-review-reference-set",
entries:$references[0]
},
truncation:{
truncation:({
occurred:$truncation_occurred,
budgets:{
diff_hunks:$budget_hunks,
Expand All @@ -1336,7 +1351,7 @@ _gate_scope_manifest_write() {
accepted:$truncation_accepted,
source:(if $acceptance_source == "" then null else $acceptance_source end)
}
},
} + (if $scan_stopped_early then {scan_stopped_early:true} else {} end)),
content:{
digest_algorithm:"sha256-canonical-json-without-content-digest",
digest:("")
Expand Down
4 changes: 4 additions & 0 deletions runtime/lib/gate-structural-schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -3912,6 +3912,10 @@
},
"type": "array",
"uniqueItems": true
},
"scan_stopped_early": {
"const": true,
"description": "Present only when the expansion scan stopped at the first exceeded budget, so the omitted counts and the expansion list are partial (lower bounds). A run that accepts truncation never stops early."
}
},
"required": [
Expand Down
77 changes: 77 additions & 0 deletions tests/shell/test-pr-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7054,6 +7054,82 @@ test_scope_manifest_semantic_search_overflow_fails_closed() {
pass "$name"
}

# Behavior: a manifest scan that has already overflowed a budget stops before
# the next source (#781), and an accepting run still scans every source.
# Steps:
# 1. Change two TypeScript sources, each with 65 callers of its own symbol.
# 2. Run without acceptance: assert INCOMPLETE, the lower-bound note, and
# that only the first source's overflow (1) was counted.
# 3. Run with --accept-scope-truncation: assert both overflows (2) counted.
test_scope_manifest_overflow_stops_scan_unless_accepted() {
local name="scope-manifest/overflow-stops-scan-unless-accepted"
should_run "$name" || return 0
local dir="$TMP_ROOT/$name"
local home="$dir/home" repo="$dir/repo"
local runner="$dir/runner" out="$dir/out" err="$dir/err"
local manifest code which
mkdir -p "$dir"
create_runner "$runner"
create_agents "$home" critic qa-tester architecture-reviewer security-reviewer risk-reviewer
git init -q -b main "$repo"
(
cd "$repo"
git config user.email test@example.com
git config user.name 'Gate Test'
mkdir -p src callers
write_managed_gitignore
for which in a b; do
printf 'export function scopeStopSymbol_%s() { return false; }\n' "$which" > "src/shared-$which.ts"
for n in $(seq -w 1 65); do
printf 'scopeStopSymbol_%s();\n' "$which" > "callers/caller-$which-${n}.ts"
done
done
git add .
git commit -q -m initial
git checkout -q -b feature
for which in a b; do
printf 'export function scopeStopSymbol_%s() { return true; }\n' "$which" > "src/shared-$which.ts"
done
git add src
git commit -q -m change
)

set +e
run_gate "$home" "$runner" "$repo" "$out" "$err" --base main --mode sequential
code=$?
set -e
[[ "$code" -eq 3 ]] || {
fail "$name" "exit $code, expected 3: $(tail -n 30 "$err" 2>/dev/null)"
return
}
assert_file_contains "$name" "$err" "omitted counts are lower bounds" || return
manifest="$(find "$repo/.gate-results" \
-maxdepth 1 -name 'gate-scope-manifest-*.json' -print -quit)"
if ! jq -e '.status == "incomplete"
and .truncation.scan_stopped_early == true
and .truncation.omitted.matches_per_query == 1' "$manifest" >/dev/null; then
fail "$name" "scan did not stop after the first overflow: $(jq -c .truncation "$manifest" 2>/dev/null)"
return
fi

# Drop the first run's manifest so the one found below is the accepting run's.
rm -f -- "$repo"/.gate-results/gate-scope-manifest-*.json
set +e
run_gate "$home" "$runner" "$repo" "$out" "$err" \
--base main --mode sequential --accept-scope-truncation
set -e
manifest="$(find "$repo/.gate-results" \
-maxdepth 1 -name 'gate-scope-manifest-*.json' -print -quit)"
if ! jq -e '.status == "accepted_truncation"
and (.truncation | has("scan_stopped_early") | not)
and .truncation.omitted.matches_per_query == 2' \
"$manifest" >/dev/null; then
fail "$name" "accepting run did not scan both sources: $(jq -c '{status,truncation}' "$manifest" 2>/dev/null)"
return
fi
pass "$name"
}

# Behavior: the manifest producer transports a maximum-size expansion through
# a file descriptor instead of one jq argv value, preserving every entry even
# when the serialized array exceeds Linux MAX_ARG_STRLEN.
Expand Down Expand Up @@ -7522,6 +7598,7 @@ run_test test_scope_manifest_contract_bundle_uses_bounded_consumer_summary
run_test test_scope_manifest_contract_bundle_overflow_is_truthful_truncation
run_test test_scope_manifest_failed_search_is_not_reported_complete
run_test test_scope_manifest_semantic_search_overflow_fails_closed
run_test test_scope_manifest_overflow_stops_scan_unless_accepted
run_test test_scope_manifest_large_expansion_uses_file_input
run_test test_large_diff_classification_transport_survives_slurpfile_boundary
run_test test_scope_reference_index_collector_direct_decode
Expand Down
Loading