diff --git a/docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md b/docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md new file mode 100644 index 00000000..40cde29d --- /dev/null +++ b/docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md @@ -0,0 +1,129 @@ +# `ccam ci status` — unblock Stage 13's CI-wait half + +**Status:** approved 2026-08-25. + +## Problem + +`ship-feature-lane`'s Stage 13 (post-merge verification) is hardcoded +skipped. Two integrations gate it: dev-CI-wait (watch CI on `development` +after a PR merges, before QCing the deployed site) and dev-QC (QC the +deployed site). Neither exists yet. + +This spec covers only the CI-wait half. `ccam lanes integration ci_wait` +already exists (`server/lib/lane-profile.js:isIntegrationEnabled`, +`bin/ccam.js:cmdLanesIntegration`) and reports whether a profile *wants* the +integration on — but nothing acts on it. `ccam ci` doesn't exist +(`skills/ship-feature-lane/SKILL.md:167` notes this explicitly at Stage 10, +where the skill falls back to raw `gh pr checks`/`gh run view`). + +Dev-QC (QCing the deployed site) is a separate, later task — out of scope +here. Stage 13's dev-QC half stays noted as unavailable until that lands. + +## Approach + +**A read-only status snapshot, not a wait loop.** Per the project's +orchestration doctrine (`docs/LANES.md` § "Orchestration: what CCAM does NOT +do" — no chaining, no queue, no retry logic), `ccam ci status` reports a +fact once; it does not poll, block, or retry. The driving skill owns the +poll loop and the fix/escalate decision, the same shape Stage 12 already +uses for watching a PR (`sleep 300` backgrounded, re-invoked per iteration). + +This also means `ccam ci status` needs no flake classification. Stage 10 +already does that triage (infra/flake signature vs. real failure) reading +`gh` output directly — Stage 13 reuses the same triage, now reading this +command's structured JSON instead of raw `gh`. + +**Server-backed, following the `sync-base` pattern**, not a CLI-local `gh` +spawn. Every existing `ccam` command is a thin HTTP client — no command in +`bin/ccam.js` spawns a child process directly (`worktree.js`'s `git()` +helper is the only child-process boundary CCAM has today, always +server-side). A new `server/lib/gh.js` mirrors that helper: + +```js +async function gh(cwd, args) { /* execFile("gh", args, { cwd }) */ } +``` + +New route: `GET /api/lanes/:id/ci/status?ref=` + +- Resolves the lane's `cwd` (same lookup every other lane route uses). +- Runs `gh run list --branch --json databaseId,conclusion,status,url --limit 1` + to find the latest run for that ref, then `gh run view --json jobs` + for per-job conclusions. (A `sha` ref: `gh run list` accepts a commit SHA + via `--commit`, not `--branch` — the route tries `--branch` first and + falls back to `--commit` if the ref doesn't look like a branch name a + lane would use, i.e. it's a 7-40 char hex string.) +- No run found for the ref at all → `state: "pending"` (CI hasn't started + yet, not an error). +- `gh` missing or not authenticated → `{ error: { message } }`, HTTP 500. + +New CLI: `ccam lanes ci status []` + +- GETs the route above, prints the JSON, sets `process.exitCode`. +- Resolves the lane the same way every other `ccam lanes ` command + does (`resolveLaneArg`), so it works with an implicit lane-from-cwd or an + explicit id, matching `ccam lanes integration`. + +## Response shape + +```json +{ + "state": "success", + "ref": "a1b2c3d", + "checks": [ + { "name": "server-tests", "conclusion": "failure", "url": "https://github.com/.../runs/123" } + ] +} +``` + +`state` is derived, never taken from a single field: any job still +`in_progress`/`queued` → `pending`; else any `failure`/`cancelled`/ +`timed_out` → `failure`; else (`success`/`skipped` only) → `success`. + +## Exit codes + +CLI mirrors the response into a process exit code, same convention as +`sync-base`'s distinct exit codes per outcome: + +- `0` — `state: "success"` +- `1` — `state: "failure"`, OR any execution error (`gh` missing, lane not + found, route error) — printed as `✖ ci status → ` on stderr +- `3` — `state: "pending"` (covers both "checks running" and "no run found + yet for this ref") + +## Skill changes + +`skills/ship-feature-lane/SKILL.md` Stage 13: replace the hardcoded-skip +note with a poll loop mirroring Stage 12's: + +- Get the merge SHA: `gh pr view --json mergeCommit -q .mergeCommit.oid`. +- Loop (`sleep 300`, `run_in_background: true`, bump heartbeat each + iteration): `ccam lanes ci status `. + - `pending` → keep waiting. + - `failure` → triage like Stage 10 (`gh run view` for job logs on the + same run id returned in `checks[].url`), fix + re-enter from Stage 2, + or escalate. + - `success` → dev-QC half is still unavailable (separate task) — note + that explicitly and proceed to Stage 14, same as today but now honest + that CI-wait specifically passed. +- Update `docs/LANES.md`: the "Stages currently hardcoded skipped" list + (Stage 13 becomes "CI-wait works; dev-QC still off"), the + `ccam lanes integration` section's claim that `ci_wait`'s consumer isn't + built, and add `ccam ci status` to the command reference. + +## Non-goals + +- No flake auto-classification or auto-rerun inside `ccam ci status` — + stays a dumb fact-reporter; the skill keeps owning that judgment, per + CCAM's no-retry-logic rule. +- No change to Stage 10 (PR CI watch) — it keeps using raw `gh` directly. + Reusing this command there is a plausible future cleanup, not required + now. +- No dev-QC implementation — separate task, tracked independently. + +## Testing + +- `server/tests` — new test for the `/api/lanes/:id/ci/status` route, + mocking `execFile`/`gh` output for: pending (no run yet), pending + (in-progress run), success, failure, and `gh`-not-found — pattern after + the existing `sync-base` route tests. +- `npm run test:server` before considering this done.