diff --git a/BACKLOG.md b/BACKLOG.md index aaa901ad..6ab67292 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -141,6 +141,7 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr | 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 pr:#788 | P2 | hygiene | | | CC-698 | 🔵 active | **[`pmctl artifacts list` 每筆 run 啟動多個外部程序,原生 Windows 約 0.18 秒/筆(issue #749)]** 60 筆約 11.8 秒、200 筆約 36 秒。改成批次 stat 取 mtime、用 bash 內建 `printf %()T` 轉時間,輸出逐字節不變;60 筆約 1.3 秒、200 筆約 2.1 秒。 | ops/portability | 2026-11-06 | feedback:2026-11-06 pr:#789 | P2 | hygiene | | +| CC-699 | 🔵 active | **[claude 審查員的 guard check 受工作目錄與命令後綴影響而被拒,gate 沒有判定]** brief 的 guard 命令改為 cd 不受影響的絕對路徑,adapter 同步放行同一字串;加 `; echo` 後綴仍被拒,維持由 brief 要求。 | ops/gate | 2026-11-06 | feedback:2026-11-06 | P2 | hygiene | | --- @@ -2651,3 +2652,13 @@ symlink 或改名的 adapter 仍被拒絕。 **未做**:`--limit`/分頁、串流輸出、Linux/WSL 前後量測、1000 筆規模、外部程序計數(issue 驗收項目,這裡只量了 wall-clock)。把批次 stat(stat 種類探測、按位元組切塊、tab 分隔的 mtime)收進 `runtime/lib/portable.sh`,讓 `pmctl-context.sh` 的 `_ctx_batch_*`(空格分隔,路徑含空格會錯)與這裡共用,並統一用位元組數而非 50 筆切塊(審查建議,另開票)。同一個 PR 夾帶 CC-697 的 `docs/platform-support.md` 說明(大型 diff 的 scope manifest 成本)。 **See**: issue #749、#742。 + +## CC-699 — claude 審查員的 guard check 受工作目錄與命令後綴影響而被拒 🔵 active + +**Problem**:2026-11-06 兩次 `--executor claude` 的 gate 都沒有判定。審查員必須先跑 `bash cli/pmctl guard check …` 才能寫結果,但無頭 claude(`--permission-prompts none`)只放行這一個窄前綴。實測重現三種偏離:加 `; echo rc=$?` 被拒;先 `cd` 讀 reviewer 定義,shell 目錄會保留,之後相對路徑的 `cli/pmctl` 回 exit 127;改用絕對路徑不符合放行規則而被拒。brief 已寫「不要加 cd 或後綴」,模型仍然違反,只靠提示不可靠。 + +**修法**:Windows 同 checkout 時,brief 的 guard 命令改為絕對路徑 `bash '/cli/pmctl' guard check …`,adapter 同步放行同一字串(路徑含單引號時維持相對形式)。實測:`cd` 之後該命令可通過,`; echo` 後綴仍被拒(維持由 brief 要求)。 + +**未做**:放寬 `echo` 或其他後綴;讓 guard check 不由模型組字串(例如由 supervisor 代跑);審查員用 `cd … && git diff`/`git -C` 讀 diff 被視為不受信任的設定而被拒,目前只能少讀內容。 + +**See**: 2026-11-06 gate 執行紀錄、CC-697。 diff --git a/adapters/claude/dispatch.sh b/adapters/claude/dispatch.sh index 1063efb7..3ff62eef 100755 --- a/adapters/claude/dispatch.sh +++ b/adapters/claude/dispatch.sh @@ -242,9 +242,16 @@ fi # match the lint's substring check. _claude_bash_allow_pattern="Bash(pmctl "$'guard check *)' _claude_checkout_guard_allow="" +_claude_checkout_guard_abs_allow="" if [[ "$(detect_platform)" == "windows" && "$WORK_DIR" == "${__claude_dispatch_source_repo:-}" \ && -x "$WORK_DIR/cli"/pmctl ]]; then _claude_checkout_guard_allow="Bash(bash cli/"'pmctl '$'guard check:*)' + # The absolute spelling pr-gate puts in the reviewer brief (GUARD_PMCTL_CMD): it keeps + # working after the reviewer's shell `cd`s away from the repo root. Same string, same + # single quotes; a path with a quote is not allowed (pr-gate then keeps the relative form). + if [[ "$WORK_DIR" != *"'"* ]]; then + _claude_checkout_guard_abs_allow="Bash(bash '$WORK_DIR/cli/"'pmctl'"' "$'guard check:*)' + fi fi CMD=(claude -p @@ -276,6 +283,7 @@ CMD=(claude -p --verbose ) [[ -n "$_claude_checkout_guard_allow" ]] && CMD+=(--allowedTools "$_claude_checkout_guard_allow") +[[ -n "$_claude_checkout_guard_abs_allow" ]] && CMD+=(--allowedTools "$_claude_checkout_guard_abs_allow") [[ -n "$MODEL" ]] && CMD+=(--model "$MODEL") [[ -n "$RESOLVED_EFFORT" ]] && CMD+=(--effort "$RESOLVED_EFFORT") diff --git a/runtime/bin/pr-gate.sh b/runtime/bin/pr-gate.sh index bcf89b8c..8bf6254a 100755 --- a/runtime/bin/pr-gate.sh +++ b/runtime/bin/pr-gate.sh @@ -686,6 +686,15 @@ if [[ "$EXECUTOR" == "claude" && "$(detect_platform)" == "windows" \ fi unset _guard_source_changes GUARD_PMCTL_CMD="bash cli/pmctl" + # A relative `bash cli/pmctl` only works while the reviewer's shell is still in the + # repo root, and the shell keeps its directory between Bash calls: one `cd` to read a + # reviewer definition made the guard check exit 127 and then fail the permission + # rule (the 2026-11-06 gate runs). The absolute spelling works from any directory; + # adapters/claude/dispatch.sh allows exactly this string. A path holding a single + # quote cannot be matched by that rule, so it keeps the relative form. + if [[ "$WORK_DIR" != *"'"* ]]; then + GUARD_PMCTL_CMD="bash '$WORK_DIR/cli/pmctl'" + fi fi if [[ "$EXECUTOR" == "codex" ]]; then # Prefer an explicit transport bundled beside a copy-mode gate before the diff --git a/tests/shell/test-claude-dispatch.sh b/tests/shell/test-claude-dispatch.sh index 3095f67b..bd1ef226 100755 --- a/tests/shell/test-claude-dispatch.sh +++ b/tests/shell/test-claude-dispatch.sh @@ -724,6 +724,12 @@ case_windows_checkout_guard_permission() { rm -f "$brief" return fi + # The absolute spelling pr-gate writes into the brief must be allowed too (cd-proof). + if [[ "$out" != *"--allowedTools Bash(bash '$REPO_ROOT/cli/pmctl' guard check:*)"* ]]; then + fail "$name" "absolute-path permission missing: $out" + rm -f "$brief" + return + fi foreign="$(mktemp -d)" out="$(PM_DISPATCH_PLATFORM=windows "$DISPATCH" --cd "$foreign" --brief-file "$brief" --print-cmd 2>/dev/null)" if [[ "$out" == *"Bash(bash cli/pmctl guard check:*)"* ]]; then @@ -737,4 +743,31 @@ case_windows_checkout_guard_permission() { case_windows_checkout_guard_permission +# A checkout path with a single quote cannot be written into the quoted allow rule, so the +# adapter must grant only the relative form there (pr-gate keeps the relative brief command). +case_windows_checkout_guard_quote_path() { + local name="print-cmd/windows checkout guard: a quote in the path gets no absolute permission" + should_run "$name" || return 0 + local brief out root d + root="$(mktemp -d)" + d="$root/it's repo" + mkdir -p "$d"; d="$(cd -P -- "$d" && pwd)" + for sub in adapters runtime cli core share ops hosts; do + [[ -e "$REPO_ROOT/$sub" ]] && cp -R "$REPO_ROOT/$sub" "$d/$sub" + done + brief="$(_mk_brief "$d")" + out="$(PM_DISPATCH_PLATFORM=windows "$d/adapters/claude/dispatch.sh" --cd "$d" --brief-file "$brief" --print-cmd 2>/dev/null)" + if [[ "$out" != *"--allowedTools Bash(bash cli/pmctl guard check:*)"* ]]; then + fail "$name" "relative permission missing: $out" + elif [[ "$out" == *"Bash(bash '"* ]]; then + fail "$name" "absolute permission granted for a quoted path: $out" + else + pass "$name" + fi + rm -f "$brief" + rm -rf "$root" +} + +case_windows_checkout_guard_quote_path + th_summary diff --git a/tests/shell/test-pr-gate.sh b/tests/shell/test-pr-gate.sh index afa2aab3..a75c2517 100755 --- a/tests/shell/test-pr-gate.sh +++ b/tests/shell/test-pr-gate.sh @@ -10284,7 +10284,8 @@ test_claude_windows_same_checkout_guard() { fail "$name" "exit $code, expected 0; stderr: $(cat "$err" 2>/dev/null)" return fi - assert_file_contains "$name" "$brief" "call: bash cli/pmctl guard check --role reviewer --runtime claude --event pre-write" || return + # Absolute spelling: it still works after the reviewer's shell cd'd away from the repo root. + assert_file_contains "$name" "$brief" "call: bash '$(cd "$runner" && pwd -P)/cli/pmctl' guard check --role reviewer --runtime claude --event pre-write" || return pass "$name" }