Files
AgentHub/docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md
T

5.6 KiB

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:

async function gh(cwd, args) { /* execFile("gh", args, { cwd }) */ }

New route: GET /api/lanes/:id/ci/status?ref=<branch-or-sha>

  • Resolves the lane's cwd (same lookup every other lane route uses).
  • Runs gh run list --branch <ref> --json databaseId,conclusion,status,url --limit 1 to find the latest run for that ref, then gh run view <id> --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 <branch-or-sha> [<lane-id>]

  • GETs the route above, prints the JSON, sets process.exitCode.
  • Resolves the lane the same way every other ccam lanes <verb> command does (resolveLaneArg), so it works with an implicit lane-from-cwd or an explicit id, matching ccam lanes integration.

Response shape

{
  "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 → <message> 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 <pr_url> --json mergeCommit -q .mergeCommit.oid.
  • Loop (sleep 300, run_in_background: true, bump heartbeat each iteration): ccam lanes ci status <sha>.
    • 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.