Skip to content
17 changes: 17 additions & 0 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-436 | 🔵 active | codex-host PreToolUse payload 驗證 probe(唯讀,驗證 CC-381 guard binding 可行性;umbrella: CC-333) | arch/install | 2026-07-02 | — | P2 | spike |
| CC-437 | 🔵 active | doctor 擴充切片:host-aware capability check(`doctor.sh` 拆出 host module 介面;umbrella: CC-333,承接 CC-381) | arch/install | 2026-07-02 | — | P2 | design |
| CC-438 | 🔵 active | host manifest schema v1:codex-host 設定面宣告化(`hosts/codex/host.yaml` + format handler;依賴 CC-436;umbrella: CC-333,承接 CC-381) | arch/install | 2026-07-02 | — | P2 | design |
| CC-439 | 🔵 active | `/ship <ticket-id>` command:明確票直接實作到開 PR,pre-flight 一致性檢查 + gate 迴圈收斂 | process/DX | 2026-07-02 | — | P2 | design |

---

Expand Down Expand Up @@ -176,6 +177,22 @@ _Terminal_ (CC-378: swept OUT to `BACKLOG-ARCHIVE.md` by `scripts/archive-closed

---

## CC-439 — `/ship <ticket-id>` command:明確票直接實作到開 PR 🔵 active

**Problem**: 目前「拿到明確 backlog 票 → 直接實作 → 派 pr-gate → 修到 GO → 開 PR」這條路徑,只存在於 memory 與 `agents/project-pm.md` 的 Rules A/B 散落文字裡,主線程每次都要自己記得拼起來完整流程,且完全沒有「開工前先檢查跟已定案決策有沒有衝突」這一步。

**Why**: 參考 [ai-night-shift](https://github.com/JudyaiLab/ai-night-shift) 的自動化紀律(非其架構):把「implement → gate → fix → PR」收斂成一個可重複呼叫的 command,讓「丟一張明確的票」到「開出 PR」變成單一動作;同時把唯一合法卡點(票跟 BACKLOG/DECISIONS 已定案內容根本性矛盾)做成明確、可執行的第一步檢查,而不是模糊的自我判斷。

**Requirement**:
- 新增 `commands/ship.md`(`/ship CC-NNN` 呼叫),依 `commands/pm.md`/`commands/spike.md` 既有格式撰寫,步驟:(0) pre-flight 一致性檢查:讀該票 `BACKLOG.md` body + grep `DECISIONS.md` `**Constraints introduced**`,若根本性矛盾或 `Dependencies` 未滿足,停止並回報,不開分支;(1) 開 `feat/CC-NNN` 分支;(2) 主線程直接 Read/Edit/Write 實作,不 dispatch codex 做實作;(3) gate 迴圈:`pmctl gate run --executor codex` → 讀 `Final:` → NO-GO 時交給 `project-pm` agent 依既有 Rule A/B synthesis → 修全部 finding → 重跑,直到 GO;停止條件只有兩種(根本性不一致、或 3-strike 審查後同批 diff-caused blocker 完全原地打轉);(4) `git push` + `gh pr create`(title/body 模板:票號/摘要/跑幾輪 gate/最終 verdict);(5) 收尾報告,GO 後不自動 merge。
- `scripts/test-commands.sh` 補結構斷言:pre-flight 段落存在、gate 迴圈段落引用 `Final:`/`pmctl gate run --executor codex`、停止條件段落明確列出兩種且只有兩種、PR 模板段落存在。
- 不新增 `open-pr.sh` 或 DECISIONS.md 解析腳本(一致性判斷是 LLM 語意工作,不做機械化);不建背景 daemon/cron supervisor(維持互動 session 內執行);不做批次掃描 BACKLOG 自動挑票。

**Dependencies**: 無阻塞依賴。
**See**: —

---

## CC-393 — design: portable-skill-substrate — CLI-agnostic skill 控制層 🟢 someday

**Type**: design seed(想法捕捉;非 milestone 承諾)
Expand Down
171 changes: 171 additions & 0 deletions commands/ship.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
description: Take one explicit backlog ticket from implementation through pr-gate to an open PR, without stopping for step-by-step confirmation.
argument-hint: "<ticket-id>"
---

Run a single, explicitly named ticket end-to-end: implement → gate → fix → gate
→ open PR. This is the main-thread's own default operating discipline made
runnable as one command, not a new background/unattended supervisor — the
session stays open and you stay reachable the whole time.

**Scope**: one ticket per invocation, named in `$ARGUMENTS`. Do not scan
`BACKLOG.md` for other candidates or batch multiple tickets in one run.

## The one legal stopping point

Everywhere in this flow, the only reason to stop and ask the user for a
substantive discussion instead of continuing is: **the ticket's premise
fundamentally conflicts with something already decided** — its approach
contradicts a `DECISIONS.md` entry's `**Constraints introduced**`, a named
`Dependencies` ticket is not actually done, or (discovered mid-implementation)
the ticket's own assumption turns out to be wrong. Ordinary reviewer findings
— hard gate or advisory, however many rounds it takes — are not a stopping
point; fix them and continue. This mirrors `agents/project-pm.md`'s PR-gate
verdict table and Rules A/B, which this command invokes rather than
re-implements.

This is distinct from Step 0's plain input validation and Step 1's dirty-tree
precondition below — those are deterministic fail-fast/fail-safe checks with
one predetermined outcome each, not a negotiated decision. "The one legal
stopping point" refers only to cases where the *next action is genuinely
ambiguous* and needs the user's judgment.

## Step 0 — Validate the ticket id, then check consistency

**Ticket-id validation** (fail fast, not a discussion point): `/ship` only
ever acts on an active `BACKLOG.md` ticket — `BACKLOG-ARCHIVE.md` is
consulted solely to produce a precise error message, never as a source to
implement from.

- If `$ARGUMENTS` is empty or does not match this repo's ticket-id shape
(`<PREFIX>-<NNN>` per `pm/schema.md`): stop and report "empty argument" or
"malformed shape".
- If there is no matching `## <ticket-id>` heading in `BACKLOG.md`: check
`BACKLOG-ARCHIVE.md`. A match there means the ticket is already terminal
(done/closed/dropped/superseded) — stop and report "ticket already
archived", not "no such ticket". No match in either file: stop and report
"no such ticket".

Either way this is a plain input error, resolved by the caller supplying a
valid, currently-active ticket id, not something to deliberate about.

**Consistency check**: once the ticket id resolves to an active `BACKLOG.md`
heading, read its full body (`grep -n '^## <ticket-id>'` then `sed -n` the
section — do not full-file Read). Extract `Problem` / `Requirement` /
`Dependencies`.

- **Dependencies**: for every ticket referenced in `Dependencies`, confirm its
status in `BACKLOG.md`'s index table (or `BACKLOG-ARCHIVE.md` if terminal)
is actually a terminal state (`✅ done`/`✅ closed`) when the requirement
reads as a hard blocker. A dependency that is merely "related" (not a
blocker) does not stop the run.
- **Decisions**: `grep -n '\*\*Constraints introduced\*\*' DECISIONS.md` and
read the handful of entries whose `Context`/`Decision` mentions the same
subsystem, files, or concept as the ticket. Judge — as PM-level judgment,
not string matching — whether the ticket's `Requirement` asks for something
a constraint explicitly rules out, or whose premise a later decision has
superseded. `DECISIONS.md` is a human/PM-only audit record and stays that
way here: read it yourself, do not paste it into any dispatch brief.

If either check finds a real conflict: **stop here.** Do not create a branch,
do not implement. Report the ticket id, the conflicting `DECISIONS.md` entry
or unmet dependency, and wait for the user's direction.

If clear: continue to Step 1.

## Step 1 — Branch

**Dirty-tree precondition** (fail fast, not a discussion point — same bucket
as Step 0's ticket-id validation): run `git status` first. If the tree is
dirty with changes unrelated to this ticket, stop immediately and report that
the tree must be clean before `/ship` will branch — do not stash, commit, or
otherwise mutate the caller's uncommitted work on their behalf. This has one
predetermined resolution (the caller commits or stashes it themselves and
re-invokes `/ship`), so it is not the negotiated stop this command reserves
for genuine ambiguity, and it is not an automatic mutation either — never
branch over uncommitted work silently, and never take a repo-mutating action
the caller did not ask for.

```bash
git checkout -b feat/<ticket-id>
```

## Step 2 — Implement

Implement directly with Read/Edit/Write/Bash in this session — do not dispatch
implementation to codex/claude/opencode. `pmctl dispatch run` is not part of
this flow; the only executor dispatch in `/ship` is the gate's own reviewer
dispatch in Step 3.

## Step 3 — Gate loop

Run `pmctl gate run --executor codex --cd "$PWD" --lifecycle foreground` (the
`/pr-gate` command is the orchestration wrapper around this exact invocation
— either entry point is acceptable, but the underlying gate call is always
this one, never `bash scripts/pr-gate.sh` directly).
`--lifecycle foreground` is required here: the default `--lifecycle detached`
returns only a `gate_id` immediately and the gate keeps running in the
background — reading `Final:` right after that call would read a stale or
missing result. `/ship` is already a long-running autonomous loop with
nothing else for the main thread to do while it waits, so there is no reason
to pay the detached/`pmctl gate wait` two-call complexity that `/pr-gate`
uses to keep the main thread free for other work; run `foreground` and read
the resulting `Final: GO|NO-GO` verdict directly from the gate result file
once the call returns.

- **GO** → go to Step 4.
- **NO-GO** → invoke the `project-pm` agent to synthesize the gate result
against the verdict table and Rules A/B in `agents/project-pm.md` (source-first
read of every cited diff file, discovery sweep of all call sites of a
flagged helper, minimum-list is a floor not a ceiling). Fix **every** finding
it returns — high, medium, and low, hard gate and advisory alike, not only
the blocking ones. Re-run `pmctl gate run --executor codex --cd "$PWD"
--lifecycle foreground --reviewers <reviewer,...>` (the `/pr-gate`
`--targeted` flag maps to this same `--reviewers` option) for the reviewers
whose territory the fix touched. Repeat.

**Stop the loop only when**:
1. Step 0's check would have caught this but didn't — implementation revealed
the ticket's own assumption is wrong, or a fix genuinely requires
contradicting a `DECISIONS.md` constraint; or
2. Rule A's 3-strike audit (`agents/project-pm.md`) has already run, the
remaining blockers are confirmed diff-caused (not the pre-existing issues
Rule A downgrades to `advise`+separate issue), and a further round produces
no new progress — same blockers, same state, nothing fixed. This is "no
fix is being found," not "this is taking many rounds": a real gate has
already needed 7 normal rounds to converge, and round count alone was not
a stop signal that time.

Any other NO-GO, at any round count, gets fixed and re-gated without asking.

## Step 4 — Open the PR

```bash
git push -u origin feat/<ticket-id>
gh pr create --title "<type>(<ticket-id>): <short summary>" --body "$(cat <<'EOF'
## Summary
- <one-line summary of the change>

## Gate
- Rounds: <N>
- Final verdict: GO
- Result file: <path from the last /pr-gate relay>

Ticket: <ticket-id>
EOF
)"
```

Do not merge. GO is not merge authorization — merge only when the user
explicitly says so.

## Step 5 — Close-out report

Report one of four outcomes: (1) invalid ticket id — the exact problem
(empty argument / malformed shape / no such ticket / already archived); (2)
dirty tree — that `/ship` aborted without touching the caller's uncommitted
work, and that a clean tree is required to re-invoke; (3) consistency-check
stop — the ticket id, the conflicting `DECISIONS.md` entry or unmet
dependency, and what decision is needed from the user; or (4) PR opened —
ticket id, what changed, how many gate rounds it took, the final verdict, and
the PR URL.
Loading