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: 7 additions & 8 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-393 | 🟢 someday | design: portable-skill-substrate — CLI-agnostic skill 控制層(design seed after v0.6.0 N≥2;3 control skills + Portable Skill v0 frontmatter;umbrella: CC-333) | arch | 2026-06-16 | — | — | design |
| CC-412 | ✅ closed 2026-07-01 | memory substrate 跨工具可攜:位置 seam(`PM_MEMORY_DIR` override)+ 注入/檢索分層(可攜核心=pmctl retrieval API)。v0.8.0 Phase 1 headline | arch/memory | 2026-06-23 | pr:#352 | P3 | retrieval |
| CC-423 | ✅ closed 2026-07-01 | gate detached lifecycle:`pmctl gate run --lifecycle detached`(現為預設)回傳 gate_id 立即退出;gate-supervisor 以 nohup/setsid 跑 pr-gate.sh;sentinel 機制 + `pmctl gate wait <gate_id>` 輪詢,result 完整性 fail-closed;session interrupt 不影響 gate 執行結果。v0.8.0 Phase 2 | arch | 2026-06-25 | pr:#353 | P3 | — |
| CC-425 | 🔵 active | **[gate: 解除 PR 綁定,改以 base..head ref 對為輸入]** 現在 `pmctl gate run` 預設從 `origin/main` fork point 推斷 base,gate result 以 PR# 為 key;改成接受任意兩個 ref(`--base <ref> --head <ref>`),讓 gate 可在開 PR 前本地跑,也可比較任意 branch 差異。需重構 gate 的 base 解析邏輯與 result 存放路徑(目前以 PR# 為 key,改以 `<base>..<head>` slug 或 run_id)。排入 v0.8.0 Phase 2。 | ops/gate | 2026-06-25 | | P3 | — |
| CC-425 | ✅ closed 2026-07-02 | `pr-gate.sh --head <ref>` 新增;diff 一組固定 base..head ref(branch/tag/commit),不涉及 PR 或 working tree;`--base` 既有支援已可省 PR。與 `--allow-dirty` 互斥(明確拒絕)。 | ops/gate | 2026-06-25 | pr:#355 | P3 | — |
| CC-431 | 🟢 someday | **[test-e2e.sh + release-verify.sh: opencode adapter support]** `--adapter` 目前只接受 `claude\|codex\|auto`;opencode 在 v0.6.0 加入後未同步更新 e2e 驗證路徑。需:(1) 將 opencode 加入兩腳本的 adapter 驗證清單;(2) Phase B dispatch 支援 opencode;(3) Phase C pr-gate smoke 評估是否可用 opencode executor(目前硬碼 codex)。觸發:release-verify --e2e --adapter opencode 被拒(exit 2)。 | ops/test | 2026-06-30 | — | P3 | — |
| CC-432 | ✅ closed 2026-07-02 | test-release-verify.sh 12 個重複 `--no-suite` 呼叫改共用快取(`rv_no_suite_once`),380s → ~127s;方向 A(假 repo 隔離)/序列化耦合窄化皆評估後擱置不追(風險高於效益) | ops/test | 2026-07-01 | pr:#354 | P2 | design |
| CC-433 | 🟢 someday | **[detached lifecycle:抽共用 sentinel lib + wait 改主動通知]** (1) `scripts/dispatch-supervisor.sh` 與 `scripts/gate-supervisor.sh` 的 setsid/nohup 啟動 + nonce-authenticated sentinel 寫入邏輯結構相同但各自重寫,應抽成共用 lib,兩邊各自只保留獨有業務邏輯(preflight+adapter vs. 直接 exec pr-gate.sh);(2) `pmctl dispatch wait`/`pmctl gate wait` 目前用 `sleep \$POLL_INTERVAL` 輪詢 sentinel 檔案,應改為主動通知(如 blocking read on FIFO、inotify 等),supervisor 完成時主動喚醒 wait 而非讓它每 N 秒醒來檢查一次。解法未定案,需先 `/pre-impl` 或 `/spike` 收斂設計。 | arch/gate | 2026-07-01 | — | P3 | design |
Expand Down Expand Up @@ -1147,16 +1147,15 @@ Fix:文件化 `GOPATH=/tmp/gopath go build` 慣例到 brief self_verify go bui

**Priority**: P3(someday).

## CC-425 — gate: 解除 PR 綁定,改以 base..head ref 對為輸入 🔵 active
## CC-425 — gate: 解除 PR 綁定,改以 base..head ref 對為輸入 ✅ 2026-07-02

**Problem**: `pmctl gate run` 目前的 base 推斷邏輯綁死在 `git merge-base --fork-point origin/main HEAD`,gate result 也以 PR# 為 primary key——這意味著 gate 只能在已有 PR(或預設對 main)的情況下有意義地跑,無法在開 PR 前本地對任意兩個 branch 做 diff-gate,也無法比較 `v0.6.0..v0.7.0` 這類 tag-to-tag diff。
**See**: pr:#355

**Why**: gate 成為通用的「diff 品質閘門」,不依賴 PR 存在。可用於:開 PR 前先本地跑確認、milestone boundary 差異審查、任意 feature branch 對 release branch 的 diff review
**Resolution**: 盤點現有程式碼後發現票面描述的兩個問題中,一個已在先前重構中解決:`pmctl gate run`(detached lifecycle,CC-423)的 result 存放路徑早已改用 `gate_id`(`sw_project_run_dir`)而非 PR#;foreground 路徑的 `--output` 預設值也是 `.gate-results/gate-<ts>.md`(timestamp-based),並非 PR# key。`--base <ref>` 亦已存在且可在無 PR 情況下運作(`gh pr view` 失敗會 fallback 到 `origin/HEAD` symbolic-ref,不 hard-fail)。真正缺的是 `--head <ref>`:diff 邏輯之前寫死比對 `HEAD`(當前 checkout),無法比較兩個任意固定 ref(如 tag-to-tag、或未 checkout 的 branch)

**Requirement**:
- `pmctl gate run [--base <ref>] [--head <ref>]`:兩者均可省略(維持現有推斷行為作為 fallback)。
- gate result 存放路徑從 PR# key 改為 `<base-slug>..<head-slug>` 或 run_id,PR# 僅在有 PR 時作為 optional metadata 加入。
- 文件 + `pmctl gate run --help` 說明新參數。
新增 `pr-gate.sh --head <ref>`:省略時維持現有行為(working tree / 當前分支 fallback);指定時走獨立分支,採與既有 `--base` 相同的 merge-base(three-dot)語意 `git diff "$BASE"..."$HEAD_REF"`(比較 head 相對 merge-base 的變更,而非字面 two-dot tree diff——base 之後的獨立進展不會滲入 diff),不觸碰 working tree 或 dirty-preflight 邏輯,因此與 `--allow-dirty`(其存在目的是把 working tree 折入 scope)明確互斥並拒絕(exit 2)。Reviewer brief context block 在 `--head` 生效時額外顯示 `Head: <ref>` 一行。`pmctl gate run` 兩條路徑(foreground exec 與 `--lifecycle detached` 透過 `gate-supervisor.sh` 的 `--` passthrough)皆無需改動即可轉發 `--head`,因為都是把未知旗標原樣傳給 `pr-gate.sh`。`--help` usage block 與 unknown-arg 提示同步補上並釐清 three-dot 語意。

**Gate 第一輪 NO-GO(critic block-soft + qa-tester block)修復**:(1) 補 `--head` 缺 operand 的受控錯誤(原本會以 raw `unbound variable` crash,比照 `--override-file` 加 guard);(2) 補 diverged base/head 拓樸測試(`test_head_override_merge_base_semantics`:main 與 feature 各自獨立前進,驗證 three-dot 只看 head 相對 merge-base 的變更、不含 base 的獨立進展);(3) 全文(`--help`、code comment、本 resolution)統一明確標註 three-dot/merge-base 語意,避免與字面 `base..head` two-dot tree diff 混淆;(4) 修正 MILESTONES.md Phase 2 註記與已完成列矛盾的過期文字。第二輪 5 個 `--head` 測試(原 3 + 新 2)、`scripts/test-pr-gate.sh` 共 124 項全數通過;`test-gate-lifecycle.sh`/`test-pmctl-gate.sh`/`test-pr-gate-profile.sh` 無回歸。

**Priority**: P3(someday).

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- **`pr-gate.sh --head <ref>` (CC-425).** Gate can now review a fixed head ref (branch, tag, or commit) with no PR and no working tree involved — review a branch before opening a PR, or diff `v0.6.0..v0.7.0` tag-to-tag. Uses the same merge-base (three-dot) semantics as the default `--base` path, so `base`'s independent progress after the fork point never leaks into the diff. Rejects `--allow-dirty` (which folds in local uncommitted state) as incompatible, and rejects a bare `--head` with a controlled error instead of crashing. Forwarded transparently through both the foreground and `--lifecycle detached` routes since `pmctl gate run` already passes unrecognized flags through to `pr-gate.sh`.

---

## [0.7.1] — 2026-06-30
Expand Down
4 changes: 2 additions & 2 deletions MILESTONES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
| CC-423 | gate detached lifecycle:`pmctl gate run --lifecycle detached`(現為預設)回傳 gate_id 立即退出;gate-supervisor 以 nohup/setsid 跑 pr-gate.sh;sentinel 機制 + `pmctl gate wait <gate_id>` 輪詢,result 完整性 fail-closed,鏡像既有 `dispatch --lifecycle detached` 模式 | ✅ done pr:#353 |
| CC-433 | detached lifecycle 收尾:(1) 抽出 dispatch/gate 兩份 supervisor 共用的 sentinel 啟動邏輯成共用 lib;(2) `pmctl dispatch wait`/`pmctl gate wait` 的輪詢(`sleep` 迴圈)改為主動通知(FIFO/inotify 等,解法未定案)。CC-423 交付後發現的簡化與效率改善項,解法待 `/pre-impl` 或 `/spike` 收斂 | 🟢 someday |
| CC-432 | run-all-tests.sh 耗時瓶頸:`test-release-verify.sh` 12 個重複 `--no-suite` 呼叫改共用快取(`rv_no_suite_once`),380s → ~127s。方向 A(Phase 3 smoke 改隔離假 repo)與 `LIVE_DB_EXCLUSIVE` 序列化耦合窄化皆評估後擱置不追(風險高於效益,未來可重新評估) | ✅ done pr:#354 |
| CC-425 | gate 解除 PR 綁定:`pmctl gate run [--base <ref>] [--head <ref>]`,兩者均可省略(維持現有 fork-point 推斷作為 fallback);gate result 存放路徑從 PR# key 改為 `<base-slug>..<head-slug>` 或 run_id,PR# 僅在有 PR 時作為 optional metadata。讓 gate 可在開 PR 前本地跑,也可比較任意 branch/tag 差異 | 🔵 active |
| CC-425 | gate 解除 PR 綁定:`pr-gate.sh --head <ref>` 新增,以既有 `--base` 相同的 merge-base(three-dot)語意 diff 一組固定 ref,不涉 PR/working tree;盤點發現 result 路徑 PR# key 問題已在 CC-423 detached lifecycle 重構中解決(改用 gate_id),`--base` 也已支援無 PR 場景,故實際範圍小於原評估 | ✅ done pr:#355 |

> CC-433 排入 Phase 2 作為 CC-423 的後續收斂項,非阻塞本 Phase 其餘票的完成。CC-432 為 CC-423 pr-gate 迭代中發現並記錄的衍生票,同樣併入 Phase 2。CC-425 原評估「需重構 gate result key schema,範圍比 CC-276/423 大一截」暫不排入,2026-07-02 使用者確認排入本 Phase 處理——需重構 base 解析邏輯與 result 存放路徑(PR# key → base..head slug/run_id),實作前建議先 `/pre-impl` 確認既有 PR# key 的 gate result 讀取路徑(`pmctl gate wait`/`pmctl artifacts show` 等)向下相容策略
> CC-433 排入 Phase 2 作為 CC-423 的後續收斂項,非阻塞本 Phase 其餘票的完成。CC-432 為 CC-423 pr-gate 迭代中發現並記錄的衍生票,同樣併入 Phase 2。CC-425 原評估「需重構 gate result key schema,範圍比 CC-276/423 大一截」暫不排入,2026-07-02 使用者確認排入本 Phase 處理;實作前盤點發現 result key 已在 CC-423 detached lifecycle 重構中改為 gate_id-keyed,範圍縮小為僅需新增 `--head <ref>`,`/pre-impl` 的向下相容顧慮已不適用,見 pr:#355

### Phase 3 — CC-381 spike-only(P3;design 收斂,非完整實作)

Expand Down
73 changes: 65 additions & 8 deletions scripts/pr-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ _kill_process_tree() {
# --targeted <list> alias for --reviewers (matches /pr-gate skill vocabulary)
# --scope <text> context hint passed into the review brief
# --base <branch> base branch for diff (default: origin/HEAD → main)
# --head <ref> head ref for diff (default: HEAD / working tree); pass a fixed ref
# (branch, tag, commit) to review it without a PR or working tree
# involved -- e.g. review a branch before opening a PR, or diff one
# tag against another (v0.6.0..v0.7.0). Uses the SAME merge-base
# (three-dot) semantics as the default HEAD path: reviews what
# changed on head since it diverged from base, not a literal
# two-dot tree diff. Incompatible with --allow-dirty.
# --run-dir <abs> out-of-repo dir for gate artifacts (briefs/results/trace); optional,
# defaults to in-repo paths under --cd when absent (backward compat)
# --output <path> result file (default: .gate-results/gate-<ts>.md)
Expand All @@ -85,6 +92,7 @@ TIER_OVERRIDE=""
REVIEWERS_OVERRIDE=""
SCOPE=""
BASE_OVERRIDE=""
HEAD_OVERRIDE=""
OUTPUT_OVERRIDE=""
TIMEOUT="1200"
SEQUENTIAL=true # default: sequential (lower token cost)
Expand Down Expand Up @@ -112,6 +120,12 @@ while [[ $# -gt 0 ]]; do
--targeted) REVIEWERS_OVERRIDE="$2"; shift 2;; # alias: /pr-gate skill + script comments say "targeted"
--scope) SCOPE="$2"; shift 2;;
--base) BASE_OVERRIDE="$2"; shift 2;;
--head)
# Guard the operand explicitly: under `set -u` a bare `--head` with no
# following arg would abort with a raw "unbound variable" instead of the
# script's controlled CLI error style (mirrors --override-file below).
[[ $# -ge 2 ]] || { printf 'Error: --head requires a ref\n' >&2; exit 2; }
HEAD_OVERRIDE="$2"; shift 2;;
--output) OUTPUT_OVERRIDE="$2"; shift 2;;
--executor) EXECUTOR_OPTION="$2"; shift 2;;
--model) DISPATCH_MODEL="$2"; shift 2;;
Expand All @@ -128,11 +142,11 @@ while [[ $# -gt 0 ]]; do
[[ $# -ge 2 ]] || { printf 'Error: --override-file requires a file path\n' >&2; exit 2; }
OVERRIDE_FILE="$2"; shift 2;;
-h|--help)
sed -n '2,63p' "$0" | sed 's/^# \{0,1\}//'
sed -n '2,87p' "$0" | sed 's/^# \{0,1\}//'
exit 0;;
*)
printf 'Unknown arg: %s\n' "$1" >&2
printf 'Accepted: --cd --run-dir --tier --brief --reviewers|--targeted --scope --base --output --executor --model --isolation --timeout --parallel --sequential --allow-hooks --allow-dirty --override-file (-h for help)\n' >&2
printf 'Accepted: --cd --run-dir --tier --brief --reviewers|--targeted --scope --base --head --output --executor --model --isolation --timeout --parallel --sequential --allow-hooks --allow-dirty --override-file (-h for help)\n' >&2
exit 2;;
esac
done
Expand Down Expand Up @@ -447,6 +461,31 @@ if ! git rev-parse --verify "$BASE" > /dev/null 2>&1; then
exit 1
fi

# ── Detect head ref ────────────────────────────────────────────────────────
# Default HEAD keeps the existing working-tree/branch-diff behavior below.
# A fixed --head ref (branch, tag, commit) diffs base..head_ref directly with
# no working tree involved, so it is incompatible with --allow-dirty (which
# exists specifically to fold uncommitted working-tree state into scope).
HEAD_REF="HEAD"
if [[ -n "$HEAD_OVERRIDE" ]]; then
if [[ "$ALLOW_DIRTY" == true ]]; then
printf 'Error: --head and --allow-dirty are incompatible (--head diffs a fixed ref pair; --allow-dirty folds in local working-tree changes)\n' >&2
exit 2
fi
HEAD_REF="$HEAD_OVERRIDE"
if ! git rev-parse --verify "$HEAD_REF" > /dev/null 2>&1; then
printf 'Error: head ref not found: %s\n' "$HEAD_REF" >&2
exit 1
fi
fi
# Surfaced in reviewer brief context blocks (Base: ${BASE}${HEAD_METADATA_LINE})
# only when a fixed --head ref is in play; a plain HEAD comparison omits the
# line entirely since it would just say "Head: HEAD" (no information).
HEAD_METADATA_LINE=""
if [[ "$HEAD_REF" != "HEAD" ]]; then
HEAD_METADATA_LINE=$'\n Head: '"${HEAD_REF}"
fi

_worktree_is_dirty() {
# uncommitted tracked changes (staged or unstaged) ...
if ! git diff --quiet HEAD 2>/dev/null; then return 0; fi
Expand All @@ -461,8 +500,9 @@ _worktree_is_dirty() {
# commits first for a complete, reproducible review -- unless they explicitly
# opt into reviewing the working tree as-is. A dirty-only tree with NO
# committed changes is handled by the working-tree fallback below and is NOT
# failed here (nothing is omitted in that case).
if ! git diff "$BASE"...HEAD --quiet 2>/dev/null && _worktree_is_dirty; then
# failed here (nothing is omitted in that case). Skipped entirely for a fixed
# --head ref: that path never reads the working tree.
if [[ "$HEAD_REF" == "HEAD" ]] && ! git diff "$BASE"...HEAD --quiet 2>/dev/null && _worktree_is_dirty; then
if [[ "$ALLOW_DIRTY" != true ]]; then
_dt=$(git diff HEAD --name-only 2>/dev/null | { grep -c . || true; })
_du=$(git ls-files --others --exclude-standard | { grep -c . || true; })
Expand All @@ -481,7 +521,24 @@ fi
# ── Collect diff ──────────────────────────────────────────────────────────────
# Use --name-status so renames expose BOTH old and new paths for sensitive matching.
# Use --numstat to detect binary files (shown as -\t-\t<file>).
if [[ "$ALLOW_DIRTY" == true ]] && _worktree_is_dirty; then
if [[ "$HEAD_REF" != "HEAD" ]]; then
# Fixed head ref (e.g. tag-to-tag, or a branch reviewed before a PR exists)
# -- no working tree involved, so no dirty/fallback branches apply. Three-dot
# (merge-base) diff, matching the default HEAD path below: reviews what
# changed on HEAD_REF since it diverged from BASE, not a literal two-dot
# tree diff -- so BASE moving forward independently does not appear here.
DIFF_FILES=$(git diff "$BASE"..."$HEAD_REF" --name-status | awk '
/^R/ { print $2; print $3; next }
/^[AMDCT]/ { print $2 }
')
DIFF_STAT=$(git diff "$BASE"..."$HEAD_REF" --stat)
BINARY_HIT=$(git diff "$BASE"..."$HEAD_REF" --numstat | { grep -c $'^-\t-\t' || true; })
LINES=$(git diff "$BASE"..."$HEAD_REF" --numstat | awk '
/^-\t-\t/ { next }
{ s += $1 + $2 }
END { print s+0 }
')
elif [[ "$ALLOW_DIRTY" == true ]] && _worktree_is_dirty; then
# --allow-dirty: fold the working tree into scope. Two-dot diff vs BASE
# captures committed + uncommitted tracked changes; untracked listed separately.
DIFF_FILES=$( { git diff "$BASE" --name-status | awk '
Expand Down Expand Up @@ -840,7 +897,7 @@ context:
Executor: ${EXECUTOR}
Reviewers: ${REVIEWER_DISPLAY}
Not reviewed: ${SKIPPED_DISPLAY}
Base: ${BASE}
Base: ${BASE}${HEAD_METADATA_LINE}
Scope: ${SCOPE:-none}
Date: $(date '+%Y-%m-%d')
${GATE_OVERRIDES_CONTEXT_BLOCK}
Expand Down Expand Up @@ -1027,7 +1084,7 @@ context:
Tier: ${TIER}
Executor: ${EXECUTOR}
Reviewer: ${r}
Base: ${BASE}
Base: ${BASE}${HEAD_METADATA_LINE}
Scope: ${SCOPE:-none}
Date: $(date '+%Y-%m-%d')
${GATE_OVERRIDES_CONTEXT_BLOCK}
Expand Down Expand Up @@ -1251,7 +1308,7 @@ context:
Executor: ${EXECUTOR}
Reviewers: ${REVIEWER_DISPLAY}
Not reviewed: ${SKIPPED_DISPLAY}
Base: ${BASE}
Base: ${BASE}${HEAD_METADATA_LINE}
Scope: ${SCOPE:-none}
Date: $(date '+%Y-%m-%d')
${GATE_OVERRIDES_CONTEXT_BLOCK}
Expand Down
Loading