refactor!: rebrand CCAM to AgentHub

Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
This commit is contained in:
2026-09-07 12:09:15 +07:00
parent ae9b3dbcd5
commit 17d56d0e01
345 changed files with 2705 additions and 2727 deletions
@@ -2,15 +2,15 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give CCAM a durable "lane" entity (one per working directory / agent) plus a Shipyard-style pipeline map that shows which stage and which gates each Claude Code agent has finished, with per-lane liveness and start/stop/resume control.
**Goal:** Give AgentHub a durable "lane" entity (one per working directory / agent) plus a Shipyard-style pipeline map that shows which stage and which gates each Claude Code agent has finished, with per-lane liveness and start/stop/resume control.
**Architecture:** Approach A — CCAM stays a monitor + control panel; it does NOT orchestrate. The Claude session itself drives its pipeline and *declares* its stage through `ccam stage <name>` (a new CLI subcommand hitting `POST /api/lanes/:id/stage`), exactly the way Shipyard's skills call `bin/state.sh N set stage=…`. A lane is bound to a session automatically by longest-prefix match on the hook payload's `cwd`, so un-instrumented sessions still appear (stage inferred from `workflows.phases` / TodoWrite, never invented). Pipeline shape is a JSON template on disk, not hardcoded coordinates.
**Architecture:** Approach A — AgentHub stays a monitor + control panel; it does NOT orchestrate. The Claude session itself drives its pipeline and *declares* its stage through `agenthub stage <name>` (a new CLI subcommand hitting `POST /api/lanes/:id/stage`), exactly the way Shipyard's skills call `bin/state.sh N set stage=…`. A lane is bound to a session automatically by longest-prefix match on the hook payload's `cwd`, so un-instrumented sessions still appear (stage inferred from `workflows.phases` / TodoWrite, never invented). Pipeline shape is a JSON template on disk, not hardcoded coordinates.
**Tech Stack:** Node 18+, Express, better-sqlite3, `node:test` (server), React 18 + TypeScript + Vite + Tailwind, Vitest + Testing Library (client), existing `server/websocket.js` broadcast.
## Global Constraints
- Fork lives at `~/MyDrive/Projects/ResearchAndDevelopment/ccam-lanes`, remote `upstream` = `https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor`, forked at `94bfdde` (2026-07-26). Keep the fork private; never push to `upstream`.
- Fork lives at `~/MyDrive/Projects/ResearchAndDevelopment/agenthub-lanes`, remote `upstream` = `https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor`, forked at `94bfdde` (2026-07-26). Keep the fork private; never push to `upstream`.
- Every `.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css` file created or modified MUST start with a file overview block plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. This is enforced by `bash .claude/skills/file-headers/scripts/check-headers.sh` whose `AUTHOR_MARK` was rewritten to this name on 2026-07-27 (fork is internal, never published). Do not substitute a different author line. - Preserve existing behavior. Additive schema only: new tables and `ALTER TABLE … ADD COLUMN` guarded by a `try { SELECT col } catch { ALTER }` probe, matching `server/db.js:412-418`.
- WebSocket message types are append-only. Add `lane_update`; do not rename or repurpose existing types.
- The hook ingest path must stay fail-safe and non-blocking: any lane logic added to `server/routes/hooks.js` runs inside `try { … } catch { /* never block a hook */ }`.
@@ -40,7 +40,7 @@
- `server/index.js` — mount `app.use("/api/lanes", lanesRouter)`.
- `server/routes/hooks.js` — bind session→lane by `cwd`; set `needs_action` on `Notification`.
- `server/routes/run.js` — export the same-origin guard for reuse (no behavior change).
- `bin/ccam.js` — `ccam stage` and `ccam lanes` subcommands.
- `bin/agenthub.js` — `agenthub stage` and `agenthub lanes` subcommands.
- `client/src/lib/api.ts` — `api.lanes.*` + `Lane` / `PipelineNode` / `LaneNodeState` types.
- `client/src/App.tsx` — `/lanes` route.
- `client/src/components/Sidebar.tsx` — nav entry.
@@ -1054,19 +1054,19 @@ git commit -m "feat(lanes): bind sessions to lanes by cwd and surface needs-you"
---
## Task 5: `ccam stage` / `ccam lanes` CLI
## Task 5: `agenthub stage` / `agenthub lanes` CLI
This is what a skill or a `CLAUDE.md` rule actually calls. A CLI subcommand — not an MCP tool — because `bin/ccam.js` already exists, needs no TypeScript build, and any agent can run it with Bash.
This is what a skill or a `CLAUDE.md` rule actually calls. A CLI subcommand — not an MCP tool — because `bin/agenthub.js` already exists, needs no TypeScript build, and any agent can run it with Bash.
**Files:**
- Modify: `bin/ccam.js` (command switch at `bin/ccam.js:2272-2330`, help text at `bin/ccam.js:1589`)
- Modify: `bin/agenthub.js` (command switch at `bin/agenthub.js:2272-2330`, help text at `bin/agenthub.js:1589`)
- Test: `server/__tests__/lanes-cli.test.js`
**Interfaces:**
- Consumes: `GET/POST /api/lanes` from Task 3, through the helpers `bin/ccam.js` already defines at `bin/ccam.js:191-192` — `get(pathname)` and `post(pathname, body)`, both wrapping `api()` (`bin/ccam.js:166`). Do not add a second HTTP client. `api()` already exits 1 with a red `✖ METHOD path → message` line on a non-2xx, so error paths below only handle the cases it does not cover.
- Consumes: `GET/POST /api/lanes` from Task 3, through the helpers `bin/agenthub.js` already defines at `bin/agenthub.js:191-192` — `get(pathname)` and `post(pathname, body)`, both wrapping `api()` (`bin/agenthub.js:166`). Do not add a second HTTP client. `api()` already exits 1 with a red `✖ METHOD path → message` line on a non-2xx, so error paths below only handle the cases it does not cover.
- Produces:
- `ccam lanes` — table of lanes: id, title, stage, status, liveness, progress.
- `ccam stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]` — resolves the lane from `--lane`, else `--cwd`, else `process.cwd()`; exits 1 with a clear message when no lane owns that path.
- `agenthub lanes` — table of lanes: id, title, stage, status, liveness, progress.
- `agenthub stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]` — resolves the lane from `--lane`, else `--cwd`, else `process.cwd()`; exits 1 with a clear message when no lane owns that path.
- [ ] **Step 1: Write the failing test**
@@ -1074,7 +1074,7 @@ Create `server/__tests__/lanes-cli.test.js`:
```js
/**
* @file Tests for the `ccam stage` / `ccam lanes` CLI subcommands: lane
* @file Tests for the `agenthub stage` / `agenthub lanes` CLI subcommands: lane
* resolution from the current directory, the stage round-trip through the HTTP
* API, and the non-zero exit when the cwd belongs to no lane.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -1095,8 +1095,8 @@ process.env.DASHBOARD_LIVENESS_PROBE = "0";
const { createApp, startServer } = require("../index");
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-cli-"));
const CLI = path.join(__dirname, "..", "..", "bin", "ccam.js");
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-cli-"));
const CLI = path.join(__dirname, "..", "..", "bin", "agenthub.js");
let server;
let BASE;
@@ -1155,7 +1155,7 @@ after(() => {
fs.rmSync(LANE_DIR, { recursive: true, force: true });
});
describe("ccam stage", () => {
describe("agenthub stage", () => {
it("reports a stage for the lane owning the current directory", () => {
const r = cli(["stage", "review", "--evidence", "3 findings"], LANE_DIR);
assert.equal(r.status, 0, r.stderr);
@@ -1175,21 +1175,21 @@ describe("ccam stage", () => {
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test server/__tests__/lanes-cli.test.js`
Expected: FAIL — `ccam stage` is an unknown command.
Expected: FAIL — `agenthub stage` is an unknown command.
- [ ] **Step 3: Implement the subcommands**
In `bin/ccam.js`, add two command functions alongside `cmdOpen` (`bin/ccam.js:1450`), using the file's existing request helper and output formatter rather than a new one:
In `bin/agenthub.js`, add two command functions alongside `cmdOpen` (`bin/agenthub.js:1450`), using the file's existing request helper and output formatter rather than a new one:
```js
/**
* `ccam lanes` — one row per lane: what it is, where it is in its pipeline, and
* `agenthub lanes` — one row per lane: what it is, where it is in its pipeline, and
* whether the driving session is still breathing.
*/
async function cmdLanes() {
const { lanes, counts } = await get("/api/lanes");
if (!lanes.length) {
console.log("no lanes yet — create one with: ccam lanes add --cwd <path> --title <text>");
console.log("no lanes yet — create one with: agenthub lanes add --cwd <path> --title <text>");
return;
}
for (const l of lanes) {
@@ -1206,14 +1206,14 @@ async function cmdLanes() {
}
/**
* `ccam stage <stage> [flags]` — the lane equivalent of Shipyard's
* `agenthub stage <stage> [flags]` — the lane equivalent of Shipyard's
* `state.sh N set stage=…`. A skill calls this at each phase boundary so the
* dashboard shows a declared stage instead of an inferred one.
*/
async function cmdStage(args) {
const stage = args[0];
if (!stage || stage.startsWith("--")) {
console.error("usage: ccam stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]");
console.error("usage: agenthub stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]");
process.exitCode = 1;
return;
}
@@ -1230,7 +1230,7 @@ async function cmdStage(args) {
.filter((l) => cwd === l.cwd || cwd.startsWith(`${l.cwd}/`))
.sort((a, b) => b.cwd.length - a.cwd.length)[0];
if (!match) {
console.error(`no lane owns ${cwd} — create one with: ccam lanes add --cwd ${cwd}`);
console.error(`no lane owns ${cwd} — create one with: agenthub lanes add --cwd ${cwd}`);
process.exitCode = 1;
return;
}
@@ -1248,7 +1248,7 @@ async function cmdStage(args) {
}
```
Wire them into the command switch (`bin/ccam.js:2272`), matching the surrounding `case` style:
Wire them into the command switch (`bin/agenthub.js:2272`), matching the surrounding `case` style:
```js
case "lanes":
@@ -1257,14 +1257,14 @@ Wire them into the command switch (`bin/ccam.js:2272`), matching the surrounding
return cmdStage(rest);
```
Add both to `cmdHelp()` (`bin/ccam.js:1589`) in the existing help layout:
Add both to `cmdHelp()` (`bin/agenthub.js:1589`) in the existing help layout:
```
lanes List lanes with stage, liveness and progress
stage <stage> [flags] Report the current pipeline stage for a lane
```
`get` and `post` are the module-level helpers already defined at `bin/ccam.js:191-192`; both are in scope for any command function in that file.
`get` and `post` are the module-level helpers already defined at `bin/agenthub.js:191-192`; both are in scope for any command function in that file.
- [ ] **Step 4: Run tests to verify they pass**
@@ -1274,8 +1274,8 @@ Expected: PASS, 2 tests.
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js server/__tests__/lanes-cli.test.js
git commit -m "feat(lanes): ccam stage and ccam lanes subcommands"
git add bin/agenthub.js server/__tests__/lanes-cli.test.js
git commit -m "feat(lanes): agenthub stage and agenthub lanes subcommands"
```
---
@@ -1801,7 +1801,7 @@ export default function Lanes() {
{!lanes.length && (
<p className="text-sm text-neutral-500">
No lanes yet. Create one from a working directory: <code>ccam lanes add --cwd $(pwd)</code>
No lanes yet. Create one from a working directory: <code>agenthub lanes add --cwd $(pwd)</code>
</p>
)}
</div>
@@ -1850,7 +1850,7 @@ The repo's own rules require docs to move with behavior. This is one task, not a
**Files:**
- Create: `docs/LANES.md`
- Modify: `CLAUDE.md` (a `## Lanes` section: what a lane is, that CCAM does not orchestrate, and that stage reporting is the agent's job via `ccam stage`)
- Modify: `CLAUDE.md` (a `## Lanes` section: what a lane is, that AgentHub does not orchestrate, and that stage reporting is the agent's job via `agenthub stage`)
- Modify: `README.md` (one paragraph + the two new CLI commands in the command list)
**Interfaces:**
@@ -1859,7 +1859,7 @@ The repo's own rules require docs to move with behavior. This is one task, not a
- [ ] **Step 1: Write `docs/LANES.md`**
It must cover, with exact paths and runnable commands: what a lane is and why it is keyed by `cwd` rather than `session_id`; the five node states and specifically why `passed-no-evidence` is amber rather than green; how a skill reports a stage (`ccam stage review --evidence "3 findings"`); how to add a custom pipeline template (`DASHBOARD_PIPELINES_DIR`, the JSON shape from `server/data/pipelines/default.json`); the liveness rule (`LANE_DEAD_SEC`, why a silent watcher is dead but a silent idle lane is not); and an explicit statement that CCAM does not chain, queue, retry, or gate — the driving Claude session does.
It must cover, with exact paths and runnable commands: what a lane is and why it is keyed by `cwd` rather than `session_id`; the five node states and specifically why `passed-no-evidence` is amber rather than green; how a skill reports a stage (`agenthub stage review --evidence "3 findings"`); how to add a custom pipeline template (`DASHBOARD_PIPELINES_DIR`, the JSON shape from `server/data/pipelines/default.json`); the liveness rule (`LANE_DEAD_SEC`, why a silent watcher is dead but a silent idle lane is not); and an explicit statement that AgentHub does not chain, queue, retry, or gate — the driving Claude session does.
- [ ] **Step 2: Update `CLAUDE.md` and `README.md`**
@@ -1878,5 +1878,5 @@ Named here so a later reader knows these were decided, not forgotten:
- **Worktree / port / database provisioning per lane.** Shipyard's `bin/lane-up.sh` territory. A lane here is just a directory that already exists.
- **Any orchestrator** — no queue, no step chaining, no retry, no gate evaluation. Approach A: adding one would create a second scheduler competing with the Claude session that is already driving.
- **An MCP `report_stage` tool.** `ccam stage` covers it with no TypeScript build step. Add the MCP tool later only if a session turns out to reach for tools more reliably than Bash.
- **An MCP `report_stage` tool.** `agenthub stage` covers it with no TypeScript build step. Add the MCP tool later only if a session turns out to reach for tools more reliably than Bash.
- **Stage inference from `TodoWrite` / `workflows.phases` for un-instrumented sessions.** Worth doing, but it is a distinct feature (inferred vs declared stage, with the UI distinguishing them) and belongs in its own plan once declared stages are proven.
@@ -97,11 +97,11 @@ Pure logic, no DB, no HTTP. Everything else depends on its shape.
## Task 5 (B5): docs, CLI surface, and the honesty pass
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/openapi-extra/lanes.js`, `openapi.yaml` (regenerated), `server/README.md`, `bin/ccam.js`, `server/__tests__/lanes-cli.test.js`.
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/openapi-extra/lanes.js`, `openapi.yaml` (regenerated), `server/README.md`, `bin/agenthub.js`, `server/__tests__/lanes-cli.test.js`.
**Produces:** `ccam lanes` gains a column or suffix showing the inferred stage when it leads the declared one. `docs/LANES.md` gains a Stage detection section stating: what signals are read; that rules live in the template and how to add one via `DASHBOARD_PIPELINES_DIR`; that detection is forward-only; that declared beats detected; and — prominently — **that an inferred stage never counts as evidence and never renders as done**, with the reason. `docs/API.md` and the OpenAPI fragment document the three new payload fields.
**Produces:** `agenthub lanes` gains a column or suffix showing the inferred stage when it leads the declared one. `docs/LANES.md` gains a Stage detection section stating: what signals are read; that rules live in the template and how to add one via `DASHBOARD_PIPELINES_DIR`; that detection is forward-only; that declared beats detected; and — prominently — **that an inferred stage never counts as evidence and never renders as done**, with the reason. `docs/API.md` and the OpenAPI fragment document the three new payload fields.
- [ ] **Step 1: write the failing CLI test:** `ccam lanes` prints the inferred stage for a lane whose detection leads its declaration, and does not print one when the declaration leads.
- [ ] **Step 1: write the failing CLI test:** `agenthub lanes` prints the inferred stage for a lane whose detection leads its declaration, and does not print one when the declaration leads.
- [ ] **Step 2: run, confirm it fails.**
- [ ] **Step 3: implement the CLI change and write the docs.** Every rule you document must match `server/data/pipelines/default.json` exactly — read the file, do not recall it.
- [ ] **Step 4:** `npm run test:server`, `npm run test:client`, `node scripts/generate-openapi-yaml.js` then confirm `git diff openapi.yaml` is empty.
@@ -14,7 +14,7 @@
- Every `.js/.ts/.tsx` file created or modified MUST start with a file overview comment plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. Verify with `bash .claude/skills/file-headers/scripts/check-headers.sh` (must exit 0).
- **Tasks A1-A3 are pure moves.** No behaviour may change, no logic may be "improved" in passing. The existing Run tests must pass with changes to import paths ONLY. If a test needs a real edit to keep passing, stop and report it — that means the move was not pure.
- **Extraction and composition never share a commit.** A1, A2, A3 are refactors; A5 builds the page.
- **The console never writes a lane's stage.** No code path from the console may call `POST /:id/stage`. Only `ccam stage` declares; only detection infers.
- **The console never writes a lane's stage.** No code path from the console may call `POST /:id/stage`. Only `agenthub stage` declares; only detection infers.
- Schema changes are additive with a per-column probe (`try { SELECT col } catch { ALTER }`).
- Preserve existing behavior: `POST /api/run` and every existing WebSocket message type keep working exactly as they do — the CLI and other callers depend on them. `lane_update` stays the only lane WS type.
- Server CommonJS; client React + TypeScript. No new npm dependencies. Exact-value assertions; bounded polling, no bare sleeps. i18n strings in all four locales (`en`, `zh`, `vi`, `ko`), genuinely translated.
@@ -102,7 +102,7 @@
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/README.md`, `ARCHITECTURE.md`, `README.md`, `server/openapi-extra/lanes.js` (+ regenerate `openapi.yaml`), `CLAUDE.md`.
**Produces:** documentation of the merged page and the new seams: that `/lanes` redirects to `/run`; that the UI starts runs through the lane while `POST /api/run` remains for the CLI; the `ensure` endpoint and when the UI calls it; `dashboard_runs.lane_id`; that a finished run releases its lane. `CLAUDE.md` gains the rule: **the console never writes a lane's stage — declared comes from `ccam stage`, inferred from detection.** Every path and command you print must exist; verify each.
**Produces:** documentation of the merged page and the new seams: that `/lanes` redirects to `/run`; that the UI starts runs through the lane while `POST /api/run` remains for the CLI; the `ensure` endpoint and when the UI calls it; `dashboard_runs.lane_id`; that a finished run releases its lane. `CLAUDE.md` gains the rule: **the console never writes a lane's stage — declared comes from `agenthub stage`, inferred from detection.** Every path and command you print must exist; verify each.
- [ ] **Step 1:** write the docs.
- [ ] **Step 2:** verify every referenced file, route and command exists (`ls`, `grep`, or run it).
@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let a lane own a git worktree that CCAM creates, resets, removes and purges, with every destructive action gated on counted facts and on three independent safety checks.
**Goal:** Let a lane own a git worktree that AgentHub creates, resets, removes and purges, with every destructive action gated on counted facts and on three independent safety checks.
**Architecture:** Design doc: `docs/superpowers/specs/2026-07-28-worktree-lanes-design.md` — read it once before Task 1. All git work goes through one module (`server/lib/worktree.js`) that shells out with `execFile` and an argv array, never a shell string, and re-verifies its own safety preconditions. Lanes gain a `kind` of `adopted` (pointer at a directory the user already had — never destroyable) or `managed` (a worktree CCAM created — destroyable). Destructive actions are serialised per lane and preceded by a preflight endpoint that returns counts, which the confirmation UI renders and the server re-checks before acting.
**Architecture:** Design doc: `docs/superpowers/specs/2026-07-28-worktree-lanes-design.md` — read it once before Task 1. All git work goes through one module (`server/lib/worktree.js`) that shells out with `execFile` and an argv array, never a shell string, and re-verifies its own safety preconditions. Lanes gain a `kind` of `adopted` (pointer at a directory the user already had — never destroyable) or `managed` (a worktree AgentHub created — destroyable). Destructive actions are serialised per lane and preceded by a preflight endpoint that returns counts, which the confirmation UI renders and the server re-checks before acting.
**Tech Stack:** Node 18+, Express, better-sqlite3, `node:child_process.execFile`, real `git` against temp-directory fixtures, `node:test` (server), React 18 + TypeScript + Vitest (client).
@@ -37,7 +37,7 @@
- `server/db.js` — four additive columns.
- `server/lib/lanes.js` — `kind`/`source_repo`/`base_branch`/`slug` in create/patch/payload; `purgeLaneSessions`.
- `server/routes/lanes.js` — `POST /worktree`, `GET /:id/preflight`, `reset` + `purge` actions, lock usage.
- `bin/ccam.js` — `ccam lanes add --repo`, `ccam lanes reset|remove|purge`.
- `bin/agenthub.js` — `agenthub lanes add --repo`, `agenthub lanes reset|remove|purge`.
- `client/src/lib/api.ts`, `client/src/lib/types.ts` — preflight + worktree types and calls.
- `client/src/components/lanes/LaneCard.tsx` — kind badge; destructive buttons only for `managed`.
- `docs/LANES.md`, `CLAUDE.md` — the new lifecycle.
@@ -53,7 +53,7 @@
**Interfaces:**
- Consumes: nothing from earlier tasks.
- Produces:
- `LANES_ROOT` — `process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "ccam-lanes")`
- `LANES_ROOT` — `process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "agenthub-lanes")`
- `git(cwd, args): Promise<{stdout, stderr}>` — rejects with `err.git = {args, code, stderr}` on non-zero
- `isGitRepo(dir): Promise<boolean>`
- `resolveBase(sourceRepo, wanted): Promise<string>` — `origin/<wanted>` → `<wanted>` → current HEAD
@@ -87,7 +87,7 @@ const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-wt-"));
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-wt-"));
process.env.LANES_ROOT = path.join(ROOT, "lanes");
const wt = require("../lib/worktree");
@@ -327,7 +327,7 @@ Expected: FAIL — `Cannot find module '../lib/lane-lock'`.
In `server/db.js`, after the `lanes` table and its index, following the probe pattern at `server/db.js:412-418`:
```js
// Managed lanes own a git worktree CCAM created and may be destroyed; adopted
// Managed lanes own a git worktree AgentHub created and may be destroyed; adopted
// lanes merely point at a directory the user already had and never may be.
// Existing rows default to 'adopted', so no lane gains a destructive path by
// upgrading.
@@ -536,18 +536,18 @@ git commit -m "feat(lanes): reset, remove and purge with preflight and stale-sta
## Task 6: CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
- Test: `server/__tests__/lanes-cli.test.js` (append)
**Interfaces:**
- Consumes: the routes from Tasks 3-5, via the existing `get` / `post` helpers (`bin/ccam.js:191-192`).
- Produces: `ccam lanes add --repo <path> [--title <t>] [--base <branch>]` (worktree mode; the existing `--cwd` form still adopts); `ccam lanes reset|remove|purge <id> [--force]`, each printing the preflight table and refusing without `--yes`.
- Consumes: the routes from Tasks 3-5, via the existing `get` / `post` helpers (`bin/agenthub.js:191-192`).
- Produces: `agenthub lanes add --repo <path> [--title <t>] [--base <branch>]` (worktree mode; the existing `--cwd` form still adopts); `agenthub lanes reset|remove|purge <id> [--force]`, each printing the preflight table and refusing without `--yes`.
- [ ] **Step 1: Write the failing tests** (append) — worktree add via CLI lands a managed lane; `reset` without `--yes` exits non-zero and changes nothing; `--yes` performs it.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement**, reusing the async `cli()` harness and the existing flag reader. Print the preflight counts as a small aligned table before asking for `--yes`, so the terminal path has the same "confirm against numbers" property as the UI.
- [ ] **Step 4: Run to verify they pass.**
- [ ] **Step 5: Commit** — `feat(lanes): ccam lanes add --repo, reset, remove, purge`
- [ ] **Step 5: Commit** — `feat(lanes): agenthub lanes add --repo, reset, remove, purge`
---
@@ -267,7 +267,7 @@ never posts a stage.
## Out of scope
- Tickets, preview-port links, per-lane credentials, and the `agents`/`creds` buttons from the reference screen — CCAM has no data behind any of them.
- Tickets, preview-port links, per-lane credentials, and the `agents`/`creds` buttons from the reference screen — AgentHub has no data behind any of them.
- Any change to `PipelineMap`'s node-state logic, the destroy guard, or the preflight contract.
- Inferring `done` or a gate outcome. Still forbidden, TTL or not.
- Re-styling any page other than `/run`.
@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ccam lanes profile init <repo>` detects a Node.js project (single-service or backend+frontend monorepo) and scaffolds a working `.ccam/profile/`; `ccam lanes profile check [<path>]` validates one before it's trusted.
**Goal:** `agenthub lanes profile init <repo>` detects a Node.js project (single-service or backend+frontend monorepo) and scaffolds a working `.agenthub/profile/`; `agenthub lanes profile check [<path>]` validates one before it's trusted.
**Architecture:** One new library module (`server/lib/lane-detect.js`) with three pure functions — `detectNode` (read-only filesystem inspection), `scaffoldProfile` (writes `.ccam/profile/**`), `checkProfile` (read-only validator) — plus two new CLI subcommands in `bin/ccam.js`. No new HTTP route, no database, no lane row required.
**Architecture:** One new library module (`server/lib/lane-detect.js`) with three pure functions — `detectNode` (read-only filesystem inspection), `scaffoldProfile` (writes `.agenthub/profile/**`), `checkProfile` (read-only validator) — plus two new CLI subcommands in `bin/agenthub.js`. No new HTTP route, no database, no lane row required.
**Tech Stack:** Node.js, `js-yaml` (already a project dependency, used here for the first time to parse `docker-compose.yml`), `node:test` + `node:assert/strict` (matches every other `server/__tests__/*.test.js` in this repo).
@@ -16,7 +16,7 @@
- **Shell-identifier validator:** any detected name (npm script name, docker-compose service name) written into generated **shell-script text** (not a `profile.env` `KEY=VALUE` line — those are already safe, parsed-never-sourced) must match `/^[\w.:-]+$/` first. A name that fails is treated as "not found" for that line.
- **TODO hook bodies always `exit 0`.** A scaffold with an unresolved TODO must still be bootable; `checkProfile` is the hard gate, not the hook itself.
- Node.js single preset only. No Python/Go/Ruby, no ORM/migration-tool detection, no pnpm/yarn/turborepo workspace layouts, no HTTP route, no `--fix` mode. If you find yourself building any of those, stop — it's out of scope, see the spec's Non-goals.
- Run `npm run test:server` before considering any task done — this repo's whole `server/__tests__/*.test.js` suite must stay green (there are 3 pre-existing, unrelated `ccam-cli.test.js` color-detection failures in this sandbox — confirmed via `git stash` — don't chase those).
- Run `npm run test:server` before considering any task done — this repo's whole `server/__tests__/*.test.js` suite must stay green (there are 3 pre-existing, unrelated `agenthub-cli.test.js` color-detection failures in this sandbox — confirmed via `git stash` — don't chase those).
---
@@ -63,13 +63,13 @@ const fs = require("node:fs");
const { describe, it, after } = require("node:test");
const assert = require("node:assert/strict");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-detect-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-detect-"));
// Must be set before the first require of anything that transitively loads
// server/lib/secrets.js (lane-detect.js does, for checkProfile) — SECRETS_PATH
// is bound once at that module's first load, and without this a test run on a
// machine with a real ~/.ccam/secrets.env would read that file instead of a
// machine with a real ~/.agenthub/secrets.env would read that file instead of a
// deterministic, isolated one.
process.env.CCAM_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
process.env.AGENTHUB_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
@@ -172,7 +172,7 @@ Create `server/lib/lane-detect.js`:
```js
/**
* @file Node.js project detection and `.ccam/profile/` scaffolding (A3).
* @file Node.js project detection and `.agenthub/profile/` scaffolding (A3).
* `detectNode` reads a repository's filesystem (package.json layout,
* docker-compose.yml) without executing anything; `scaffoldProfile` turns
* those facts into a working profile; `checkProfile` validates one. Only a
@@ -504,7 +504,7 @@ git commit -m "feat(lanes): detect docker-compose database/Redis and wire ENV_RE
---
### Task 3: `scaffoldProfile` — write `.ccam/profile/`
### Task 3: `scaffoldProfile` — write `.agenthub/profile/`
**Files:**
- Modify: `server/lib/lane-detect.js`
@@ -513,7 +513,7 @@ git commit -m "feat(lanes): detect docker-compose database/Redis and wire ENV_RE
**Interfaces:**
- Consumes: `detectNode`'s facts object (Task 1+2 shape) and `IDENTIFIER_RE`.
- Produces: `scaffoldProfile(repoPath, facts, options = {})` → `{ written: string[], todos: string[] }`. `options.force` (boolean) allows overwriting an existing `.ccam/profile/profile.env`. Throws `Object.assign(new Error(...), { code: "EPROFILEEXISTS" })` when one exists and `force` is not `true`.
- Produces: `scaffoldProfile(repoPath, facts, options = {})` → `{ written: string[], todos: string[] }`. `options.force` (boolean) allows overwriting an existing `.agenthub/profile/profile.env`. Throws `Object.assign(new Error(...), { code: "EPROFILEEXISTS" })` when one exists and `force` is not `true`.
- [ ] **Step 1: Write the failing test**
@@ -528,7 +528,7 @@ describe("scaffoldProfile", () => {
const result = detect.scaffoldProfile(repo, facts);
const profileDir = path.join(repo, ".ccam", "profile");
const profileDir = path.join(repo, ".agenthub", "profile");
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
assert.match(env, /PORTS="app"/);
assert.match(env, /PORT_BASE_app=3000/);
@@ -557,7 +557,7 @@ describe("scaffoldProfile", () => {
const result = detect.scaffoldProfile(repo, facts);
const profileDir = path.join(repo, ".ccam", "profile");
const profileDir = path.join(repo, ".agenthub", "profile");
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
assert.match(env, /DB_PREFIX=/);
assert.match(env, /REDIS=1/);
@@ -583,7 +583,7 @@ describe("scaffoldProfile", () => {
writePkg(repo, { test: "jest" }); // no start, no dev
const facts = { layout: "single-service", services: { app: { dir: ".", script: null } }, database: null, redis: false, env: null };
const result = detect.scaffoldProfile(repo, facts);
const boot = fs.readFileSync(path.join(repo, ".ccam", "profile", "hooks", "boot.sh"), "utf8");
const boot = fs.readFileSync(path.join(repo, ".agenthub", "profile", "hooks", "boot.sh"), "utf8");
assert.match(boot, /# TODO: no start\/dev script found/);
assert.equal(result.todos.length, 1);
});
@@ -652,7 +652,7 @@ function bootLine(name, service) {
}
/**
* Scaffold `.ccam/profile/` under `repoPath` from `detectNode`'s facts.
* Scaffold `.agenthub/profile/` under `repoPath` from `detectNode`'s facts.
* Refuses to overwrite an existing `profile.env` unless `options.force`.
*
* @param {string} repoPath - Absolute repository path.
@@ -661,7 +661,7 @@ function bootLine(name, service) {
* @returns {{written: string[], todos: string[]}}
*/
function scaffoldProfile(repoPath, facts, options = {}) {
const profileDir = path.join(repoPath, ".ccam", "profile");
const profileDir = path.join(repoPath, ".agenthub", "profile");
const envPath = path.join(profileDir, "profile.env");
if (fs.existsSync(envPath) && !options.force) {
throw Object.assign(new Error(`profile already exists at ${envPath}`), {
@@ -768,7 +768,7 @@ Expected: pass (no new applicable files created this task — `db-create.sh`/`db
```bash
git add server/lib/lane-detect.js server/__tests__/lane-detect.test.js
git commit -m "feat(lanes): scaffold .ccam/profile/ from detected Node facts (A3)"
git commit -m "feat(lanes): scaffold .agenthub/profile/ from detected Node facts (A3)"
```
---
@@ -781,7 +781,7 @@ git commit -m "feat(lanes): scaffold .ccam/profile/ from detected Node facts (A3
**Interfaces:**
- Consumes: `server/lib/lane-profile.js:parseEnvFile` (already exported), `server/lib/ports.js:isListening` (already exported, async), `server/lib/secrets.js:SECRETS_PATH` (already exported).
- Produces: `async checkProfile(dir)` → `{ ok: boolean, errors: string[], warnings: string[] }`. `dir` is a directory that may contain `.ccam/profile/` directly, OR may already point at a `.ccam/profile/` directory itself — check `.ccam/profile/profile.env` first, then `profile.env` directly, so `checkProfile(repoPath)` and `checkProfile(profileDir)` both work.
- Produces: `async checkProfile(dir)` → `{ ok: boolean, errors: string[], warnings: string[] }`. `dir` is a directory that may contain `.agenthub/profile/` directly, OR may already point at a `.agenthub/profile/` directory itself — check `.agenthub/profile/profile.env` first, then `profile.env` directly, so `checkProfile(repoPath)` and `checkProfile(profileDir)` both work.
- [ ] **Step 1: Write the failing test**
@@ -832,7 +832,7 @@ describe("checkProfile", () => {
const repo = makeRepo();
writePkg(repo, { start: "node index.js" });
detect.scaffoldProfile(repo, detect.detectNode(repo));
fs.rmSync(path.join(repo, ".ccam", "profile", "hooks", "health.sh"));
fs.rmSync(path.join(repo, ".agenthub", "profile", "hooks", "health.sh"));
const result = await detect.checkProfile(repo);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /health/.test(e)));
@@ -844,7 +844,7 @@ describe("checkProfile", () => {
writeCompose(repo, " postgres:\n image: postgres:16\n");
detect.scaffoldProfile(repo, detect.detectNode(repo));
// Remove the migrate/seed TODO lines so only the secrets warning is left to check.
const hooksDir = path.join(repo, ".ccam", "profile", "hooks");
const hooksDir = path.join(repo, ".agenthub", "profile", "hooks");
fs.writeFileSync(path.join(hooksDir, "migrate.sh"), "#!/usr/bin/env bash\nexit 0\n");
fs.writeFileSync(path.join(hooksDir, "seed.sh"), "#!/usr/bin/env bash\nexit 0\n");
const result = await detect.checkProfile(repo);
@@ -883,7 +883,7 @@ function containsTodo(text) {
/**
* Validate a scaffolded (or hand-written) profile. Read-only: never mutates
* anything, never spawns a hook. `dir` may be a repo root (profile at
* `.ccam/profile/`) or a `.ccam/profile/` directory itself.
* `.agenthub/profile/`) or a `.agenthub/profile/` directory itself.
*
* @param {string} dir
* @returns {Promise<{ok: boolean, errors: string[], warnings: string[]}>}
@@ -892,7 +892,7 @@ async function checkProfile(dir) {
const errors = [];
const warnings = [];
const candidates = [path.join(dir, ".ccam", "profile"), dir];
const candidates = [path.join(dir, ".agenthub", "profile"), dir];
const profileDir = candidates.find((p) => fs.existsSync(path.join(p, "profile.env")));
if (!profileDir) {
return { ok: false, errors: [`no profile.env found under ${dir}`], warnings: [] };
@@ -962,10 +962,10 @@ git commit -m "feat(lanes): validate a scaffolded profile with checkProfile (A3)
---
### Task 5: CLI — `ccam lanes profile init` / `ccam lanes profile check`
### Task 5: CLI — `agenthub lanes profile init` / `agenthub lanes profile check`
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
- Test: manual (this repo's CLI tests spawn the real binary against a live/offline server; profile init/check are pure-filesystem and don't need the server — verified by hand per this task's steps, folded into Task 6's fixtures for automated coverage)
**Interfaces:**
@@ -973,19 +973,19 @@ git commit -m "feat(lanes): validate a scaffolded profile with checkProfile (A3)
- [ ] **Step 1: Add the command implementations**
In `bin/ccam.js`, near `cmdLanesAdd` (search for that function), add:
In `bin/agenthub.js`, near `cmdLanesAdd` (search for that function), add:
```js
/**
* `ccam lanes profile init <repo>` — detect a Node.js project and scaffold
* `.ccam/profile/`. Pure filesystem action against the SOURCE repo; does not
* `agenthub lanes profile init <repo>` — detect a Node.js project and scaffold
* `.agenthub/profile/`. Pure filesystem action against the SOURCE repo; does not
* talk to the dashboard server at all.
*/
function cmdLanesProfileInit(args) {
const repo = args.find((arg) => !arg.startsWith("--"));
const force = args.includes("--force");
if (!repo) {
console.error("usage: ccam lanes profile init <repo> [--force]");
console.error("usage: agenthub lanes profile init <repo> [--force]");
process.exitCode = 1;
return;
}
@@ -1003,7 +1003,7 @@ function cmdLanesProfileInit(args) {
`✖ No detectable Node.js project at ${resolved} (looked for backend/package.json +\n` +
" frontend/package.json, or a root package.json).\n" +
" Auto-scaffolding currently supports Node.js repos in that layout only.\n" +
" Write .ccam/profile/ by hand — see docs/LANES.md."
" Write .agenthub/profile/ by hand — see docs/LANES.md."
);
process.exitCode = 1;
return;
@@ -1021,17 +1021,17 @@ function cmdLanesProfileInit(args) {
throw err;
}
console.log(`${c.green("✔")} Scaffolded .ccam/profile/ at ${resolved} (${facts.layout})`);
console.log(`${c.green("✔")} Scaffolded .agenthub/profile/ at ${resolved} (${facts.layout})`);
for (const file of result.written) console.log(` wrote ${file}`);
if (result.todos.length) {
console.log(`\n${c.yellow(`⚠ ${result.todos.length} item(s) need manual attention:`)}`);
for (const todo of result.todos) console.log(` ${todo}`);
}
console.log(`\nNext: ccam lanes profile check ${repo}`);
console.log(`\nNext: agenthub lanes profile check ${repo}`);
}
/**
* `ccam lanes profile check [<path>]` — validate a profile without needing a
* `agenthub lanes profile check [<path>]` — validate a profile without needing a
* lane to exist for it yet. Defaults to the current directory, NOT lane-id
* resolution (unlike every other `lanes` subcommand) — this is meant to run
* against a bare repo right after `profile init`.
@@ -1055,7 +1055,7 @@ async function cmdLanesProfileCheck(args) {
- [ ] **Step 2: Wire the subcommand dispatch**
In `bin/ccam.js`, the dispatcher (`const [cmd, ...rest] = argv;` around line 2695) has this exact `case "lanes":` block around line 2755 — find it with `grep -n 'case "lanes"' bin/ccam.js`:
In `bin/agenthub.js`, the dispatcher (`const [cmd, ...rest] = argv;` around line 2695) has this exact `case "lanes":` block around line 2755 — find it with `grep -n 'case "lanes"' bin/agenthub.js`:
```js
case "lanes":
@@ -1082,7 +1082,7 @@ Replace it with (adds one `if` block for `profile`, everything else byte-identic
if (rest[1] === "init") return cmdLanesProfileInit(rest.slice(2));
if (rest[1] === "check") return cmdLanesProfileCheck(rest.slice(2));
console.error(
"usage: ccam lanes profile init <repo> [--force] | ccam lanes profile check [<path>]"
"usage: agenthub lanes profile init <repo> [--force] | agenthub lanes profile check [<path>]"
);
process.exitCode = 1;
return;
@@ -1098,13 +1098,13 @@ Replace it with (adds one `if` block for `profile`, everything else byte-identic
- [ ] **Step 3: Add the help-table entries**
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes runtime"`), add two rows right after the `lanes add --repo` row:
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes runtime"`), add two rows right after the `lanes add --repo` row:
```js
[
"lanes profile init",
"<repo> [--force]",
"Detect a Node.js project and scaffold .ccam/profile/",
"Detect a Node.js project and scaffold .agenthub/profile/",
],
[
"lanes profile check",
@@ -1116,30 +1116,30 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes
- [ ] **Step 4: Manual smoke test**
```bash
mkdir -p /tmp/ccam-smoke/backend /tmp/ccam-smoke/frontend
echo '{"scripts":{"start":"node -e \"require(\\\"http\\\").createServer((_,r)=>r.end(\\\"ok\\\")).listen(process.env.PORT||8000)\"}}' > /tmp/ccam-smoke/backend/package.json
echo '{"scripts":{"dev":"true"}}' > /tmp/ccam-smoke/frontend/package.json
node bin/ccam.js lanes profile init /tmp/ccam-smoke
node bin/ccam.js lanes profile check /tmp/ccam-smoke
mkdir -p /tmp/agenthub-smoke/backend /tmp/agenthub-smoke/frontend
echo '{"scripts":{"start":"node -e \"require(\\\"http\\\").createServer((_,r)=>r.end(\\\"ok\\\")).listen(process.env.PORT||8000)\"}}' > /tmp/agenthub-smoke/backend/package.json
echo '{"scripts":{"dev":"true"}}' > /tmp/agenthub-smoke/frontend/package.json
node bin/agenthub.js lanes profile init /tmp/agenthub-smoke
node bin/agenthub.js lanes profile check /tmp/agenthub-smoke
```
Expected: `init` prints `✔ Scaffolded ...`, no TODOs listed (both packages have runnable scripts, no database detected); `check` prints `✔ profile at ... looks good`, exit 0.
```bash
node bin/ccam.js lanes profile init /tmp/ccam-smoke
node bin/agenthub.js lanes profile init /tmp/agenthub-smoke
```
Expected (run again without `--force`): `✖ profile already exists ... — pass --force to overwrite it.`, exit 1.
```bash
rm -rf /tmp/ccam-smoke
rm -rf /tmp/agenthub-smoke
```
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes profile init/check CLI (A3)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes profile init/check CLI (A3)"
```
---
@@ -1222,7 +1222,7 @@ Expected: PASS (29 tests total)
- [ ] **Step 4: Run the full server suite**
Run: `npm run test:server`
Expected: green, aside from the 3 pre-existing unrelated `ccam-cli.test.js` failures noted in Global Constraints.
Expected: green, aside from the 3 pre-existing unrelated `agenthub-cli.test.js` failures noted in Global Constraints.
- [ ] **Step 5: Commit**
@@ -1251,24 +1251,24 @@ Find the `### Declaring a profile` section (search for that heading). Immediatel
### Scaffolding a profile automatically
```bash
ccam lanes profile init <repo> [--force] # detect + write .ccam/profile/
ccam lanes profile check [<path>] # validate one (path defaults to cwd)
agenthub lanes profile init <repo> [--force] # detect + write .agenthub/profile/
agenthub lanes profile check [<path>] # validate one (path defaults to cwd)
```
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.ccam/profile/` by hand for those, following the reference above.
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.agenthub/profile/` by hand for those, following the reference above.
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.ccam/secrets.env` doesn't exist yet for a database-declaring profile.
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.agenthub/secrets.env` doesn't exist yet for a database-declaring profile.
`profile check` takes a **path**, not a lane id or `--lane`/`--cwd` flag like every other `lanes` subcommand — it is meant to run against a bare repository right after `init`, before any lane or worktree exists for it.
```
- [ ] **Step 2: `docs/CLI.md`**
Find the `### Lanes` table (search for `ccam lanes reset`). Add two rows right after the `ccam lanes add --repo ...` row:
Find the `### Lanes` table (search for `agenthub lanes reset`). Add two rows right after the `agenthub lanes add --repo ...` row:
```markdown
| `ccam lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.ccam/profile/`. Refuses to overwrite an existing one without `--force` |
| `ccam lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
| `agenthub lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.agenthub/profile/`. Refuses to overwrite an existing one without `--force` |
| `agenthub lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
```
- [ ] **Step 3: `ARCHITECTURE.md`**
@@ -1276,7 +1276,7 @@ Find the `### Lanes` table (search for `ccam lanes reset`). Add two rows right a
Find the `lib/lane-slots.js` row in the module responsibility table (search for that exact string) and add a new row directly after the `lib/lane-runtime.js` row:
```markdown
| `lib/lane-detect.js` | (A3) Node.js project detection and `.ccam/profile/` scaffolding. `detectNode` reads `package.json` layout (root, or `backend/`+`frontend/`) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`) — read-only, never executes anything. Any detected name written into GENERATED SHELL TEXT (an npm script name, a compose service name) must pass a strict identifier check first; a `profile.env` value is already safe regardless, since that file is parsed, never sourced. `scaffoldProfile` writes the profile, always leaving `migrate`/`seed` as an `exit 0` TODO stub when a database is found rather than guessing a migration tool. `checkProfile` is the read-only hard gate: unresolved `TODO:`, a missing/non-executable hook, or a port already in use all fail it; a missing `~/.ccam/secrets.env` only warns |
| `lib/lane-detect.js` | (A3) Node.js project detection and `.agenthub/profile/` scaffolding. `detectNode` reads `package.json` layout (root, or `backend/`+`frontend/`) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`) — read-only, never executes anything. Any detected name written into GENERATED SHELL TEXT (an npm script name, a compose service name) must pass a strict identifier check first; a `profile.env` value is already safe regardless, since that file is parsed, never sourced. `scaffoldProfile` writes the profile, always leaving `migrate`/`seed` as an `exit 0` TODO stub when a database is found rather than guessing a migration tool. `checkProfile` is the read-only hard gate: unresolved `TODO:`, a missing/non-executable hook, or a port already in use all fail it; a missing `~/.agenthub/secrets.env` only warns |
```
- [ ] **Step 4: Mark A3 done in the parent plan**
@@ -1290,11 +1290,11 @@ bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
```
Expected: header check passes; full suite green apart from the 3 known-unrelated `ccam-cli.test.js` failures.
Expected: header check passes; full suite green apart from the 3 known-unrelated `agenthub-cli.test.js` failures.
- [ ] **Step 6: Commit**
```bash
git add docs/LANES.md docs/CLI.md ARCHITECTURE.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ccam lanes profile init/check (A3)"
git commit -m "docs(lanes): document agenthub lanes profile init/check (A3)"
```
@@ -1,10 +1,10 @@
# Shipyard parity for CCAM lanes — full plan
# Shipyard parity for AgentHub lanes — full plan
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`
(the "Parallel Feature Harness"). Every capability below traces to a real file
there, so each port can be checked against the original rather than a memory of it.
**Goal:** bring the whole harness into CCAM — isolated per-lane runtime and data,
**Goal:** bring the whole harness into AgentHub — isolated per-lane runtime and data,
per-feature state with archive, proof gallery, cross-lane locks, the
`ship-feature` pipeline with its QC agents, and the optional integrations.
@@ -45,11 +45,11 @@ spec → plan → implement cycle; this document is the map, not a substitute.
Carried from `CLAUDE.md` and proven in A1. Re-check each at review time.
1. **CCAM does not orchestrate.** It offers primitives; the session sequences them.
1. **AgentHub does not orchestrate.** It offers primitives; the session sequences them.
No chaining, no queue, no retry, no gate evaluation.
2. **The runtime never writes `stage`, `status` or `notes`** — only allocation
facts. `status=running` means an agent is working, not that a server listens.
3. **Liveness is computed, never stored.** Anything CCAM does not control
3. **Liveness is computed, never stored.** Anything AgentHub does not control
(a process, a port, a deploy) is re-derived on read.
4. **Never build a shell command as a string.** `execFile`/`spawn` with argv
arrays. Hooks are the deliberate exception and are spawned with a fixed argv.
@@ -74,22 +74,22 @@ sharing state. This is the half of Shipyard's isolation A1 did not cover.
| Question | Proposed answer | Why |
|---|---|---|
| Who creates the database? | A `db-create.sh` / `db-drop.sh` **hook** (allowlist already exists in A1) | CCAM must not assume Postgres. `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ; the template ships Shipyard's docker-compose Postgres commands so a Postgres user gets parity by copying. |
| Who rewrites `.env`? | **CCAM**, not a hook | Mechanical and identical across stacks: parse `KEY=VALUE`, replace a declared set, keep the rest. Pushing it to every repo would duplicate ~30 lines and re-lose the `JWT_SECRET` lesson below in each. |
| Where do database credentials live? | `~/.ccam/secrets.env`, mode `0600` | Machine-level, not repo-level (Shipyard's `config/secrets.env`). **Never** returned by any route and never logged; `GET /runtime` may report key *names* present, never values. |
| Who creates the database? | A `db-create.sh` / `db-drop.sh` **hook** (allowlist already exists in A1) | AgentHub must not assume Postgres. `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ; the template ships Shipyard's docker-compose Postgres commands so a Postgres user gets parity by copying. |
| Who rewrites `.env`? | **AgentHub**, not a hook | Mechanical and identical across stacks: parse `KEY=VALUE`, replace a declared set, keep the rest. Pushing it to every repo would duplicate ~30 lines and re-lose the `JWT_SECRET` lesson below in each. |
| Where do database credentials live? | `~/.agenthub/secrets.env`, mode `0600` | Machine-level, not repo-level (Shipyard's `config/secrets.env`). **Never** returned by any route and never logged; `GET /runtime` may report key *names* present, never values. |
| Does a lane without a database break? | No | `DB_PREFIX` empty (the default) means "no per-lane database": no name allocated, no hook called, no dead code. Same for `REDIS=0`. |
### Profile additions
```bash
# .ccam/profile/profile.env (all optional; empty = feature off)
# .agenthub/profile/profile.env (all optional; empty = feature off)
DB_PREFIX="myapp_l" # lane in slot 3 -> myapp_l3 ; empty = no per-lane DB
DB_KIND="postgres" # informational for A3's scaffolder; A2 gates on DB_PREFIX
DB_URL_SCHEME="postgresql" # DATABASE_URL scheme
REDIS=1 # 1 = allocate logical index = slot
ENV_FILES="backend/.env" # files to seed, relative to the lane
ENV_SOURCE="backend/.env" # source path in the source repo (defaults to ENV_FILES)
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys CCAM overwrites per lane
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys AgentHub overwrites per lane
ENV_PRESERVE="JWT_SECRET" # keys kept from the lane's EXISTING file on a --force refresh
UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
```
@@ -99,7 +99,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
### Steps
1. **`server/lib/secrets.js`** — read `~/.ccam/secrets.env` with the A1 parser
1. **`server/lib/secrets.js`** — read `~/.agenthub/secrets.env` with the A1 parser
(`lane-profile.js:parseEnvFile`, reuse it). Warn once and continue when the
file is absent; refuse to load it when its mode is group/world-readable.
2. **`server/lib/lane-slots.js`** — extend `slotFacts`: `dbName`,
@@ -119,7 +119,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
calling the `db-create` / `db-drop` hooks.
- `dropDatabase` runs `assertManaged` **and** asserts
`DB_NAME === slotFacts(lane.slot).dbName` before spawning anything. Only a
name CCAM derived can ever be dropped; nothing from a request reaches it.
name AgentHub derived can ever be dropped; nothing from a request reaches it.
5. **Wire into the lifecycle** (`lane-runtime.js`, `routes/lanes.js`):
- *provision* → allocate slot → `seedEnv` → `bootstrap` → `db-create` →
`migrate` → `seed`
@@ -133,7 +133,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
`reset`/`remove` will drop, and the confirm dialog echoes it back like the
existing counted facts. Dropping a database is the most destructive thing this
whole roadmap adds; it gets the same echo-or-refuse contract as the rest.
7. **CLI** — `ccam lanes reset --keep-db`; `ccam lanes runtime` gains the DB name
7. **CLI** — `agenthub lanes reset --keep-db`; `agenthub lanes runtime` gains the DB name
and Redis index rows.
8. **Profile template** — `server/data/profile-templates/postgres-compose/` with
`db-create.sh` / `db-drop.sh` holding Shipyard's exact
@@ -148,7 +148,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
back to `.env.example` and warns.
- `dropDatabase` with a tampered `DB_NAME` throws **before** spawning.
- `seedEnv` and `dropDatabase` both throw `ENOTMANAGED` on an adopted lane.
- Secrets: a `0644` `~/.ccam/secrets.env` is refused; `GET /runtime` response
- Secrets: a `0644` `~/.agenthub/secrets.env` is refused; `GET /runtime` response
contains no secret value (assert on the serialized JSON).
- Two lanes up at once, each writing its own database, neither seeing the other's
rows — the actual point of the phase.
@@ -165,7 +165,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
## A3 — Detection and scaffolding
**Goal:** `ccam lanes profile init <repo>` writes a working `.ccam/profile/`, so
**Goal:** `agenthub lanes profile init <repo>` writes a working `.agenthub/profile/`, so
adopting a repo is one command instead of nine hand-written hooks. This is the
gap that left Shipyard's own `profiles/` empty.
@@ -179,18 +179,18 @@ reading them.
`requirements.txt` (`django`/`fastapi`), `manage.py`, `alembic.ini`,
`prisma/schema.prisma`, `go.mod`, `Gemfile`.
- **Scaffold, don't interpret.** Detection writes a concrete, readable
`.ccam/profile/` the user owns and edits. The runtime never re-detects at boot;
a wrong guess is fixed by editing a file, not by changing CCAM.
`.agenthub/profile/` the user owns and edits. The runtime never re-detects at boot;
a wrong guess is fixed by editing a file, not by changing AgentHub.
- Unknown values are written as explicit `TODO:` markers rather than plausible
defaults — a wrong default that boots something is worse than a refusal.
- **`ccam lanes profile check`** — the `harness-doctor` equivalent: profile
- **`agenthub lanes profile check`** — the `harness-doctor` equivalent: profile
parses, declared hooks exist and are executable, no `TODO:` left, declared
ports are free, `secrets.env` has what the declared database needs. Must exit
non-zero on any of these.
- Templates in `server/data/profile-templates/<preset>/`.
**Verify:** a fixture repo per preset scaffolds, `profile check` passes, and
`ccam lanes up` boots it — the same end-to-end proof A1 used, once per preset.
`agenthub lanes up` boots it — the same end-to-end proof A1 used, once per preset.
---
@@ -216,7 +216,7 @@ keeps every past feature browsable.
slug back** so callers store what the server stored. Reuse
`worktree.js:slugify` only if its output is identical; otherwise a separate
function with its own test. Do not let the two drift.
- Routes `GET/POST /api/lanes/:id/features…`; CLI `ccam feature list|activate|show`.
- Routes `GET/POST /api/lanes/:id/features…`; CLI `agenthub feature list|activate|show`.
- UI: a feature picker that swaps the detail panel to an archived snapshot while
the lane keeps running.
@@ -266,7 +266,7 @@ work *within one lane, within one process*. This is the other axis and must be a
- **Port the etiquette text into `docs/LANES.md`.** Waiting is normal; never kill
a holder, never delete the lock directory, never shrink `LOCK_MAX_HOLD`. Agents
read docs, and this is the rule they break.
- `GET /api/locks`, `ccam lock status|acquire|release`, and a lock indicator on
- `GET /api/locks`, `agenthub lock status|acquire|release`, and a lock indicator on
the lane card so a human can see *why* a lane is sitting still.
**Verify:** second acquire blocks then succeeds after release; a holder backdated
@@ -281,32 +281,32 @@ with `LOCK_MAX_HOLD=1`; release from a non-holder is refused and leaves the lock
**Progress:** pipeline template + skill text (E1) done 2026-08-04 — see `docs/superpowers/specs/2026-08-04-ship-feature-skill-design.md`. `sync-base` (E2) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-sync-base-design.md`. `qc-local` + `senior-gate-reviewer` agents (E3) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-agents-port-design.md`. `ticketer`/`dev-qc`/`pr-reviewer` and F's integrations remain.
**This is where "CCAM does not orchestrate" is preserved by construction:** the
skill runs in the session and calls `ccam` commands; the dashboard still only
**This is where "AgentHub does not orchestrate" is preserved by construction:** the
skill runs in the session and calls `agenthub` commands; the dashboard still only
records.
- **Pipeline template** `server/data/pipelines/ship-feature.json` — the stages
with aliases and `detect` rules in the existing node format. `default.json`
untouched; a lane opts in via `pipeline`.
- **Skill** `.claude/skills/ship-feature-lane/SKILL.md` — every `"$HARNESS/bin/…"`
call rewritten to its `ccam` equivalent (`state.sh set` → `ccam stage`;
`activate` → `ccam feature activate`; `lane-ci-gate.sh` → `ccam lanes hook
ci-gate`; `lane-up.sh --qc` → `ccam lanes up --qc`; the `.harness-lane` marker
disappears because CCAM already resolves a lane from `cwd`).
call rewritten to its `agenthub` equivalent (`state.sh set` → `agenthub stage`;
`activate` → `agenthub feature activate`; `lane-ci-gate.sh` → `agenthub lanes hook
ci-gate`; `lane-up.sh --qc` → `agenthub lanes up --qc`; the `.harness-lane` marker
disappears because AgentHub already resolves a lane from `cwd`).
- **Keep, do not trim, the hard-won rules.** Each encodes a real failure: the
turn-liveness rule (every turn between stage 1 and done leaves a re-invoker
pending), backgrounded-`sleep` polling, the e2e active-poll (a hung suite never
fires its completion re-invoke), no-retry-cap/phase-clock, one-driver-per-MCP,
never-push-base, and the test-only / localized re-entry fast paths.
- **Drop what CCAM makes unnecessary:** the `@@HARNESS_ROOT@@` install-time
- **Drop what AgentHub makes unnecessary:** the `@@HARNESS_ROOT@@` install-time
placeholder and the zsh `source _common.sh` warning. Keep an
integration-mismatch check even though its original cause is gone — silently
skipping ticket/dev-QC while reporting success is the failure that matters.
- **Agents** → `.claude/agents/`: `qc-local`, `dev-qc`, `senior-gate-reviewer`,
`ticketer`, `pr-reviewer`. Shipyard generates these per lane with embedded
credentials; port that as `ccam lanes agents install`, writing to
credentials; port that as `agenthub lanes agents install`, writing to
`<lane>/.claude/agents/`, git-excluded, creds at mode `0600`.
- **`ccam lanes sync-base`** — port `lane-sync-dev.sh`. The valuable part is the
- **`agenthub lanes sync-base`** — port `lane-sync-dev.sh`. The valuable part is the
**migration-number collision pre-check (exit 5)**: another lane's migration
landed on the base with the number yours uses. Generic form driven by
`MIGRATIONS_DIR` + a filename-number pattern. Also port the keep-ours merge
@@ -314,7 +314,7 @@ records.
the single most common cross-lane conflict.
**Verify:** the skill is prose, so verification is a real dry run on a scratch
repo walking stages 0→8, confirming every `ccam` command it names exists and
repo walking stages 0→8, confirming every `agenthub` command it names exists and
behaves as documented. Each of those commands gets a CLI test.
`sync-base --check` gets a unit test with a fixture collision.
@@ -323,10 +323,10 @@ behaves as documented. Each of those commands gets a CLI test.
## F — Integrations
**Goal:** tracker, dev-site QC, CI deploy-wait. All off by default, read from
`<repo>/.ccam/profile/integrations.env`, exposed as `ccam lanes integration <name>`
`<repo>/.agenthub/profile/integrations.env`, exposed as `agenthub lanes integration <name>`
(exit 0/1) and in `GET /runtime`.
**Progress:** `mcp sync` (F1) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-mcp-sync-design.md`. `integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`ccam ci`), and dev-QC remain.
**Progress:** `mcp sync` (F1) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-mcp-sync-design.md`. `integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`agenthub ci`), and dev-QC remain.
- **Tracker** — file one ticket per feature, idempotent (update, never
duplicate), writing `proof/<slug>/ticket/REPORT.html`.
@@ -334,8 +334,8 @@ behaves as documented. Each of those commands gets a CLI test.
per-lane account; owns the `qc_dev` field; checkpoints to
`RESULTS.partial.md` so a died agent resumes instead of restarting.
- **CI deploy-wait** — port `ci-job.sh` (`status`/`failures`/`rerun`/`cancel`) as
`ccam ci`, which is what makes the skill's flake-triage rule executable.
- **`ccam lanes mcp sync`** — port `lane-mcp-sync.sh`: writes the lane's
`agenthub ci`, which is what makes the skill's flake-triage rule executable.
- **`agenthub lanes mcp sync`** — port `lane-mcp-sync.sh`: writes the lane's
`.mcp.json` with Playwright servers pinned to the lane's own `--output-dir`.
This is what stops two lanes' browser QC from sharing a profile directory.
@@ -4,7 +4,7 @@
**Goal:** serialize machine-thrashing steps (builds, e2e runs) across ALL lanes with a named lock — independent of `server/lib/lane-lock.js`'s per-lane serialization, which is a different axis (one lane, one process) and must not be touched.
**Architecture:** A new `server/lib/named-lock.js` (mkdir-based, filesystem-atomic, no `flock` dependency) holding lock directories under `LANES_ROOT/.locks/<name>/`; a thin `server/routes/locks.js` exposing single-shot acquire/release/status (the server never blocks — "CCAM does not orchestrate" — the CLI does the waiting loop); `ccam lock status|acquire|release` CLI subcommands; a lane-card UI badge polling the same endpoint `LaneCard.tsx`'s runtime hook already polls.
**Architecture:** A new `server/lib/named-lock.js` (mkdir-based, filesystem-atomic, no `flock` dependency) holding lock directories under `LANES_ROOT/.locks/<name>/`; a thin `server/routes/locks.js` exposing single-shot acquire/release/status (the server never blocks — "AgentHub does not orchestrate" — the CLI does the waiting loop); `agenthub lock status|acquire|release` CLI subcommands; a lane-card UI badge polling the same endpoint `LaneCard.tsx`'s runtime hook already polls.
**Tech Stack:** Node.js `fs` (mkdir/rmdir for atomicity — no new dependency), existing Express/route conventions, existing React hook + WebSocket broadcast conventions.
@@ -14,7 +14,7 @@
- `named-lock.js` is a **separate module** from `server/lib/lane-lock.js`. Do not touch `lane-lock.js`, do not import it, do not conflate the two axes (in-process-per-lane serialization vs cross-lane named locks).
- **Time-based staleness with a floor.** `LOCK_MAX_HOLD` env var, default `2700` (45 min) seconds. The *effective* break threshold is `Math.max(LOCK_MAX_HOLD, 300)` — a holder can never be force-broken by setting `LOCK_MAX_HOLD` below 300 seconds. This floor is the whole point: it stops an impatient env-var edit from breaking a genuinely live holder.
- Atomicity comes from `fs.mkdirSync` throwing `EEXIST` when the directory already exists — never a check-then-create race (`existsSync` then `mkdirSync` is NOT atomic and must not be used for the acquire decision).
- The server-side route layer is a single-shot primitive (try once, answer immediately) — it never loops or blocks waiting for a lock to free up. The CLI's `acquire` command owns the polling loop. This mirrors the project's standing rule: CCAM offers primitives, the caller sequences them.
- The server-side route layer is a single-shot primitive (try once, answer immediately) — it never loops or blocks waiting for a lock to free up. The CLI's `acquire` command owns the polling loop. This mirrors the project's standing rule: AgentHub offers primitives, the caller sequences them.
- Run `npm run test:server` (full suite) and `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit — both must stay green/passing. This repo's pre-commit hook enforces the full suite (backend + frontend) already; a clean run here avoids a blocked commit.
- Never use `git add -A`. Stage exactly the files each task names.
@@ -48,7 +48,7 @@ const fs = require("node:fs");
const { describe, it, after, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-named-lock-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-named-lock-"));
process.env.LANES_ROOT = SUITE_ROOT;
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
@@ -216,7 +216,7 @@ function readOwner(name) {
/**
* Try to acquire a named lock once. Never blocks, never retries — the caller
* (the CLI) owns the polling loop; CCAM's server-side primitives are always
* (the CLI) owns the polling loop; AgentHub's server-side primitives are always
* single-shot.
*
* A stale holder (older than `effectiveMaxHoldSec()`) is broken automatically:
@@ -364,7 +364,7 @@ const path = require("node:path");
const fs = require("node:fs");
const http = require("node:http");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-locks-api-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-locks-api-"));
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
@@ -494,7 +494,7 @@ Create `server/routes/locks.js`:
/**
* @file HTTP surface for cross-lane named locks (`server/lib/named-lock.js`).
* Every route is single-shot — this layer never blocks waiting for a lock to
* free up. The CLI (`ccam lock acquire`) owns the polling loop; CCAM's
* free up. The CLI (`agenthub lock acquire`) owns the polling loop; AgentHub's
* server-side primitives stay non-orchestrating, same as every other lane
* route in this project.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -578,18 +578,18 @@ git commit -m "feat(locks): expose GET/POST /api/locks over named-lock.js (D)"
---
### Task 3: `ccam lock status|acquire|release` CLI
### Task 3: `agenthub lock status|acquire|release` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `GET /api/locks`, `POST /api/locks/:name/acquire`, `POST /api/locks/:name/release` (Task 2, already committed) via the existing `get`/`post` helpers in `bin/ccam.js`.
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`, used by `cmdLanesRuntime`/`cmdStage`) to resolve which lane is acting, when no explicit `--holder` is given.
- Consumes: `GET /api/locks`, `POST /api/locks/:name/acquire`, `POST /api/locks/:name/release` (Task 2, already committed) via the existing `get`/`post` helpers in `bin/agenthub.js`.
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`, used by `cmdLanesRuntime`/`cmdStage`) to resolve which lane is acting, when no explicit `--holder` is given.
- [ ] **Step 1: Add the command implementations**
In `bin/ccam.js`, near `cmdLanesRuntime` (search for that function), add:
In `bin/agenthub.js`, near `cmdLanesRuntime` (search for that function), add:
```js
/**
@@ -622,7 +622,7 @@ function fmtLockRow(lock) {
return `${lock.name.padEnd(20)} held by ${lock.holder.padEnd(10)} for ${mins}m`;
}
/** `ccam lock status [<name>]` — one lock, or every held lock. */
/** `agenthub lock status [<name>]` — one lock, or every held lock. */
async function cmdLockStatus(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (name) {
@@ -640,7 +640,7 @@ async function cmdLockStatus(args) {
}
/**
* `ccam lock acquire <name> [--holder X] [--timeout N]` — polls until the
* `agenthub lock acquire <name> [--holder X] [--timeout N]` — polls until the
* lock is free (or `--timeout` seconds elapse). Prints a status line every
* ~60s of continued waiting so a long wait never reads as a hung command —
* this is the CLI-side "heartbeat" the design calls for; it is terminal
@@ -649,7 +649,7 @@ async function cmdLockStatus(args) {
async function cmdLockAcquire(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (!name) {
console.error("usage: ccam lock acquire <name> [--holder X] [--timeout seconds]");
console.error("usage: agenthub lock acquire <name> [--holder X] [--timeout seconds]");
process.exitCode = 1;
return;
}
@@ -689,11 +689,11 @@ async function cmdLockAcquire(args) {
}
}
/** `ccam lock release <name> [--holder X]`. */
/** `agenthub lock release <name> [--holder X]`. */
async function cmdLockRelease(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (!name) {
console.error("usage: ccam lock release <name> [--holder X]");
console.error("usage: agenthub lock release <name> [--holder X]");
process.exitCode = 1;
return;
}
@@ -713,7 +713,7 @@ async function cmdLockRelease(args) {
- [ ] **Step 2: Wire the subcommand dispatch**
In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new case right after the `case "lanes":` block:
In `bin/agenthub.js`'s `runCommand` switch (search for `case "stage":`), add a new case right after the `case "lanes":` block:
```js
case "lock": {
@@ -721,7 +721,7 @@ In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new c
if (sub === "status") return cmdLockStatus(rest.slice(1));
if (sub === "acquire") return cmdLockAcquire(rest.slice(1));
if (sub === "release") return cmdLockRelease(rest.slice(1));
console.error("usage: ccam lock status [<name>] | ccam lock acquire <name> [--holder X] [--timeout N] | ccam lock release <name> [--holder X]");
console.error("usage: agenthub lock status [<name>] | agenthub lock acquire <name> [--holder X] [--timeout N] | agenthub lock release <name> [--holder X]");
process.exitCode = 1;
return;
}
@@ -729,7 +729,7 @@ In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new c
- [ ] **Step 3: Add the help-table entry**
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the `lanes hook` row:
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the `lanes hook` row:
```js
[
@@ -742,12 +742,12 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the
- [ ] **Step 4: Manual smoke test**
```bash
ccam lock status
ccam lock acquire smoke-test --holder test-a
ccam lock status smoke-test
ccam lock acquire smoke-test --holder test-b --timeout 2 # should time out, exit 1
ccam lock release smoke-test --holder test-a
ccam lock status smoke-test
agenthub lock status
agenthub lock acquire smoke-test --holder test-a
agenthub lock status smoke-test
agenthub lock acquire smoke-test --holder test-b --timeout 2 # should time out, exit 1
agenthub lock release smoke-test --holder test-a
agenthub lock status smoke-test
```
Expected: first acquire succeeds; status shows it held by `test-a`; second acquire (different holder, 2s timeout) prints a timeout error and exits 1; release succeeds; final status shows free.
@@ -755,8 +755,8 @@ Expected: first acquire succeeds; status shows it held by `test-a`; second acqui
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(locks): add ccam lock status/acquire/release CLI (D)"
git add bin/agenthub.js
git commit -m "feat(locks): add agenthub lock status/acquire/release CLI (D)"
```
---
@@ -914,10 +914,10 @@ git commit -m "feat(locks): show a lock indicator on the lane card (D)"
- [ ] **Step 1: `docs/LANES.md`**
Add a new top-level section (after the "Lane actions" section, before "Orchestration: what CCAM does NOT do" — search for both headings to place it precisely) titled `## Cross-lane named locks`, covering:
Add a new top-level section (after the "Lane actions" section, before "Orchestration: what AgentHub does NOT do" — search for both headings to place it precisely) titled `## Cross-lane named locks`, covering:
- What they're for (serializing a machine-thrashing step like a build or e2e run across every lane, not just within one lane — the other axis from the per-lane `withLaneLock` used internally by destructive actions).
- The `LOCK_MAX_HOLD` env var (default 2700s / 45 min) and its 300s floor — a holder can never be force-broken by setting `LOCK_MAX_HOLD` below 300, even to `1`.
- The CLI: `ccam lock status [<name>]`, `ccam lock acquire <name> [--holder X] [--timeout N]`, `ccam lock release <name> [--holder X]` — holder defaults to `lane<slot>` for the calling lane.
- The CLI: `agenthub lock status [<name>]`, `agenthub lock acquire <name> [--holder X] [--timeout N]`, `agenthub lock release <name> [--holder X]` — holder defaults to `lane<slot>` for the calling lane.
- **The etiquette text, verbatim in spirit from Shipyard**: waiting for a lock is normal, not a failure; never kill a holder to "fix" a stuck wait; never delete the lock directory (`LANES_ROOT/.locks/<name>/`) by hand; never shrink `LOCK_MAX_HOLD` to force through a wait — the 300s floor exists specifically to stop that.
- The lane card's lock badge (client-side, polled every 30s like the git/runtime facts).
@@ -926,9 +926,9 @@ Add a new top-level section (after the "Lane actions" section, before "Orchestra
Add to the `### Lanes` table, after the `lanes hook` row:
```markdown
| `ccam lock status [<name>]` | Show one lock's holder, or every currently-held lock |
| `ccam lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
| `ccam lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
| `agenthub lock status [<name>]` | Show one lock's holder, or every currently-held lock |
| `agenthub lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
| `agenthub lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
```
- [ ] **Step 3: `docs/API.md`**
@@ -940,7 +940,7 @@ Add a new section documenting `GET /api/locks`, `POST /api/locks/:name/acquire`,
Add a new row to the module responsibility table (search for the `lib/lane-lock.js` row if one exists, or place alphabetically near the other `lib/lane-*` rows):
```markdown
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `ccam lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `agenthub lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
```
- [ ] **Step 5: Mark D done in the parent plan**
@@ -4,7 +4,7 @@
**Goal:** a lane's history survives switching features. Today `clearLane` erases the live row's bookkeeping; this makes `clearLane` archive it first into a new `lane_features` table, and adds `activate`/`list`/`show` so a lane can carry many features across its lifetime, each independently browsable after the lane has moved on.
**Architecture:** One new table (`lane_features`) plus one new nullable column on `lanes` (`active_feature_id`). One new library module, `server/lib/lane-features.js`, owning slug canonicalization, archive-on-switch, and read access — the `lanes` row itself stays the single live view (nothing downstream that already reads a lane's `stage`/`status`/etc. needs to change). Routes are added inline to the existing `server/routes/lanes.js` (matching how `/:id/git`, `/:id/preflight`, `/:id/runtime` are already sub-resources of that same file, not separate routers). CLI gains `ccam feature list|activate|show`. The Workspace UI gains a read-only feature picker that swaps the detail panel to an archived snapshot — it never mutates anything, matching the standing rule that the console never writes a lane's stage.
**Architecture:** One new table (`lane_features`) plus one new nullable column on `lanes` (`active_feature_id`). One new library module, `server/lib/lane-features.js`, owning slug canonicalization, archive-on-switch, and read access — the `lanes` row itself stays the single live view (nothing downstream that already reads a lane's `stage`/`status`/etc. needs to change). Routes are added inline to the existing `server/routes/lanes.js` (matching how `/:id/git`, `/:id/preflight`, `/:id/runtime` are already sub-resources of that same file, not separate routers). CLI gains `agenthub feature list|activate|show`. The Workspace UI gains a read-only feature picker that swaps the detail panel to an archived snapshot — it never mutates anything, matching the standing rule that the console never writes a lane's stage.
**Tech Stack:** better-sqlite3 (existing), Express (existing), the existing `server/lib/pipelines.js` node-state renderer reused verbatim for archived snapshots.
@@ -66,7 +66,7 @@ Then, as a **separate, additive migration block** right after the existing lane-
```js
// Migrate: per-feature state (B). `active_feature_id` points at the
// lane_features row currently "live" (unarchived) for this lane — null for a
// lane that has never called `ccam feature activate`, which is why this
// lane that has never called `agenthub feature activate`, which is why this
// column is nullable and every downstream reader of a lane row is unaffected
// by its addition. ON DELETE SET NULL, not CASCADE: deleting the ACTIVE
// feature row (which normally only happens via cascade when the LANE itself
@@ -96,7 +96,7 @@ const os = require("node:os");
const path = require("node:path");
const fs = require("node:fs");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-features-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-features-"));
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
@@ -579,7 +579,7 @@ const path = require("node:path");
const fs = require("node:fs");
const http = require("node:http");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-features-api-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-features-api-"));
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
@@ -800,18 +800,18 @@ git commit -m "feat(lanes): expose GET/POST /api/lanes/:id/features over lane-fe
---
### Task 4: `ccam feature list|activate|show` CLI
### Task 4: `agenthub feature list|activate|show` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `GET /api/lanes/:id/features`, `GET /api/lanes/:id/features/:slug`, `POST /api/lanes/:id/features/activate` (Task 3, already committed) via the existing `get`/`post` helpers.
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`) to resolve which lane a command targets.
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`) to resolve which lane a command targets.
- [ ] **Step 1: Add the command implementations**
In `bin/ccam.js`, near `cmdStage` (search for that function), add:
In `bin/agenthub.js`, near `cmdStage` (search for that function), add:
```js
function fmtFeatureRow(f) {
@@ -821,23 +821,23 @@ function fmtFeatureRow(f) {
}`;
}
/** `ccam feature list [<id>] [--cwd path]` — every feature this lane has activated. */
/** `agenthub feature list [<id>] [--cwd path]` — every feature this lane has activated. */
async function cmdFeatureList(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { features } = await get(`/api/lanes/${resolved.laneId}/features`);
if (!features.length) {
console.log("no features activated yet — start one with: ccam feature activate <slug>");
console.log("no features activated yet — start one with: agenthub feature activate <slug>");
return;
}
for (const f of features) console.log(fmtFeatureRow(f));
}
/** `ccam feature activate <slug> [--title X] [<id>] [--cwd path]`. */
/** `agenthub feature activate <slug> [--title X] [<id>] [--cwd path]`. */
async function cmdFeatureActivate(args) {
const slug = args.find((arg) => !arg.startsWith("--"));
if (!slug) {
console.error("usage: ccam feature activate <slug> [--title text]");
console.error("usage: agenthub feature activate <slug> [--title text]");
process.exitCode = 1;
return;
}
@@ -856,11 +856,11 @@ async function cmdFeatureActivate(args) {
);
}
/** `ccam feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
/** `agenthub feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
async function cmdFeatureShow(args) {
const slug = args.find((arg) => !arg.startsWith("--"));
if (!slug) {
console.error("usage: ccam feature show <slug>");
console.error("usage: agenthub feature show <slug>");
process.exitCode = 1;
return;
}
@@ -887,7 +887,7 @@ Check whether `get()` in this file already supports a third `options` argument (
- [ ] **Step 2: Wire the subcommand dispatch**
In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere among the other top-level cases, e.g. right after the `case "lock":` block added in the previous plan):
In `bin/agenthub.js`'s `runCommand` switch, add a new case (placement: anywhere among the other top-level cases, e.g. right after the `case "lock":` block added in the previous plan):
```js
case "feature": {
@@ -895,7 +895,7 @@ In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere amon
if (sub === "list") return cmdFeatureList(rest.slice(1));
if (sub === "activate") return cmdFeatureActivate(rest.slice(1));
if (sub === "show") return cmdFeatureShow(rest.slice(1));
console.error("usage: ccam feature list | ccam feature activate <slug> [--title text] | ccam feature show <slug>");
console.error("usage: agenthub feature list | agenthub feature activate <slug> [--title text] | agenthub feature show <slug>");
process.exitCode = 1;
return;
}
@@ -903,7 +903,7 @@ In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere amon
- [ ] **Step 3: Add the help-table entries**
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stage <stage> [flags]` row:
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stage <stage> [flags]` row:
```js
["feature list", "[<id>]", "List every feature this lane has activated, archived or live"],
@@ -918,12 +918,12 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stag
- [ ] **Step 4: Manual smoke test**
```bash
ccam lanes add --cwd $(pwd) --title "smoke"
ccam feature activate one --title "First thing"
ccam stage review --evidence "looks fine"
ccam feature activate two --title "Second thing"
ccam feature list
ccam feature show one
agenthub lanes add --cwd $(pwd) --title "smoke"
agenthub feature activate one --title "First thing"
agenthub stage review --evidence "looks fine"
agenthub feature activate two --title "Second thing"
agenthub feature list
agenthub feature show one
```
Expected: `feature list` shows `two` marked active (`▶`) and `one` marked archived with a timestamp; `feature show one` prints `stage: review` and its saved pipeline nodes, not `two`'s.
@@ -931,8 +931,8 @@ Expected: `feature list` shows `two` marked active (`▶`) and `one` marked arch
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam feature list/activate/show CLI (B)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub feature list/activate/show CLI (B)"
```
---
@@ -1130,10 +1130,10 @@ git commit -m "feat(lanes): add a read-only feature picker to the Workspace page
Add a new top-level section (after "Pipeline stages and the five node states", before "Stage detection" — search for both headings) titled `## Per-feature state and archive`, covering:
- Why: `clearLane` used to erase; now a lane can carry many features across its lifetime.
- The opt-in model: nothing changes for a lane that never calls `ccam feature activate` — `clearLane` behaves exactly as before.
- The opt-in model: nothing changes for a lane that never calls `agenthub feature activate` — `clearLane` behaves exactly as before.
- Slug canonicalization rule, stated exactly: drops a leading `feat/`, turns `/` and whitespace into `-`, keeps `[A-Za-z0-9._-]`, does **not** lowercase — a deliberately different rule from `worktree.js:slugify`'s branch-name slugification, and every endpoint/CLI command echoes back the canonicalized form.
- `activate` semantics: archives the current active feature (if any and if different), restores the target's saved stage onto the live lane row (so switching back to a past feature resumes where it left off), creates a fresh feature row for a never-seen slug.
- The CLI: `ccam feature list|activate|show`.
- The CLI: `agenthub feature list|activate|show`.
- The Workspace picker is **read-only** — selecting an archived feature shows its saved pipeline; it never changes the live lane, matching the standing "console never writes a lane's stage" rule.
- [ ] **Step 2: `docs/CLI.md`**
@@ -1141,9 +1141,9 @@ Add a new top-level section (after "Pipeline stages and the five node states", b
Add to the `### Lanes` table, after the `stage <stage> [flags]` row:
```markdown
| `ccam feature list [<id>]` | List every feature this lane has activated, archived or live |
| `ccam feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
| `ccam feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
| `agenthub feature list [<id>]` | List every feature this lane has activated, archived or live |
| `agenthub feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
| `agenthub feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
```
- [ ] **Step 3: `docs/API.md`**
@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** the screenshots QC agents capture become visible in the dashboard, grouped by feature (B's `lane_features` slug) and phase. Ported from Shipyard's real implementation (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/dashboard/server/{routes,services}/proof.js`, `src/components/ProofGallery.jsx`, `src/hooks/useProof.js`), adapted from Shipyard's flat `state/laneN/*.json` file model to CCAM's `lane_features` DB rows (Task B, already shipped).
**Goal:** the screenshots QC agents capture become visible in the dashboard, grouped by feature (B's `lane_features` slug) and phase. Ported from Shipyard's real implementation (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/dashboard/server/{routes,services}/proof.js`, `src/components/ProofGallery.jsx`, `src/hooks/useProof.js`), adapted from Shipyard's flat `state/laneN/*.json` file model to AgentHub's `lane_features` DB rows (Task B, already shipped).
**Architecture:** One new library module `server/lib/proof.js` (list/file/delete/link, all containment-checked against the lane's own proof root — never `LANES_ROOT`, since an adopted lane's `cwd` need not live under it). Routes added inline to `server/routes/lanes.js`, grouped with the other `/:id/*` sub-resources, same placement rule B's `/:id/features` used. CLI gains one primitive, `ccam lanes proof-link`, which is never auto-invoked — matching "CCAM does not orchestrate," a session or hook calls it explicitly. The Workspace UI adds a `ProofGallery` panel keyed off the **same `viewedFeatureSlug`** state Task B's picker already introduced — no second picker.
**Architecture:** One new library module `server/lib/proof.js` (list/file/delete/link, all containment-checked against the lane's own proof root — never `LANES_ROOT`, since an adopted lane's `cwd` need not live under it). Routes added inline to `server/routes/lanes.js`, grouped with the other `/:id/*` sub-resources, same placement rule B's `/:id/features` used. CLI gains one primitive, `agenthub lanes proof-link`, which is never auto-invoked — matching "AgentHub does not orchestrate," a session or hook calls it explicitly. The Workspace UI adds a `ProofGallery` panel keyed off the **same `viewedFeatureSlug`** state Task B's picker already introduced — no second picker.
**Tech Stack:** Node `fs`/`path` (realpath-based containment, no new dependency), existing Express/route conventions, existing React state from Task B.
@@ -14,8 +14,8 @@
- **Path containment is the entire security surface.** Every one of `proofFile`/`deleteProof`/`listProof` resolves + `fs.realpathSync`s + asserts the result is inside the lane's proof root (`base === p || p.startsWith(base + path.sep)`) before any read/write/delete reaches `fs`. No request path — slug, group, file, image name — ever reaches `fs` unresolved. Port Shipyard's `proofFile`/`deleteProof`/`badName` check nearly verbatim; do not "clean up" or abstract this logic, it is the one part of C worth reviewing line-by-line.
- **`deleteProof` never touches `ticket/`.** Whole-feature delete iterates every subdirectory except `ticket`; group delete refuses `group === "ticket"` outright.
- **The proof root is `<lane.cwd>/.playwright-mcp/proof`, not anything under `LANES_ROOT`.** An adopted lane's `cwd` can be anywhere the user can read; proof storage follows the lane, same as Shipyard's `.playwright-mcp/proof` living inside the worktree.
- **Grouping comes from `lane_features` (Task B), not from re-deriving "current" via a branch regex.** Shipyard's `useProof.js` parses `feat/<slug>` out of the git branch because it has no DB; CCAM already has `getFeature`/`active_feature_id` as the source of truth, so that derivation is deliberately dropped, not ported.
- **`ccam lanes proof-link` is a primitive, not automatic.** It runs `ensureProofLink` once, on request. Nothing in this plan calls it from `feature activate`, `lanes up`, or any hook — a session or a project's own hook script decides when to call it, same reasoning as every other "CCAM does not orchestrate" primitive.
- **Grouping comes from `lane_features` (Task B), not from re-deriving "current" via a branch regex.** Shipyard's `useProof.js` parses `feat/<slug>` out of the git branch because it has no DB; AgentHub already has `getFeature`/`active_feature_id` as the source of truth, so that derivation is deliberately dropped, not ported.
- **`agenthub lanes proof-link` is a primitive, not automatic.** It runs `ensureProofLink` once, on request. Nothing in this plan calls it from `feature activate`, `lanes up`, or any hook — a session or a project's own hook script decides when to call it, same reasoning as every other "AgentHub does not orchestrate" primitive.
- `DELETE /api/lanes/:id/proof/:slug` takes `{group?, images?}` in the **JSON body** (`express.json()` is already mounted globally in `server/index.js`), matching Shipyard's `deleteProof(n, req.body || {})` — not query params.
- Run `npm run test:server` (full suite) and `npm run test:client` (when a task touches `client/`) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit.
- Never use `git add -A`. Stage exactly the files each task names.
@@ -30,7 +30,7 @@
**Interfaces:**
- Produces: `proofRoot(lane)` → `string` (`<lane.cwd>/.playwright-mcp/proof`, NOT realpath'd — the directory may not exist yet); `ensureProofLink(lane)` → `{linked: boolean}` (idempotent; `false` when there was nothing to link, e.g. clone-root `proof/` already the correct symlink or absent); `listProof(laneId)` → `Array<{slug, groups: {[group]: string[]}, ticket_report: string, mtime: number}>` (mtime = latest file mtime under that slug's dir, seconds; sorted most-recently-touched first); `proofFile(laneId, slug, group, file)` → `string | null` (absolute realpath, or null if missing/outside root/wrong extension); `deleteProof(laneId, {slug, group, images} = {})` → `{deleted: number}`, throws `Object.assign(new Error(...), {code: "EBADPATH"})` for a bad slug/group/image name, `{code: "ENOFEATURE"}` when the slug's directory doesn't exist.
- `listProof` merges in every slug from `lane-features.js`'s `listFeatures(laneId)` that has no proof directory yet (so a just-activated feature with zero screenshots still appears, with empty `groups`) — the CCAM-native replacement for Shipyard's `_pending`/`_active` special-casing, since B's `lane_features` row already exists the moment `activate` is called.
- `listProof` merges in every slug from `lane-features.js`'s `listFeatures(laneId)` that has no proof directory yet (so a just-activated feature with zero screenshots still appears, with empty `groups`) — the AgentHub-native replacement for Shipyard's `_pending`/`_active` special-casing, since B's `lane_features` row already exists the moment `activate` is called.
**Step 1: Write the failing test**
@@ -48,7 +48,7 @@ const os = require("node:os");
const path = require("node:path");
const fs = require("node:fs");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-"));
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
@@ -278,7 +278,7 @@ Read `server/lib/lane-features.js` first (already committed) to match its hydrat
* `lane_features` already tracks. Ported from Shipyard's
* `dashboard/server/services/proof.js`; the "current feature" special-casing
* that file needed (it has no DB, so it parsed a branch regex) is dropped —
* CCAM's `lane_features` row already IS that source of truth.
* AgentHub's `lane_features` row already IS that source of truth.
*
* Path containment is the entire security surface here: every path this
* module touches is resolved, realpath'd, and checked to be inside the
@@ -519,7 +519,7 @@ function deleteProof(laneId, { slug, group, images } = {}) {
* already points at the right place.
*
* Never auto-invoked by this codebase; exposed only as the explicit
* `ccam lanes proof-link` primitive.
* `agenthub lanes proof-link` primitive.
*
* @param {object} lane - A hydrated lane row (must have `cwd`).
* @returns {{linked: boolean}} `linked: false` when nothing changed.
@@ -632,7 +632,7 @@ const path = require("node:path");
const fs = require("node:fs");
const http = require("node:http");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-api-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-api-"));
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
@@ -861,21 +861,21 @@ git commit -m "feat(lanes): expose GET/DELETE /api/lanes/:id/proof + proof-link
---
### Task 3: `ccam lanes proof-link` CLI
### Task 3: `agenthub lanes proof-link` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `POST /api/lanes/:id/proof-link` (Task 2, already committed) via the existing `post` helper.
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`).
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`).
**Step 1: Add the command implementation**
In `bin/ccam.js`, near `cmdFeatureShow` (search for that function, added in Task B), add:
In `bin/agenthub.js`, near `cmdFeatureShow` (search for that function, added in Task B), add:
```js
/** `ccam lanes proof-link [<id>] [--cwd path]` — converge the clone-root
/** `agenthub lanes proof-link [<id>] [--cwd path]` — converge the clone-root
* `proof/` onto `.playwright-mcp/proof` (idempotent). Never run automatically
* by anything else in this codebase; a session or hook calls it explicitly. */
async function cmdLanesProofLink(args) {
@@ -892,7 +892,7 @@ async function cmdLanesProofLink(args) {
**Step 2: Wire the subcommand dispatch**
In `bin/ccam.js`'s `case "lanes":` block (search for other `lanes` subcommands like `up`/`down`/`hook`), add:
In `bin/agenthub.js`'s `case "lanes":` block (search for other `lanes` subcommands like `up`/`down`/`hook`), add:
```js
if (sub === "proof-link") return cmdLanesProofLink(rest.slice(1));
@@ -900,7 +900,7 @@ In `bin/ccam.js`'s `case "lanes":` block (search for other `lanes` subcommands l
**Step 3: Add the help-table entry**
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lanes hook` row:
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lanes hook` row:
```js
["lanes proof-link", "[<id>]", "Converge clone-root proof/ onto .playwright-mcp/proof (idempotent, never automatic)"],
@@ -909,17 +909,17 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lane
**Step 4: Manual smoke test**
```bash
ccam lanes add --cwd $(pwd) --title "smoke"
ccam lanes proof-link
agenthub lanes add --cwd $(pwd) --title "smoke"
agenthub lanes proof-link
ls -la proof # should be a symlink -> .playwright-mcp/proof
ccam lanes proof-link # second run: "already linked, nothing to do"
agenthub lanes proof-link # second run: "already linked, nothing to do"
```
**Step 5: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes proof-link CLI (C)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes proof-link CLI (C)"
```
---
@@ -1101,7 +1101,7 @@ git commit -m "feat(lanes): show proof gallery panel in the Workspace page (C)"
Add a new top-level section (after "Per-feature state and archive", before "Cross-lane named locks" — search for both headings) titled `## Proof gallery`, covering:
- Why: QC screenshots land on disk during a lane's work; this makes them visible in the dashboard, grouped by feature (Task B's slug) and phase.
- Storage layout: `<lane.cwd>/.playwright-mcp/proof/<slug>/{<phase-group>,ticket}/`.
- `ensure_proof_link`'s purpose and that it is **never automatic** — `ccam lanes proof-link` is an explicit primitive, same "CCAM does not orchestrate" reasoning as every other primitive in this project.
- `ensure_proof_link`'s purpose and that it is **never automatic** — `agenthub lanes proof-link` is an explicit primitive, same "AgentHub does not orchestrate" reasoning as every other primitive in this project.
- The security invariant stated exactly: every proof route resolves + realpaths + checks containment before touching `fs`; `deleteProof` never removes `ticket/`.
- The Workspace panel reuses Task B's feature picker — no second selector.
@@ -1110,7 +1110,7 @@ Add a new top-level section (after "Per-feature state and archive", before "Cros
Add to the `### Lanes` table, after the `lanes hook` row:
```markdown
| `ccam lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
| `agenthub lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
```
**Step 3: `docs/API.md`**
@@ -1122,7 +1122,7 @@ Add a `### Lane proof gallery` section documenting `GET /api/lanes/:id/proof`, `
Add a new row to the module responsibility table, near the `lib/lane-features.js` row:
```markdown
| `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `<lane.cwd>/.playwright-mcp/proof/<slug>/<group>/`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `ccam lanes proof-link` |
| `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `<lane.cwd>/.playwright-mcp/proof/<slug>/<group>/`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `agenthub lanes proof-link` |
```
**Step 5: Mark C done in the parent plan**
@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** a lane can opt into the `ship-feature` pipeline template and a session can drive it with `.claude/skills/ship-feature-lane/SKILL.md`, whose every command is a real, working `ccam` command today (stages 0–2 fully runnable; later stages reference the not-yet-built agents/`sync-base`/F integrations by name, same as the roadmap's own text does).
**Goal:** a lane can opt into the `ship-feature` pipeline template and a session can drive it with `.claude/skills/ship-feature-lane/SKILL.md`, whose every command is a real, working `agenthub` command today (stages 0–2 fully runnable; later stages reference the not-yet-built agents/`sync-base`/F integrations by name, same as the roadmap's own text does).
**Architecture:** One backend addition (`--qc` boot flag + `QC_BOOT_ENV`, threaded through `server/lib/lane-profile.js`'s `hookEnv`/`runHook` and `server/lib/lane-runtime.js`'s `upLane`), one new pipeline template JSON, one new skill file. No new routes, no new CLI subcommands beyond one flag on the existing `ccam lanes up`.
**Architecture:** One backend addition (`--qc` boot flag + `QC_BOOT_ENV`, threaded through `server/lib/lane-profile.js`'s `hookEnv`/`runHook` and `server/lib/lane-runtime.js`'s `upLane`), one new pipeline template JSON, one new skill file. No new routes, no new CLI subcommands beyond one flag on the existing `agenthub lanes up`.
**Tech Stack:** Existing profile/hook system (`server/lib/lane-profile.js`), existing runtime lifecycle (`server/lib/lane-runtime.js`), existing pipeline node-state renderer (`server/lib/pipelines.js`, reused verbatim).
@@ -25,7 +25,7 @@
- Modify: `server/lib/lane-profile.js` (`DEFAULTS`, `hookEnv`, `runHook`)
- Modify: `server/lib/lane-runtime.js` (`upLane`)
- Modify: `server/routes/lanes.js` (`POST /:id/up`)
- Modify: `bin/ccam.js` (`cmdLanesRuntime`'s `up` branch)
- Modify: `bin/agenthub.js` (`cmdLanesRuntime`'s `up` branch)
- Test: `server/__tests__/lane-profile.test.js` (or wherever `hookEnv`/`runHook` are already tested — find with `grep -rln "hookEnv\|runHook" server/__tests__/*.test.js` and add there), `server/__tests__/lane-runtime.test.js`
**Interfaces:**
@@ -242,7 +242,7 @@ In `server/routes/lanes.js`, find `router.post("/:id/up", ...)` (already read ab
const facts = await upLane(lanesLib.getLane(lane.id), { build, qc, onLine });
```
In `bin/ccam.js`, find the `if (sub === "up") {` block inside `cmdLanesRuntime` (confirmed at the line already read: `const body = laneArgs.includes("--no-build") ? { build: false } : {};`) and change:
In `bin/agenthub.js`, find the `if (sub === "up") {` block inside `cmdLanesRuntime` (confirmed at the line already read: `const body = laneArgs.includes("--no-build") ? { build: false } : {};`) and change:
```js
if (sub === "up") {
@@ -262,7 +262,7 @@ Update the help-table row for `lanes up` (search `COMMAND_GROUPS` for the existi
],
```
(Read the exact current row text first with `grep -n "no-build" bin/ccam.js` — merge into the real current wording rather than overwriting unrelated parts of the description.)
(Read the exact current row text first with `grep -n "no-build" bin/agenthub.js` — merge into the real current wording rather than overwriting unrelated parts of the description.)
- [ ] **Step 10: Run the full suite and header audit**
@@ -271,7 +271,7 @@ Run: `npm run test:server && bash .claude/skills/file-headers/scripts/check-head
- [ ] **Step 11: Commit**
```bash
git add server/lib/lane-profile.js server/lib/lane-runtime.js server/routes/lanes.js bin/ccam.js server/__tests__/lane-profile.test.js server/__tests__/lane-runtime.test.js
git add server/lib/lane-profile.js server/lib/lane-runtime.js server/routes/lanes.js bin/agenthub.js server/__tests__/lane-profile.test.js server/__tests__/lane-runtime.test.js
git commit -m "feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic QC stacks (E1)"
```
@@ -291,7 +291,7 @@ git commit -m "feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic Q
- [ ] **Step 1: Read the existing template format**
`server/data/pipelines/default.json`'s real shape (read it yourself with `cat server/data/pipelines/default.json` to confirm this plan's transcription is still current before writing the new file): top-level `{id, name, nodes}`; each node is `{id, label, icon, gate, aliases, detect?}` — `id` is the canonical stage name, `aliases` are other declared-stage strings (from `ccam stage <name>`) that also map to this node, `gate: true` marks a node that needs `--evidence` to render `done` rather than the amber `passed-no-evidence` state (per this repo's five node states, documented in `docs/LANES.md`), and `detect` (an array of `{tool, match}` rules) is for INFERRING a stage from tool-call patterns when no session ever explicitly declares it — `ship-feature-lane` (Task 3) always explicitly declares every stage via `ccam stage`, so no `ship-feature.json` node needs a `detect` block.
`server/data/pipelines/default.json`'s real shape (read it yourself with `cat server/data/pipelines/default.json` to confirm this plan's transcription is still current before writing the new file): top-level `{id, name, nodes}`; each node is `{id, label, icon, gate, aliases, detect?}` — `id` is the canonical stage name, `aliases` are other declared-stage strings (from `agenthub stage <name>`) that also map to this node, `gate: true` marks a node that needs `--evidence` to render `done` rather than the amber `passed-no-evidence` state (per this repo's five node states, documented in `docs/LANES.md`), and `detect` (an array of `{tool, match}` rules) is for INFERRING a stage from tool-call patterns when no session ever explicitly declares it — `ship-feature-lane` (Task 3) always explicitly declares every stage via `agenthub stage`, so no `ship-feature.json` node needs a `detect` block.
- [ ] **Step 2: Write the failing test**
@@ -364,7 +364,7 @@ Expected: FAIL — `getPipeline` never throws on an unknown id (it falls back to
}
```
(`gate: true` is a rendering hint only — `nodeStates` passes it straight through to the UI unchanged; it does NOT change whether a node needs `--evidence` to show `done` (every node does, regardless of `gate`). It's set here on the five real checkpoints a fix-loop can bounce off of — CI gates, e2e-passed, code review, QC, and the senior gate — purely so the dashboard can visually distinguish a checkpoint from a transit stage, matching `default.json`'s own use of `gate` on `tests`/`review`/`gate`. `pr-open`'s `"ship"` alias and `done`'s `"complete"/"completed"` aliases mirror `default.json`'s own aliasing for the same concepts, so a stray `ccam stage ship` or `ccam stage complete` from habit still resolves to the right node.)
(`gate: true` is a rendering hint only — `nodeStates` passes it straight through to the UI unchanged; it does NOT change whether a node needs `--evidence` to show `done` (every node does, regardless of `gate`). It's set here on the five real checkpoints a fix-loop can bounce off of — CI gates, e2e-passed, code review, QC, and the senior gate — purely so the dashboard can visually distinguish a checkpoint from a transit stage, matching `default.json`'s own use of `gate` on `tests`/`review`/`gate`. `pr-open`'s `"ship"` alias and `done`'s `"complete"/"completed"` aliases mirror `default.json`'s own aliasing for the same concepts, so a stray `agenthub stage ship` or `agenthub stage complete` from habit still resolves to the right node.)
- [ ] **Step 5: Run test to verify it passes**
@@ -398,25 +398,25 @@ Create `.claude/skills/ship-feature-lane/SKILL.md` with this exact content:
````markdown
---
name: ship-feature-lane
description: "Autonomous end-to-end feature pipeline for ONE CCAM lane. Invoke inside a lane's working directory with a requirement: `/ship-feature-lane <requirement>`. Frontloads ALL clarifying questions once, then runs unattended: implement (TDD) → pre-push CI gates + dev preflight → e2e on the feature branch → code review → local QC → senior GO/NO-GO gate → push branch + open PR (base `development`) → CI watch → report, then watches the PR (new comments → gated fix-loop; base conflicts → sync development into the branch) until a HUMAN merges it. The PR is only published after all local gates pass and the senior gate says GO — it's finalized when reviewers see it. Declares lane stage at every step via `ccam stage` for the dashboard. Use when the user wants to build/ship/implement a feature in a CCAM lane. NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands."
description: "Autonomous end-to-end feature pipeline for ONE AgentHub lane. Invoke inside a lane's working directory with a requirement: `/ship-feature-lane <requirement>`. Frontloads ALL clarifying questions once, then runs unattended: implement (TDD) → pre-push CI gates + dev preflight → e2e on the feature branch → code review → local QC → senior GO/NO-GO gate → push branch + open PR (base `development`) → CI watch → report, then watches the PR (new comments → gated fix-loop; base conflicts → sync development into the branch) until a HUMAN merges it. The PR is only published after all local gates pass and the senior gate says GO — it's finalized when reviewers see it. Declares lane stage at every step via `agenthub stage` for the dashboard. Use when the user wants to build/ship/implement a feature in a AgentHub lane. NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands."
---
# Ship Feature Lane (CCAM lane pipeline)
# Ship Feature Lane (AgentHub lane pipeline)
You are running the autonomous feature pipeline for **one CCAM lane**. The human's only interactive touchpoints are **Stage 0 (frontloaded Q&A)** and **merging the PR on GitHub**; everything else runs to completion or to a `blocked` escalation, reporting progress through `ccam stage` (which the dashboard renders).
You are running the autonomous feature pipeline for **one AgentHub lane**. The human's only interactive touchpoints are **Stage 0 (frontloaded Q&A)** and **merging the PR on GitHub**; everything else runs to completion or to a `blocked` escalation, reporting progress through `agenthub stage` (which the dashboard renders).
## Setup — do this first, every run
```bash
LANE_DIR="$(pwd)" # the lane clone IS your cwd — CCAM resolves the lane from this, never a hardcoded path
LANE_DIR="$(pwd)" # the lane clone IS your cwd — AgentHub resolves the lane from this, never a hardcoded path
```
- CCAM resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `ccam stage` or `ccam feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
- All stage updates go through `ccam stage <stage> [--status <s>] [--evidence "..."]` — **call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in CCAM yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `ccam ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 12–13) can run many minutes between stage transitions — bump the heartbeat with `ccam stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
- Profile hooks (`bootstrap`/`boot`/`migrate`/`seed`/`ci-gate`/`e2e`/`health`/`regen`) run through `ccam lanes hook <name> [args…]` and `ccam lanes up`/`down`. Use them; don't reinvent their logic.
- **NEVER merge or rebase branches manually.** The ONLY merge that ever happens in this flow is `origin/development` INTO the feature branch, and only through `ccam lanes sync-base` (fetches fresh, pre-checks migration collisions, auto-regenerates generated files — see the note on this command in Stage 2; it is a LATER task, referenced here by its intended contract). There is no other direction: never merge a feature branch into anything locally, never commit on `development`, and never touch `main`.
- **NEVER push `origin/development` or `origin/main` (HARD).** `development` moves ONLY when a human merges a PR on GitHub. The only branch you ever push is your own `feat/<slug>` — and only after the senior gate's GO (Stage 8). If you ever find yourself typing `git push` with `development` or `main` on the line: STOP, `ccam stage <current> --status blocked`.
- AgentHub resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `agenthub stage` or `agenthub feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
- All stage updates go through `agenthub stage <stage> [--status <s>] [--evidence "..."]` — **call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in AgentHub yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `agenthub ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 12–13) can run many minutes between stage transitions — bump the heartbeat with `agenthub stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
- Profile hooks (`bootstrap`/`boot`/`migrate`/`seed`/`ci-gate`/`e2e`/`health`/`regen`) run through `agenthub lanes hook <name> [args…]` and `agenthub lanes up`/`down`. Use them; don't reinvent their logic.
- **NEVER merge or rebase branches manually.** The ONLY merge that ever happens in this flow is `origin/development` INTO the feature branch, and only through `agenthub lanes sync-base` (fetches fresh, pre-checks migration collisions, auto-regenerates generated files — see the note on this command in Stage 2; it is a LATER task, referenced here by its intended contract). There is no other direction: never merge a feature branch into anything locally, never commit on `development`, and never touch `main`.
- **NEVER push `origin/development` or `origin/main` (HARD).** `development` moves ONLY when a human merges a PR on GitHub. The only branch you ever push is your own `feat/<slug>` — and only after the senior gate's GO (Stage 8). If you ever find yourself typing `git push` with `development` or `main` on the line: STOP, `agenthub stage <current> --status blocked`.
## Context recovery — after conversation compaction
@@ -427,12 +427,12 @@ LANE_DIR="$(pwd)" # lane = cwd, resolved fresh
```
Then check your current position:
- **Lane state**: `ccam feature show <slug>` (or `ccam lanes` for the lane's own row) — shows current stage, status, feature title, branch, gate decision, PR URL, notes.
- **Lane state**: `agenthub feature show <slug>` (or `agenthub lanes` for the lane's own row) — shows current stage, status, feature title, branch, gate decision, PR URL, notes.
- **Git branch**: `git rev-parse --abbrev-ref HEAD` — which branch you're on.
- **Feature slug**: from the branch name (`feat/X` → `X`), or from `ccam feature list` (the lane's currently-active feature is marked `▶`).
- **Feature slug**: from the branch name (`feat/X` → `X`), or from `agenthub feature list` (the lane's currently-active feature is marked `▶`).
Resume from the stage shown. If state says `stage=X status=running`, you were mid-stage X when context compacted — re-run that stage from the top (all `ccam` commands are idempotent).
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `ccam lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
Resume from the stage shown. If state says `stage=X status=running`, you were mid-stage X when context compacted — re-run that stage from the top (all `agenthub` commands are idempotent).
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `agenthub lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
## Hard rules
@@ -443,17 +443,17 @@ Resume from the stage shown. If state says `stage=X status=running`, you were mi
- **Run only if e2e spec files are among the changed tests:** Stage 3 (boot + e2e on the feature branch). No e2e specs changed → skip it.
- **Always SKIP** (runtime UI unchanged): Stage 5 (QC plan), Stage 6 (qc-local). Record `--evidence "QC skipped: test-only change"`.
- If the diff contains ANY non-test file → this fast-path does NOT apply; take the full path above. The first pass (not a re-entry) always runs full QC.
- **EXCEPTION — localized re-entry (scoped-e2e fast-path).** On a re-entry whose diff since the last fully-validated commit is SMALL and LOCALIZED — only files inside the feature's own surface, NO migrations, NO contract/generated files, NO shared fixtures/utilities, NO dependency changes — you may shrink Stage 3's e2e to a SCOPED run of the specs covering the touched surface: `ccam lanes hook e2e -- <spec files>` (scoped runs still heartbeat + lock + time-bound like the full suite), and browser QC runs SCOPED to the affected QC-Plan scenarios (tell the qc agent exactly which scenario numbers). Know the trade-off: there is no dev-merged full suite anymore — post-merge dev CI + dev-QC (Stage 13, currently off) are normally the integration net; without them, a localized re-entry after this lands is a real gap until F ships. If in doubt whether the change is localized, it isn't — run the full path.
- **Run long helpers so they can't be killed mid-flight or hang your turn.** `ccam lanes hook ci-gate`, `ccam lanes up`, `ccam lanes hook e2e` legitimately run 3–20+ minutes (builds, tests, Playwright, lock waits). NEVER invoke them with the default Bash timeout (2 min kills them mid-flight and strands the lane half-done): use `run_in_background: true` and poll the output file until done, or set `timeout: 600000` for the shorter gates. If a helper does die mid-run, don't panic: every hook is idempotent — re-run the step (e.g. re-run `ccam lanes up --no-build` to revive a stack).
- **Waiting + polling NEVER use a foreground `sleep`** (the Bash tool blocks it) or `ScheduleWakeup` (that's a `/loop`-only primitive — this pipeline is not a `/loop` session, so it won't sustain your watch). To pace a poll loop or wait out a timer, **background the wait**: run `sleep <secs>` with `run_in_background: true` — you're re-invoked when it exits, and re-invoked the moment a backgrounded Agent or helper finishes, so you never busy-poll for background work. **If you ever can't sustain a wait/loop in this session, set `ccam stage <current> --note "<honest note>"` and STOP — never narrate a watch or loop you are not actually running.**
- **e2e: actively poll — never wait on the completion re-invoke alone.** A hung suite never fires it, stranding the lane at `stage=e2e-feature`. When running `ccam lanes hook e2e` (Stage 3): start it `run_in_background: true` AND background a `sleep 90` beside it. Each wake — finished → parse PASS/FAIL; still running → read the e2e log tail (`ccam lanes logs <id> e2e`), bump the heartbeat, and re-background `sleep 90`, UNLESS it's erroring or has run past ~22 min, in which case kill the e2e task and treat it as FAIL → re-enter Stage 2.
- **EXCEPTION — localized re-entry (scoped-e2e fast-path).** On a re-entry whose diff since the last fully-validated commit is SMALL and LOCALIZED — only files inside the feature's own surface, NO migrations, NO contract/generated files, NO shared fixtures/utilities, NO dependency changes — you may shrink Stage 3's e2e to a SCOPED run of the specs covering the touched surface: `agenthub lanes hook e2e -- <spec files>` (scoped runs still heartbeat + lock + time-bound like the full suite), and browser QC runs SCOPED to the affected QC-Plan scenarios (tell the qc agent exactly which scenario numbers). Know the trade-off: there is no dev-merged full suite anymore — post-merge dev CI + dev-QC (Stage 13, currently off) are normally the integration net; without them, a localized re-entry after this lands is a real gap until F ships. If in doubt whether the change is localized, it isn't — run the full path.
- **Run long helpers so they can't be killed mid-flight or hang your turn.** `agenthub lanes hook ci-gate`, `agenthub lanes up`, `agenthub lanes hook e2e` legitimately run 3–20+ minutes (builds, tests, Playwright, lock waits). NEVER invoke them with the default Bash timeout (2 min kills them mid-flight and strands the lane half-done): use `run_in_background: true` and poll the output file until done, or set `timeout: 600000` for the shorter gates. If a helper does die mid-run, don't panic: every hook is idempotent — re-run the step (e.g. re-run `agenthub lanes up --no-build` to revive a stack).
- **Waiting + polling NEVER use a foreground `sleep`** (the Bash tool blocks it) or `ScheduleWakeup` (that's a `/loop`-only primitive — this pipeline is not a `/loop` session, so it won't sustain your watch). To pace a poll loop or wait out a timer, **background the wait**: run `sleep <secs>` with `run_in_background: true` — you're re-invoked when it exits, and re-invoked the moment a backgrounded Agent or helper finishes, so you never busy-poll for background work. **If you ever can't sustain a wait/loop in this session, set `agenthub stage <current> --note "<honest note>"` and STOP — never narrate a watch or loop you are not actually running.**
- **e2e: actively poll — never wait on the completion re-invoke alone.** A hung suite never fires it, stranding the lane at `stage=e2e-feature`. When running `agenthub lanes hook e2e` (Stage 3): start it `run_in_background: true` AND background a `sleep 90` beside it. Each wake — finished → parse PASS/FAIL; still running → read the e2e log tail (`agenthub lanes logs <id> e2e`), bump the heartbeat, and re-background `sleep 90`, UNLESS it's erroring or has run past ~22 min, in which case kill the e2e task and treat it as FAIL → re-enter Stage 2.
- **Turn-liveness: never let the pipeline die silently.** A "stalled" lane usually died one of two ways: (a) a turn ended with NOTHING pending — no backgrounded wait, no running helper, no background agent — so nothing ever re-invoked the session; or (b) a transient API error (rate-limit, 529/overload, connection refused) killed the turn mid-stage. Rules: while the pipeline is anywhere between Stage 1 and Stage 14 (done), every turn you end MUST leave at least one re-invoker pending (a `run_in_background` helper/sleep or a background agent) — check before ending the turn. And on ANY resume after an error or a human nudge ("continue"), do not ask questions: re-derive position from lane state (Context recovery above) and continue the stage. If you truly cannot leave a re-invoker, set `--note "watch needs re-trigger: <what to do>"` so the dashboard shows it honestly.
- **No retry cap — the phase clock is the signal.** A failing gate/QC/CI/e2e just re-enters the loop (fix on the feature branch, re-run from Stage 2); there is NO automatic block after N attempts. The dashboard shows how long the lane has sat in its current stage, so the human can spot a stuck or endlessly-looping lane and step in. Reserve `--status blocked` for GENUINE blockers you cannot resolve (an ambiguous merge conflict, a hard/unrecoverable error).
- **Commit only on the feature branch.** Never commit on `development`/`main`. Stage only intended files (never `git add .` blindly — this repo collects stray build/QA artifacts).
- Keep the lane's state truthful: on any stop, set an accurate `ccam stage <stage> --status <status> --note "<why>"`.
- Keep the lane's state truthful: on any stop, set an accurate `agenthub stage <stage> --status <status> --note "<why>"`.
- **Quality bar (applies to every code change, including fix-loop re-entries and follow-up PRs).** Tests are sharp and meaningful — each pins a real behavior/edge case (happy + negative + boundary), none trivial, redundant, or coverage-padding. Comments are minimal — only the non-obvious *why*, matching the surrounding density; never narrate the *what*. Investigate before fixing (root cause, not symptom — use **systematic-debugging**). Prefer reusing/extending existing code over duplicating it.
- **One driver per MCP browser server.** Each MCP server owns ONE browser; two agents driving the SAME server interleave clicks in one tab. The local-QC MCP → the qc-local agent (Stage 6) only; the general `playwright` MCP → the main session for ad-hoc checks only (never while qc-local runs). Parallel agents on DIFFERENT servers are safe by design; a second concurrent driver on the SAME server is never OK.
- **Cross-lane etiquette (locks + siblings).** Lanes share one machine and one dev site. Waiting on a cross-lane serializer (`ccam lock acquire <name>`, e.g. around a shared build/e2e step) is NORMAL — it heartbeats while it waits, so you won't look stalled. NEVER free a lock by killing another lane's session or processes, deleting the lock's directory by hand, or shrinking `LOCK_MAX_HOLD`; a dead holder's lock auto-expires on its own. If a lock wait times out: re-try with a longer `--timeout`, or set `--status blocked` with a note and report. Touch ONLY your own lane's clone, state, and locks you hold.
- **Cross-lane etiquette (locks + siblings).** Lanes share one machine and one dev site. Waiting on a cross-lane serializer (`agenthub lock acquire <name>`, e.g. around a shared build/e2e step) is NORMAL — it heartbeats while it waits, so you won't look stalled. NEVER free a lock by killing another lane's session or processes, deleting the lock's directory by hand, or shrinking `LOCK_MAX_HOLD`; a dead holder's lock auto-expires on its own. If a lock wait times out: re-try with a longer `--timeout`, or set `--status blocked` with a note and report. Touch ONLY your own lane's clone, state, and locks you hold.
## Stages
@@ -461,10 +461,10 @@ Resume from the stage shown. If state says `stage=X status=running`, you were mi
Do NOT jump to code. Understand the requirement first.
- **Restate + quick scan.** Restate the requirement. Do a fast targeted scan of the relevant code (use the `Explore` agent for breadth; the **brainstorming** skill if the requirement is fuzzy) so your questions are grounded in what actually exists.
- **Frontloaded Q&A.** Ask the human **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. **Sibling-surface check (mandatory):** if your scan shows the app has N parallel surfaces of the pattern the requirement touches (e.g. several foldered areas, several list pages sharing a component) and the requirement names fewer than N, explicitly ask "this exists in [all N places] — apply to all, or only [the named ones]?" A missed sibling here costs a full second pipeline pass when a reviewer catches it on the PR. Then activate a clean feature slot for this run: `ccam feature activate <slug> --title "<short title>"` — Task B's `activate` archives whatever feature was previously active on this lane automatically, so this run's dashboard state starts clean without a separate "init" step. Then mark intake: `ccam stage intake --status running`. Announce "Questions answered — going autonomous now." After this, don't ask the human anything unless you hit a `blocked` escalation.
- **Frontloaded Q&A.** Ask the human **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. **Sibling-surface check (mandatory):** if your scan shows the app has N parallel surfaces of the pattern the requirement touches (e.g. several foldered areas, several list pages sharing a component) and the requirement names fewer than N, explicitly ask "this exists in [all N places] — apply to all, or only [the named ones]?" A missed sibling here costs a full second pipeline pass when a reviewer catches it on the PR. Then activate a clean feature slot for this run: `agenthub feature activate <slug> --title "<short title>"` — Task B's `activate` archives whatever feature was previously active on this lane automatically, so this run's dashboard state starts clean without a separate "init" step. Then mark intake: `agenthub stage intake --status running`. Announce "Questions answered — going autonomous now." After this, don't ask the human anything unless you hit a `blocked` escalation.
### 0b — Investigate & plan *(autonomous)*
- `ccam stage plan` — now design a real plan and have it independently challenged before you implement.
- `agenthub stage plan` — now design a real plan and have it independently challenged before you implement.
- **Investigate (autonomous, thorough).** Read the actual code paths, models, existing tests, and conventions the feature touches — `Explore`/`general-purpose` subagents for breadth, then read the key files yourself for depth. Pin down: integration points, data/migration needs, API/contract impact, reuse opportunities, and risks. Use **systematic-debugging** if the feature is a fix (root-cause first, no symptom patches).
- **Plan.** Produce a concrete implementation plan (the **writing-plans** skill): approach, files to change, the test strategy (which behaviors/edge cases each test will pin), migration/contract impact, and how each acceptance criterion is met.
- **Debate the plan (adversarial review).** Spawn a SEPARATE sub-agent (Agent tool — `Plan` or `general-purpose`) to critique the plan + investigation: missed requirements, wrong assumptions, a simpler approach, unhandled edge cases, acceptance-criteria gaps. Apply the worthwhile critiques (use **receiving-code-review** judgment — verify each point, don't blindly accept or reject). Iterate once or twice until the plan holds up.
@@ -472,32 +472,32 @@ Do NOT jump to code. Understand the requirement first.
### 1 — Implement (TDD, to the plan)
- Choose a **single-segment slug** for the feature — lowercase, hyphens, NO slashes. Cut the feature branch from **development** (the PR base): `git fetch origin && git checkout -b feat/<slug> origin/development`.
- If Stage 0 activated a placeholder slug different from the final chosen one, reconcile: `ccam feature activate <slug>` — it echoes back the canonicalized slug it actually stored; use THAT for the branch and every later reference.
- If Stage 0 activated a placeholder slug different from the final chosen one, reconcile: `agenthub feature activate <slug>` — it echoes back the canonicalized slug it actually stored; use THAT for the branch and every later reference.
- Implement the agreed plan with the **test-driven-development** skill: failing test → minimal code → green → commit. Frequent small commits.
- **Tests must be sharp and meaningful.** Each test pins a real behavior or edge case from the plan / acceptance criteria — cover the happy path, the negative/error path, and boundaries. NO trivial or redundant tests: don't assert constants or framework internals, don't re-test the same path twice, don't pad for coverage. A few precise tests that would actually catch a regression beat many shallow ones.
- **Comment only when it earns its place.** Match the surrounding code's comment density. Comment the non-obvious *why* (intent, invariants, gotchas, links to context) — never narrate the *what* the code already says. Delete redundant/boilerplate/restating comments rather than adding them.
- If your stack generates an API contract/client and the API changed, regenerate it (`ccam lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
- `ccam stage implementing`
- If your stack generates an API contract/client and the API changed, regenerate it (`agenthub lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
- `agenthub stage implementing`
### 2 — Pre-push CI gates + dev preflight (on the feature branch)
- `ccam lanes hook ci-gate` — runs the profile's CI gate (lint / test / contract checks) against an isolated per-lane test DB. On failure: read the output, fix on the feature branch, commit, re-run. Loop until green.
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`ccam stage gates --evidence "sync-base preflight unavailable, skipped"`).
- `agenthub lanes hook ci-gate` — runs the profile's CI gate (lint / test / contract checks) against an isolated per-lane test DB. On failure: read the output, fix on the feature branch, commit, re-run. Loop until green.
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`agenthub stage gates --evidence "sync-base preflight unavailable, skipped"`).
- Once built, its contract is: exit 5 on a migration-number collision (print the exact rename, do it on the feature branch, re-run Stage 2); informational `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise (you do NOT sync the branch for it — GitHub merges non-conflicting histories fine).
### 3 — E2E on the feature branch
The e2e hook doesn't run migrations itself — it tests the already-running stack. To exercise the feature's code and any new schema, boot the lane stack with the feature branch first:
- `ccam stage e2e-feature --status running`
- `ccam lanes up --qc` — boots with the profile's QC env (mock/stub flags so QC is deterministic, from `QC_BOOT_ENV`), applies the feature branch's own migrations, and reboots the stack. Idempotent; safe to re-run. (The branch was cut from `origin/development`, so this stack IS development + your feature as of the branch point.)
- `ccam lanes hook e2e` — Playwright e2e under the e2e lock against the now-booted stack. **This is the only e2e gate in the flow** — there is no dev-merged suite behind it.
- `agenthub stage e2e-feature --status running`
- `agenthub lanes up --qc` — boots with the profile's QC env (mock/stub flags so QC is deterministic, from `QC_BOOT_ENV`), applies the feature branch's own migrations, and reboots the stack. Idempotent; safe to re-run. (The branch was cut from `origin/development`, so this stack IS development + your feature as of the branch point.)
- `agenthub lanes hook e2e` — Playwright e2e under the e2e lock against the now-booted stack. **This is the only e2e gate in the flow** — there is no dev-merged suite behind it.
- On failure: fix on the feature branch, commit, re-run from Stage 2.
- **Iterating on a failing spec:** use scoped runs through the hook — `ccam lanes hook e2e -- <spec file/filter>` — never a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout, and the heartbeat, so the dashboard false-flags STALLED). A scoped green is never the gate; finish with the full suite (unless the localized fast-path applies — see Hard rules).
- On success: `ccam stage e2e-feature-passed --status running`
- **Iterating on a failing spec:** use scoped runs through the hook — `agenthub lanes hook e2e -- <spec file/filter>` — never a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout, and the heartbeat, so the dashboard false-flags STALLED). A scoped green is never the gate; finish with the full suite (unless the localized fast-path applies — see Hard rules).
- On success: `agenthub stage e2e-feature-passed --status running`
### 4 — Code review *(no open PR yet — use local diff)*
- Run the **`code-review` skill at effort `high`** on the feature diff vs `origin/development` — this is the deterministic code-review gate, not an ad-hoc read. The PR isn't open yet, so point it at the local diff: `git diff origin/development...feat/<slug>` (and `git log origin/development..feat/<slug>` for commits). ONLY if the `code-review` skill is unavailable, fall back to a manual review of that diff (correctness, security, tests, migration/contract safety). The Stage-6 `qc-local` report covers the user-flow review for the senior gate.
- Apply the fixes worth making on the feature branch; if you change code, re-run **from Stage 2**.
- `ccam stage review`
- `agenthub stage review`
### 5 — QC plan *(bound the test scope before any browser QC)*
- Author a **QC Plan** the browser-QC agents (Stage 6 now, and a future Stage 13 once dev-QC exists) will execute against — so QC covers everything that matters and nothing that doesn't (no missed scenarios, no wandering into unrelated areas). Derive it from the acceptance points (lane spec) + the real change surface (`git diff origin/development...feat/<slug>` and `--stat`). Three parts:
@@ -505,11 +505,11 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
- **Out-of-scope** (explicit): areas NOT to test because the change cannot affect them — this is what stops QC from over-testing.
- **Smoke set**: login + main nav + ≥3 unaffected major areas.
- Append it to the lane spec under a `## QC Plan` heading (`docs/superpowers/specs/lane-<slug>.md`) — the same file the senior gate reads. You are the **single writer** of this section; the QC agent only *proposes* additions in its report and you fold them in (Stage 6). This keeps the plan race-free yet living.
- `ccam stage qc-plan --status running`
- `agenthub stage qc-plan --status running`
### 6 — Browser QC via the qc-local agent
- **Test-only fast-path:** on a fix-loop re-entry whose change is ENTIRELY test files (see the fix-loop rule), SKIP this stage — the app's runtime UI is unchanged — and record `--evidence "QC skipped: test-only change"`. Otherwise run it:
- `ccam stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local` — FOREGROUND; it gates the pipeline. **This agent does not exist yet as of this skill's authoring — a separate, later task ports it.** Until then, this stage cannot run; treat a lane that reaches here as `--status blocked --note "qc-local agent not yet available"` and report to the human). When it exists, give it: the lane's working directory, the feature slug, the feature title, the acceptance points (lane spec), and the **QC Plan** (lane spec, Stage 5) as the authoritative scope to execute against. It owns the whole local browser QC and proof capture (`ccam lanes proof-link` first, then screenshots land under the proof gallery automatically). It runs against the lane's feature-branch stack from Stage 3. Do NOT drive the browser yourself at this stage.
- `agenthub stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local` — FOREGROUND; it gates the pipeline. **This agent does not exist yet as of this skill's authoring — a separate, later task ports it.** Until then, this stage cannot run; treat a lane that reaches here as `--status blocked --note "qc-local agent not yet available"` and report to the human). When it exists, give it: the lane's working directory, the feature slug, the feature title, the acceptance points (lane spec), and the **QC Plan** (lane spec, Stage 5) as the authoritative scope to execute against. It owns the whole local browser QC and proof capture (`agenthub lanes proof-link` first, then screenshots land under the proof gallery automatically). It runs against the lane's feature-branch stack from Stage 3. Do NOT drive the browser yourself at this stage.
- Parse its last line: `LOCAL-QC: PASS` → continue. `LOCAL-QC: FAIL — <reasons>` → fix on the feature branch → re-run from Stage 2. Keep its report — it is the feature user-flow review for the senior gate.
- **Fold back discoveries:** if its report lists scenarios it found that weren't in the plan (its "Scenarios discovered during QC" section), add them to the `## QC Plan` in-scope list in the lane spec.
@@ -518,39 +518,39 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
- Parse its final line:
- `VERDICT: GO` → proceed to Stage 8.
- `VERDICT: NO-GO — <fixes>` → fix on the feature branch, re-run **from Stage 2**. No attempt cap — the loop re-enters; the dashboard's time-on-stage surfaces a lane stuck cycling so the human can step in. Set `--status blocked` only for a genuine blocker you can't resolve.
- `ccam stage gate --evidence "GO"` (or `NO-GO — <reason>`)
- `agenthub stage gate --evidence "GO"` (or `NO-GO — <reason>`)
### 8 — Publish: push branch + open/update PR *(GATED — only on GO)*
- `ccam stage publishing --status running`
- Re-run the preflight if `ccam lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
- `agenthub stage publishing --status running`
- Re-run the preflight if `agenthub lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
- `git push -u origin feat/<slug>` — first push of the feature branch to remote. All gates have passed before this point; the PR is finalized before reviewers see it.
- Open or update the PR **based on and targeting `development`**: `gh pr create --base development --fill` (or `gh pr edit` / the push itself if a prior run already created it). Capture the URL.
- `ccam stage pr-open --evidence "<pr-url>"` — the dashboard shows the PR link from here (via `--evidence` in `ccam feature show`/`ccam lanes`).
- `agenthub stage pr-open --evidence "<pr-url>"` — the dashboard shows the PR link from here (via `--evidence` in `agenthub feature show`/`agenthub lanes`).
### 9 — Ticket *(currently SKIPPED — tracker integration is off)*
- Tracker integration is hardcoded off (see Setup). Do nothing here; do not attempt to file a ticket. When F ships `ccam lanes integration tracker`, this stage gets a real implementation.
- Tracker integration is hardcoded off (see Setup). Do nothing here; do not attempt to file a ticket. When F ships `agenthub lanes integration tracker`, this stage gets a real implementation.
### 10 — CI watch on the PR *(non-blocking)*
- Check the PR's CI with `gh pr checks` / `gh`. Green → continue to the report + watch — never idle waiting for green.
- **Red CI → triage before the fix-loop** (shared CI flakes under multi-lane load; treating every red as your defect wastes cycles):
1. Read WHICH job/tests failed with `gh` (there is no `ccam ci` yet — F builds that; use `gh run view`/`gh pr checks --watch` directly).
1. Read WHICH job/tests failed with `gh` (there is no `agenthub ci` yet — F builds that; use `gh run view`/`gh pr checks --watch` directly).
2. **Your tests / your code implicated** → real failure: fix on the feature branch, re-enter from Stage 2.
3. **Infra/flake signature** (a job with no test failures, OOM/contention on the shared runner, a hung job with no output, or a test that is green locally on the identical tree) → re-run the workflow via `gh run rerun <run-id> --failed` — ONCE. Still red after the rerun → treat it as real (or escalate with the evidence). Never rerun more than twice, and never push an empty commit to re-trigger CI.
4. A **hung** workflow (running way past its normal duration with no output) → `gh run cancel <run-id>` then rerun once.
### 11 — Report
- Post a concise report: PR URL, CI status, what shipped, and that the PR now **awaits a human merge** (this pipeline never merges).
- `ccam stage reported --status running`
- `agenthub stage reported --status running`
- Do **NOT** clear or reset the lane's feature state. Cleanup is the human's call, from the dashboard — they may still be manually testing.
### 12 — Watch the PR *(until a human merges or closes it)*
- `ccam stage watching-pr --note "watching PR for comments + base conflicts + the merge"`
- `agenthub stage watching-pr --note "watching PR for comments + base conflicts + the merge"`
- Loop every ~5 minutes, paced by a **backgrounded** wait so the turn isn't pinned (see the waiting-primitive rule above): run `sleep 300` with `run_in_background: true` — you're re-invoked when it elapses. Each iteration:
- Check PR state: `gh pr view <pr_url> --json state,mergeable -q '.state + " " + (.mergeable|tostring)'`, and bump the heartbeat (`ccam stage watching-pr`).
- Check PR state: `gh pr view <pr_url> --json state,mergeable -q '.state + " " + (.mergeable|tostring)'`, and bump the heartbeat (`agenthub stage watching-pr`).
- `MERGED` → a human merged it: go to **Stage 13** (post-merge verification).
- `CLOSED` (unmerged) → the human rejected/abandoned it: `ccam stage done --status passed --note "PR closed unmerged by human"` → STOP.
- `CLOSED` (unmerged) → the human rejected/abandoned it: `agenthub stage done --status passed --note "PR closed unmerged by human"` → STOP.
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
- If `ccam lanes sync-base` exists by now: `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
- If `agenthub lanes sync-base` exists by now: `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
- If it doesn't exist yet: `git fetch origin && git merge origin/development` directly on the feature branch, resolve conflicts the same way, commit.
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
- For each new PR comment (list with `gh pr view <pr_url> --json comments`, tracking which you've already handled by comment id in your own notes), triage AND **always reply on its thread** (every comment gets a response — no silent handling, so reviewers see it was considered):
@@ -561,15 +561,15 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
- Nothing new → background another `sleep 300` (`run_in_background: true`) and end the turn; you'll be re-invoked for the next poll. A PR can sit for days — that's fine.
### 13 — Post-merge verification *(currently SKIPPED — CI-wait and dev-QC integrations are off)*
- `ccam stage merged --status running --note "PR merged — post-merge verification unavailable (F not built)"`
- `agenthub stage merged --status running --note "PR merged — post-merge verification unavailable (F not built)"`
- Both dev-CI-wait and dev-QC are hardcoded off (see Setup). Go straight to Stage 14. When F ships these integrations, this stage gets its real implementation (mirroring Shipyard's: dev CI watch, then a `dev-qc` agent QCing the deployed site, looping to a Stage 15 follow-up-fix pattern on any issue found).
### 14 — Done
- Post the final report: PR merged, that post-merge verification is unavailable pending F.
- `ccam stage done --status passed --note "merged; post-merge verification unavailable (F not built)"` → STOP (leave the lane for the human to clear from the dashboard whenever).
- `agenthub stage done --status passed --note "merged; post-merge verification unavailable (F not built)"` → STOP (leave the lane for the human to clear from the dashboard whenever).
## Escalation
Whenever you STOP early (an ambiguous merge conflict, an unexpected/unrecoverable failure, a missing agent this skill depends on), set `ccam stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
Whenever you STOP early (an ambiguous merge conflict, an unexpected/unrecoverable failure, a missing agent this skill depends on), set `agenthub stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
````
- [ ] **Step 2: Verify frontmatter parses**
@@ -601,22 +601,22 @@ git commit -m "feat(lanes): port ship-feature-lane skill (Stages 0-14, E1)"
- [ ] **Step 1: Dry run on a scratch repo**
Create a throwaway repo with a minimal profile (reuse this repo's existing profile fixtures from `server/__tests__/` as a template — search `grep -rl "profile.env" server/__tests__/*.test.js` for one that builds a full working `bootstrap`/`boot`/`ci-gate` hook set) and a lane pointed at it via `ccam lanes add`. Walk Stage 0 through Stage 2 by hand, running the exact commands the skill text specifies:
Create a throwaway repo with a minimal profile (reuse this repo's existing profile fixtures from `server/__tests__/` as a template — search `grep -rl "profile.env" server/__tests__/*.test.js` for one that builds a full working `bootstrap`/`boot`/`ci-gate` hook set) and a lane pointed at it via `agenthub lanes add`. Walk Stage 0 through Stage 2 by hand, running the exact commands the skill text specifies:
```bash
ccam feature activate smoke-test --title "Dry run"
ccam stage intake --status running
ccam stage plan
ccam stage implementing
ccam lanes hook ci-gate
ccam stage gates --evidence "sync-base preflight unavailable, skipped"
agenthub feature activate smoke-test --title "Dry run"
agenthub stage intake --status running
agenthub stage plan
agenthub stage implementing
agenthub lanes hook ci-gate
agenthub stage gates --evidence "sync-base preflight unavailable, skipped"
```
Expected: every command exits 0 and the lane's stage visibly advances (`ccam feature show smoke-test` after each step). Also exercise Stage 3's new pieces in isolation:
Expected: every command exits 0 and the lane's stage visibly advances (`agenthub feature show smoke-test` after each step). Also exercise Stage 3's new pieces in isolation:
```bash
ccam lanes up --qc
ccam lanes hook e2e
agenthub lanes up --qc
agenthub lanes hook e2e
```
Expected: `up --qc` succeeds (or fails with a clear profile-level error if the scratch profile has no e2e hook — that's fine, the point is `--qc` itself doesn't error).
@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ccam lanes agents install` writes the `qc-local` and `senior-gate-reviewer` agent definitions into a lane's `.claude/agents/`, so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 6 and Stage 7 stop being hard-blocked on agents that don't exist.
**Goal:** `agenthub lanes agents install` writes the `qc-local` and `senior-gate-reviewer` agent definitions into a lane's `.claude/agents/`, so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 6 and Stage 7 stop being hard-blocked on agents that don't exist.
**Architecture:** Two static, CCAM-final agent template files (no runtime templating — every Shipyard placeholder/script reference is resolved once, at port time, into the template text itself), one small filesystem-only library module, one route, one CLI subcommand.
**Architecture:** Two static, AgentHub-final agent template files (no runtime templating — every Shipyard placeholder/script reference is resolved once, at port time, into the template text itself), one small filesystem-only library module, one route, one CLI subcommand.
**Tech Stack:** Existing `git(cwd, args)` helper (`server/lib/worktree.js`) — needed for exactly one call (`--git-common-dir`), same git-worktree correctness concern E2 had for `.git/info/attributes`. Plain `node:fs` for everything else.
@@ -39,7 +39,7 @@ Write `server/data/agent-templates/ship-feature-lane/qc-local.md` with this exac
```markdown
---
name: "qc-local"
description: "Local browser-QC agent for the ship-feature-lane pipeline in CCAM. Launched by the ship-feature-lane skill at Stage 6 (foreground — it gates the pipeline) after e2e, against the lane's OWN feature-branch stack (booted via `ccam lanes up --qc`; the branch is cut from origin/development, so the stack is development + this feature). Exercises the feature + smoke + reload/re-login coverage via the lane's local-QC Playwright MCP, saves every screenshot to `proof/<feature-slug>/qc-local/` (the CCAM proof gallery's canonical path), and returns a parseable LOCAL-QC: PASS/FAIL verdict that doubles as the feature user-flow review for the senior gate. <example>Context: ship-feature-lane in a lane finished Stage 3's e2e green. assistant: 'e2e is green — launching the qc-local agent to browser-QC the feature-branch stack and capture the proof gallery.' <commentary>Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser.</commentary></example>"
description: "Local browser-QC agent for the ship-feature-lane pipeline in AgentHub. Launched by the ship-feature-lane skill at Stage 6 (foreground — it gates the pipeline) after e2e, against the lane's OWN feature-branch stack (booted via `agenthub lanes up --qc`; the branch is cut from origin/development, so the stack is development + this feature). Exercises the feature + smoke + reload/re-login coverage via the lane's local-QC Playwright MCP, saves every screenshot to `proof/<feature-slug>/qc-local/` (the AgentHub proof gallery's canonical path), and returns a parseable LOCAL-QC: PASS/FAIL verdict that doubles as the feature user-flow review for the senior gate. <example>Context: ship-feature-lane in a lane finished Stage 3's e2e green. assistant: 'e2e is green — launching the qc-local agent to browser-QC the feature-branch stack and capture the proof gallery.' <commentary>Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser.</commentary></example>"
model: opus
color: green
memory: project
@@ -49,15 +49,15 @@ You are an elite QA automation engineer validating ONE lane's feature on that la
## Context you will be given (by the ship-feature-lane skill)
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically; there is no marker file to check).
- **The lane's working directory** (your cwd — AgentHub resolves the lane from it automatically; there is no marker file to check).
- **Feature slug** (the `<slug>` of `feat/<slug>`), feature title, and the acceptance points to verify (from the lane spec at `docs/superpowers/specs/lane-<slug>.md` — read it yourself if the summary is thin; `git diff origin/development...HEAD` shows the change surface).
Setup: your cwd is the lane clone. You do NOT write `stage`/`status`/`notes` — the main session owns all state; you have no heartbeat call to make (you run as a synchronous subagent inside the driving session's own turn, so the lane is never seen as stalled while you work).
## Target environment
- **App**: the lane's own booted frontend — check `ccam lanes runtime` (or the `FE_PORT`/equivalent env the skill already resolved) for the actual port; it is isolated per lane. If it's down, don't boot it yourself blindly — return `LOCAL-QC: FAIL — lane stack down (re-run: ccam lanes up --no-build)`.
- **QC boot env**: the QC stack boots via `ccam lanes up --qc`, which applies the profile's `QC_BOOT_ENV` (typically mock/stub flags) — LLM/paid-backend features then return CANNED responses. Canned/stub content is EXPECTED there, not a bug; judge the surrounding UX (loading states, rendering, persistence), not the stub text itself.
- **App**: the lane's own booted frontend — check `agenthub lanes runtime` (or the `FE_PORT`/equivalent env the skill already resolved) for the actual port; it is isolated per lane. If it's down, don't boot it yourself blindly — return `LOCAL-QC: FAIL — lane stack down (re-run: agenthub lanes up --no-build)`.
- **QC boot env**: the QC stack boots via `agenthub lanes up --qc`, which applies the profile's `QC_BOOT_ENV` (typically mock/stub flags) — LLM/paid-backend features then return CANNED responses. Canned/stub content is EXPECTED there, not a bug; judge the surrounding UX (loading states, rendering, persistence), not the stub text itself.
- **Account / login.** Two paths:
- **If a credentials block is embedded at the TOP of this agent** ("⚙ This lane's local-QC credentials"): those ARE your login — type them into the form yourself (`browser_type`, submit) when you hit a login page. You already have them; never read a file/script to fetch them.
- **If there is NO embedded block:** the app should already be in a usable/logged-in state (or requires no login for the flows you're testing). If you hit a login page you cannot get past, this profile has no seeded QC account configured yet — return `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile`. Do not guess at credentials or invent a login flow.
@@ -79,9 +79,9 @@ Setup: your cwd is the lane clone. You do NOT write `stage`/`status`/`notes` —
2. **Execute with the local-QC MCP's tools**:
- Use `browser_type` for React controlled inputs — never direct DOM value assignment (it doesn't fire onChange).
- After each meaningful action: `browser_snapshot` to verify state, then screenshot (see proof convention).
- Don't trust `browser_network_requests` alone for HTTP verification (it double-lists requests); prefer the lane's own backend/service logs if the profile exposes them (`ccam lanes logs <svc>`).
- Don't trust `browser_network_requests` alone for HTTP verification (it double-lists requests); prefer the lane's own backend/service logs if the profile exposes them (`agenthub lanes logs <svc>`).
- **Failure triage**: capture a failure screenshot + `browser_snapshot` + `browser_console_messages`, note repro steps, expected vs actual, severity (blocker/major/minor/cosmetic) — then CONTINUE with remaining scenarios unless the app is unusable.
3. **Proof convention (mandatory — the path is fixed infrastructure, NOT your choice):** EVERY screenshot via `browser_take_screenshot` with `filename: "proof/<feature-slug>/qc-local/<NN>-<what>.png"`, numbered in flow order (e.g. `proof/edit-report/qc-local/03-dialog-open.png`). The MCP server's pinned `--output-dir` lands these in `<lane clone>/.playwright-mcp/proof/...` — the ONLY place the dashboard's gallery reads (`ccam lanes proof-link` converges a stray `proof/` symlink if needed). Absolute paths, other folders, or invented layouts = the proof is lost.
3. **Proof convention (mandatory — the path is fixed infrastructure, NOT your choice):** EVERY screenshot via `browser_take_screenshot` with `filename: "proof/<feature-slug>/qc-local/<NN>-<what>.png"`, numbered in flow order (e.g. `proof/edit-report/qc-local/03-dialog-open.png`). The MCP server's pinned `--output-dir` lands these in `<lane clone>/.playwright-mcp/proof/...` — the ONLY place the dashboard's gallery reads (`agenthub lanes proof-link` converges a stray `proof/` symlink if needed). Absolute paths, other folders, or invented layouts = the proof is lost.
## Checkpoint & resume *(rate-limits and crashes must not zero your work)*
@@ -115,12 +115,12 @@ Record local-stack QA knowledge as you find it: flaky selectors/flows, features
- [ ] **Step 2: Write `senior-gate-reviewer.md`**
Write `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` with this exact content (ported from `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/senior-gate-reviewer.md` — `@@HARNESS_ROOT@@` dropped, `lane-sync-dev.sh --check` mapped to `ccam lanes sync-base --check`, `profile-cat.sh review-checks.md` mapped to a direct Read-tool reference; everything else carried over verbatim):
Write `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` with this exact content (ported from `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/senior-gate-reviewer.md` — `@@HARNESS_ROOT@@` dropped, `lane-sync-dev.sh --check` mapped to `agenthub lanes sync-base --check`, `profile-cat.sh review-checks.md` mapped to a direct Read-tool reference; everything else carried over verbatim):
```markdown
---
name: "senior-gate-reviewer"
description: "Final GO/NO-GO publish-readiness gate for the ship-feature-lane pipeline in CCAM. Invoke AFTER the code-review and feature-user-flow-review (qc-local) and AFTER local CI gates + e2e + QC have passed, immediately BEFORE the feature branch is pushed and its PR is opened (base `development`). Returns a single GO or NO-GO verdict with a required-fix list. A GO is the sole authorization to publish the PR. <example>Context: ship-feature-lane in a lane has finished implementation, gates, and reviews and is about to publish the PR. assistant: 'I'll launch the senior-gate-reviewer agent to make the final go/no-go decision before pushing the branch and opening the PR.' <commentary>This is the publish gate — it decides whether the feature is allowed in front of reviewers.</commentary></example>"
description: "Final GO/NO-GO publish-readiness gate for the ship-feature-lane pipeline in AgentHub. Invoke AFTER the code-review and feature-user-flow-review (qc-local) and AFTER local CI gates + e2e + QC have passed, immediately BEFORE the feature branch is pushed and its PR is opened (base `development`). Returns a single GO or NO-GO verdict with a required-fix list. A GO is the sole authorization to publish the PR. <example>Context: ship-feature-lane in a lane has finished implementation, gates, and reviews and is about to publish the PR. assistant: 'I'll launch the senior-gate-reviewer agent to make the final go/no-go decision before pushing the branch and opening the PR.' <commentary>This is the publish gate — it decides whether the feature is allowed in front of reviewers.</commentary></example>"
model: opus
color: red
memory: project
@@ -130,11 +130,11 @@ You are the **Senior Gate Reviewer** — the final, independent publish-readines
## Context you will be given (by the ship-feature-lane skill)
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically).
- **The lane's working directory** (your cwd — AgentHub resolves the lane from it automatically).
- **The original requirement** and the **upfront Q&A answers** (acceptance criteria).
- The **feature branch name** (cut from `origin/development` — the PR base it will target).
- A summary of the **two prior reviews** (code-review + the qc-local user-flow review) and which findings were resolved.
- The **`ccam lanes sync-base --check` preflight output** (migration-collision check + how far `origin/development` moved since the branch's merge-base).
- The **`agenthub lanes sync-base --check` preflight output** (migration-collision check + how far `origin/development` moved since the branch's merge-base).
- Confirmation that **local CI gates** (the profile's lint / test / contract checks), **e2e**, and **browser QC** already passed.
If any of this context is missing, gather it yourself (read the lane spec file the skill wrote, run `git -C <lane> log/diff`).
@@ -147,8 +147,8 @@ If any of this context is missing, gather it yourself (read the lane spec file t
2. **Findings resolved** — Were the PR-code-review and user-flow-review findings actually addressed (not just acknowledged)? Spot-check **the PR diff** for each claimed fix. Unresolved material findings ⇒ NO-GO.
3. **Merge-safety** — Independently verify the branch is safe for a human to merge into `development`. **General principle (any stack):** "the branch is green standalone" is NOT proof the MERGE will be — check for collisions that DON'T surface as git conflicts — duplicate migration identifiers, duplicate fixture/test IDs, API/schema-contract drift, lockfile divergence — in whatever form this stack expresses them, comparing the branch against the CURRENT `origin/development`. Re-run the deterministic preflight yourself if the provided output is stale: `git -C <lane> fetch origin && ccam lanes sync-base --check <feature-branch>` (exit 5 = collision ⇒ NO-GO with the printed rename; also read its `DEV_DELTA`/`DEV_OVERLAP` — a large overlapping upstream delta means the locally-QC'd behavior may not match post-merge dev, weigh it).
- **Stack-specific checks (from the active profile):** read `<lane>/.ccam/profile/review-checks.md` with the Read tool if it exists, and apply every check it lists (e.g. migration-identifier collisions, API/contract regeneration). Missing or empty → derive the equivalents yourself from the general principle by inspecting the branch vs `origin/development`.
3. **Merge-safety** — Independently verify the branch is safe for a human to merge into `development`. **General principle (any stack):** "the branch is green standalone" is NOT proof the MERGE will be — check for collisions that DON'T surface as git conflicts — duplicate migration identifiers, duplicate fixture/test IDs, API/schema-contract drift, lockfile divergence — in whatever form this stack expresses them, comparing the branch against the CURRENT `origin/development`. Re-run the deterministic preflight yourself if the provided output is stale: `git -C <lane> fetch origin && agenthub lanes sync-base --check <feature-branch>` (exit 5 = collision ⇒ NO-GO with the printed rename; also read its `DEV_DELTA`/`DEV_OVERLAP` — a large overlapping upstream delta means the locally-QC'd behavior may not match post-merge dev, weigh it).
- **Stack-specific checks (from the active profile):** read `<lane>/.agenthub/profile/review-checks.md` with the Read tool if it exists, and apply every check it lists (e.g. migration-identifier collisions, API/contract regeneration). Missing or empty → derive the equivalents yourself from the general principle by inspecting the branch vs `origin/development`.
- **Obvious regressions / scope creep**: scan **the PR diff** for debug code, secrets, commented-out blocks, unrelated churn, `console.log`/`print` debugging, TODO/FIXME left in critical paths.
4. **UI/UX diligence (when the feature touches any form/screen)** — don't rubber-stamp the QC report; confirm it actually exercised layout rigor, because these defects slip through happy-path QC:
@@ -242,7 +242,7 @@ const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-agents-"));
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-agents-"));
const laneAgents = require("../lib/lane-agents");
@@ -505,21 +505,21 @@ git commit -m "feat(lanes): add POST /:id/agents/install route (E3)"
---
### Task 4: `ccam lanes agents install` CLI
### Task 4: `agenthub lanes agents install` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `POST /api/lanes/:id/agents/install` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
- Produces: `ccam lanes agents install [<id>]`.
- Consumes: `POST /api/lanes/:id/agents/install` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
- Produces: `agenthub lanes agents install [<id>]`.
- [ ] **Step 1: Add the subcommand function**
In `bin/ccam.js`, add a new function near `cmdLanesProofLink` (`bin/ccam.js:2147`):
In `bin/agenthub.js`, add a new function near `cmdLanesProofLink` (`bin/agenthub.js:2147`):
```js
/** `ccam lanes agents install [<id>] [--cwd path]` — write the ship-feature-lane
/** `agenthub lanes agents install [<id>] [--cwd path]` — write the ship-feature-lane
* agent templates (qc-local, senior-gate-reviewer) into <lane>/.claude/agents/,
* git-excluded. Never run automatically; a session calls it explicitly. */
async function cmdLanesAgentsInstall(args) {
@@ -541,7 +541,7 @@ async function cmdLanesAgentsInstall(args) {
- [ ] **Step 2: Wire the dispatcher**
In `bin/ccam.js`'s `lanes` case (`bin/ccam.js:3072` area, right after the `proof-link` line):
In `bin/agenthub.js`'s `lanes` case (`bin/agenthub.js:3072` area, right after the `proof-link` line):
```js
if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));
@@ -557,7 +557,7 @@ Add directly below it:
- [ ] **Step 3: Add the help-catalog entry**
Right after the `lanes proof-link` catalog entry (`bin/ccam.js:2270-2273` area):
Right after the `lanes proof-link` catalog entry (`bin/agenthub.js:2270-2273` area):
```js
[
@@ -572,7 +572,7 @@ Right after the `lanes proof-link` catalog entry (`bin/ccam.js:2270-2273` area):
```bash
npm run dev &
sleep 3
node bin/ccam.js lanes agents install 1
node bin/agenthub.js lanes agents install 1
echo "exit: $?"
```
@@ -588,8 +588,8 @@ npm run test:server
- [ ] **Step 6: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes agents install CLI (E3)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes agents install CLI (E3)"
```
---
@@ -612,10 +612,10 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)` (search `grep -n
Stage 6 (browser QC) and Stage 7 (senior GO/NO-GO gate) run as subagents — `qc-local` and `senior-gate-reviewer` — that must exist in the lane's own `.claude/agents/` before the skill can launch them:
```bash
ccam lanes agents install
agenthub lanes agents install
```
Writes both agent templates into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `ccam lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a CCAM upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
Writes both agent templates into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `agenthub lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a AgentHub upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
**Credentials are not embedded.** Unlike Shipyard's original per-lane agent generation, these templates carry no QA account credentials — this repo has no seeded-QA-account system yet. `qc-local` degrades gracefully: if it hits a login page with no credentials block present, it reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile` rather than guessing.
@@ -637,7 +637,7 @@ Until these agents are ported, a lane reaching Stage 6 or 7 will be marked `bloc
Replace with:
```
**Stages 6 and 7 are now unblocked** — run `ccam lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
**Stages 6 and 7 are now unblocked** — run `agenthub lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
See [the skill text](../.claude/skills/ship-feature-lane/SKILL.md) for the full pipeline definition.
```
@@ -665,5 +665,5 @@ npm run test:server
```bash
git add docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ccam lanes agents install (E3)"
git commit -m "docs(lanes): document agenthub lanes agents install (E3)"
```
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ccam lanes integration <name>` reads a lane's `.ccam/profile/integrations.env` and reports whether `<NAME>_ENABLED=1` is set, exit 0/1 — so a driving skill can gate a stage on a real check instead of a hardcoded assumption.
**Goal:** `agenthub lanes integration <name>` reads a lane's `.agenthub/profile/integrations.env` and reports whether `<NAME>_ENABLED=1` is set, exit 0/1 — so a driving skill can gate a stage on a real check instead of a hardcoded assumption.
**Architecture:** One function in the existing `lane-profile.js` (reusing its already-exported `parseEnvFile`), one lightweight route, one CLI subcommand. No new files beyond a test.
@@ -43,7 +43,7 @@ describe("isIntegrationEnabled", () => {
const lane = makeLane();
writeProfile(lane.cwd, "PORTS=api\n");
fs.writeFileSync(
path.join(lane.cwd, ".ccam", "profile", "integrations.env"),
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
"TRACKER_ENABLED=1\nTRACKER_PROJECT=demo\n"
);
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), true);
@@ -53,7 +53,7 @@ describe("isIntegrationEnabled", () => {
const lane = makeLane();
writeProfile(lane.cwd, "PORTS=api\n");
fs.writeFileSync(
path.join(lane.cwd, ".ccam", "profile", "integrations.env"),
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
"DEV_QC_ENABLED=0\n"
);
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "dev_qc"), false);
@@ -74,7 +74,7 @@ In `server/lib/lane-profile.js`, add near `resolveProfile` (after it, since it r
```js
/**
* Whether a named integration is turned on for this lane — reads
* .ccam/profile/integrations.env (same two-location search as profile.env:
* .agenthub/profile/integrations.env (same two-location search as profile.env:
* the lane's own working copy first, the source repo second) and checks
* <NAME>_ENABLED=1. A missing file or missing key is off, never an error —
* same off-by-default shape every other optional declaration follows.
@@ -144,11 +144,11 @@ git commit -m "feat(lanes): add isIntegrationEnabled toggle check (F3a)"
**Files:**
- Modify: `server/routes/lanes.js`
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `isIntegrationEnabled(lane, name) => boolean` from Task 1 (`require("../lib/lane-profile")` — already imported in `server/routes/lanes.js` for `HOOKS`/`runHook`/`resolveProfile`, just add `isIntegrationEnabled` to that existing destructure).
- Produces: `GET /api/lanes/:id/integrations/:name` → `{enabled: boolean}`. `ccam lanes integration <name> [<id>]` → prints `on`/`off`, `process.exitCode` 0 when enabled, 1 when disabled.
- Produces: `GET /api/lanes/:id/integrations/:name` → `{enabled: boolean}`. `agenthub lanes integration <name> [<id>]` → prints `on`/`off`, `process.exitCode` 0 when enabled, 1 when disabled.
- [ ] **Step 1: Add the route**
@@ -163,7 +163,7 @@ Add the route directly after the `/:id/mcp/sync` route (added in F1, search `rou
```js
/**
* Whether a named integration (tracker, dev_qc, ci_wait, ...) is turned on
* for this lane — reads .ccam/profile/integrations.env. Read-only, no
* for this lane — reads .agenthub/profile/integrations.env. Read-only, no
* sameOriginGuard needed (same reasoning GET /:id/git already documents:
* that guard exists for destructive actions).
*/
@@ -176,16 +176,16 @@ router.get("/:id/integrations/:name", (req, res) => {
- [ ] **Step 2: Add the CLI subcommand**
In `bin/ccam.js`, add near `cmdLanesMcpSync`:
In `bin/agenthub.js`, add near `cmdLanesMcpSync`:
```js
/** `ccam lanes integration <name> [<id>]` — check whether a named
/** `agenthub lanes integration <name> [<id>]` — check whether a named
* integration (tracker, dev_qc, ci_wait) is turned on for this lane, per
* its .ccam/profile/integrations.env. Exit 0 = on, 1 = off. */
* its .agenthub/profile/integrations.env. Exit 0 = on, 1 = off. */
async function cmdLanesIntegration(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (!name) {
console.error("usage: ccam lanes integration <name> [<id>]");
console.error("usage: agenthub lanes integration <name> [<id>]");
process.exitCode = 1;
return;
}
@@ -230,7 +230,7 @@ npm run dev &
sleep 3
curl -s http://localhost:4820/api/lanes/1/integrations/tracker
echo
node bin/ccam.js lanes integration tracker 1
node bin/agenthub.js lanes integration tracker 1
echo "exit: $?"
```
@@ -246,8 +246,8 @@ npm run test:server
- [ ] **Step 5: Commit**
```bash
git add server/routes/lanes.js bin/ccam.js
git commit -m "feat(lanes): add ccam lanes integration CLI + route (F3a)"
git add server/routes/lanes.js bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes integration CLI + route (F3a)"
```
---
@@ -266,33 +266,33 @@ git commit -m "feat(lanes): add ccam lanes integration CLI + route (F3a)"
Find:
```
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in CCAM yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `ccam ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in AgentHub yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `agenthub ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
```
Replace with:
```
- **Integration toggles: check, don't assume.** `ccam lanes integration tracker`, `ccam lanes integration dev_qc`, `ccam lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`ccam ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether CCAM can act on it yet.
- **Integration toggles: check, don't assume.** `agenthub lanes integration tracker`, `agenthub lanes integration dev_qc`, `agenthub lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`agenthub ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether AgentHub can act on it yet.
```
- [ ] **Step 2: Stage 9 (ticket) — record the toggle in evidence**
Find the Stage 9 section (search `grep -n "^### 9" .claude/skills/ship-feature-lane/SKILL.md`) and its skip line (likely something like `ccam stage reported --status ... ` or similar noting the skip — read the current text first with `sed -n '/^### 9/,/^### 10/p' .claude/skills/ship-feature-lane/SKILL.md` to get its exact current wording before editing, since this section's precise text was written in E1 and may have shifted slightly across later edits). Add one line noting `ccam lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`ccam lanes integration tracker\` — even when on, no ticketer agent exists yet to act on it)`.
Find the Stage 9 section (search `grep -n "^### 9" .claude/skills/ship-feature-lane/SKILL.md`) and its skip line (likely something like `agenthub stage reported --status ... ` or similar noting the skip — read the current text first with `sed -n '/^### 9/,/^### 10/p' .claude/skills/ship-feature-lane/SKILL.md` to get its exact current wording before editing, since this section's precise text was written in E1 and may have shifted slightly across later edits). Add one line noting `agenthub lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`agenthub lanes integration tracker\` — even when on, no ticketer agent exists yet to act on it)`.
- [ ] **Step 3: `docs/LANES.md` — add a short subsection**
Under `## The ship-feature-lane skill (E1)`, after the `### Syncing MCP servers: mcp sync` subsection (added in F1), insert:
```markdown
### Checking integration toggles: ccam lanes integration
### Checking integration toggles: agenthub lanes integration
A profile can declare `.ccam/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
```bash
ccam lanes integration tracker # exit 0 = on, 1 = off
agenthub lanes integration tracker # exit 0 = on, 1 = off
```
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`ccam ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`agenthub ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.
```
- [ ] **Step 4: Roadmap progress line**
@@ -300,7 +300,7 @@ This only reports what the PROFILE wants — it doesn't file a ticket, run dev-Q
In `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, find the `## F` section's `**Progress:**` line (added in F1) and append:
```
`integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`ccam ci`), and dev-QC remain.
`integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`agenthub ci`), and dev-QC remain.
```
- [ ] **Step 5: Verify and commit**
+19 -19
View File
@@ -1,8 +1,8 @@
# F1 — `ccam lanes mcp sync` Implementation Plan
# F1 — `agenthub lanes mcp sync` Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ccam lanes mcp sync` writes a lane's `.mcp.json` from its source repo's already-configured MCP servers (relocating paths, pinning Playwright's proof output dir) and seeds Chromium browser profiles, so `SKILL.md`'s manual-fallback text stops being the only option.
**Goal:** `agenthub lanes mcp sync` writes a lane's `.mcp.json` from its source repo's already-configured MCP servers (relocating paths, pinning Playwright's proof output dir) and seeds Chromium browser profiles, so `SKILL.md`'s manual-fallback text stops being the only option.
**Architecture:** One filesystem/JSON-only core module (`server/lib/lane-mcp.js` — no git calls, unlike E2/E3's cores), one route, one CLI subcommand, a `SKILL.md` edit.
@@ -48,7 +48,7 @@ const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-mcp-"));
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-mcp-"));
const FAKE_HOME = path.join(SUITE_ROOT, "home");
fs.mkdirSync(FAKE_HOME, { recursive: true });
process.env.HOME = FAKE_HOME;
@@ -479,21 +479,21 @@ git commit -m "feat(lanes): add POST /:id/mcp/sync route (F1)"
---
### Task 3: `ccam lanes mcp sync` CLI
### Task 3: `agenthub lanes mcp sync` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `POST /api/lanes/:id/mcp/sync` (Task 2); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
- Produces: `ccam lanes mcp sync [<id>]`.
- Consumes: `POST /api/lanes/:id/mcp/sync` (Task 2); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
- Produces: `agenthub lanes mcp sync [<id>]`.
- [ ] **Step 1: Add the subcommand function**
In `bin/ccam.js`, add near `cmdLanesAgentsInstall` (`bin/ccam.js:2161`):
In `bin/agenthub.js`, add near `cmdLanesAgentsInstall` (`bin/agenthub.js:2161`):
```js
/** `ccam lanes mcp sync [<id>] [--cwd path]` — relocate the source repo's
/** `agenthub lanes mcp sync [<id>] [--cwd path]` — relocate the source repo's
* already-configured MCP servers into <lane>/.mcp.json and seed Chromium
* profiles. Never run automatically; a session calls it explicitly. */
async function cmdLanesMcpSync(args) {
@@ -518,7 +518,7 @@ async function cmdLanesMcpSync(args) {
- [ ] **Step 2: Wire the dispatcher**
In `bin/ccam.js`'s `lanes` case, right after the `agents install` check (`bin/ccam.js:3186-3188`):
In `bin/agenthub.js`'s `lanes` case, right after the `agents install` check (`bin/agenthub.js:3186-3188`):
```js
if (rest[0] === "agents" && rest[1] === "install") {
@@ -536,7 +536,7 @@ Add directly below it:
- [ ] **Step 3: Add the help-catalog entry**
Right after the `lanes agents install` catalog entry (`bin/ccam.js:2364` area):
Right after the `lanes agents install` catalog entry (`bin/agenthub.js:2364` area):
```js
[
@@ -551,7 +551,7 @@ Right after the `lanes agents install` catalog entry (`bin/ccam.js:2364` area):
```bash
npm run dev &
sleep 3
node bin/ccam.js lanes mcp sync 1
node bin/agenthub.js lanes mcp sync 1
echo "exit: $?"
```
@@ -567,8 +567,8 @@ npm run test:server
- [ ] **Step 6: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes mcp sync CLI (F1)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes mcp sync CLI (F1)"
```
---
@@ -587,13 +587,13 @@ git commit -m "feat(lanes): add ccam lanes mcp sync CLI (F1)"
Find (Setup section, `.claude/skills/ship-feature-lane/SKILL.md:37`):
```
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `ccam lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `agenthub lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
```
Replace with:
```
- **MCP preflight (fail fast):** if this is the lane's first run, or a required Playwright MCP's `browser_*` tools aren't available in this session, run `ccam lanes mcp sync` — it relocates the source repo's already-configured MCP servers into this lane's `.mcp.json`. If the source repo has none configured (`ENOMCPCONFIG`), tell the human to configure `.mcp.json` manually for the source repo first. Either way, restart the session after syncing so the new config loads — `playwright` and a local-QC MCP are always required for Stage 3/6. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
- **MCP preflight (fail fast):** if this is the lane's first run, or a required Playwright MCP's `browser_*` tools aren't available in this session, run `agenthub lanes mcp sync` — it relocates the source repo's already-configured MCP servers into this lane's `.mcp.json`. If the source repo has none configured (`ENOMCPCONFIG`), tell the human to configure `.mcp.json` manually for the source repo first. Either way, restart the session after syncing so the new config loads — `playwright` and a local-QC MCP are always required for Stage 3/6. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
```
- [ ] **Step 2: `docs/LANES.md` — add an mcp-sync subsection**
@@ -603,10 +603,10 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)`, insert a new su
```markdown
### Syncing MCP servers: mcp sync
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `ccam lanes mcp sync` gives it those:
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `agenthub lanes mcp sync` gives it those:
```bash
ccam lanes mcp sync
agenthub lanes mcp sync
```
Reads the source repo's already-configured `mcpServers` from `~/.claude.json` (normal Claude Code project-scope config — set this up for the source repo once, the same way you would for any project), relocates any absolute path under the source repo to the lane's own directory, pins a `@playwright/mcp` server's `--output-dir` to the lane's `.playwright-mcp` (so proof screenshots land where the proof gallery reads them), and writes `<lane>/.mcp.json`. Also seeds the lane's Chromium browser profiles from the source repo's own — preserves saved logins, and never overwrites a profile that already exists at the destination.
@@ -635,5 +635,5 @@ npm run test:server
```bash
git add .claude/skills/ship-feature-lane/SKILL.md docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ccam lanes mcp sync (F1)"
git commit -m "docs(lanes): document agenthub lanes mcp sync (F1)"
```
+33 -33
View File
@@ -1,8 +1,8 @@
# E2 — `ccam lanes sync-base` Implementation Plan
# E2 — `agenthub lanes sync-base` Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ccam lanes sync-base` exists as a real, working command — the migration-collision preflight (`--check`), the one sanctioned merge (`origin/development` into a feature branch), and its conflict-resolution follow-up (`--continue`) — so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 2/8/12 references to it stop being "later task" placeholders.
**Goal:** `agenthub lanes sync-base` exists as a real, working command — the migration-collision preflight (`--check`), the one sanctioned merge (`origin/development` into a feature branch), and its conflict-resolution follow-up (`--continue`) — so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 2/8/12 references to it stop being "later task" placeholders.
**Architecture:** One new core module (`server/lib/lane-sync.js`, pure git operations via `worktree.js`'s existing `git()` helper), two new profile declarations (`MIGRATIONS_DIR`, `GENERATED_MERGE_PATHS`), one new synchronous route (`POST /:id/sync-base`), one new CLI subcommand, and a `SKILL.md` edit that turns three "if it exists yet" conditionals into real instructions.
@@ -145,7 +145,7 @@ const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-sync-"));
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-sync-"));
const laneSync = require("../lib/lane-sync");
@@ -488,7 +488,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
gc(GEN_PUSHER, "push", "-q", "origin", "development");
// A regen hook the fold-in step will run.
const profileDir = path.join(GEN_LANE, ".ccam", "profile");
const profileDir = path.join(GEN_LANE, ".agenthub", "profile");
fs.mkdirSync(path.join(profileDir, "hooks"), { recursive: true });
fs.writeFileSync(path.join(profileDir, "profile.env"), "PORTS=api\n");
fs.writeFileSync(
@@ -503,7 +503,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
env: { MIGRATIONS_DIR: "", GENERATED_MERGE_PATHS: "api/openapi.json" },
generatedMergePaths: ["api/openapi.json"],
hooks: new Set(["regen"]),
dir: path.join(GEN_LANE, ".ccam", "profile"),
dir: path.join(GEN_LANE, ".agenthub", "profile"),
};
}
@@ -567,10 +567,10 @@ async function unmergedFiles(cwd) {
* already established. */
async function setupMergeDriver(cwd, generatedPaths) {
if (!generatedPaths.length) return;
await git(cwd, ["config", "merge.ccam-generated.driver", "true"]);
await git(cwd, ["config", "merge.agenthub-generated.driver", "true"]);
await git(cwd, [
"config",
"merge.ccam-generated.name",
"merge.agenthub-generated.name",
"keep ours; regenerated post-merge by the profile regen hook",
]);
@@ -581,7 +581,7 @@ async function setupMergeDriver(cwd, generatedPaths) {
const lines = new Set(existing.split("\n").filter(Boolean));
let changed = false;
for (const gp of generatedPaths) {
const line = `${gp} merge=ccam-generated`;
const line = `${gp} merge=agenthub-generated`;
if (!lines.has(line)) {
lines.add(line);
changed = true;
@@ -924,7 +924,7 @@ There's no HTTP-level test harness for lane routes in this repo (per the Global
```bash
npm run dev &
sleep 3
# Replace 1 with a real lane id that has a .ccam/profile and a feature branch.
# Replace 1 with a real lane id that has a .agenthub/profile and a feature branch.
curl -s -X POST http://localhost:4820/api/lanes/1/sync-base \
-H 'Content-Type: application/json' \
-d '{"mode":"check","branch":"feat/some-branch"}' | node -e "process.stdin.pipe(require('node:fs').createWriteStream('/dev/stdout'))"
@@ -948,18 +948,18 @@ git commit -m "feat(lanes): add POST /:id/sync-base route (E2)"
---
### Task 4: `ccam lanes sync-base` CLI
### Task 4: `agenthub lanes sync-base` CLI
**Files:**
- Modify: `bin/ccam.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `POST /api/lanes/:id/sync-base` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
- Produces: `ccam lanes sync-base [<id>] [--check|--continue] [branch]` — `process.exitCode` set to `0`/`4`/`5` matching the route's `code`, so `SKILL.md`'s documented exit-code contract holds when scripted.
- Consumes: `POST /api/lanes/:id/sync-base` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
- Produces: `agenthub lanes sync-base [<id>] [--check|--continue] [branch]` — `process.exitCode` set to `0`/`4`/`5` matching the route's `code`, so `SKILL.md`'s documented exit-code contract holds when scripted.
- [ ] **Step 1: Add the subcommand to `cmdLanesRuntime`**
In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/ccam.js:1902`) with a new `sync-base` branch, inside the same `cmdLanesRuntime(sub, args)` function:
In `bin/agenthub.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/agenthub.js:1902`) with a new `sync-base` branch, inside the same `cmdLanesRuntime(sub, args)` function:
```js
if (sub === "sync-base") {
@@ -994,7 +994,7 @@ In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/c
console.error(` conflicted: ${result.conflictedFiles.join(", ")}`);
console.error(" resolve, then: git add <resolved files> && git commit --no-edit");
console.error(
` then: ccam lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}`
` then: agenthub lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}`
);
process.exitCode = 4;
return;
@@ -1028,7 +1028,7 @@ In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/c
- [ ] **Step 2: Wire the subcommand into the dispatcher**
`bin/ccam.js:3088` currently reads:
`bin/agenthub.js:3088` currently reads:
```js
if (["up", "down", "runtime", "logs", "hook"].includes(rest[0])) {
@@ -1046,7 +1046,7 @@ Change to:
- [ ] **Step 3: Add the help-catalog entry**
In the command catalog array (`bin/ccam.js:2268`, right after the `lanes hook` entry), add:
In the command catalog array (`bin/agenthub.js:2268`, right after the `lanes hook` entry), add:
```js
[
@@ -1061,7 +1061,7 @@ In the command catalog array (`bin/ccam.js:2268`, right after the `lanes hook` e
```bash
npm run dev &
sleep 3
node bin/ccam.js lanes sync-base --check feat/some-branch 1
node bin/agenthub.js lanes sync-base --check feat/some-branch 1
echo "exit: $?"
```
@@ -1077,8 +1077,8 @@ npm run test:server
- [ ] **Step 6: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes sync-base CLI (E2)"
git add bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes sync-base CLI (E2)"
```
---
@@ -1097,14 +1097,14 @@ git commit -m "feat(lanes): add ccam lanes sync-base CLI (E2)"
In `.claude/skills/ship-feature-lane/SKILL.md`, find (Stage 2, `### 2 — Pre-push CI gates + dev preflight`):
```
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`ccam stage gates --evidence "sync-base preflight unavailable, skipped"`).
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`agenthub stage gates --evidence "sync-base preflight unavailable, skipped"`).
- Once built, its contract is: exit 5 on a migration-number collision (print the exact rename, do it on the feature branch, re-run Stage 2); informational `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise (you do NOT sync the branch for it — GitHub merges non-conflicting histories fine).
```
Replace with:
```
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. Exit 5 on a migration-number collision: rename the printed file to the suggested number on the feature branch (`git mv`, update any in-file references), then re-run Stage 2. Exit 0 with `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise — informational, you do NOT sync the branch for it (GitHub merges non-conflicting histories fine); a large overlapping delta is a heads-up that post-merge behavior may differ from what you test locally.
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. Exit 5 on a migration-number collision: rename the printed file to the suggested number on the feature branch (`git mv`, update any in-file references), then re-run Stage 2. Exit 0 with `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise — informational, you do NOT sync the branch for it (GitHub merges non-conflicting histories fine); a large overlapping delta is a heads-up that post-merge behavior may differ from what you test locally.
```
- [ ] **Step 2: Stage 8 — make the re-run unconditional**
@@ -1112,13 +1112,13 @@ Replace with:
Find (Stage 8, around `SKILL.md:127`):
```
- Re-run the preflight if `ccam lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
- Re-run the preflight if `agenthub lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
```
Replace with:
```
- Re-run the preflight: `ccam lanes sync-base --check feat/<slug>` — development may have moved while you were in QC. A migration collision here (exit 5) sends you back to Stage 2 with the rename; a clean result (exit 0) proceeds to the push.
- Re-run the preflight: `agenthub lanes sync-base --check feat/<slug>` — development may have moved while you were in QC. A migration collision here (exit 5) sends you back to Stage 2 with the rename; a clean result (exit 0) proceeds to the push.
```
- [ ] **Step 3: Stage 12 — unconditional merge + the exit-4/`--continue` mechanics**
@@ -1127,7 +1127,7 @@ Find (Stage 12, around `SKILL.md:155`):
```
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
- If `ccam lanes sync-base` exists by now: `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
- If `agenthub lanes sync-base` exists by now: `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
- If it doesn't exist yet: `git fetch origin && git merge origin/development` directly on the feature branch, resolve conflicts the same way, commit.
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
```
@@ -1136,8 +1136,8 @@ Replace with:
```
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
- `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). A migration-number collision (exit 5) means nothing was merged — rename the printed file on the feature branch, re-run Stage 2, then retry this step.
- **Exit 4 — merge conflict, left in place on purpose.** Resolve every conflict thoughtfully on the feature branch — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. Never hand-merge a generated contract/client file listed in the profile's `GENERATED_MERGE_PATHS` — the keep-ours driver + regen own them. `git add` ONLY the conflicted files, `git commit --no-edit`, then `ccam lanes sync-base --continue feat/<slug>` (folds any regenerated artifacts into a follow-up commit).
- `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). A migration-number collision (exit 5) means nothing was merged — rename the printed file on the feature branch, re-run Stage 2, then retry this step.
- **Exit 4 — merge conflict, left in place on purpose.** Resolve every conflict thoughtfully on the feature branch — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. Never hand-merge a generated contract/client file listed in the profile's `GENERATED_MERGE_PATHS` — the keep-ours driver + regen own them. `git add` ONLY the conflicted files, `git commit --no-edit`, then `agenthub lanes sync-base --continue feat/<slug>` (folds any regenerated artifacts into a follow-up commit).
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
```
@@ -1148,12 +1148,12 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)` (`docs/LANES.md:
```markdown
### Dev preflight and merge safety: sync-base
`ccam lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
`agenthub lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
```bash
ccam lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
ccam lanes sync-base feat/<slug> # merge origin/development into the feature branch
ccam lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
agenthub lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
agenthub lanes sync-base feat/<slug> # merge origin/development into the feature branch
agenthub lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
```
Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, then `--continue`), `5` migration-number collision (nothing merged — rename the printed file, re-run).
@@ -1161,7 +1161,7 @@ Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, th
Two profile declarations control it, both empty (off) by default:
```bash
# .ccam/profile/profile.env
# .agenthub/profile/profile.env
MIGRATIONS_DIR="db/migrations"
GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
```
@@ -1194,5 +1194,5 @@ npm run test:server
```bash
git add .claude/skills/ship-feature-lane/SKILL.md docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ccam lanes sync-base (E2)"
git commit -m "docs(lanes): document agenthub lanes sync-base (E2)"
```
@@ -45,8 +45,8 @@ Insert directly after the `/:id/mcp/sync` route closes (`server/routes/lanes.js`
```js
/**
* Detect a Node.js project at this lane's OWN directory and scaffold
* .ccam/profile/ if one is found — the HTTP equivalent of
* `ccam lanes profile init`, always targeting lane.cwd (never an arbitrary
* .agenthub/profile/ if one is found — the HTTP equivalent of
* `agenthub lanes profile init`, always targeting lane.cwd (never an arbitrary
* path; the CLI's <repo> argument has no meaning here, this lane's own
* directory is the only sensible target). "No Node.js project detected" is
* a normal 200 outcome, not an error — most lanes won't be auto-detectable
@@ -322,7 +322,7 @@ git commit -m "feat(lanes): auto-setup (profile/agents/mcp) after Add Lane (F5)"
In `docs/LANES.md`, find the `## Creating a lane` section (search `grep -n "^## Creating a lane" docs/LANES.md`) and add a short paragraph after its existing content:
```markdown
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `ccam lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `ccam lanes agents install`, and `ccam lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `agenthub lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `agenthub lanes agents install`, and `agenthub lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
```
- [ ] **Step 2: Verify and commit**
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Surface `ccam lanes agents install`, `ccam lanes mcp sync`, `ccam lanes integration`, `ccam lanes sync-base --check`, `ccam skills install`, and `ccam lanes gc` as clickable UI, so they aren't terminal-only.
**Goal:** Surface `agenthub lanes agents install`, `agenthub lanes mcp sync`, `agenthub lanes integration`, `agenthub lanes sync-base --check`, `agenthub skills install`, and `agenthub lanes gc` as clickable UI, so they aren't terminal-only.
**Architecture:** Two new machine-wide backend routes (`POST /api/skills/install`, `POST /api/lanes/gc`) backed by a small extracted lib module (`server/lib/skills-install.js`) and the already-built `lane-gc.js`; six new client API methods; four additions to `LaneCard.tsx` (gated on `runtime?.available`); two additions to `Settings.tsx`; bilingual i18n keys; test coverage for both.
@@ -12,7 +12,7 @@
- Every applicable `.js`/`.tsx` source file MUST start with the project's authorship header — verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`. `.json` i18n files do not need one (confirm against an existing locale file, which has none).
- **No merge button anywhere.** The "Check dev sync" button in `LaneCard.tsx` calls `sync-base` in `mode: "check"` ONLY — read-only preflight. Merging (`mode: "merge"` / `--continue`) is never exposed in this UI; that stays a session/skill action.
- `bin/ccam.js`'s `cmdSkillsInstall` must be refactored to CALL the extracted `server/lib/skills-install.js` function, not duplicate its logic — the CLI and the new route share one implementation.
- `bin/agenthub.js`'s `cmdSkillsInstall` must be refactored to CALL the extracted `server/lib/skills-install.js` function, not duplicate its logic — the CLI and the new route share one implementation.
- New routes follow this repo's existing error-shape convention: `200` with the resource on success, `4xx` with `{error: {code, message}}` on a documented failure, `500` with `{error: {code, message}}` on anything else.
- Never use `git add -A`. Stage exactly the files each task names.
- Run `npm run test:server` (full suite) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every backend-touching commit; run `npm run test:client` before every frontend-touching commit.
@@ -24,7 +24,7 @@
**Files:**
- Create: `server/lib/skills-install.js`
- Modify: `bin/ccam.js:1531-1542` (`cmdSkillsInstall`, refactor to call the new lib)
- Modify: `bin/agenthub.js:1531-1542` (`cmdSkillsInstall`, refactor to call the new lib)
- Modify: `server/routes/lanes.js` (new route — see note below on router mounting)
- Modify: `server/index.js` or wherever routers are mounted (need to confirm — see Step 0)
@@ -38,7 +38,7 @@
* @file Installs .claude/skills/ship-feature-lane/ into ~/.claude/skills/, so
* /ship-feature-lane is discoverable from a session running inside any
* lane's own working directory — not just inside this repo, where it lives
* until installed. Shared by bin/ccam.js's `ccam skills install` CLI and the
* until installed. Shared by bin/agenthub.js's `agenthub skills install` CLI and the
* POST /api/skills/install route, so there is exactly one copy of this
* logic. Pure filesystem action.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -50,7 +50,7 @@ const path = require("node:path");
/**
* @param {{repoRoot: string}} options - `repoRoot` is this checkout's own
* root (bin/ccam.js already computes this as `REPO_ROOT`; the server
* root (bin/agenthub.js already computes this as `REPO_ROOT`; the server
* computes its own equivalent — see the route for how).
* @returns {{installed: true, path: string}}
*/
@@ -68,9 +68,9 @@ function installShipFeatureLaneSkill({ repoRoot }) {
module.exports = { installShipFeatureLaneSkill };
```
- [ ] **Step 2: Refactor `bin/ccam.js`'s `cmdSkillsInstall` to call it**
- [ ] **Step 2: Refactor `bin/agenthub.js`'s `cmdSkillsInstall` to call it**
Replace the current body (`bin/ccam.js:1531-1542`):
Replace the current body (`bin/agenthub.js:1531-1542`):
```js
function cmdSkillsInstall() {
@@ -121,7 +121,7 @@ const { sameOriginGuard } = require("./run");
const router = Router();
/** This server's own checkout root — server/routes/skills.js is two levels
* under it (server/routes/), same computation bin/ccam.js's REPO_ROOT
* under it (server/routes/), same computation bin/agenthub.js's REPO_ROOT
* already does from its own location. */
const REPO_ROOT = path.resolve(__dirname, "..", "..");
@@ -158,7 +158,7 @@ app.use("/api/skills", skillsRouter);
```bash
npm run dev &
sleep 3
node bin/ccam.js skills install
node bin/agenthub.js skills install
echo "CLI exit: $?"
curl -s -X POST http://localhost:4820/api/skills/install | head -c 200
echo
@@ -176,7 +176,7 @@ npm run test:server
- [ ] **Step 6: Commit**
```bash
git add server/lib/skills-install.js server/routes/skills.js server/index.js bin/ccam.js
git add server/lib/skills-install.js server/routes/skills.js server/index.js bin/agenthub.js
git commit -m "feat(lanes): extract skills-install lib, add POST /api/skills/install (F4)"
```
@@ -551,7 +551,7 @@ npm run dev &
sleep 3
```
Open `http://localhost:5173`, navigate to Workspace, select a lane with a `.ccam/profile` (`runtime.available`). Confirm: three new buttons appear, three integration badges appear (gray = off, since no `integrations.env` exists yet on a fresh lane), clicking "Check dev sync" shows a result line. Stop the dev server afterward.
Open `http://localhost:5173`, navigate to Workspace, select a lane with a `.agenthub/profile` (`runtime.available`). Confirm: three new buttons appear, three integration badges appear (gray = off, since no `integrations.env` exists yet on a fresh lane), clicking "Check dev sync" shows a result line. Stop the dev server afterward.
- [ ] **Step 6: Run the client suite**
@@ -767,7 +767,7 @@ describe("agents install / mcp sync / integration badges / sync check", () => {
provisioned: true,
up: false,
slot: 1,
profileDir: "/work/demo/.ccam/profile",
profileDir: "/work/demo/.agenthub/profile",
ports: {},
});
vi.mocked(api.lanes.integration).mockImplementation((_id, name) =>
@@ -4,7 +4,7 @@
**Goal:** Replace the dashboard's "Run Claude from the browser" feature (spawns `claude --output-format stream-json`, renders custom chat bubbles) with a real tmux-backed PTY streamed to the browser via `xterm.js`, giving full two-way interactivity (type, resume, kill) from both the dashboard and a real terminal attached to the same named tmux session.
**Architecture:** One tmux session per lane (`ccam-lane-<id>`), created/attached/killed via `tmux` CLI calls from a new `server/lib/tmux.js`. `server/lib/pty-run.js` owns the run lifecycle (Start/Resume/Kill/List — List is now a computed read of `tmux list-sessions`, not cached in-memory state) and a `node-pty`-backed attach used per WebSocket connection on a new `/ws-pty/:runId` path. The client's `TerminalView.tsx` (`@xterm/xterm`) replaces `RunConsole.tsx`. Hook-derived session/agent data is untouched — it already updates independent of the run transport.
**Architecture:** One tmux session per lane (`agenthub-lane-<id>`), created/attached/killed via `tmux` CLI calls from a new `server/lib/tmux.js`. `server/lib/pty-run.js` owns the run lifecycle (Start/Resume/Kill/List — List is now a computed read of `tmux list-sessions`, not cached in-memory state) and a `node-pty`-backed attach used per WebSocket connection on a new `/ws-pty/:runId` path. The client's `TerminalView.tsx` (`@xterm/xterm`) replaces `RunConsole.tsx`. Hook-derived session/agent data is untouched — it already updates independent of the run transport.
**Tech Stack:** Node.js/Express, `ws`, `node-pty` (new), `better-sqlite3`, React, `@xterm/xterm` + `@xterm/addon-fit` (new), `node:test`, Vitest.
@@ -12,7 +12,7 @@
- No fallback to the old stream-json mode — it is deleted, not flagged off (spec: "Approach").
- The `/ws-pty/:runId` path must reuse the exact same `verifyClient` auth (Host allowlist + `DASHBOARD_TOKEN`) as the existing `/ws` path (spec: "WebSocket transport").
- `runId` (tmux session name) must be validated against `^ccam-lane-\d+$` before any tmux command touches it — untrusted input must never reach a shell-adjacent command unvalidated (spec: "WebSocket transport" trust boundary).
- `runId` (tmux session name) must be validated against `^agenthub-lane-\d+$` before any tmux command touches it — untrusted input must never reach a shell-adjacent command unvalidated (spec: "WebSocket transport" trust boundary).
- All tmux/`node-pty` invocations use `execFile`/`node-pty.spawn` with an explicit argument array — never a concatenated shell string (matches this repo's existing rule for git in `server/lib/worktree.js`, `CLAUDE.md`).
- Server tests must not exec real tmux (no CI has it installed) — mock the `tmux.js` module via an injectable exec seam, same style as `run-spawner.js`'s existing `__injectChildForTest`/`__reset` test seams.
- Every new/modified `.js`/`.ts`/`.tsx` file keeps this repo's required file header (file overview + `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>` — `.claude/skills/file-headers/`).
@@ -99,8 +99,8 @@ try {
- [ ] **Step 5: Verify the migration runs clean on a fresh DB and an existing one**
```bash
rm -f /tmp/ccam-migration-check.db
DASHBOARD_DB_PATH=/tmp/ccam-migration-check.db node -e "require('./server/db')" && echo OK
rm -f /tmp/agenthub-migration-check.db
DASHBOARD_DB_PATH=/tmp/agenthub-migration-check.db node -e "require('./server/db')" && echo OK
node --test server/__tests__/*.test.js 2>&1 | tail -20
```
@@ -152,7 +152,7 @@ describe("tmux wrapper", () => {
it("hasSession returns true when execFileSync exits 0", () => {
tmux.__setExecImpl(() => "");
assert.equal(tmux.hasSession("ccam-lane-1"), true);
assert.equal(tmux.hasSession("agenthub-lane-1"), true);
});
it("hasSession returns false when execFileSync throws", () => {
@@ -161,7 +161,7 @@ describe("tmux wrapper", () => {
e.status = 1;
throw e;
});
assert.equal(tmux.hasSession("ccam-lane-1"), false);
assert.equal(tmux.hasSession("agenthub-lane-1"), false);
});
it("newSession builds the correct argv", () => {
@@ -170,12 +170,12 @@ describe("tmux wrapper", () => {
calls.push(args);
return "";
});
tmux.newSession({ name: "ccam-lane-1", cwd: "/tmp/repo", argv: ["claude", "--model", "opus"] });
tmux.newSession({ name: "agenthub-lane-1", cwd: "/tmp/repo", argv: ["claude", "--model", "opus"] });
assert.deepEqual(calls[0], [
"new-session",
"-d",
"-s",
"ccam-lane-1",
"agenthub-lane-1",
"-c",
"/tmp/repo",
"--",
@@ -191,12 +191,12 @@ describe("tmux wrapper", () => {
e.status = 1;
throw e;
});
assert.doesNotThrow(() => tmux.killSession("ccam-lane-1"));
assert.doesNotThrow(() => tmux.killSession("agenthub-lane-1"));
});
it("listSessions filters by prefix and ignores unrelated sessions", () => {
tmux.__setExecImpl(() => "ccam-lane-1\nccam-lane-2\nsome-other-session\n");
assert.deepEqual(tmux.listSessions("ccam-lane-"), ["ccam-lane-1", "ccam-lane-2"]);
tmux.__setExecImpl(() => "agenthub-lane-1\nagenthub-lane-2\nsome-other-session\n");
assert.deepEqual(tmux.listSessions("agenthub-lane-"), ["agenthub-lane-1", "agenthub-lane-2"]);
});
it("listSessions returns [] when tmux has no sessions at all (exit 1)", () => {
@@ -205,7 +205,7 @@ describe("tmux wrapper", () => {
e.status = 1;
throw e;
});
assert.deepEqual(tmux.listSessions("ccam-lane-"), []);
assert.deepEqual(tmux.listSessions("agenthub-lane-"), []);
});
it("isTmuxAvailable reflects whether the binary resolves on PATH", () => {
@@ -233,9 +233,9 @@ Expected: FAIL — `Cannot find module '../lib/tmux'`.
/**
* @file tmux.js
* @description Thin wrapper around the `tmux` CLI for the terminal-run
* feature. Every dashboard-managed session is named `ccam-lane-<id>` (see
* feature. Every dashboard-managed session is named `agenthub-lane-<id>` (see
* `pty-run.js`) so a real terminal can attach to the exact same session with
* `tmux attach -t ccam-lane-<id>` (or `ccam lanes shell`). Never builds a
* `tmux attach -t agenthub-lane-<id>` (or `agenthub lanes shell`). Never builds a
* shell string — every call is `execFileSync("tmux", [...argv])` with an
* explicit argument array (matches this repo's rule for git in worktree.js).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -342,9 +342,9 @@ git commit -m "feat(run): add tmux command wrapper with an injectable exec seam"
- Produces (consumed by Task 4's route rewrite and Task 5's WS attach):
- `spawnRun({laneId, cwd, model, permissionMode, effort, resumeSessionId, initialPrompt}): {id: string, ...}` — `id` is the tmux session name.
- `killRun(id: string): boolean`
- `listRuns(): Array<PublicRun>` — computed fresh from `tmux.listSessions("ccam-lane-")` each call, no cached Map.
- `listRuns(): Array<PublicRun>` — computed fresh from `tmux.listSessions("agenthub-lane-")` each call, no cached Map.
- `getRun(id: string): PublicRun | null`
- `laneIdFromRunId(id: string): number | null` — parses `ccam-lane-<id>` back to the numeric lane id; used by Task 4's WS runId validation.
- `laneIdFromRunId(id: string): number | null` — parses `agenthub-lane-<id>` back to the numeric lane id; used by Task 4's WS runId validation.
Where `PublicRun` is `{id, laneId, cwd, model, permissionMode, effort, resumeSessionId, status: "running"|"gone", startedAt, sessionId}` (`sessionId`/`startedAt` come from the `dashboard_runs` row via `dashboard-runs.getRun(id)`, since tmux itself doesn't know Claude's session id).
@@ -384,7 +384,7 @@ describe("pty-run", () => {
tmux.__reset();
});
it("spawnRun creates a new tmux session named ccam-lane-<id> when none exists", () => {
it("spawnRun creates a new tmux session named agenthub-lane-<id> when none exists", () => {
const calls = [];
tmux.__setExecImpl((args) => {
calls.push(args);
@@ -396,10 +396,10 @@ describe("pty-run", () => {
return "";
});
const handle = pty.spawnRun({ laneId: 42, cwd: "/tmp/repo", model: "opus" });
assert.equal(handle.id, "ccam-lane-42");
assert.equal(handle.id, "agenthub-lane-42");
const newSessionCall = calls.find((c) => c[0] === "new-session");
assert.ok(newSessionCall, "expected a new-session call");
assert.deepEqual(newSessionCall.slice(0, 6), ["new-session", "-d", "-s", "ccam-lane-42", "-c", "/tmp/repo"]);
assert.deepEqual(newSessionCall.slice(0, 6), ["new-session", "-d", "-s", "agenthub-lane-42", "-c", "/tmp/repo"]);
assert.ok(newSessionCall.includes("claude"));
assert.ok(newSessionCall.includes("--model"));
assert.ok(newSessionCall.includes("opus"));
@@ -412,7 +412,7 @@ describe("pty-run", () => {
return ""; // has-session succeeds → already running
});
const handle = pty.spawnRun({ laneId: 7, cwd: "/tmp/repo" });
assert.equal(handle.id, "ccam-lane-7");
assert.equal(handle.id, "agenthub-lane-7");
assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
});
@@ -453,36 +453,36 @@ describe("pty-run", () => {
calls.push(args);
return "";
});
assert.equal(pty.killRun("ccam-lane-5"), true);
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "ccam-lane-5"));
assert.equal(pty.killRun("agenthub-lane-5"), true);
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "agenthub-lane-5"));
});
it("listRuns reflects live tmux-session state, not cached memory", () => {
tmux.__setExecImpl((args) => {
if (args[0] === "list-sessions") return "ccam-lane-1\nccam-lane-2\n";
if (args[0] === "list-sessions") return "agenthub-lane-1\nagenthub-lane-2\n";
return "";
});
const first = pty.listRuns();
assert.deepEqual(
first.map((r) => r.id).sort(),
["ccam-lane-1", "ccam-lane-2"]
["agenthub-lane-1", "agenthub-lane-2"]
);
// Session killed out-of-band (not through killRun) — next list() call
// must self-correct, proving state is computed, not stored.
tmux.__setExecImpl((args) => {
if (args[0] === "list-sessions") return "ccam-lane-1\n";
if (args[0] === "list-sessions") return "agenthub-lane-1\n";
return "";
});
const second = pty.listRuns();
assert.deepEqual(
second.map((r) => r.id),
["ccam-lane-1"]
["agenthub-lane-1"]
);
});
it("laneIdFromRunId parses the numeric lane id back out", () => {
assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
assert.equal(pty.laneIdFromRunId("agenthub-lane-42"), 42);
assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
});
});
@@ -510,9 +510,9 @@ Expected: FAIL — `Cannot find module '../lib/pty-run'`.
* out-of-band (crash, manual `tmux kill-session`, host reboot) self-corrects
* on the next read instead of leaving a ghost "running" row.
*
* Every session is named `ccam-lane-<laneId>` so a real terminal can attach
* to the exact same session (`tmux attach -t ccam-lane-<id>`, or
* `ccam lanes shell`) — that's the whole point: the dashboard both creates
* Every session is named `agenthub-lane-<laneId>` so a real terminal can attach
* to the exact same session (`tmux attach -t agenthub-lane-<id>`, or
* `agenthub lanes shell`) — that's the whole point: the dashboard both creates
* the session (one-click Start/Resume) and is just one of possibly several
* attached clients tmux already keeps in sync.
*
@@ -528,12 +528,12 @@ try {
/* db-less environment, skip persistence */
}
const RUN_ID_RE = /^ccam-lane-(\d+)$/;
const RUN_ID_RE = /^agenthub-lane-(\d+)$/;
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
function runIdForLane(laneId) {
return `ccam-lane-${laneId}`;
return `agenthub-lane-${laneId}`;
}
function laneIdFromRunId(id) {
@@ -657,7 +657,7 @@ function getRun(id) {
/** Computed fresh from tmux state every call — see file header. */
function listRuns() {
return tmux.listSessions("ccam-lane-").map(publicRun);
return tmux.listSessions("agenthub-lane-").map(publicRun);
}
module.exports = {
@@ -955,7 +955,7 @@ describe("/api/run", () => {
});
const spawned = await fetchJson("/api/run", { method: "POST", body: { laneId: 9, cwd: TMP } });
assert.equal(spawned.status, 201);
assert.equal(spawned.body.id, "ccam-lane-9");
assert.equal(spawned.body.id, "agenthub-lane-9");
const fetched = await fetchJson(`/api/run/${spawned.body.id}`);
assert.equal(fetched.status, 200);
@@ -964,7 +964,7 @@ describe("/api/run", () => {
it("DELETE /:id kills a live tmux session", async () => {
tmux.__setExecImpl(() => ""); // has-session succeeds; kill-session succeeds
const { status, body } = await fetchJson("/api/run/ccam-lane-9", { method: "DELETE" });
const { status, body } = await fetchJson("/api/run/agenthub-lane-9", { method: "DELETE" });
assert.equal(status, 200);
assert.deepEqual(body, { ok: true });
});
@@ -978,7 +978,7 @@ describe("/api/run", () => {
}
return "";
});
const { status } = await fetchJson("/api/run/ccam-lane-999", { method: "DELETE" });
const { status } = await fetchJson("/api/run/agenthub-lane-999", { method: "DELETE" });
assert.equal(status, 404);
});
@@ -1073,18 +1073,18 @@ describe("pty-attach", () => {
ptyAttach.__setSpawnImpl(() => fakePty);
});
it("rejects a runId that doesn't match ccam-lane-<digits>", () => {
it("rejects a runId that doesn't match agenthub-lane-<digits>", () => {
assert.throws(() => ptyAttach.validateRunId("../../etc/passwd"), /EBADRUNID/);
assert.throws(() => ptyAttach.validateRunId("some-other-session"), /EBADRUNID/);
});
it("accepts a well-formed runId", () => {
assert.doesNotThrow(() => ptyAttach.validateRunId("ccam-lane-42"));
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42"));
});
it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
fakePty.__emitter.emit("data", "hello from claude");
assert.equal(ws.__sent.length, 1);
@@ -1097,7 +1097,7 @@ describe("pty-attach", () => {
it("routes a JSON text frame with type resize to pty.resize", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
ws.__emitter.emit("message", Buffer.from(JSON.stringify({ type: "resize", cols: 100, rows: 40 })), {
binary: false,
});
@@ -1110,7 +1110,7 @@ describe("pty-attach", () => {
ws.close = () => {
closed = true;
};
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
fakePty.__emitter.emit("exit", { exitCode: 0 });
const last = ws.__sent[ws.__sent.length - 1];
assert.equal(last.binary, false);
@@ -1120,7 +1120,7 @@ describe("pty-attach", () => {
it("kills the PTY attach process when the WS connection closes", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
let killed = false;
fakePty.kill = () => {
killed = true;
@@ -1154,10 +1154,10 @@ Expected: FAIL — `Cannot find module '../lib/pty-attach'`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const RUN_ID_RE = /^ccam-lane-\d+$/;
const RUN_ID_RE = /^agenthub-lane-\d+$/;
/**
* Reject anything that isn't exactly `ccam-lane-<digits>` before it can ever
* Reject anything that isn't exactly `agenthub-lane-<digits>` before it can ever
* reach a tmux/PTY command — the trust boundary for this WS path, since a
* validated runId is the only thing standing between an authenticated WS
* client and naming an arbitrary session on the host.
@@ -1262,7 +1262,7 @@ Add below the existing `initWebSocket` function (after its closing brace, before
```javascript
let ptyWss = null;
const PTY_PATH_RE = /^\/ws-pty\/(ccam-lane-\d+)$/;
const PTY_PATH_RE = /^\/ws-pty\/(agenthub-lane-\d+)$/;
/**
* Second WebSocket server, dedicated to the terminal-run PTY transport
@@ -1372,16 +1372,16 @@ Find `initWebSocket(server);` (line 152) and add directly below it:
```bash
npm start &
sleep 1
tmux new-session -d -s ccam-lane-999 -c /tmp -- sleep 60
tmux new-session -d -s agenthub-lane-999 -c /tmp -- sleep 60
node -e "
const WebSocket = require('ws');
const ws = new WebSocket('ws://127.0.0.1:4820/ws-pty/ccam-lane-999');
const ws = new WebSocket('ws://127.0.0.1:4820/ws-pty/agenthub-lane-999');
ws.on('open', () => console.log('connected'));
ws.on('message', (d) => console.log('got', d.length, 'bytes'));
ws.on('close', (code) => console.log('closed', code));
setTimeout(() => process.exit(0), 3000);
"
tmux kill-session -t ccam-lane-999
tmux kill-session -t agenthub-lane-999
kill %1
```
@@ -1471,13 +1471,13 @@ describe("TerminalView", () => {
afterEach(cleanup);
it("opens a WS connection to the run's ws-pty path", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
expect(MockWebSocket.instances).toHaveLength(1);
expect(MockWebSocket.instances[0].url).toBe("ws://localhost:4820/ws-pty/ccam-lane-1");
expect(MockWebSocket.instances[0].url).toBe("ws://localhost:4820/ws-pty/agenthub-lane-1");
});
it("writes incoming WS data to the terminal", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
const ws = MockWebSocket.instances[0];
ws.onopen?.();
ws.onmessage?.({ data: "hello" });
@@ -1485,7 +1485,7 @@ describe("TerminalView", () => {
});
it("forwards terminal keystrokes as outgoing WS sends", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
const ws = MockWebSocket.instances[0];
onDataHandlers[0]("ls -la\r");
expect(ws.sent).toEqual(["ls -la\r"]);
@@ -1638,7 +1638,7 @@ export interface RunStartArgs {
}
/** A lane's tmux-backed terminal run — one per lane, id is the tmux session
* name (`ccam-lane-<laneId>`). */
* name (`agenthub-lane-<laneId>`). */
export interface RunHandle {
id: string;
laneId: number | null;
@@ -1729,7 +1729,7 @@ Replace the block from `// ───── Interactive run streaming ───
/** Payload for the `run_status` WebSocket message. */
export interface RunStatusPayload {
/** The run id (tmux session name, `ccam-lane-<laneId>`). */
/** The run id (tmux session name, `agenthub-lane-<laneId>`). */
id: string;
status: "running" | "gone";
/** Epoch-ms timestamp of this transition. */
@@ -1989,10 +1989,10 @@ git commit -m "feat(run): wire Workspace to TerminalView, drop stream-json envel
---
### Task 11: `ccam lanes shell` CLI command
### Task 11: `agenthub lanes shell` CLI command
**Files:**
- Modify: `bin/ccam.js` (dispatch table + new `cmdLanesShell` function)
- Modify: `bin/agenthub.js` (dispatch table + new `cmdLanesShell` function)
**Interfaces:**
- Consumes: `resolveLaneArg` (existing helper, same one `cmdLanesPipeline`/`cmdStage` use), `get`/`patch` HTTP helpers (existing), `node:child_process`'s `spawn` (already imported at the top of this file per the earlier research).
@@ -2021,7 +2021,7 @@ async function cmdLanesShell(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
const sessionName = `ccam-lane-${lane.id}`;
const sessionName = `agenthub-lane-${lane.id}`;
const child = spawn("tmux", ["new-session", "-A", "-s", sessionName, "-c", lane.cwd], {
stdio: "inherit",
});
@@ -2040,24 +2040,24 @@ async function cmdLanesShell(args) {
```bash
cd /path/to/some/adopted/lane
node /path/to/ccam-lanes/bin/ccam.js lanes shell
node /path/to/agenthub-lanes/bin/agenthub.js lanes shell
```
Expected: drops into a tmux session named `ccam-lane-<id>`; typing `claude` starts a real interactive session; `Ctrl-B D` detaches without killing it; running the same command again re-attaches to the same session instead of creating a new one.
Expected: drops into a tmux session named `agenthub-lane-<id>`; typing `claude` starts a real interactive session; `Ctrl-B D` detaches without killing it; running the same command again re-attaches to the same session instead of creating a new one.
- [ ] **Step 4: Update `ccam --help` and `docs/LANES.md`'s command reference**
- [ ] **Step 4: Update `agenthub --help` and `docs/LANES.md`'s command reference**
```bash
grep -n '"help"' bin/ccam.js | head -5
grep -n '"help"' bin/agenthub.js | head -5
```
Add a one-line entry for `ccam lanes shell` next to the existing `ccam lanes pipeline` entry in whatever help-text block that grep surfaces (match the existing entries' format exactly).
Add a one-line entry for `agenthub lanes shell` next to the existing `agenthub lanes pipeline` entry in whatever help-text block that grep surfaces (match the existing entries' format exactly).
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js
git commit -m "feat(cli): add 'ccam lanes shell' to attach a real terminal to a lane's tmux session"
git add bin/agenthub.js
git commit -m "feat(cli): add 'agenthub lanes shell' to attach a real terminal to a lane's tmux session"
```
---
@@ -2069,7 +2069,7 @@ git commit -m "feat(cli): add 'ccam lanes shell' to attach a real terminal to a
- Modify: `README.vi.md` (mirror the same edit — this repo keeps a Vietnamese translation in sync)
- Modify: `ARCHITECTURE.md` (find and update whatever section documents the old stream-json Run feature — search first)
- Modify: `docs/API.md` (the `/api/run/*` endpoint reference)
- Modify: `docs/LANES.md` (mention `ccam lanes shell` next to the existing `ccam lanes pipeline` documentation added in the prior session's work)
- Modify: `docs/LANES.md` (mention `agenthub lanes shell` next to the existing `agenthub lanes pipeline` documentation added in the prior session's work)
**Interfaces:** none — documentation only.
@@ -2094,7 +2094,7 @@ with:
- **Run Claude from the browser.** A real terminal (tmux + a real PTY,
rendered with xterm.js) attached to a lane's directory — the exact TUI you'd
see locally, fully interactive, resumable, and attachable from a real
terminal too via `ccam lanes shell`.
terminal too via `agenthub lanes shell`.
```
- [ ] **Step 3: Mirror the same edit in `README.vi.md`**
@@ -2105,13 +2105,13 @@ Find the corresponding Vietnamese bullet (search for "Chạy Claude từ trình
Rewrite the endpoint table/prose to match Task 4's actual routes: `GET /api/run`, `GET /api/run/history`, `GET /api/run/binary`, `GET /api/run/tmux`, `GET /api/run/cwds`, `GET /api/run/files`, `POST /api/run` (now requires `laneId`, body shape per `RunStartArgs`), `GET /api/run/:id`, `DELETE /api/run/:id`. Remove the `POST /api/run/:id/message` entry entirely (deleted in Task 4) and any `run_stream`/`run_input_ack` WebSocket message documentation (deleted in Task 7) — add a `/ws-pty/:runId` entry describing the binary-frame PTY transport instead, matching this doc's existing WebSocket-message table format.
- [ ] **Step 5: Add `ccam lanes shell` to `docs/LANES.md`**
- [ ] **Step 5: Add `agenthub lanes shell` to `docs/LANES.md`**
Find the existing `ccam lanes pipeline` documentation (added in the prior session, per this repo's `docs/LANES.md` "Reporting a stage"/pipeline sections) and add a short paragraph for `ccam lanes shell` directly after it, matching that section's tone:
Find the existing `agenthub lanes pipeline` documentation (added in the prior session, per this repo's `docs/LANES.md` "Reporting a stage"/pipeline sections) and add a short paragraph for `agenthub lanes shell` directly after it, matching that section's tone:
```markdown
`ccam lanes shell` attaches a real terminal to the exact tmux session the
dashboard's Start/Resume buttons use for this lane (`ccam-lane-<id>`),
`agenthub lanes shell` attaches a real terminal to the exact tmux session the
dashboard's Start/Resume buttons use for this lane (`agenthub-lane-<id>`),
creating it if it doesn't exist yet. Type `claude` inside it like any normal
terminal session — the dashboard's Workspace terminal view is just another
client attached to the same tmux session, so both stay in sync live.
@@ -2168,7 +2168,7 @@ Expected: no type errors, build succeeds.
bash .claude/skills/file-headers/scripts/check-headers.sh
```
Expected: exit 0 (aside from the pre-existing, unrelated `.ccam/profile/hooks/*.sh` gaps noted in this repo's own history — do not fix those as part of this plan).
Expected: exit 0 (aside from the pre-existing, unrelated `.agenthub/profile/hooks/*.sh` gaps noted in this repo's own history — do not fix those as part of this plan).
- [ ] **Step 5: Manual end-to-end click-through (per the spec's "Verify" section)**
@@ -2176,7 +2176,7 @@ Install `tmux` locally if not already present, then:
1. `npm run dev`, open Workspace for an adopted lane.
2. Click Start — confirm the real Claude Code TUI renders in the browser via `TerminalView`.
3. Type in the browser terminal — confirm input reaches the pane.
4. In a real terminal, run `ccam lanes shell` for the same lane — confirm it drops into the exact same live session (typed output appears in both places).
4. In a real terminal, run `agenthub lanes shell` for the same lane — confirm it drops into the exact same live session (typed output appears in both places).
5. Click Kill on the dashboard — confirm the tmux session ends and the real terminal's attach also exits.
6. Confirm `GET /api/run` no longer lists that run once killed.
@@ -13,7 +13,7 @@
- No server/API changes — this is a client-only feature (per the approved spec, docs/superpowers/specs/2026-08-14-split-terminal-view-design.md).
- No synchronized input across panes — each `TerminalView` keeps its own independent WebSocket connection.
- Layout 1 must remain behaviorally and structurally identical to today's Workspace (existing `Workspace.test.tsx` assertions about `console-body`, `lane-detail` nesting, and the no-`/stage`-call invariant must still pass unmodified where they test layout-1 behavior).
- Layout + pane lane selections persist to `localStorage` under key `ccam.workspace.splitView`; a persisted lane id that no longer exists in the loaded lane list falls back to unselected for that pane.
- Layout + pane lane selections persist to `localStorage` under key `agenthub.workspace.splitView`; a persisted lane id that no longer exists in the loaded lane list falls back to unselected for that pane.
- Every new/modified `.ts`/`.tsx` file must carry the project's file header (see `.claude/skills/file-headers/`).
- Run `npm run test:client` before finishing; this is a client-only change so `npm run test:server` is not required, but do not skip `test:client`.
@@ -61,12 +61,12 @@ describe("splitViewStorage", () => {
});
it("falls back to the default when stored JSON is malformed", () => {
localStorage.setItem("ccam.workspace.splitView", "{not json");
localStorage.setItem("agenthub.workspace.splitView", "{not json");
expect(readSplitViewState()).toEqual(defaultSplitViewState());
});
it("falls back to the default when the stored layout is not 1, 2, or 4", () => {
localStorage.setItem("ccam.workspace.splitView", JSON.stringify({ layout: 3, paneLaneIds: [] }));
localStorage.setItem("agenthub.workspace.splitView", JSON.stringify({ layout: 3, paneLaneIds: [] }));
expect(readSplitViewState()).toEqual(defaultSplitViewState());
});
});
@@ -96,7 +96,7 @@ export interface SplitViewState {
paneLaneIds: (number | null)[];
}
const STORAGE_KEY = "ccam.workspace.splitView";
const STORAGE_KEY = "agenthub.workspace.splitView";
export function defaultSplitViewState(): SplitViewState {
return { layout: 1, paneLaneIds: [null] };
@@ -888,7 +888,7 @@ describe("split terminal view", () => {
it("falls back to unselected when a persisted lane id no longer exists", async () => {
localStorage.setItem(
"ccam.workspace.splitView",
"agenthub.workspace.splitView",
JSON.stringify({ layout: 2, paneLaneIds: [9999, null] })
);
await renderWorkspace();
@@ -1048,7 +1048,7 @@ git commit -m "feat(workspace): add 1/2/4-pane split terminal view toggle"
In `docs/LANES.md`, in the "## The Workspace page (`/run`)" section, after the existing "**Terminal**" bullet (line 312), add:
```markdown
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2x2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`ccam.workspace.splitView`) across reloads.
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2x2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`agenthub.workspace.splitView`) across reloads.
```
- [ ] **Step 2: Verify doc accuracy against the implementation**
@@ -1,8 +1,8 @@
# `ccam ci status` Implementation Plan
# `agenthub ci status` Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add `ccam lanes ci status <branch-or-sha>` — a one-shot GitHub Actions status snapshot backed by a new server route — and wire it into `ship-feature-lane` Stage 13 so the CI-wait half of post-merge verification stops being hardcoded-skipped.
**Goal:** Add `agenthub lanes ci status <branch-or-sha>` — a one-shot GitHub Actions status snapshot backed by a new server route — and wire it into `ship-feature-lane` Stage 13 so the CI-wait half of post-merge verification stops being hardcoded-skipped.
**Architecture:** A `server/lib/gh.js` execFile wrapper (mirrors `worktree.js`'s `git()`), a `server/lib/ci-status.js` pure-logic module that turns `gh run list`/`gh run view` output into `{state, ref, checks}`, a thin `GET /:id/ci/status` route (mirrors `GET /:id/git`'s try/available shape), and a thin CLI command (mirrors `cmdLanesIntegration`) that maps the response onto exit codes 0/1/3. No polling, no retry, no flake classification anywhere in this stack — the skill keeps that judgment, per the project's no-orchestration rule.
@@ -10,7 +10,7 @@
## Global Constraints
- Spec: `docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md` — read it before starting; every task below implements one section of it.
- Spec: `docs/superpowers/specs/2026-08-25-agenthub-ci-status-design.md` — read it before starting; every task below implements one section of it.
- Every new/edited source file needs the repo's copyright header (file overview + `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`) — see `skills/file-headers/`.
- No flake auto-classification or auto-rerun logic anywhere in this stack — `state` is a derived fact, nothing more.
- Route never writes lane `stage`/`status`/`notes` (same boundary every other read-only lane route keeps).
@@ -45,7 +45,7 @@ const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gh-"));
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gh-"));
const { gh } = require("../lib/gh");
/** Puts a fake `gh` script on PATH for the duration of one test. */
@@ -188,7 +188,7 @@ const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-"));
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-"));
const { ciStatus } = require("../lib/ci-status");
/**
@@ -322,7 +322,7 @@ Expected: FAIL — `Cannot find module '../lib/ci-status'`
* @file Turns a GitHub Actions run for one ref into a single derived fact —
* {state, checks} — for ship-feature-lane's Stage 13 CI-wait. Deliberately a
* snapshot, not a poll loop: the driving skill owns the wait/retry decision
* (docs/LANES.md "Orchestration: what CCAM does NOT do"), this module only
* (docs/LANES.md "Orchestration: what AgentHub does NOT do"), this module only
* reports what gh already knows.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -421,11 +421,11 @@ describe("GET /:id/ci/status", () => {
let restorePath;
before(async () => {
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-route-"));
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-route-"));
const r = await request("POST", "/api/lanes", { title: "CI status lane", cwd: laneCwd });
laneId = r.body.lane.id;
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-bin-"));
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-bin-"));
fs.writeFileSync(
path.join(bin, "gh"),
`#!/usr/bin/env bash
@@ -492,7 +492,7 @@ Add the route directly after the existing `GET /:id/git` route (after its closin
/**
* A single GitHub Actions status snapshot for one ref (branch or commit sha)
* in the lane's repo — NOT a poll loop. ship-feature-lane's Stage 13 owns
* the wait/retry decision (docs/LANES.md "Orchestration: what CCAM does NOT
* the wait/retry decision (docs/LANES.md "Orchestration: what AgentHub does NOT
* do"); this route only reports what `gh` currently knows. Read-only, no
* same-origin guard, same reasoning as GET /:id/git.
*/
@@ -525,26 +525,26 @@ git commit -m "feat(api): add GET /:id/ci/status"
---
### Task 4: `ccam lanes ci status` CLI command
### Task 4: `agenthub lanes ci status` CLI command
**Files:**
- Modify: `bin/ccam.js` — add `cmdLanesCi`, wire the dispatch, add the help table row.
- Modify: `bin/agenthub.js` — add `cmdLanesCi`, wire the dispatch, add the help table row.
- Modify: `server/__tests__/lanes-cli.test.js` — add a describe block.
**Interfaces:**
- Consumes: `GET /api/lanes/:id/ci/status?ref=...` from Task 3; `resolveLaneArg`, `get`, `c` (chalk-like color helper already used elsewhere in `bin/ccam.js`).
- Produces: `ccam lanes ci status <branch-or-sha> [<lane-id>]` — prints the JSON response to stdout, sets `process.exitCode` to `0` (success), `1` (failure or any error), or `3` (pending).
- Consumes: `GET /api/lanes/:id/ci/status?ref=...` from Task 3; `resolveLaneArg`, `get`, `c` (chalk-like color helper already used elsewhere in `bin/agenthub.js`).
- Produces: `agenthub lanes ci status <branch-or-sha> [<lane-id>]` — prints the JSON response to stdout, sets `process.exitCode` to `0` (success), `1` (failure or any error), or `3` (pending).
- [ ] **Step 1: Write the failing test**
Add to `server/__tests__/lanes-cli.test.js`, in a new `describe` block (reusing that file's existing `cli()`, `git()`, `BASE` helpers and `SOURCE_REPO`/`LANE_DIR` fixtures already set up in its `before()`):
```js
describe("ccam lanes ci status", () => {
describe("agenthub lanes ci status", () => {
let restorePath;
before(() => {
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-cli-bin-"));
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-cli-bin-"));
fs.writeFileSync(
path.join(bin, "gh"),
`#!/usr/bin/env bash
@@ -625,26 +625,26 @@ function cli(args, cwd, extraEnv = {}) {
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test server/__tests__/lanes-cli.test.js`
Expected: FAIL — `ccam lanes ci` prints a "no such subcommand" usage error, exit code 1 (not the expected 0/1/3 from real data), and no `"state"` in stdout
Expected: FAIL — `agenthub lanes ci` prints a "no such subcommand" usage error, exit code 1 (not the expected 0/1/3 from real data), and no `"state"` in stdout
- [ ] **Step 3: Add the CLI command**
In `bin/ccam.js`, add near `cmdLanesIntegration` (after its closing `}`):
In `bin/agenthub.js`, add near `cmdLanesIntegration` (after its closing `}`):
```js
/** `ccam lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
/** `agenthub lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
* Actions status snapshot for a ref, via GET /:id/ci/status. Never polls;
* the caller (ship-feature-lane Stage 13) owns the wait loop. Exit code
* mirrors the derived state: 0 success, 1 failure or any error, 3 pending. */
async function cmdLanesCi(args) {
if (args[0] !== "status") {
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
console.error("usage: agenthub lanes ci status <branch-or-sha> [<lane-id>]");
process.exitCode = 1;
return;
}
const ref = args[1];
if (!ref) {
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
console.error("usage: agenthub lanes ci status <branch-or-sha> [<lane-id>]");
process.exitCode = 1;
return;
}
@@ -691,8 +691,8 @@ Expected: PASS (all tests in the file, including the 3 new ones)
- [ ] **Step 5: Commit**
```bash
git add bin/ccam.js server/__tests__/lanes-cli.test.js
git commit -m "feat(cli): add ccam lanes ci status"
git add bin/agenthub.js server/__tests__/lanes-cli.test.js
git commit -m "feat(cli): add agenthub lanes ci status"
```
---
@@ -701,7 +701,7 @@ git commit -m "feat(cli): add ccam lanes ci status"
**Files:**
- Modify: `skills/ship-feature-lane/SKILL.md` (Stage 13 section, and the "Current status" summary near the top)
- Modify: `docs/LANES.md` ("Stages currently hardcoded skipped", the `ccam lanes integration` section's claim about `ci_wait`'s consumer, and the command reference)
- Modify: `docs/LANES.md` ("Stages currently hardcoded skipped", the `agenthub lanes integration` section's claim about `ci_wait`'s consumer, and the command reference)
No test for this task — it's documentation/skill-text only, verified by reading it back and by the `update-project-docs` checklist. This task has no code, so it isn't gated behind `npm run test:server`; run that suite once at the very end of the plan instead (see Task 5 Step 4).
@@ -711,13 +711,13 @@ Replace the existing Stage 13 section (currently: `### 13 — Post-merge verific
```markdown
### 13 — Post-merge verification *(CI-wait works; dev-QC still unavailable)*
- `ccam stage merged --status running --note "watching development CI post-merge"`
- `agenthub stage merged --status running --note "watching development CI post-merge"`
- Get the merge commit: `gh pr view <pr_url> --json mergeCommit -q .mergeCommit.oid`.
- Loop every ~5 minutes, paced by a **backgrounded** wait (same shape as Stage 12): `sleep 300` with `run_in_background: true`. Each iteration, bump the heartbeat and check:
- `ccam lanes ci status <mergeSha>` — exit `3` (pending) → keep waiting, background another `sleep 300`, end the turn.
- Exit `1` (failure) → triage exactly like Stage 10 (read the failing job via `gh run view <run-id>` — the run id is embedded in each failing check's `url` in the command's JSON output; distinguish real failure from shared-runner flake the same way). A real failure on `development` post-merge is a regression the fix-loop doesn't reach (the PR is already merged) — `ccam stage merged --status blocked --note "<job> red on development after merge — human decision"` and STOP; this is not something the pipeline auto-fixes on a shared branch.
- Exit `0` (success) → CI-wait passed. Dev-QC (QCing the deployed site) is a separate, not-yet-built integration — `ccam stage merged --status passed --note "CI green on development; dev-QC unavailable (separate task)"` and proceed to Stage 14.
- If `ccam lanes integration ci_wait` reads off for this profile, skip the loop entirely and go straight to Stage 14 with `--note "ci_wait integration off for this profile"` — the toggle means the profile doesn't want this check, not that the command is unavailable.
- `agenthub lanes ci status <mergeSha>` — exit `3` (pending) → keep waiting, background another `sleep 300`, end the turn.
- Exit `1` (failure) → triage exactly like Stage 10 (read the failing job via `gh run view <run-id>` — the run id is embedded in each failing check's `url` in the command's JSON output; distinguish real failure from shared-runner flake the same way). A real failure on `development` post-merge is a regression the fix-loop doesn't reach (the PR is already merged) — `agenthub stage merged --status blocked --note "<job> red on development after merge — human decision"` and STOP; this is not something the pipeline auto-fixes on a shared branch.
- Exit `0` (success) → CI-wait passed. Dev-QC (QCing the deployed site) is a separate, not-yet-built integration — `agenthub stage merged --status passed --note "CI green on development; dev-QC unavailable (separate task)"` and proceed to Stage 14.
- If `agenthub lanes integration ci_wait` reads off for this profile, skip the loop entirely and go straight to Stage 14 with `--note "ci_wait integration off for this profile"` — the toggle means the profile doesn't want this check, not that the command is unavailable.
```
- [ ] **Step 2: Update the skill's front-matter description and "Current status" summary**
@@ -726,7 +726,7 @@ In `skills/ship-feature-lane/SKILL.md`, the YAML front-matter `description` fiel
> `NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands.`
Change to:
> `NOTE: ticket-filing (Stage 9) and post-merge dev-site QC (Stage 13's second half) are currently OFF (F's integrations aren't built yet) — Stage 13's CI-wait half now runs via ccam lanes ci status.`
> `NOTE: ticket-filing (Stage 9) and post-merge dev-site QC (Stage 13's second half) are currently OFF (F's integrations aren't built yet) — Stage 13's CI-wait half now runs via agenthub lanes ci status.`
Find the "Current status" bullet list (`**Stages currently hardcoded skipped**`) and change the Stage 13 line from:
> `**13 (Post-merge verification)**: Dev-CI-wait and dev-QC integrations are off — the stage transitions immediately to `done` with a note explaining the gap.`
@@ -738,19 +738,19 @@ to a line under a renamed heading that separates the two:
- **9 (Ticket)**: Ticket-filing integration not yet built — this stage is skipped entirely.
**Partially wired:**
- **13 (Post-merge verification)**: CI-wait half now runs for real via `ccam lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
- **13 (Post-merge verification)**: CI-wait half now runs for real via `agenthub lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
```
- [ ] **Step 3: Update `docs/LANES.md`**
1. In the "Stages currently hardcoded skipped" list (around line 1137-1139), change the Stage 13 bullet to match the SKILL.md wording from Step 2 above (CI-wait works, dev-QC doesn't).
2. In the `ccam lanes integration` section (around line 1224), change:
> `The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`ccam ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.`
2. In the `agenthub lanes integration` section (around line 1224), change:
> `The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`agenthub ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.`
to:
> `The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `ccam lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.`
3. Add `ccam lanes ci status` to wherever `docs/LANES.md` lists `ccam lanes` subcommands (same table/section `ccam lanes sync-base`, `ccam lanes integration` etc. already appear in) — one row: `ccam lanes ci status <branch-or-sha> [<id>]` — one-shot GitHub Actions status for a ref (exit 0/1/3), used by Stage 13's CI-wait.
4. Update the `**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet" line (around line 1198) to drop `ccam ci`/`ci_wait` framing if it's still described there as unbuilt — cross-check by re-reading that paragraph once Step 1-3 are done, since it may already only be about the two agents.
> `The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `agenthub lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.`
3. Add `agenthub lanes ci status` to wherever `docs/LANES.md` lists `agenthub lanes` subcommands (same table/section `agenthub lanes sync-base`, `agenthub lanes integration` etc. already appear in) — one row: `agenthub lanes ci status <branch-or-sha> [<id>]` — one-shot GitHub Actions status for a ref (exit 0/1/3), used by Stage 13's CI-wait.
4. Update the `**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet" line (around line 1198) to drop `agenthub ci`/`ci_wait` framing if it's still described there as unbuilt — cross-check by re-reading that paragraph once Step 1-3 are done, since it may already only be about the two agents.
- [ ] **Step 4: Full verification pass**
@@ -765,5 +765,5 @@ Both must pass clean — this is the first point every file this plan touched (c
```bash
git add skills/ship-feature-lane/SKILL.md docs/LANES.md
git commit -m "docs(lanes): wire Stage 13 CI-wait to ccam lanes ci status"
git commit -m "docs(lanes): wire Stage 13 CI-wait to agenthub lanes ci status"
```
@@ -4,7 +4,7 @@
**Goal:** A lane can be driven by the `codex` CLI instead of `claude`, started and watched from the Workspace page with the same lane strip, pipeline map, terminal console and run history.
**Architecture:** The tool is a lane-level, immutable column (`lanes.tool`), chosen in the Add-lane modal. `pty-run.js` branches its argv builder on that tool; everything else (tmux session naming `ccam-lane-<id>`, create-or-adopt, console attach) is untouched. Runs record their tool in `dashboard_runs.tool` so history and the active-runs list can badge them.
**Architecture:** The tool is a lane-level, immutable column (`lanes.tool`), chosen in the Add-lane modal. `pty-run.js` branches its argv builder on that tool; everything else (tmux session naming `agenthub-lane-<id>`, create-or-adopt, console attach) is untouched. Runs record their tool in `dashboard_runs.tool` so history and the active-runs list can badge them.
**Tech Stack:** Node 20 + Express + better-sqlite3 (server, `node:test`), React 18 + Vite + TypeScript + Vitest/Testing Library (client), tmux for panes.
@@ -851,7 +851,7 @@ Claude lanes render nothing new, so existing lane cards are pixel-identical.
- [ ] **Step 4: Update the docs**
In `docs/LANES.md`, add a "Lane tool" section stating: a lane is driven by `claude` (default) or `codex`, chosen in Add lane and immutable afterwards; the Codex pane maps model/sandbox/approval/reasoning-effort onto `codex -m/-s/-a/-c model_reasoning_effort=`; resume for a Codex lane is `codex resume --last`; and the known limitations — Codex emits no Claude Code hooks, so Sessions, Analytics, cost and `stage-detect` stay empty for a Codex lane, `ccam stage` still works because the agent calls it itself, and liveness comes from the tmux pane plus `dashboard_runs`.
In `docs/LANES.md`, add a "Lane tool" section stating: a lane is driven by `claude` (default) or `codex`, chosen in Add lane and immutable afterwards; the Codex pane maps model/sandbox/approval/reasoning-effort onto `codex -m/-s/-a/-c model_reasoning_effort=`; resume for a Codex lane is `codex resume --last`; and the known limitations — Codex emits no Claude Code hooks, so Sessions, Analytics, cost and `stage-detect` stay empty for a Codex lane, `agenthub stage` still works because the agent calls it itself, and liveness comes from the tmux pane plus `dashboard_runs`.
In `docs/API.md`, document `tool` on the lane payload and on run history, the new `tool` body field for `POST /api/lanes/ensure` and `POST /api/lanes/worktree` (400 `EBADTOOL`), the `sandbox`/`approval`/`resumeLast` body fields on `POST /api/lanes/:id/start`, and the `?tool=` query on `GET /api/run/binary`.
@@ -2,11 +2,11 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** A lane on the 16-node `ship-feature` template shows real progress again — a `codex` lane gets auto-detected stages and a live status, a lane whose declared stage is the pipeline's last node can be detected again, and `ccam stage` from a cwd that carries both a `claude` and a `codex` lane writes to the right one.
**Goal:** A lane on the 16-node `ship-feature` template shows real progress again — a `codex` lane gets auto-detected stages and a live status, a lane whose declared stage is the pipeline's last node can be detected again, and `agenthub stage` from a cwd that carries both a `claude` and a `codex` lane writes to the right one.
**Architecture:** Four independent fixes at their shared choke points. (1) `bin/ccam.js`'s single `resolveLaneArg` learns a tool, sourced from `CCAM_LANE_ID` (injected by `pty-run` into the tmux pane), an explicit `--tool`, or the harness's own env markers — all 15 cwd-resolving commands inherit it. (2) `recordDetection` drops the declared-wins veto only when the declaration sits on the pipeline's LAST node. (3) A new `server/lib/codex-ingest.js` tails Codex's own on-disk rollout JSONL — Codex emits no hooks, so this is the codex analogue of `server/lib/workflow-ingest.js` — maps its tool calls onto hook-shaped `{tool_name, tool_input}` events, and runs the existing `detect()` + `recordDetection()` + idle/running mirror. (4) The run console stops pre-filling a Claude slash command into a codex pane.
**Architecture:** Four independent fixes at their shared choke points. (1) `bin/agenthub.js`'s single `resolveLaneArg` learns a tool, sourced from `AGENTHUB_LANE_ID` (injected by `pty-run` into the tmux pane), an explicit `--tool`, or the harness's own env markers — all 15 cwd-resolving commands inherit it. (2) `recordDetection` drops the declared-wins veto only when the declaration sits on the pipeline's LAST node. (3) A new `server/lib/codex-ingest.js` tails Codex's own on-disk rollout JSONL — Codex emits no hooks, so this is the codex analogue of `server/lib/workflow-ingest.js` — maps its tool calls onto hook-shaped `{tool_name, tool_input}` events, and runs the existing `detect()` + `recordDetection()` + idle/running mirror. (4) The run console stops pre-filling a Claude slash command into a codex pane.
**Tech Stack:** Node 20 CommonJS (server, `bin/ccam.js`), `node:test` + `node --test` for server tests, React 19 + Vite + Vitest for the client, tmux 3.4 CLI, SQLite via better-sqlite3.
**Tech Stack:** Node 20 CommonJS (server, `bin/agenthub.js`), `node:test` + `node --test` for server tests, React 19 + Vite + Vitest for the client, tmux 3.4 CLI, SQLite via better-sqlite3.
## Global Constraints
@@ -23,12 +23,12 @@
### Task 1: Tool-aware lane resolution for the CLI
A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_tool`). `resolveLaneArg` matches on cwd only and takes the first of the tie, so every cwd-resolved command — `ccam stage`, `ccam lanes pipeline`, `ccam feature activate`, 15 call sites in total — can silently drive the sibling lane's card. Fix it once in the shared function, and make the dashboard-launched pane unambiguous by construction.
A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_tool`). `resolveLaneArg` matches on cwd only and takes the first of the tie, so every cwd-resolved command — `agenthub stage`, `agenthub lanes pipeline`, `agenthub feature activate`, 15 call sites in total — can silently drive the sibling lane's card. Fix it once in the shared function, and make the dashboard-launched pane unambiguous by construction.
**Files:**
- Modify: `bin/ccam.js:1788-1809` (`resolveLaneArg`)
- Modify: `bin/agenthub.js:1788-1809` (`resolveLaneArg`)
- Modify: `server/lib/tmux.js:41-44` (`newSession` gains `env`)
- Modify: `server/lib/pty-run.js:184-236` (`spawnRun` passes `CCAM_LANE_ID`)
- Modify: `server/lib/pty-run.js:184-236` (`spawnRun` passes `AGENTHUB_LANE_ID`)
- Test: `server/__tests__/lanes-cli.test.js` (resolution precedence)
- Test: `server/__tests__/pty-run.test.js` (env injection)
@@ -37,17 +37,17 @@ A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_
- Produces:
- `resolveLaneArg(args) -> Promise<{laneId: string|number, rest: string[]}|null>` — unchanged signature, new precedence.
- `tmux.newSession({name, cwd, argv, env})` — `env` is an optional plain object of string→string; omitted/empty behaves exactly as before.
- Env contract: `CCAM_LANE_ID=<lane id>` is present in every pane the dashboard starts.
- Env contract: `AGENTHUB_LANE_ID=<lane id>` is present in every pane the dashboard starts.
- [ ] **Step 1: Write the failing CLI-resolution tests**
In `server/__tests__/lanes-cli.test.js`, following that file's existing harness (it already boots the API and shells out to `bin/ccam.js`): create two lanes at the SAME cwd, one `claude` and one `codex`, then assert
1. with `CCAM_LANE_ID` set to the codex lane's id, `ccam stage plan` moves the codex lane and leaves the claude lane untouched;
In `server/__tests__/lanes-cli.test.js`, following that file's existing harness (it already boots the API and shells out to `bin/agenthub.js`): create two lanes at the SAME cwd, one `claude` and one `codex`, then assert
1. with `AGENTHUB_LANE_ID` set to the codex lane's id, `agenthub stage plan` moves the codex lane and leaves the claude lane untouched;
2. with `--tool codex` and no env, the codex lane moves;
3. with `CLAUDECODE=1` in the environment and no flag, the claude lane moves;
4. with neither a flag nor any env marker (delete `CLAUDECODE` and `CLAUDE_CODE_ENTRYPOINT` from the child env — the test itself runs under Claude Code), the command FAILS (non-zero exit) and its stderr names both candidate lanes — an ambiguous write is worse than no write.
Note for case 2/3: there is deliberately no codex env marker (see the comment in Step 3), so `--tool codex` and `CCAM_LANE_ID` are the only two ways a codex session outside a dashboard pane can identify itself.
Note for case 2/3: there is deliberately no codex env marker (see the comment in Step 3), so `--tool codex` and `AGENTHUB_LANE_ID` are the only two ways a codex session outside a dashboard pane can identify itself.
- [ ] **Step 2: Run them to verify they fail**
@@ -58,7 +58,7 @@ Expected: FAIL — cases 1, 2 and 4 write to the claude lane (the first tie-brea
```js
/**
* Resolution order: `--lane`/positional id → `CCAM_LANE_ID` (the dashboard
* Resolution order: `--lane`/positional id → `AGENTHUB_LANE_ID` (the dashboard
* injects it into every pane it starts) → `--tool` → the harness's own env
* markers → the lane owning the cwd. The tool matters because a cwd can back
* one `claude` lane AND one `codex` lane (composite idx_lanes_cwd_tool): with
@@ -72,7 +72,7 @@ Expected: FAIL — cases 1, 2 and 4 write to the claude lane (the first tie-brea
// READS) and CODEX_SANDBOX* (set only under a sandbox policy), so sniffing for
// a CODEX_ prefix would be a guess. A codex session identifies itself with
// `--tool codex`, or needs nothing at all inside a dashboard-started pane
// (CCAM_LANE_ID above). Absent any signal with two candidates we error.
// (AGENTHUB_LANE_ID above). Absent any signal with two candidates we error.
function toolFromEnv() {
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return "claude";
return null;
@@ -87,8 +87,8 @@ async function resolveLaneArg(args) {
const explicit = flagValue("lane");
if (explicit) return { laneId: explicit, rest: args };
if (args.length && /^\d+$/.test(args[0])) return { laneId: args[0], rest: args.slice(1) };
if (process.env.CCAM_LANE_ID && /^\d+$/.test(process.env.CCAM_LANE_ID)) {
return { laneId: process.env.CCAM_LANE_ID, rest: args };
if (process.env.AGENTHUB_LANE_ID && /^\d+$/.test(process.env.AGENTHUB_LANE_ID)) {
return { laneId: process.env.AGENTHUB_LANE_ID, rest: args };
}
const tool = flagValue("tool") || toolFromEnv();
@@ -98,7 +98,7 @@ async function resolveLaneArg(args) {
.filter((l) => cwd === l.cwd || cwd.startsWith(`${l.cwd}/`))
.sort((a, b) => b.cwd.length - a.cwd.length);
if (!owning.length) {
console.error(`no lane owns ${cwd} — create one with: ccam lanes add --cwd ${cwd}`);
console.error(`no lane owns ${cwd} — create one with: agenthub lanes add --cwd ${cwd}`);
process.exitCode = 1;
return null;
}
@@ -123,7 +123,7 @@ async function resolveLaneArg(args) {
}
```
Also add `--tool <claude|codex>` to the `usage:` line of `cmdStage` (`bin/ccam.js:2470-2473`) and to the `stage` entry in `COMMAND_GROUPS`.
Also add `--tool <claude|codex>` to the `usage:` line of `cmdStage` (`bin/agenthub.js:2470-2473`) and to the `stage` entry in `COMMAND_GROUPS`.
- [ ] **Step 4: Run the CLI tests to verify they pass**
@@ -132,7 +132,7 @@ Expected: PASS.
- [ ] **Step 5: Write the failing env-injection test**
In `server/__tests__/pty-run.test.js`, using that file's existing `tmux.__setExecImpl` capture: assert `spawnRun({laneId: 7, cwd: "/tmp/x", tool: "codex"})` produces a `new-session` argv containing `-e` immediately followed by `CCAM_LANE_ID=7`, and that the shell-pane branch (pane command `bash`) first sends `export CCAM_LANE_ID=7` and then the tool argv.
In `server/__tests__/pty-run.test.js`, using that file's existing `tmux.__setExecImpl` capture: assert `spawnRun({laneId: 7, cwd: "/tmp/x", tool: "codex"})` produces a `new-session` argv containing `-e` immediately followed by `AGENTHUB_LANE_ID=7`, and that the shell-pane branch (pane command `bash`) first sends `export AGENTHUB_LANE_ID=7` and then the tool argv.
- [ ] **Step 6: Run it to verify it fails**
@@ -147,8 +147,8 @@ Expected: FAIL — no `-e` in the argv, single `send-keys` sequence.
/**
* `env` entries become tmux `-e NAME=value` pairs, which tmux puts in the
* pane's own environment (tmux >= 3.2). The dashboard uses it for
* CCAM_LANE_ID so a CLI call from inside the pane resolves its lane exactly
* instead of guessing from cwd — see bin/ccam.js's resolveLaneArg.
* AGENTHUB_LANE_ID so a CLI call from inside the pane resolves its lane exactly
* instead of guessing from cwd — see bin/agenthub.js's resolveLaneArg.
*/
function newSession({ name, cwd, argv, env }) {
const envFlags = [];
@@ -162,13 +162,13 @@ function newSession({ name, cwd, argv, env }) {
```js
if (!tmux.hasSession(id)) {
tmux.newSession({ name: id, cwd, argv, env: { CCAM_LANE_ID: String(laneId) } });
tmux.newSession({ name: id, cwd, argv, env: { AGENTHUB_LANE_ID: String(laneId) } });
record();
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
// tmux's -e only applies at session creation, so an existing shell pane
// gets the variable the only way a live shell can: an export line typed
// ahead of the tool's own argv.
tmux.sendCommand(id, ["export", `CCAM_LANE_ID=${laneId}`]);
tmux.sendCommand(id, ["export", `AGENTHUB_LANE_ID=${laneId}`]);
tmux.sendCommand(id, argv);
record();
}
@@ -182,7 +182,7 @@ Expected: PASS.
- [ ] **Step 9: Commit**
```bash
git add bin/ccam.js server/lib/tmux.js server/lib/pty-run.js server/__tests__/pty-run.test.js server/__tests__/lanes-cli.test.js
git add bin/agenthub.js server/lib/tmux.js server/lib/pty-run.js server/__tests__/pty-run.test.js server/__tests__/lanes-cli.test.js
git commit -m "fix(cli): resolve a lane by tool, not by cwd alone"
```
@@ -720,7 +720,7 @@ git commit -m "feat(lanes): detect codex lane stages from codex rollout journals
### Task 4: The run console stops pre-filling a Claude slash command for codex lanes
`pipeline_skill` is a Claude Code slash command (`/ccam:ship-feature-lane`). Pre-filled into a codex pane it is plain text Codex cannot run, which is why the codex lane in issue 11 ran for 34 minutes and never declared a stage. A codex lane opens with an empty prompt box instead.
`pipeline_skill` is a Claude Code slash command (`/agenthub:ship-feature-lane`). Pre-filled into a codex pane it is plain text Codex cannot run, which is why the codex lane in issue 11 ran for 34 minutes and never declared a stage. A codex lane opens with an empty prompt box instead.
**Files:**
- Modify: `client/src/components/run/LaneConsolePane.tsx:141-156`
@@ -732,7 +732,7 @@ git commit -m "feat(lanes): detect codex lane stages from codex rollout journals
- [ ] **Step 1: Write the failing test**
In `client/src/components/run/__tests__/LaneConsolePane.test.tsx`, following the existing tool-aware tests there: render the pane for a lane with `tool: "codex"` and `pipeline_skill: "/ccam:ship-feature-lane"`, assert the prompt textarea is empty; render for the same lane with `tool: "claude"`, assert it holds `/ccam:ship-feature-lane `.
In `client/src/components/run/__tests__/LaneConsolePane.test.tsx`, following the existing tool-aware tests there: render the pane for a lane with `tool: "codex"` and `pipeline_skill: "/agenthub:ship-feature-lane"`, assert the prompt textarea is empty; render for the same lane with `tool: "claude"`, assert it holds `/agenthub:ship-feature-lane `.
- [ ] **Step 2: Run to verify it fails**
@@ -769,26 +769,26 @@ git commit -m "fix(run): don't prefill a claude slash command into a codex pane"
### Task 5: Docs, and the instruction file Codex actually reads
`CLAUDE.md` tells a Claude session to declare `ccam stage` even in plain chat. Codex never sees that file, and this repo has no `AGENTS.md` at all — so a codex lane has no instruction to declare anything. That file is also the generic answer for any repo a codex lane runs in, so it is documented as such.
`CLAUDE.md` tells a Claude session to declare `agenthub stage` even in plain chat. Codex never sees that file, and this repo has no `AGENTS.md` at all — so a codex lane has no instruction to declare anything. That file is also the generic answer for any repo a codex lane runs in, so it is documented as such.
**Files:**
- Create: `AGENTS.md`
- Modify: `CLAUDE.md` (the `_Last synced:_` marker; the detection invariant bullet; the lanes bullet list)
- Modify: `docs/LANES.md` (codex detection, tool-aware CLI resolution, terminal-declaration exception)
- Modify: `docs/CLI.md` (`--tool` on cwd-resolved commands, `CCAM_LANE_ID`)
- Modify: `docs/CLI.md` (`--tool` on cwd-resolved commands, `AGENTHUB_LANE_ID`)
- Modify: `docs/DEPLOYMENT.md` (the new `DASHBOARD_CODEX_POLL_MS` / `CODEX_HOME` env vars, beside `DASHBOARD_WORKFLOW_POLL_MS`)
- Modify: `ARCHITECTURE.md` (the ingestion paths list, beside workflow-ingest)
**What this task does NOT fix, stated plainly:** after Tasks 1-4 a codex lane's
pipeline map moves on its own (detection) and its card shows running/idle, but
its *declared* stage still depends on the codex session actually calling
`ccam stage` — CCAM has no way to inject an instruction into a CLI it merely
`agenthub stage` — AgentHub has no way to inject an instruction into a CLI it merely
spawns. `AGENTS.md` is the only lever, and it is per-repository. A codex lane in
a repo without one will show detected stages and no declarations.
- [ ] **Step 1: Write `AGENTS.md`**
Short and behavioral, not a second copy of `CLAUDE.md`: point at `CLAUDE.md` for the engineering rules, then state the two things Codex specifically has to be told — declare `ccam stage <stage>` at every real transition (with `--tool codex`, or rely on `CCAM_LANE_ID` inside a dashboard-started pane), and the file-header rule from `.claude/rules/file-headers.md`.
Short and behavioral, not a second copy of `CLAUDE.md`: point at `CLAUDE.md` for the engineering rules, then state the two things Codex specifically has to be told — declare `agenthub stage <stage>` at every real transition (with `--tool codex`, or rely on `AGENTHUB_LANE_ID` inside a dashboard-started pane), and the file-header rule from `.claude/rules/file-headers.md`.
- [ ] **Step 2: Update the docs above**