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
@@ -4,7 +4,7 @@
## Problem
A lane's stage only moves when the driving agent calls `ccam stage <name>`. Every un-instrumented session — which is most of them — sits at `idle` forever while its agent works, so the pipeline map shows nothing. The dashboard already receives every tool call the agent makes; it just never reads them.
A lane's stage only moves when the driving agent calls `agenthub stage <name>`. Every un-instrumented session — which is most of them — sits at `idle` forever while its agent works, so the pipeline map shows nothing. The dashboard already receives every tool call the agent makes; it just never reads them.
## Goal
@@ -12,7 +12,7 @@ Infer a lane's stage from the hook stream it already ingests, and show it **with
## The rule that shapes everything
**Inference never renders green.** A node the dashboard inferred reaches `passed-no-evidence` (amber) at most; `done` requires a declared stage carrying evidence via `ccam stage --evidence`. If inference could paint a node green, the amber/green distinction — the reason this feature exists — would be worthless.
**Inference never renders green.** A node the dashboard inferred reaches `passed-no-evidence` (amber) at most; `done` requires a declared stage carrying evidence via `agenthub stage --evidence`. If inference could paint a node green, the amber/green distinction — the reason this feature exists — would be worthless.
Declared always outranks detected. A lane that has declared `review` ignores a detection for `implement`.
@@ -42,7 +42,7 @@ Four small pieces, each independently useful:
## What the console must not do
**It never touches the lane's stage.** Typing `/code-review` in the UI does not move the lane to `review`; only `ccam stage` declares, and only detection (sub-project B) infers. The console is a window onto a process, not a driver of the pipeline. Keeping that boundary is what stops the pipeline from becoming a lie.
**It never touches the lane's stage.** Typing `/code-review` in the UI does not move the lane to `review`; only `agenthub stage` declares, and only detection (sub-project B) infers. The console is a window onto a process, not a driver of the pipeline. Keeping that boundary is what stops the pipeline from becoming a lie.
## Risks and how they are contained
@@ -4,14 +4,14 @@
## Problem
A lane today is a pointer at a directory that already exists. Two agents working in parallel therefore work in the *same* checkout and collide — the exact failure Shipyard solves by giving every lane its own clone. CCAM has no provisioning at all: no way to create a lane's working copy, no way to reset it between features, no way to remove it, and no way to reclaim the database a finished lane leaves behind (a real install reached 121 MB).
A lane today is a pointer at a directory that already exists. Two agents working in parallel therefore work in the *same* checkout and collide — the exact failure Shipyard solves by giving every lane its own clone. AgentHub has no provisioning at all: no way to create a lane's working copy, no way to reset it between features, no way to remove it, and no way to reclaim the database a finished lane leaves behind (a real install reached 121 MB).
## Goals
- A lane can own a **git worktree** that CCAM creates, resets and removes.
- The lifecycle verbs mirror Shipyard's, because that vocabulary is proven: `add`, `clear`, `reset`, `remove`, plus `purge` as CCAM's analogue of Shipyard's per-lane `dropdb`.
- A lane can own a **git worktree** that AgentHub creates, resets and removes.
- The lifecycle verbs mirror Shipyard's, because that vocabulary is proven: `add`, `clear`, `reset`, `remove`, plus `purge` as AgentHub's analogue of Shipyard's per-lane `dropdb`.
- Every destructive action is confirmed **against counted facts**, not adjectives.
- Directories the user already had must be impossible for CCAM to destroy.
- Directories the user already had must be impossible for AgentHub to destroy.
## Non-goals
@@ -33,9 +33,9 @@ Additive columns on `lanes`, each guarded by the repo's `try { SELECT col } catc
`branch`, `cwd`, `stage`, `stages` and the rest keep their current meaning. `cwd` stays `UNIQUE`.
Environment: `LANES_ROOT` (default `~/.claude/ccam-lanes`), `LANE_BASE_BRANCH`, `LANE_BRANCH_PREFIX` (default `feat/`).
Environment: `LANES_ROOT` (default `~/.claude/agenthub-lanes`), `LANE_BASE_BRANCH`, `LANE_BRANCH_PREFIX` (default `feat/`).
Layout: worktree at `$LANES_ROOT/<repo-basename>__<slug>`, branch `<prefix><slug>`. Numbered `lane1..lane9` slots were considered and rejected — CCAM is multi-repo, and slot numbers carry no meaning without Shipyard's per-lane ports.
Layout: worktree at `$LANES_ROOT/<repo-basename>__<slug>`, branch `<prefix><slug>`. Numbered `lane1..lane9` slots were considered and rejected — AgentHub is multi-repo, and slot numbers carry no meaning without Shipyard's per-lane ports.
## Safety model
@@ -49,7 +49,7 @@ A `managed` lane may be destroyed only when **all three** independent checks pas
Shipyard gets away with one check (`case "$DIR" in */lane$N`) because its directory names are fixed. Dropping numbered slots costs that guarantee, so three cheaper checks replace it. Every destructive function in `server/lib/worktree.js` re-runs the three checks itself rather than trusting its caller.
CCAM never runs `rm -rf` on a lane. Removal goes through `git worktree remove`; if git refuses, the error surfaces unchanged.
AgentHub never runs `rm -rf` on a lane. Removal goes through `git worktree remove`; if git refuses, the error surfaces unchanged.
## Verbs
@@ -110,7 +110,7 @@ SQLite does not shrink on `DELETE`. `purge` runs `DELETE` plus `PRAGMA optimize`
**API:** `POST /api/lanes/worktree` (add), `GET /api/lanes/:id/preflight`, and `reset` / `purge` joining the existing `POST /api/lanes/:id/:action` set, all behind the existing same-origin guard.
**CLI:** `ccam lanes add --repo <path> [--title <t>] [--base <branch>]`, `ccam lanes reset|remove|purge <id> [--force]`.
**CLI:** `agenthub lanes add --repo <path> [--title <t>] [--base <branch>]`, `agenthub lanes reset|remove|purge <id> [--force]`.
**UI:** a `managed` / `adopted` badge on the lane card; destructive buttons rendered only for `managed`; the existing `ConfirmModal` showing the preflight table.
@@ -27,7 +27,7 @@ Two facts also make lanes look emptier than they are:
## Goal
The reference screen's legibility, on CCAM's real data: a lane's state readable
The reference screen's legibility, on AgentHub's real data: a lane's state readable
from across the room, the pipeline large enough to trace, and the console present
but out of the way until wanted.
@@ -37,7 +37,7 @@ but out of the way until wanted.
- **The console collapses.** It keeps every capability from A; it starts
collapsed and opens for the selected lane. Watching lanes is the default
posture, driving one is the exception.
- **Only real data.** No placeholder tiles for facts CCAM does not have
- **Only real data.** No placeholder tiles for facts AgentHub does not have
(tickets, preview ports, per-lane credentials). Branch/commit/CI are added
because they can be read for real — see below.
- **Detection expires.** A detection older than a TTL stops holding the floor.
@@ -59,7 +59,7 @@ A wired it. The console is unchanged behind its new disclosure — no prop of
## The card
Reference layout, CCAM's fields, nothing invented:
Reference layout, AgentHub's fields, nothing invented:
| Row | Content | Source |
|---|---|---|
@@ -1,10 +1,10 @@
# Lane profile scaffolding (A3, v1: Node preset) — design
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`. This is the third phase of the Shipyard-parity roadmap (`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, phase A3), scoped down to exactly the piece that phase's own text asked for: **detect a Node.js project and scaffold a working `.ccam/profile/`**, so adopting a repo becomes one command instead of hand-writing nine hook scripts. Python/Go/Ruby detection, named in the long-range plan, is explicitly **out of scope** for this design — see Non-goals.
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`. This is the third phase of the Shipyard-parity roadmap (`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, phase A3), scoped down to exactly the piece that phase's own text asked for: **detect a Node.js project and scaffold a working `.agenthub/profile/`**, so adopting a repo becomes one command instead of hand-writing nine hook scripts. Python/Go/Ruby detection, named in the long-range plan, is explicitly **out of scope** for this design — see Non-goals.
## Problem
Today, using CCAM's runtime/data-isolation features (A1: slots+ports, A2: database/Redis/`.env`) on a new repo means a human reads `docs/LANES.md` and hand-writes `.ccam/profile/profile.env` plus every hook script from scratch. That's the adoption barrier this removes.
Today, using AgentHub's runtime/data-isolation features (A1: slots+ports, A2: database/Redis/`.env`) on a new repo means a human reads `docs/LANES.md` and hand-writes `.agenthub/profile/profile.env` plus every hook script from scratch. That's the adoption barrier this removes.
## Scope (v1)
@@ -19,7 +19,7 @@ Anything else (no package.json anywhere, or a `backend/` without a matching `fro
- Detecting Python (Django/FastAPI), Go, Ruby, or any non-Node stack. That is real, planned work (see the parent plan's A3 section) but a separate preset, added later, informed by whatever this Node preset's implementation actually looks like once it exists.
- Detecting ORMs/migration tools (Prisma, Knex, TypeORM, Sequelize, alembic, …). `migrate`/`seed` are always scaffolded as an explicit TODO stub when a database is detected — never a guessed command.
- pnpm/yarn/npm **workspaces** or turborepo-style (`apps/`, `packages/`) monorepo layouts. Only the flat `backend/` + `frontend/` layout already documented as CCAM's convention (`docs/LANES.md`'s `BACKEND_DIR`/`FRONTEND_DIR` example) is detected.
- pnpm/yarn/npm **workspaces** or turborepo-style (`apps/`, `packages/`) monorepo layouts. Only the flat `backend/` + `frontend/` layout already documented as AgentHub's convention (`docs/LANES.md`'s `BACKEND_DIR`/`FRONTEND_DIR` example) is detected.
- Any HTTP API route. `profile init`/`profile check` are CLI-only, local filesystem actions against the *source* repo — no dashboard surface needs them.
- A `--fix` mode for `profile check`. It is read-only/diagnostic only.
@@ -30,17 +30,17 @@ One new library module, **`server/lib/lane-detect.js`**, plus two new CLI subcom
```
server/lib/lane-detect.js
detectNode(repoPath) -> { layout: "monorepo"|"single-service", ... facts } | null
scaffoldProfile(repoPath, facts, { force }) -> writes .ccam/profile/**, returns { written: [...], todos: [...] }
scaffoldProfile(repoPath, facts, { force }) -> writes .agenthub/profile/**, returns { written: [...], todos: [...] }
checkProfile(dir) -> { ok: boolean, errors: [...], warnings: [...] }
```
`bin/ccam.js` gains:
`bin/agenthub.js` gains:
```
ccam lanes profile init <repo> [--force]
ccam lanes profile check [<path>] # defaults to cwd
agenthub lanes profile init <repo> [--force]
agenthub lanes profile check [<path>] # defaults to cwd
```
Both are pure local actions on whatever machine runs the CLI — no dashboard server round trip needed for the write itself (unlike `lanes add --repo`, this never creates a worktree or a lane row; it just prepares a repo so that a *later* `lanes add --repo` on it gets a working profile for free, because `resolveProfile` already reads `.ccam/profile/` from the lane's own working copy or its source repo — nothing downstream needs to change).
Both are pure local actions on whatever machine runs the CLI — no dashboard server round trip needed for the write itself (unlike `lanes add --repo`, this never creates a worktree or a lane row; it just prepares a repo so that a *later* `lanes add --repo` on it gets a working profile for free, because `resolveProfile` already reads `.agenthub/profile/` from the lane's own working copy or its source repo — nothing downstream needs to change).
## Detection rules (`detectNode`)
@@ -77,7 +77,7 @@ Reads only; never executes `npm`, never shells out.
### `scaffoldProfile`
Writes `<repo>/.ccam/profile/profile.env` and `<repo>/.ccam/profile/hooks/*.sh` (mode `0755`) from the facts `detectNode` returned. Refuses if `.ccam/profile/profile.env` already exists, unless `--force`. Returns which files it wrote and which lines are TODO placeholders, so the CLI can print a clear "N things need your attention" summary instead of silently declaring success.
Writes `<repo>/.agenthub/profile/profile.env` and `<repo>/.agenthub/profile/hooks/*.sh` (mode `0755`) from the facts `detectNode` returned. Refuses if `.agenthub/profile/profile.env` already exists, unless `--force`. Returns which files it wrote and which lines are TODO placeholders, so the CLI can print a clear "N things need your attention" summary instead of silently declaring success.
### `checkProfile` (validator, read-only)
@@ -87,7 +87,7 @@ Given a directory (a repo root, or a lane's cwd — the same two places `resolve
- Every hook referenced (`bootstrap`, `boot`, `health`, plus `db-create`/`db-drop` when `DB_PREFIX` is set) exists on disk and is executable (`fs.statSync(...).mode & 0o111`).
- No literal string `TODO:` remains anywhere in `profile.env` or any hook file's contents. **This is expected to fail immediately after `profile init` scaffolds a repo whose detection left any TODO (no start/dev script, or a database with unconfigured migrate/seed) — that is the intended signal to finish the manual step, not a bug.**
- If `DB_PREFIX` is set: `db-create.sh`/`db-drop.sh` both exist (redundant with the hook check above, but reported as its own line so the message reads as "database configuration" rather than a generic missing-hook error).
- If `DB_PREFIX` is set and `~/.ccam/secrets.env` (`server/lib/secrets.js:SECRETS_PATH`) does not exist: a **warning** (not a failure) — "no secrets.env; database will use built-in defaults until you create one."
- If `DB_PREFIX` is set and `~/.agenthub/secrets.env` (`server/lib/secrets.js:SECRETS_PATH`) does not exist: a **warning** (not a failure) — "no secrets.env; database will use built-in defaults until you create one."
- Every declared `PORT_BASE_<name>` port is currently free (`server/lib/ports.js:isListening`, reused as-is).
Exits non-zero and prints every failure found (not just the first) when any of the hard checks fail; warnings never affect the exit code.
@@ -95,20 +95,20 @@ Exits non-zero and prints every failure found (not just the first) when any of t
## CLI
```
ccam lanes profile init <repo> [--force]
agenthub lanes profile init <repo> [--force]
```
- `<repo>` must be an absolute, existing, git-repository path (same validation `POST /api/lanes/worktree` already applies to `sourceRepo`).
- On success: prints what was scaffolded, and — if anything was left as a TODO — a clear "N item(s) need manual attention before this profile is usable: <list>" block, plus the suggested next command (`ccam lanes profile check <repo>`).
- On success: prints what was scaffolded, and — if anything was left as a TODO — a clear "N item(s) need manual attention before this profile is usable: <list>" block, plus the suggested next command (`agenthub lanes profile check <repo>`).
- On "nothing detected": a clear, actionable refusal — *not* a bare error:
```
✖ No detectable Node.js project at <repo> (looked for backend/package.json +
frontend/package.json, or a root package.json).
Auto-scaffolding currently supports Node.js repos in that layout only.
Write .ccam/profile/ by hand — see docs/LANES.md.
Write .agenthub/profile/ by hand — see docs/LANES.md.
```
```
ccam lanes profile check [<path>]
agenthub lanes profile check [<path>]
```
- `<path>` defaults to `process.cwd()`. Deliberately **not** lane-id/cwd-resolved the way `lanes up`/`lanes logs`/etc. are — `profile check` is meant to run against a bare repo right after `profile init`, before any lane/worktree exists for it.
- Prints every failure and warning found; exits 0 only when there are zero failures (warnings are fine).
@@ -22,7 +22,7 @@ isolation — goes first because it blocks the rest, and splits into three:
- **A1 (this spec)** — a lane can boot its stack on isolated ports and directories,
and the stack survives a dashboard restart. Profiles are hand-written.
- **A2** — data isolation: `.env` seeding, database create/drop/migrate/seed, Redis index.
- **A3** — stack detection + `ccam lane profile init` scaffolding, designed **from**
- **A3** — stack detection + `agenthub lane profile init` scaffolding, designed **from**
the real profiles written during A1/A2.
The split point matters. Shipyard was genericized but its `profiles/` is still
@@ -30,14 +30,14 @@ empty (`PROFILE=_template`, `SOURCE_REPO=""`, `state/` empty) — a clean seam t
never ran against a real app. Designing presets before a real profile exists
repeats exactly that.
**A1 done means:** `ccam lane up` boots a lane's stack on ports no other lane uses,
and `ccam update` (or any dashboard restart) leaves running stacks alone.
**A1 done means:** `agenthub lane up` boots a lane's stack on ports no other lane uses,
and `agenthub update` (or any dashboard restart) leaves running stacks alone.
## Decisions
| | Choice | Rationale |
|---|---|---|
| Profile location | `<repo>/.ccam/profile/` | CCAM is multi-repo (lanes keyed by `cwd`); one global `PROFILE` is wrong the moment two repos have lanes. A profile that lives in the repo is already present in a fresh worktree. |
| Profile location | `<repo>/.agenthub/profile/` | AgentHub is multi-repo (lanes keyed by `cwd`); one global `PROFILE` is wrong the moment two repos have lanes. A profile that lives in the repo is already present in a fresh worktree. |
| Engine | Node, `server/lib/` | `execFile` with argument arrays (repo rule: never build a git/shell command as a string). Shares the SQLite DB and the websocket. Hooks stay shell scripts; the engine spawns them. |
| Resource model | Declared per repo | `PORTS="api fe worker"`, `LANE_DIRS="…"`. An app with no database declares none, so A2 generates no database hooks — no dead code. |
| Process ownership | Fully detached | pid files outside the worktree; a dashboard restart does not touch a running stack. |
@@ -48,11 +48,11 @@ and `ccam update` (or any dashboard restart) leaves running stacks alone.
## Invariants
1. **The runtime never writes `stage`, `status`, or `notes`** — only `slot` and
`ports`. In CCAM `status=running` means *an agent is working*, not *a stack is
`ports`. In AgentHub `status=running` means *an agent is working*, not *a stack is
up*; conflating the two breaks `classifyLiveness`. Boot failures surface through
`GET /runtime` and the hook log. Same reasoning as "the console never writes a
lane's stage".
2. **CCAM still does not orchestrate.** A1 adds *primitives* (`up`/`down`/`hook`)
2. **AgentHub still does not orchestrate.** A1 adds *primitives* (`up`/`down`/`hook`)
that a session calls. The dashboard chains nothing, evaluates no gate, retries nothing.
3. **No shell command built as a string.** `execFile` + argv arrays everywhere.
Hooks are the deliberate exception — they *are* scripts, spawned as
@@ -89,7 +89,7 @@ No column records "is up". Deliberate.
|---|---|
| `server/lib/ports.js` | `isListening(port)` via a short-timeout `net.connect`; `listenerPids(port)` via `lsof`, falling back to `ss`, returning `[]` with a one-time warning when neither exists. |
| `server/lib/lane-slots.js` | `allocateSlot()` — lowest free in `[1, LANE_MAX_SLOTS]` (default 9), called inside the existing `withLaneLock`, with the unique index as the backstop. `releaseSlot()` on `remove` only — **not** on `reset`, or a lane's ports and database move under a running session. `resolvePorts(slot, profile)` — prefers `PORT_BASE_<name> + slot`, then `+100`, `+200`… (the last digit stays the slot, so the number still reads as "lane 3"), up to 10 tries before `EPORTBUSY` naming the occupying pid. |
| `server/lib/lane-profile.js` | `resolveProfile(lane)` — looks in `lane.cwd/.ccam/profile/` **first**, `lane.source_repo/.ccam/profile/` second, because the profile lives in the repo and a branch that edits its hooks must run its own. `runHook(lane, name, args, {onLine})` — fixed hook-name allowlist, `bash <hook>` with an argv array, Shipyard's env contract verbatim so its profiles port unchanged (`LANE` = the **slot**, `LANE_DIR`, `RUN_DIR`, `LOG_DIR`, `PROFILE_DIR`, `SOURCE_REPO`, `<NAME>_PORT` per declared port), `harness_spawn` exported as a bash function writing the same `$RUN_DIR/<name>.pid` + `$LOG_DIR/<name>.log`, the `GIT_*` scrub from `worktree.js:git()`, and output streamed both to the log file and to a new `lane_hook_output` websocket message. |
| `server/lib/lane-profile.js` | `resolveProfile(lane)` — looks in `lane.cwd/.agenthub/profile/` **first**, `lane.source_repo/.agenthub/profile/` second, because the profile lives in the repo and a branch that edits its hooks must run its own. `runHook(lane, name, args, {onLine})` — fixed hook-name allowlist, `bash <hook>` with an argv array, Shipyard's env contract verbatim so its profiles port unchanged (`LANE` = the **slot**, `LANE_DIR`, `RUN_DIR`, `LOG_DIR`, `PROFILE_DIR`, `SOURCE_REPO`, `<NAME>_PORT` per declared port), `harness_spawn` exported as a bash function writing the same `$RUN_DIR/<name>.pid` + `$LOG_DIR/<name>.log`, the `GIT_*` scrub from `worktree.js:git()`, and output streamed both to the log file and to a new `lane_hook_output` websocket message. |
| `server/lib/lane-runtime.js` | `upLane` (defensive down → mkdir run/log/`LANE_DIRS` → `boot` → `health`; no `bootstrap`, which belongs to provision/reset as in `lane-up.sh`), `downLane` (recursive pid-tree kill from the pid files — ported from `lane-down.sh:kill_tree`, which exists because detached uvicorn/celery prefork children were being missed — then a port-listener backstop; idempotent, sleep-free), `runtimeFacts` (**computed**: `kill -0` per pid file, `isListening` per port, plus `last-error.json`). |
State lives at `LANES_ROOT/.state/lane<slot>/{run,logs}/`, **outside** the
@@ -123,7 +123,7 @@ argv, never joined.
| Condition | Code | Behaviour |
|---|---|---|
| No `.ccam/profile/` | `ENOPROFILE` | 400, naming both paths searched |
| No `.agenthub/profile/` | `ENOPROFILE` | 400, naming both paths searched |
| Unknown hook | `ENOHOOK` | 400 |
| Hook exits non-zero | — | `lane_hook_output` + `last-error.json`; reflected by `/runtime` |
| Every candidate port busy | `EPORTBUSY` | names the occupying pid |
@@ -154,8 +154,8 @@ Covered:
the allowlist returns 400 and spawns nothing.
- **Env** — with `GIT_DIR` set on the parent, the hook does not see it.
Manual: create a lane on ccam-lanes itself, write a three-line profile,
`ccam lane up`, open the port, restart the server, confirm the stack survived.
Manual: create a lane on agenthub-lanes itself, write a three-line profile,
`agenthub lane up`, open the port, restart the server, confirm the stack survived.
## Out of scope
@@ -5,20 +5,20 @@
## Problem
Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/skills/ship-feature/SKILL.md`, 208 lines, 16 stages) is the driving skill that runs a lane end-to-end: intake → TDD implement → gates → e2e → review → QC → senior gate → publish PR → watch → post-merge verify → done. CCAM has no equivalent yet. E (per the roadmap) is four independent pieces — this spec covers only the first: the pipeline template + the skill text itself. Agents (qc-local, senior-gate-reviewer, ticketer, dev-qc, pr-reviewer) and `ccam lanes sync-base` are separate, later tasks; F's integrations (tracker, dev-QC, CI-wait) are later still.
Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/skills/ship-feature/SKILL.md`, 208 lines, 16 stages) is the driving skill that runs a lane end-to-end: intake → TDD implement → gates → e2e → review → QC → senior gate → publish PR → watch → post-merge verify → done. AgentHub has no equivalent yet. E (per the roadmap) is four independent pieces — this spec covers only the first: the pipeline template + the skill text itself. Agents (qc-local, senior-gate-reviewer, ticketer, dev-qc, pr-reviewer) and `agenthub lanes sync-base` are separate, later tasks; F's integrations (tracker, dev-QC, CI-wait) are later still.
## Scope
**In scope:**
1. A backend gap the skill text requires to be truthful: `ccam lanes up --qc`, backed by a new `QC_BOOT_ENV` profile declaration.
1. A backend gap the skill text requires to be truthful: `agenthub lanes up --qc`, backed by a new `QC_BOOT_ENV` profile declaration.
2. `server/data/pipelines/ship-feature.json` — the node/stage definitions the dashboard renders against.
3. `.claude/skills/ship-feature-lane/SKILL.md` — the ported skill, every Shipyard `bin/…` call rewritten to its CCAM equivalent.
3. `.claude/skills/ship-feature-lane/SKILL.md` — the ported skill, every Shipyard `bin/…` call rewritten to its AgentHub equivalent.
**Out of scope (later tasks, referenced by name in the ported skill but not built here):**
- `ccam lanes sync-base` (the migration-collision + merge-driver command Stage 2/8/12 call).
- `ccam lanes mcp sync` (F) — Stage 0's MCP preflight becomes a manual check + a message to the human, not an auto-sync call.
- `ccam lanes integration <name>` / `.ccam/profile/integrations.env` (F) — tracker/dev_qc/ci_wait are **hardcoded off** in this port; Stage 9 (ticket) and the dev-QC half of Stage 13 unconditionally skip; Stage 10's CI watch unconditionally uses the `gh pr checks` fallback the original skill already documents for "helper not configured" (no conditional branch needed — there is nothing to branch on yet).
- `ccam ci` (F, `ci-job.sh` port) — same reasoning; Stage 10/13's CI-triage logic that depends on it is dropped to the fallback path.
- `agenthub lanes sync-base` (the migration-collision + merge-driver command Stage 2/8/12 call).
- `agenthub lanes mcp sync` (F) — Stage 0's MCP preflight becomes a manual check + a message to the human, not an auto-sync call.
- `agenthub lanes integration <name>` / `.agenthub/profile/integrations.env` (F) — tracker/dev_qc/ci_wait are **hardcoded off** in this port; Stage 9 (ticket) and the dev-QC half of Stage 13 unconditionally skip; Stage 10's CI watch unconditionally uses the `gh pr checks` fallback the original skill already documents for "helper not configured" (no conditional branch needed — there is nothing to branch on yet).
- `agenthub ci` (F, `ci-job.sh` port) — same reasoning; Stage 10/13's CI-triage logic that depends on it is dropped to the fallback path.
- The five agents — the skill references them by `subagent_type` at Stages 6/7/9/13/12(pr-reviewer callout), but none exist yet. This spec's own verification stops where an agent invocation would occur.
## Decisions
@@ -26,26 +26,26 @@ Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/A
| | Choice | Rationale |
|---|---|---|
| `--qc` boot flag | New `QC_BOOT_ENV` profile declaration (space-separated `KEY=value` pairs, same shape as Shipyard's), injected into the boot hook's environment only when `up` is called with `qc: true` | The skill calls `lane-up.sh "$N" --qc` in Stage 3 to get a deterministic (mocked/stubbed) stack for QC. Without a real flag this line in the ported skill would be false documentation — violates this repo's docs rule (executable, aligned with actual scripts). A profile that never declares `QC_BOOT_ENV` gets no extra env (dead code doesn't run), same pattern every other A2 declaration in `DEFAULTS` already follows. |
| `state.sh "$N" init` (open a clean feature slot before Stage 0's Q&A) | Dropped — replaced by a plain `ccam feature activate <slug>` at Stage 1 | Task B's `activateFeature` already archives the outgoing active feature automatically on switch; Shipyard needed a separate `init` step because it had no DB-backed archive. Porting `init` as a distinct call would be redundant with what `activate` already does. |
| Integration toggles (tracker/dev_qc/ci_wait) | Hardcoded off, not a runtime check | F doesn't exist yet. The alternative (a stub `ccam lanes integration` that always returns off) adds surface area for no behavioral difference today; the skill's own text already documents the "toggle off" behavior for each stage, so hardcoding is truthful, not a shortcut. When F ships, this becomes a follow-up edit to swap hardcoded skips for real checks — an explicit, later, reviewable diff. |
| Hook names for `lane-ci-gate.sh` / `lane-e2e.sh` | `ccam lanes hook ci-gate` / `ccam lanes hook e2e` | Both names are **already** in `server/lib/lane-profile.js`'s `HOOKS` allowlist, added during A1 specifically "because... the driving skill needs that surface stable" — this spec is that driving skill. No backend change needed for these two. |
| `.harness-lane` marker / `lane-assign.sh` | Dropped entirely | CCAM already resolves a lane from `cwd` (path-boundary prefix match, documented in this repo's CLAUDE.md) — the marker file and the assign step exist in Shipyard only because it has no such resolution. |
| `@@HARNESS_ROOT@@` / `@@DEV_QC_MCP@@` / `@@TRACKER_MCP@@` placeholders | Dropped | Install-time templating that doesn't exist in CCAM; every reference becomes the literal CCAM command or, for the two MCP placeholders, is deleted along with the (out-of-scope) integrations that used them. |
| `state.sh "$N" init` (open a clean feature slot before Stage 0's Q&A) | Dropped — replaced by a plain `agenthub feature activate <slug>` at Stage 1 | Task B's `activateFeature` already archives the outgoing active feature automatically on switch; Shipyard needed a separate `init` step because it had no DB-backed archive. Porting `init` as a distinct call would be redundant with what `activate` already does. |
| Integration toggles (tracker/dev_qc/ci_wait) | Hardcoded off, not a runtime check | F doesn't exist yet. The alternative (a stub `agenthub lanes integration` that always returns off) adds surface area for no behavioral difference today; the skill's own text already documents the "toggle off" behavior for each stage, so hardcoding is truthful, not a shortcut. When F ships, this becomes a follow-up edit to swap hardcoded skips for real checks — an explicit, later, reviewable diff. |
| Hook names for `lane-ci-gate.sh` / `lane-e2e.sh` | `agenthub lanes hook ci-gate` / `agenthub lanes hook e2e` | Both names are **already** in `server/lib/lane-profile.js`'s `HOOKS` allowlist, added during A1 specifically "because... the driving skill needs that surface stable" — this spec is that driving skill. No backend change needed for these two. |
| `.harness-lane` marker / `lane-assign.sh` | Dropped entirely | AgentHub already resolves a lane from `cwd` (path-boundary prefix match, documented in this repo's CLAUDE.md) — the marker file and the assign step exist in Shipyard only because it has no such resolution. |
| `@@HARNESS_ROOT@@` / `@@DEV_QC_MCP@@` / `@@TRACKER_MCP@@` placeholders | Dropped | Install-time templating that doesn't exist in AgentHub; every reference becomes the literal AgentHub command or, for the two MCP placeholders, is deleted along with the (out-of-scope) integrations that used them. |
## Command mapping
| Shipyard | CCAM | Status |
| Shipyard | AgentHub | Status |
|---|---|---|
| `state.sh "$N" set stage=X status=Y` | `ccam stage X --status Y` | exists |
| `state.sh "$N" activate feat/<slug>` | `ccam feature activate <slug>` | exists (Task B) |
| `state.sh "$N" set stage=X status=Y` | `agenthub stage X --status Y` | exists |
| `state.sh "$N" activate feat/<slug>` | `agenthub feature activate <slug>` | exists (Task B) |
| `state.sh "$N" init` | (dropped — see Decisions) | n/a |
| `lane-ci-gate.sh "$N"` | `ccam lanes hook ci-gate` | exists (hook allowlisted) |
| `lane-up.sh "$N" --qc` | `ccam lanes up --qc` | **new in this task** |
| `lane-e2e.sh "$N" [-- specs]` | `ccam lanes hook e2e [-- specs]` | exists (hook allowlisted) |
| `lane-sync-dev.sh …` | `ccam lanes sync-base …` | later task (referenced, not built) |
| `lane-mcp-sync.sh "$N"` | `ccam lanes mcp sync` | F (referenced as manual fallback) |
| `lane-ci-gate.sh "$N"` | `agenthub lanes hook ci-gate` | exists (hook allowlisted) |
| `lane-up.sh "$N" --qc` | `agenthub lanes up --qc` | **new in this task** |
| `lane-e2e.sh "$N" [-- specs]` | `agenthub lanes hook e2e [-- specs]` | exists (hook allowlisted) |
| `lane-sync-dev.sh …` | `agenthub lanes sync-base …` | later task (referenced, not built) |
| `lane-mcp-sync.sh "$N"` | `agenthub lanes mcp sync` | F (referenced as manual fallback) |
| `lane-env.sh "$N" --check X` | (hardcoded off) | F (deferred) |
| `ci-job.sh status\|failures\|rerun\|cancel` | `ccam ci …` | F (deferred; skill uses its own `gh`-only fallback) |
| `ci-job.sh status\|failures\|rerun\|cancel` | `agenthub ci …` | F (deferred; skill uses its own `gh`-only fallback) |
## Design
@@ -53,23 +53,23 @@ Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/A
- `QC_BOOT_ENV` added to `server/lib/lane-profile.js`'s `DEFAULTS` (empty string default — off).
- `POST /:id/up` accepts `{build, qc}` in its body (currently only reads `build`); `qc: true` is threaded through to whatever assembles the boot hook's environment (read the actual `upLane`/hook-invocation code path in `server/lib/lane-runtime.js` before implementing — this spec names the profile declaration and the route contract, not the exact internal plumbing function).
- `ccam lanes up [--no-build] [--qc]` in `bin/ccam.js` — `--qc` sets `{qc: true}` in the POST body alongside the existing `--no-build` → `{build: false}` handling.
- `agenthub lanes up [--no-build] [--qc]` in `bin/agenthub.js` — `--qc` sets `{qc: true}` in the POST body alongside the existing `--no-build` → `{build: false}` handling.
- `QC_BOOT_ENV` format: space-separated `KEY=value` pairs, parsed the same way this file already parses `PORTS`/`LANE_DIRS` (space-separated lists) — not a new parsing convention.
### Pipeline template
`server/data/pipelines/ship-feature.json` — one node per skill stage (0 through 15, using the skill's own stage names: `intake`, `plan`, `implementing`, `gates`, `e2e-feature`, `e2e-feature-passed`, `review`, `qc-plan`, `qc`, `gate`, `publishing`, `pr-open`, `reported`, `watching-pr`, `merged`, `done`), same node-definition shape `default.json` already uses (label, `detect` rules matching the `stage=` values `ccam stage` sets). `default.json` untouched. A lane opts into this template via its `pipeline` field, same mechanism every other custom template already uses.
`server/data/pipelines/ship-feature.json` — one node per skill stage (0 through 15, using the skill's own stage names: `intake`, `plan`, `implementing`, `gates`, `e2e-feature`, `e2e-feature-passed`, `review`, `qc-plan`, `qc`, `gate`, `publishing`, `pr-open`, `reported`, `watching-pr`, `merged`, `done`), same node-definition shape `default.json` already uses (label, `detect` rules matching the `stage=` values `agenthub stage` sets). `default.json` untouched. A lane opts into this template via its `pipeline` field, same mechanism every other custom template already uses.
### Skill
`.claude/skills/ship-feature-lane/SKILL.md` — the full port. Frontmatter, hard rules, and all 16 stages carried over with the Command mapping table's substitutions applied line-by-line. Every hard-won rule (turn-liveness, no-retry-cap/phase-clock, one-driver-per-MCP, never-push-base, the fix-loop's two fast-path exceptions, cross-lane etiquette) is kept verbatim in spirit, with `lane-lock.sh` etiquette re-pointed at D's `ccam lock` CLI and named locks.
`.claude/skills/ship-feature-lane/SKILL.md` — the full port. Frontmatter, hard rules, and all 16 stages carried over with the Command mapping table's substitutions applied line-by-line. Every hard-won rule (turn-liveness, no-retry-cap/phase-clock, one-driver-per-MCP, never-push-base, the fix-loop's two fast-path exceptions, cross-lane etiquette) is kept verbatim in spirit, with `lane-lock.sh` etiquette re-pointed at D's `agenthub lock` CLI and named locks.
## Invariants
1. **CCAM still does not orchestrate.** The skill runs in the session and calls `ccam` commands one at a time; nothing in this task adds chaining, gate evaluation, or retry logic to the dashboard itself.
1. **AgentHub still does not orchestrate.** The skill runs in the session and calls `agenthub` commands one at a time; nothing in this task adds chaining, gate evaluation, or retry logic to the dashboard itself.
2. **Every command the skill text names must actually exist and behave as documented** — the docs-markdown rule (executable examples, aligned with actual scripts) applies to a skill file exactly as it applies to `docs/*.md`.
3. **A profile that never declares `QC_BOOT_ENV` sees no behavior change** — `--qc` degrades to a plain boot when the declaration is absent, same as every other optional A2 declaration.
## Verify
A real dry run on a scratch repo with a profile, walking Stage 0 through Stage 2 (intake → plan → `ccam lanes hook ci-gate`) — the furthest the pipeline can go without the not-yet-built agents. Confirms every `ccam` command named in stages 0–2 exists and behaves as documented. `ccam lanes up --qc` gets its own unit/integration test verifying `QC_BOOT_ENV` reaches the boot hook's environment when declared, and is a no-op when absent. Full Stage 0→14 verification is the agents-port task's responsibility, once qc-local/senior-gate-reviewer exist.
A real dry run on a scratch repo with a profile, walking Stage 0 through Stage 2 (intake → plan → `agenthub lanes hook ci-gate`) — the furthest the pipeline can go without the not-yet-built agents. Confirms every `agenthub` command named in stages 0–2 exists and behaves as documented. `agenthub lanes up --qc` gets its own unit/integration test verifying `QC_BOOT_ENV` reaches the boot hook's environment when declared, and is a no-op when absent. Full Stage 0→14 verification is the agents-port task's responsibility, once qc-local/senior-gate-reviewer exist.
@@ -12,39 +12,39 @@
**In scope:**
1. `server/data/agent-templates/ship-feature-lane/qc-local.md` — the ported browser-QC agent template.
2. `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` — the ported publish-gate agent template.
3. `ccam lanes agents install [<id>]` — copies both templates into `<lane>/.claude/agents/`, git-excluded.
3. `agenthub lanes agents install [<id>]` — copies both templates into `<lane>/.claude/agents/`, git-excluded.
4. `docs/LANES.md` + roadmap progress line.
**Out of scope (later, when their dependencies exist):**
- **`ticketer.md`, `dev-qc.md`** — both are invoked only by Stages 9 and 13, which are hardcoded-skipped pending F's tracker/dev-QC integrations. Porting them now would ship agent files nothing calls.
- **`pr-reviewer.md`** — not referenced anywhere in this repo's `SKILL.md` (the CCAM port dropped the reference during E1; confirmed by grep). Nothing to unblock.
- **Per-lane embedded credentials** — Shipyard's installer injects a real QA account's email/password directly into `qc-local.md`'s prompt text, sourced from a per-lane seeded account (`.harness-qa.env`) that has no CCAM equivalent (no seed-account system, no matching `secrets.js` category). This spec ports the agent's *fallback* path (no embedded creds → report `FAIL` with a clear reason) rather than inventing a seed-account system to unblock a credentials feature nobody asked for yet.
- **MCP auto-provisioning** (`ccam lanes mcp sync`) — F, not built. `qc-local.md` keeps referencing the `playwright-qa-local` MCP server name by convention; `SKILL.md`'s Stage 0 already documents the manual-`.mcp.json` fallback this depends on.
- **`review-checks.md` reader command** — `senior-gate-reviewer.md` reads the profile's optional `review-checks.md` directly with the Read tool (a plain file, no CLI needed), not through a new `ccam lanes profile cat` command.
- **`pr-reviewer.md`** — not referenced anywhere in this repo's `SKILL.md` (the AgentHub port dropped the reference during E1; confirmed by grep). Nothing to unblock.
- **Per-lane embedded credentials** — Shipyard's installer injects a real QA account's email/password directly into `qc-local.md`'s prompt text, sourced from a per-lane seeded account (`.harness-qa.env`) that has no AgentHub equivalent (no seed-account system, no matching `secrets.js` category). This spec ports the agent's *fallback* path (no embedded creds → report `FAIL` with a clear reason) rather than inventing a seed-account system to unblock a credentials feature nobody asked for yet.
- **MCP auto-provisioning** (`agenthub lanes mcp sync`) — F, not built. `qc-local.md` keeps referencing the `playwright-qa-local` MCP server name by convention; `SKILL.md`'s Stage 0 already documents the manual-`.mcp.json` fallback this depends on.
- **`review-checks.md` reader command** — `senior-gate-reviewer.md` reads the profile's optional `review-checks.md` directly with the Read tool (a plain file, no CLI needed), not through a new `agenthub lanes profile cat` command.
## Decisions
| | Choice | Rationale |
|---|---|---|
| Template location | `server/data/agent-templates/ship-feature-lane/*.md` | Parallel to `server/data/pipelines/*.json` (E1's pattern) — data files, not source. **Not** `ccam-lanes/.claude/agents/`, which already holds this repo's own dev-facing agents (`backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`); mixing a lane's runtime-installed agents into that directory would be a name-collision risk and conflate two different agent populations. |
| Heartbeat calls (`state.sh set` between QC scenarios) | Dropped entirely | Shipyard dispatches these agents as a separate background-capable process, so a stalled multi-minute run needs an explicit touch to avoid a false "stalled" flag. CCAM's `qc-local`/`senior-gate-reviewer` run as a synchronous Agent-tool call inside the driving session's own turn — the turn is blocked on the subagent's return, so the session is already "alive" by definition for the whole run. No CCAM heartbeat primitive exists for a subagent to call anyway (this repo's own rule: only `ccam stage` writes progress, and these agents must never call it). |
| Auto pre-seed login (`lane-qa-login.sh`, `lane-reset.sh`) | Dropped; `qc-local.md`'s login fallback becomes an honest failure, not a script call | CCAM has no seeded-QA-account system these scripts assumed. Calling a command that doesn't exist would be the same "docs must be executable" violation E1's Decisions table already ruled against for hardcoded-off integrations — this spec's version reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured` instead. |
| `lane-sync-dev.sh --check` reference (senior-gate-reviewer's merge-safety check) | → `ccam lanes sync-base --check <branch>` | Direct 1:1 mapping — E2 already built and shipped this command with the exact contract (exit 5 collision, `DEV_DELTA`/`DEV_OVERLAP` output) the agent's text already describes. |
| `@@HARNESS_ROOT@@` / `.harness-lane` | Dropped | Same reasoning as E1's skill port — CCAM resolves a lane from `cwd` directly. |
| Install command semantics | `ccam lanes agents install [<id>]`, idempotent (overwrite-on-reinstall, not merge) | Matches the "explicit, never automatic" shape this repo already uses for `proof-link` and the E2 merge-driver setup — a session calls it when it wants updated templates, the dashboard never calls it for them. |
| Template location | `server/data/agent-templates/ship-feature-lane/*.md` | Parallel to `server/data/pipelines/*.json` (E1's pattern) — data files, not source. **Not** `agenthub-lanes/.claude/agents/`, which already holds this repo's own dev-facing agents (`backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`); mixing a lane's runtime-installed agents into that directory would be a name-collision risk and conflate two different agent populations. |
| Heartbeat calls (`state.sh set` between QC scenarios) | Dropped entirely | Shipyard dispatches these agents as a separate background-capable process, so a stalled multi-minute run needs an explicit touch to avoid a false "stalled" flag. AgentHub's `qc-local`/`senior-gate-reviewer` run as a synchronous Agent-tool call inside the driving session's own turn — the turn is blocked on the subagent's return, so the session is already "alive" by definition for the whole run. No AgentHub heartbeat primitive exists for a subagent to call anyway (this repo's own rule: only `agenthub stage` writes progress, and these agents must never call it). |
| Auto pre-seed login (`lane-qa-login.sh`, `lane-reset.sh`) | Dropped; `qc-local.md`'s login fallback becomes an honest failure, not a script call | AgentHub has no seeded-QA-account system these scripts assumed. Calling a command that doesn't exist would be the same "docs must be executable" violation E1's Decisions table already ruled against for hardcoded-off integrations — this spec's version reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured` instead. |
| `lane-sync-dev.sh --check` reference (senior-gate-reviewer's merge-safety check) | → `agenthub lanes sync-base --check <branch>` | Direct 1:1 mapping — E2 already built and shipped this command with the exact contract (exit 5 collision, `DEV_DELTA`/`DEV_OVERLAP` output) the agent's text already describes. |
| `@@HARNESS_ROOT@@` / `.harness-lane` | Dropped | Same reasoning as E1's skill port — AgentHub resolves a lane from `cwd` directly. |
| Install command semantics | `agenthub lanes agents install [<id>]`, idempotent (overwrite-on-reinstall, not merge) | Matches the "explicit, never automatic" shape this repo already uses for `proof-link` and the E2 merge-driver setup — a session calls it when it wants updated templates, the dashboard never calls it for them. |
| `.claude/agents/` git-exclusion | Appended to `<lane>/.git/info/exclude` (clone-local, not the tracked `.gitignore`) | Same mechanism `write_lane_markers` used in Shipyard and the same file this repo's proof-link machinery already touches — a lane-runtime concern never becomes a change to the app repo's own tracked ignore rules. |
## Command mapping
| Shipyard | CCAM | Status |
| Shipyard | AgentHub | Status |
|---|---|---|
| `lane-agents-install.sh <N>` | `ccam lanes agents install [<id>]` | **new in this task** |
| `lane-agents-install.sh <N>` | `agenthub lanes agents install [<id>]` | **new in this task** |
| `state.sh "$N" set` (mid-run heartbeat) | dropped — see Decisions | n/a |
| `lane-qa-login.sh "$N" local` | dropped — see Decisions | n/a |
| `lane-reset.sh "$N"` (reseed on login failure) | dropped — see Decisions | n/a |
| `lane-sync-dev.sh "$N" --check <branch>` (senior-gate-reviewer's merge-safety check) | `ccam lanes sync-base --check <branch>` | exists (E2) |
| `lane-up.sh "$N" --no-build` (revive a down stack) | `ccam lanes up --no-build` | exists (E1 runtime, pre-existing flag) |
| `profile-cat.sh review-checks.md` | Read tool on `<lane>/.ccam/profile/review-checks.md` directly | no command needed |
| `lane-sync-dev.sh "$N" --check <branch>` (senior-gate-reviewer's merge-safety check) | `agenthub lanes sync-base --check <branch>` | exists (E2) |
| `lane-up.sh "$N" --no-build` (revive a down stack) | `agenthub lanes up --no-build` | exists (E1 runtime, pre-existing flag) |
| `profile-cat.sh review-checks.md` | Read tool on `<lane>/.agenthub/profile/review-checks.md` directly | no command needed |
| `"$HARNESS/bin/lane-mcp-sync.sh"` (implied MCP setup) | manual `.mcp.json` — `SKILL.md` Stage 0 already documents this fallback | F (deferred) |
## Design
@@ -54,17 +54,17 @@
Both files are near-verbatim ports of the Shipyard source with the Command-mapping substitutions applied and the out-of-scope credential/heartbeat/login sections rewritten per the Decisions table:
- **`qc-local.md`** — frontmatter (`name`, `description`, `model: opus`, `color: green`) unchanged in shape. Body: drops the heartbeat loop and the `@@HARNESS_ROOT@@`/`.harness-lane` setup; the "Account / login" section keeps the "if a credentials block is embedded at the top of this agent, use it" path (future-proofed for when a seed-account system exists) but replaces the auto-login-script fallback with an honest `FAIL`. Proof convention (`proof/<feature-slug>/qc-local/<NN>-<what>.png`) is unchanged — it already matches this repo's proof gallery (C) verbatim.
- **`senior-gate-reviewer.md`** — frontmatter unchanged (`model: opus`, `color: red`). Body: the merge-safety check's command becomes `ccam lanes sync-base --check <branch>`; the stack-specific-checks step reads `review-checks.md` with the Read tool if present, otherwise falls back to "derive the equivalents yourself" exactly as the original already instructs when that file is empty. Nothing else changes — this agent never touched credentials or heartbeats to begin with.
- **`senior-gate-reviewer.md`** — frontmatter unchanged (`model: opus`, `color: red`). Body: the merge-safety check's command becomes `agenthub lanes sync-base --check <branch>`; the stack-specific-checks step reads `review-checks.md` with the Read tool if present, otherwise falls back to "derive the equivalents yourself" exactly as the original already instructs when that file is empty. Nothing else changes — this agent never touched credentials or heartbeats to begin with.
### `ccam lanes agents install`
### `agenthub lanes agents install`
- `POST /api/lanes/:id/agents/install` — reads both templates from `server/data/agent-templates/ship-feature-lane/`, writes them to `<lane>/.claude/agents/{qc-local.md,senior-gate-reviewer.md}` (creating the directory if needed), appends `.claude/agents/` to `<lane>/.git/info/exclude` if not already present (same idempotent read-existing-then-append pattern E2's `setupMergeDriver` already established for `.git/info/attributes`). Returns `{installed: ["qc-local.md", "senior-gate-reviewer.md"]}`.
- No templating/placeholder substitution needed at install time — both files are already CCAM-final text (all substitutions happened once, at port time, into the template files themselves). This is a deliberate simplification from Shipyard's Python templating step, which existed only to inject per-lane credentials and MCP names this scope doesn't have.
- CLI: `ccam lanes agents install [<id>]`, same `resolveLaneArg` pattern as `lanes proof-link`.
- No templating/placeholder substitution needed at install time — both files are already AgentHub-final text (all substitutions happened once, at port time, into the template files themselves). This is a deliberate simplification from Shipyard's Python templating step, which existed only to inject per-lane credentials and MCP names this scope doesn't have.
- CLI: `agenthub lanes agents install [<id>]`, same `resolveLaneArg` pattern as `lanes proof-link`.
## Invariants
1. **CCAM still does not orchestrate.** `agents install` copies static files and returns; nothing about it triggers, gates, or sequences pipeline stages.
1. **AgentHub still does not orchestrate.** `agents install` copies static files and returns; nothing about it triggers, gates, or sequences pipeline stages.
2. **Neither agent ever writes `stage`/`status`/`notes`.** Both already say so explicitly in their own prompt text (ported verbatim); this spec doesn't change that boundary.
3. **A lane that never runs `agents install` sees no behavior change** — Stage 6/7 remain `blocked` exactly as today until a session explicitly installs the agents, same "explicit, never automatic" shape as `proof-link`.
@@ -1,27 +1,27 @@
# F3a — `ccam lanes integration <name>` toggle reader
# F3a — `agenthub lanes integration <name>` toggle reader
**Status:** approved 2026-08-05. Roadmap it belongs to:
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**).
## Problem
`SKILL.md`'s Setup section hardcodes tracker/dev-QC/CI-wait as permanently off ("treat all three as permanently off for this run") because F's `integrations.env` toggle system doesn't exist in CCAM yet. Shipyard's source, `lane-env.sh --check NAME`, is the reference primitive: read one `<NAME>_ENABLED` flag from a profile's `integrations.env`, exit 0/1.
`SKILL.md`'s Setup section hardcodes tracker/dev-QC/CI-wait as permanently off ("treat all three as permanently off for this run") because F's `integrations.env` toggle system doesn't exist in AgentHub yet. Shipyard's source, `lane-env.sh --check NAME`, is the reference primitive: read one `<NAME>_ENABLED` flag from a profile's `integrations.env`, exit 0/1.
This is a narrow slice of F, not all of F3. `ticketer` (the agent that would actually consume `TRACKER_ENABLED`) is explicitly deferred — no tracker MCP is configured for any lane yet, and CCAM has no per-lane credential source (same gap E3 already documented for `qc-local`). This task only builds the toggle-reading primitive itself, so `SKILL.md`'s hardcoded-off text can become a real check instead of a permanent assumption, once something exists to check.
This is a narrow slice of F, not all of F3. `ticketer` (the agent that would actually consume `TRACKER_ENABLED`) is explicitly deferred — no tracker MCP is configured for any lane yet, and AgentHub has no per-lane credential source (same gap E3 already documented for `qc-local`). This task only builds the toggle-reading primitive itself, so `SKILL.md`'s hardcoded-off text can become a real check instead of a permanent assumption, once something exists to check.
## Scope
**In scope:**
1. `<repo>/.ccam/profile/integrations.env` — a new, optional profile file (same directory as `profile.env`), parsed with the EXISTING `parseEnvFile` helper in `server/lib/lane-profile.js` (comments/quotes/`export` already handled — no new parser).
1. `<repo>/.agenthub/profile/integrations.env` — a new, optional profile file (same directory as `profile.env`), parsed with the EXISTING `parseEnvFile` helper in `server/lib/lane-profile.js` (comments/quotes/`export` already handled — no new parser).
2. `isIntegrationEnabled(lane, name)` in `server/lib/lane-profile.js` — reads `integrations.env` (falling back `lane.cwd` → `lane.source_repo`, same two-location search `resolveProfile` already does for `profile.env`), returns `true` iff `<NAME_UPPER>_ENABLED === "1"`. A missing file or missing key is `false` — off by default, consistent with A2's declarations.
3. `GET /api/lanes/:id/integrations/:name` route — `{enabled: boolean}`.
4. `ccam lanes integration <name> [<id>]` CLI — exit `0` when enabled, `1` when disabled (the exit-code contract Shipyard's `--check` already established, and what a skill's `&&`-gated stage-skip logic needs).
4. `agenthub lanes integration <name> [<id>]` CLI — exit `0` when enabled, `1` when disabled (the exit-code contract Shipyard's `--check` already established, and what a skill's `&&`-gated stage-skip logic needs).
**Explicitly out of scope:**
- `ticketer.md` / `dev-qc.md` agents — deferred pending a tracker/dev-QC MCP and credential source.
- Every other field `integrations.env` can declare (`TRACKER_URL`, `TRACKER_PROJECT`, `DEV_SITE_URL`, `CI_REPO`, etc.) — only the `<NAME>_ENABLED` flags matter for this primitive; the rest is read by whatever later task actually consumes a specific integration (F3/F4's real agent work), not by this generic toggle check.
- Surfacing toggle state in `GET /api/lanes/:id/runtime` — that endpoint is polled and broadcast on every hook; adding fields nothing consumes yet is premature. `GET /:id/integrations/:name` is its own lightweight endpoint instead.
- `lane-env.sh`'s default (no `--check`) mode — dumping the full per-lane runtime env as `export` lines exists in Shipyard because bash/zsh sourcing has a HARNESS_ROOT resolution footgun CCAM's cwd-based lane resolution doesn't have. Nothing in CCAM needs this.
- `lane-env.sh`'s default (no `--check`) mode — dumping the full per-lane runtime env as `export` lines exists in Shipyard because bash/zsh sourcing has a HARNESS_ROOT resolution footgun AgentHub's cwd-based lane resolution doesn't have. Nothing in AgentHub needs this.
## Decisions
@@ -1,10 +1,10 @@
# F3c — `ccam lanes gc`: orphan MCP reap + log capping
# F3c — `agenthub lanes gc`: orphan MCP reap + log capping
**Status:** approved 2026-08-05. Roadmap it belongs to: E (found auditing E1-F3b for Shipyard parity).
## Problem
Shipyard's `lane-gc.sh` does 5 things. Two match CCAM's actual architecture; three don't (see Decisions). No CCAM equivalent exists for either of the two that do.
Shipyard's `lane-gc.sh` does 5 things. Two match AgentHub's actual architecture; three don't (see Decisions). No AgentHub equivalent exists for either of the two that do.
## Scope
@@ -13,15 +13,15 @@ Shipyard's `lane-gc.sh` does 5 things. Two match CCAM's actual architecture; thr
2. **Cap oversized hook logs.** `LANES_ROOT/.state/lane<slot>/logs/*.log` (written by `runHook`) grow unbounded across many boots/hooks; cap anything over 10MB to its last 2MB, in place (write-then-replace, not a truncate-while-open, so a concurrent writer's fd stays valid).
**Explicitly out of scope (architecture mismatch, not an oversight):**
- **Auto-removing stale worktrees by age.** This is the piece that actually conflicts with a hard CCAM rule: "Never `rm -rf` a lane... destructive lane operations go through `worktree.js`/`lanes.js`, never a raw filesystem delete." An age-based automatic worktree removal is exactly the kind of automatic destructive action this repo's own CLAUDE.md forbids. `ccam lanes reset|remove|purge` already exist for a human to call explicitly.
- **Feature-state archiving.** Shipyard's version moves flat JSON state files to an archive dir. CCAM's lane/feature state lives in the SQLite DB (`lane_features` table, from subsystem B) — there's no flat-file equivalent to archive.
- **Scratch-debris sweep** (`scratch-lane*.log`, `manual-*.png`, `state.bak.*`). These are artifacts of Shipyard's own scripts (`lane-add.sh` writing `scratch-lane*.log`, manual QA screenshots named that way, `state.sh`'s own backup rotation). CCAM doesn't generate any of these — nothing to sweep.
- **Auto-removing stale worktrees by age.** This is the piece that actually conflicts with a hard AgentHub rule: "Never `rm -rf` a lane... destructive lane operations go through `worktree.js`/`lanes.js`, never a raw filesystem delete." An age-based automatic worktree removal is exactly the kind of automatic destructive action this repo's own CLAUDE.md forbids. `agenthub lanes reset|remove|purge` already exist for a human to call explicitly.
- **Feature-state archiving.** Shipyard's version moves flat JSON state files to an archive dir. AgentHub's lane/feature state lives in the SQLite DB (`lane_features` table, from subsystem B) — there's no flat-file equivalent to archive.
- **Scratch-debris sweep** (`scratch-lane*.log`, `manual-*.png`, `state.bak.*`). These are artifacts of Shipyard's own scripts (`lane-add.sh` writing `scratch-lane*.log`, manual QA screenshots named that way, `state.sh`'s own backup rotation). AgentHub doesn't generate any of these — nothing to sweep.
## Design
`ccam lanes gc [--dry-run]` — no lane argument; this scans the whole machine's CCAM footprint, not one lane. Pure local operation (process listing + file I/O), no HTTP route — same shape as `ccam skills install`.
`agenthub lanes gc [--dry-run]` — no lane argument; this scans the whole machine's AgentHub footprint, not one lane. Pure local operation (process listing + file I/O), no HTTP route — same shape as `agenthub skills install`.
- **Orphan reap:** `pgrep -f "<LANES_ROOT>.*\.playwright-mcp"` to find candidate pids (scoped to CCAM's own `LANES_ROOT`, same safety scoping Shipyard's own pattern used — an adopted lane's `cwd` living outside `LANES_ROOT` is a known gap, same one Shipyard's own numbered-lane-dir pattern had). For each match, `ps -o ppid= -p <pid>`; `ppid === 1` → orphaned, kill its process tree (`pgrep -P` recursively, then `SIGKILL` each, leaves not children first — same shape as Shipyard's `kill_tree`).
- **Orphan reap:** `pgrep -f "<LANES_ROOT>.*\.playwright-mcp"` to find candidate pids (scoped to AgentHub's own `LANES_ROOT`, same safety scoping Shipyard's own pattern used — an adopted lane's `cwd` living outside `LANES_ROOT` is a known gap, same one Shipyard's own numbered-lane-dir pattern had). For each match, `ps -o ppid= -p <pid>`; `ppid === 1` → orphaned, kill its process tree (`pgrep -P` recursively, then `SIGKILL` each, leaves not children first — same shape as Shipyard's `kill_tree`).
- **Log cap:** walk `LANES_ROOT/.state/lane*/logs/*.log`; for any file over 10MB, read its last 2MB and rewrite the file in place.
- `--dry-run` prints what would happen without doing it (same flag Shipyard's version has).
@@ -1,4 +1,4 @@
# F1 — `ccam lanes mcp sync`
# F1 — `agenthub lanes mcp sync`
**Status:** approved 2026-08-05. Roadmap it belongs to:
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**).
@@ -13,12 +13,12 @@
1. `server/lib/lane-mcp.js` — reads the lane's `source_repo`'s already-configured MCP servers from `~/.claude.json`, relocates any absolute path under `source_repo` to the lane's own `cwd`, pins a `playwright`-family server's `--output-dir` to the lane's `.playwright-mcp`, writes `<lane.cwd>/.mcp.json`.
2. Seeding the lane's `.playwright-mcp/profiles/*` Chromium profile directories from the source repo's own (preserves saved MCP browser logins — a real QA-account login only needs to happen once per machine, not once per lane).
3. `.mcp.json` added to the lane's local `.git/info/exclude`.
4. `ccam lanes mcp sync [<id>]` CLI + `POST /api/lanes/:id/mcp/sync` route.
4. `agenthub lanes mcp sync [<id>]` CLI + `POST /api/lanes/:id/mcp/sync` route.
5. `SKILL.md` Setup section edit: replace the manual-fallback text with the real command.
6. `docs/LANES.md` + roadmap progress line.
**Deliberately out of scope (deviates from Shipyard, by explicit decision — not an oversight):**
- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `<lane>/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__<server>` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. CCAM has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated.
- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `<lane>/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__<server>` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. AgentHub has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated.
- **The `mcp-node-exec.sh` node-version wrapper.** Shipyard's script routes `npx`/`node`-based MCP servers through a wrapper that resolves the newest Node ≥ 18 at spawn time, working around an nvm/PATH resolution bug specific to that environment. Not a problem this repo has evidence of; add it later if it actually bites someone (YAGNI).
- **Merging with an existing `.mcp.json`.** This is a full overwrite from the source repo's current MCP config, same "reinstall, not merge" shape E3's `agents install` already established — not a per-server incremental sync.
@@ -30,14 +30,14 @@
| Missing config | Throws `ENOMCPCONFIG` with a clear message pointing at `SKILL.md`'s Setup section, not a silent no-op | A lane with zero MCP servers synced would silently break Stage 3/6 later, at a much less debuggable point. Fail loud, fail early, matching `requireProfile`'s existing shape for a missing profile. |
| Overwrite semantics | Full overwrite of `.mcp.json` on every call, profiles seeded only if the destination directory doesn't already exist | Matches E3's "reinstall, not merge" for `.mcp.json` (config should always reflect the source repo's current state) but "never clobber a working login" for profiles (an existing profile dir means a session already logged in there — copying over it would lose that). |
| `--output-dir` pinning | Only added to a server whose args reference `@playwright/mcp` and don't already declare `--output-dir` | Direct port of Shipyard's own conditional — this is what makes proof screenshots land in the lane's own canonical `.playwright-mcp/proof` (the same path the proof gallery, C, already reads) instead of wherever the server's cwd happens to be. |
| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing CCAM pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). |
| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing AgentHub pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). |
## Command mapping
| Shipyard | CCAM | Status |
| Shipyard | AgentHub | Status |
|---|---|---|
| `lane-mcp-sync.sh <N>` | `ccam lanes mcp sync [<id>]` | **new in this task** |
| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `ccam lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) |
| `lane-mcp-sync.sh <N>` | `agenthub lanes mcp sync [<id>]` | **new in this task** |
| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `agenthub lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) |
| Chromium profile seeding | ported directly, same logic | **new in this task** |
| `.mcp.json` write + relocate + `--output-dir` pin | ported directly, same logic | **new in this task** |
| `settings.local.json` permission/autoMode writes | dropped | n/a (see Scope) |
@@ -55,11 +55,11 @@
### Route + CLI
`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `ccam lanes mcp sync [<id>]` — same `resolveLaneArg` pattern as `lanes agents install`.
`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `agenthub lanes mcp sync [<id>]` — same `resolveLaneArg` pattern as `lanes agents install`.
## Invariants
1. **CCAM still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything.
1. **AgentHub still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything.
2. **Never a silent partial state.** A lane with no source-repo MCP config gets a clear error, not an empty-but-present `.mcp.json`.
3. **A lane's existing browser-profile login is never overwritten**, only ever seeded once.
@@ -1,32 +1,32 @@
# F3b — `ccam skills install`: making `/ship-feature-lane` actually invokable
# F3b — `agenthub skills install`: making `/ship-feature-lane` actually invokable
**Status:** approved 2026-08-05. Roadmap it belongs to:
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **E**, a gap found auditing E1-F3a for Shipyard parity).
## Problem
`.claude/skills/ship-feature-lane/SKILL.md`'s own description says "Invoke inside a lane's working directory" — but Claude Code only auto-discovers a `.claude/skills/` directory when the session's cwd is inside the repo that OWNS that directory. The skill lives only at `ccam-lanes/.claude/skills/ship-feature-lane/SKILL.md`; confirmed empty: `~/.claude/skills/` has no `ship-feature-lane` entry. Every piece built across E1/E2/E3/F1/F3a is unreachable from its actual intended invocation context — a session inside some OTHER lane's repo, not inside ccam-lanes itself.
`.claude/skills/ship-feature-lane/SKILL.md`'s own description says "Invoke inside a lane's working directory" — but Claude Code only auto-discovers a `.claude/skills/` directory when the session's cwd is inside the repo that OWNS that directory. The skill lives only at `agenthub-lanes/.claude/skills/ship-feature-lane/SKILL.md`; confirmed empty: `~/.claude/skills/` has no `ship-feature-lane` entry. Every piece built across E1/E2/E3/F1/F3a is unreachable from its actual intended invocation context — a session inside some OTHER lane's repo, not inside agenthub-lanes itself.
Shipyard's own `install-claude-assets.sh` solves exactly this: it copies bundled skills into `~/.claude/skills/` (user-level, global) so every project's session sees them.
## Scope
**In scope:**
- `ccam skills install` — copies `<ccam-lanes repo>/.claude/skills/ship-feature-lane/` to `~/.claude/skills/ship-feature-lane/`, overwriting on reinstall (same "reinstall, not merge" shape as `ccam lanes agents install`).
- `agenthub skills install` — copies `<agenthub-lanes repo>/.claude/skills/ship-feature-lane/` to `~/.claude/skills/ship-feature-lane/`, overwriting on reinstall (same "reinstall, not merge" shape as `agenthub lanes agents install`).
- `docs/LANES.md` note.
**Out of scope:**
- Changing `ccam lanes agents install`'s existing per-lane `.claude/agents/` mechanism. That one's already reachable correctly (Claude Code discovers `.claude/agents/` from the lane's own cwd, which IS where a driving session runs) — this gap is specific to the SKILL, not the agents. No architecture change needed there.
- Changing `agenthub lanes agents install`'s existing per-lane `.claude/agents/` mechanism. That one's already reachable correctly (Claude Code discovers `.claude/agents/` from the lane's own cwd, which IS where a driving session runs) — this gap is specific to the SKILL, not the agents. No architecture change needed there.
- Auto-running this from `npm run setup` or any other command. Explicit, like every other install primitive in this codebase (`lanes agents install`, `lanes mcp sync`, `lanes proof-link`) — a human runs it once per machine.
- Any other bundled skill Shipyard ships (`/review-prs`, `/harness-adapt`) — CCAM has no equivalent of either; out of scope by the same reasoning `pr-reviewer` was dropped (no driving skill to attach to).
- Any other bundled skill Shipyard ships (`/review-prs`, `/harness-adapt`) — AgentHub has no equivalent of either; out of scope by the same reasoning `pr-reviewer` was dropped (no driving skill to attach to).
## Design
`bin/ccam.js` already has `REPO_ROOT` (`path.resolve(path.dirname(fs.realpathSync(__filename)), "..")`) — the exact self-location primitive Shipyard needed `HARNESS_ROOT` for, already present because `ccam` is npm-linked. No new resolution logic needed.
`bin/agenthub.js` already has `REPO_ROOT` (`path.resolve(path.dirname(fs.realpathSync(__filename)), "..")`) — the exact self-location primitive Shipyard needed `HARNESS_ROOT` for, already present because `agenthub` is npm-linked. No new resolution logic needed.
- `cmdSkillsInstall()` in `bin/ccam.js` (no HTTP round-trip — this has nothing to do with any lane or the dashboard's DB, purely a local file copy from the CLI's own checkout to the user's home directory; matches the existing local-only pattern `cmdLanesProfileInit` already uses via `REPO_ROOT`).
- `cmdSkillsInstall()` in `bin/agenthub.js` (no HTTP round-trip — this has nothing to do with any lane or the dashboard's DB, purely a local file copy from the CLI's own checkout to the user's home directory; matches the existing local-only pattern `cmdLanesProfileInit` already uses via `REPO_ROOT`).
- Copies `path.join(REPO_ROOT, ".claude", "skills", "ship-feature-lane")` → `path.join(os.homedir(), ".claude", "skills", "ship-feature-lane")` via `fs.cpSync(..., {recursive: true, force: true})` (force: true = overwrite existing files, matching reinstall semantics).
- Command: `ccam skills install`, no lane argument (nothing lane-specific here).
- Command: `agenthub skills install`, no lane argument (nothing lane-specific here).
## Verify
@@ -1,11 +1,11 @@
# E2 — `ccam lanes sync-base`
# E2 — `agenthub lanes sync-base`
**Status:** approved 2026-08-05. Roadmap it belongs to:
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **E**).
## Problem
`.claude/skills/ship-feature-lane/SKILL.md` (ported in E1) names `ccam lanes sync-base` at Stages 2, 8 and 12 by its intended contract, but the command doesn't exist — Stage 2 hardcodes "preflight unavailable, skipped" and Stage 12 falls back to a bare `git merge`. Shipyard's source, `lane-sync-dev.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-sync-dev.sh`, 173 lines, plus its test `tests/test_sync_dev.sh`), is the reference: the ONE sanctioned merge in the pipeline (`origin/development` INTO the feature branch), with a migration-number collision preflight and a keep-ours merge driver for generated files. E (per the roadmap) has three pieces left after E1: agents, sync-base, F's integrations. This spec covers sync-base only.
`.claude/skills/ship-feature-lane/SKILL.md` (ported in E1) names `agenthub lanes sync-base` at Stages 2, 8 and 12 by its intended contract, but the command doesn't exist — Stage 2 hardcodes "preflight unavailable, skipped" and Stage 12 falls back to a bare `git merge`. Shipyard's source, `lane-sync-dev.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-sync-dev.sh`, 173 lines, plus its test `tests/test_sync_dev.sh`), is the reference: the ONE sanctioned merge in the pipeline (`origin/development` INTO the feature branch), with a migration-number collision preflight and a keep-ours merge driver for generated files. E (per the roadmap) has three pieces left after E1: agents, sync-base, F's integrations. This spec covers sync-base only.
## Scope
@@ -13,8 +13,8 @@
1. Two new profile declarations: `MIGRATIONS_DIR`, `GENERATED_MERGE_PATHS` (empty = off, same `DEFAULTS` pattern as every other optional A2 declaration).
2. `server/lib/lane-sync.js` — the git-only core: collision check, dev-delta report, merge, continue-after-conflict.
3. `POST /api/lanes/:id/sync-base` route.
4. `ccam lanes sync-base [--check|--continue] [branch] [<id>]` CLI subcommand.
5. `.claude/skills/ship-feature-lane/SKILL.md` — Stage 2/8/12 edits: drop the "if it exists yet" conditionals, add the exit-4/`--continue` mechanics (currently absent from the CCAM port; Shipyard's original `ship-feature/SKILL.md` Stage 12 has the text to carry over).
4. `agenthub lanes sync-base [--check|--continue] [branch] [<id>]` CLI subcommand.
5. `.claude/skills/ship-feature-lane/SKILL.md` — Stage 2/8/12 edits: drop the "if it exists yet" conditionals, add the exit-4/`--continue` mechanics (currently absent from the AgentHub port; Shipyard's original `ship-feature/SKILL.md` Stage 12 has the text to carry over).
6. `docs/LANES.md` + roadmap progress line.
**Out of scope:**
@@ -27,19 +27,19 @@
| | Choice | Rationale |
|---|---|---|
| Route timing model | Synchronous — the route runs the git operation and returns JSON directly, no `202` + websocket broadcast | Unlike `ci-gate`/`e2e` (test suites, minutes), a fetch + collision-check + merge is a few seconds of git work. Matches the existing `GET /:id/git` pattern (working-copy facts, synchronous), not the `hook` pattern. |
| Merge-driver setup timing | Lazy, inside `mergeSync` itself, idempotent every call | Matches this repo's own established precedent: proof-link is "idempotent, never automatic" rather than a provision-time step. No other CCAM lane-creation path (`addWorktree`/`provisionLane`) currently touches git config or `.git/info/*`; adding one there for this alone would be a new concern in an unrelated file for a need only sync-base has. |
| Stage/status writes | None. `sync-base` returns a structured result (`code`, `collisions`, `devDelta`, `overlap`, `conflictedFiles`); it never calls anything that sets a lane's `stage`, `status`, or `notes` | This repo's own rule: "declared stages come from `ccam stage`... The console never writes a lane's stage" and "the runtime never writes stage, status or notes." Shipyard's `state.sh set` heartbeat/notes calls inside `lane-sync-dev.sh` are dropped for the same reason `--qc`'s design dropped `state.sh init` in E1 — CCAM has a narrower, already-established boundary for who writes stage, and it's not this command. The skill is responsible for calling `ccam stage ... --status blocked` itself on a collision or conflict, same as any other gate failure. |
| Merge-driver setup timing | Lazy, inside `mergeSync` itself, idempotent every call | Matches this repo's own established precedent: proof-link is "idempotent, never automatic" rather than a provision-time step. No other AgentHub lane-creation path (`addWorktree`/`provisionLane`) currently touches git config or `.git/info/*`; adding one there for this alone would be a new concern in an unrelated file for a need only sync-base has. |
| Stage/status writes | None. `sync-base` returns a structured result (`code`, `collisions`, `devDelta`, `overlap`, `conflictedFiles`); it never calls anything that sets a lane's `stage`, `status`, or `notes` | This repo's own rule: "declared stages come from `agenthub stage`... The console never writes a lane's stage" and "the runtime never writes stage, status or notes." Shipyard's `state.sh set` heartbeat/notes calls inside `lane-sync-dev.sh` are dropped for the same reason `--qc`'s design dropped `state.sh init` in E1 — AgentHub has a narrower, already-established boundary for who writes stage, and it's not this command. The skill is responsible for calling `agenthub stage ... --status blocked` itself on a collision or conflict, same as any other gate failure. |
| Exit codes | Preserved verbatim: `0` clean, `4` conflict (left in place), `5` migration collision | `SKILL.md` already documents these two non-zero codes by name (written during E1, before the command existed) — the contract predates this implementation and this spec fulfills it rather than inventing a new one. |
| `git` execution | Reuse `worktree.js`'s `git(cwd, args)` helper (export it — not currently exported) | This repo's binding rule: never build a git command as a shell string; `worktree.js` already has the one approved execFile wrapper. A second implementation in `lane-sync.js` would duplicate it. |
| State for `--continue` | None — read straight from the lane's own git state (`git ls-files -u`, presence of `.git/MERGE_HEAD`), exactly like the source script | Stateless by construction: the working copy IS the source of truth for "is this conflict resolved yet," so a separate DB flag would just be a second place to get out of sync with the first. |
## Command mapping
| Shipyard | CCAM | Status |
| Shipyard | AgentHub | Status |
|---|---|---|
| `lane-sync-dev.sh <N> --check [branch]` | `ccam lanes sync-base --check [branch] [<id>]` | **new in this task** |
| `lane-sync-dev.sh <N> [branch]` | `ccam lanes sync-base [branch] [<id>]` | **new in this task** |
| `lane-sync-dev.sh <N> --continue [branch]` | `ccam lanes sync-base --continue [branch] [<id>]` | **new in this task** |
| `lane-sync-dev.sh <N> --check [branch]` | `agenthub lanes sync-base --check [branch] [<id>]` | **new in this task** |
| `lane-sync-dev.sh <N> [branch]` | `agenthub lanes sync-base [branch] [<id>]` | **new in this task** |
| `lane-sync-dev.sh <N> --continue [branch]` | `agenthub lanes sync-base --continue [branch] [<id>]` | **new in this task** |
| `state.sh "$N" set stage=migration-collision ...` (on exit 5) | dropped — see Decisions | n/a |
| `state.sh "$N" set stage=sync-conflict ...` (on exit 4) | dropped — see Decisions | n/a |
| `write_lane_markers` (push guards, `.harness-lane` exclude, proof-link) | dropped — out of scope, unrelated to sync | n/a |
@@ -59,7 +59,7 @@
Three exported async functions, each taking `(lane, profile, branch)`:
- **`checkSync(lane, profile, branch)`** — validates `branch` isn't `development`/`main` and exists; `git fetch origin --prune`; runs the collision check (below); if a collision is found, returns `{code: 5, collisions: [{file, num, clash, suggestion}]}` without going further. Otherwise computes `devDelta` (files changed on `origin/development` since the merge-base with `branch`, filtered against `GENERATED_MERGE_PATHS`) and `overlap` (the subset of that delta touching `branch`'s own changed files), returns `{code: 0, devDelta, overlap}`.
- **`mergeSync(lane, profile, branch)`** — same validation + fetch; installs the keep-ours merge driver for every `GENERATED_MERGE_PATHS` entry via `git config merge.ccam-generated.driver true` + a line per path in `.git/info/attributes` (both local-only, idempotent — `grep -qxF` before appending, exactly like the source's guard); runs the collision check (exit 5 short-circuits, nothing merged); checks out `branch`; runs `git merge --no-edit origin/development`. A clean merge or a fully rerere-auto-resolved one falls through to the regen fold-in (below) and returns `{code: 0}`. A real conflict is left in place — returns `{code: 4, conflictedFiles: [...]}` from `git diff --name-only --diff-filter=U`.
- **`mergeSync(lane, profile, branch)`** — same validation + fetch; installs the keep-ours merge driver for every `GENERATED_MERGE_PATHS` entry via `git config merge.agenthub-generated.driver true` + a line per path in `.git/info/attributes` (both local-only, idempotent — `grep -qxF` before appending, exactly like the source's guard); runs the collision check (exit 5 short-circuits, nothing merged); checks out `branch`; runs `git merge --no-edit origin/development`. A clean merge or a fully rerere-auto-resolved one falls through to the regen fold-in (below) and returns `{code: 0}`. A real conflict is left in place — returns `{code: 4, conflictedFiles: [...]}` from `git diff --name-only --diff-filter=U`.
- **`continueSync(lane, profile, branch)`** — asserts current branch equals `branch`, no unmerged paths (`git ls-files -u` empty), and no `.git/MERGE_HEAD` (already committed) — any violation is a thrown error with a clear message, not a silent no-op. Then runs the regen fold-in and returns `{code: 0}`.
**Collision check** (shared helper, not exported): if `MIGRATIONS_DIR` is unset, no-op. Otherwise diff `origin/development...branch` for added files under `MIGRATIONS_DIR`, list `origin/development`'s existing `NNN_*` files in that dir, and for each added file whose number already exists upstream, compute the next free number and report it — same arithmetic as the source script (`10#${maxnum:-0} + 1`, incrementing per additional clash in the same batch).
@@ -72,17 +72,17 @@ Three exported async functions, each taking `(lane, profile, branch)`:
### CLI
`ccam lanes sync-base [--check|--continue] [branch] [<id>]` in `bin/ccam.js`, same flag-parsing style as `lanes up`. Maps the response `code` to `process.exitCode` (0/4/5) so a driving skill's `$?`/exit-code check works exactly as `SKILL.md` already documents. On `code: 5`, prints each collision's suggested rename; on `code: 4`, prints the conflicted file list plus the exact `--continue` follow-up command; on `code: 0`, prints `DEV_DELTA:`/`DEV_OVERLAP:` (check mode) or a plain success line (merge/continue mode) — same output shape as the source script, since the skill's stage text already expects to read `DEV_DELTA:`/`DEV_OVERLAP:` lines.
`agenthub lanes sync-base [--check|--continue] [branch] [<id>]` in `bin/agenthub.js`, same flag-parsing style as `lanes up`. Maps the response `code` to `process.exitCode` (0/4/5) so a driving skill's `$?`/exit-code check works exactly as `SKILL.md` already documents. On `code: 5`, prints each collision's suggested rename; on `code: 4`, prints the conflicted file list plus the exact `--continue` follow-up command; on `code: 0`, prints `DEV_DELTA:`/`DEV_OVERLAP:` (check mode) or a plain success line (merge/continue mode) — same output shape as the source script, since the skill's stage text already expects to read `DEV_DELTA:`/`DEV_OVERLAP:` lines.
### `SKILL.md` edits
- Stage 2: replace the "later task, not yet built" paragraph with the real command + its exit-5 handling (rename, re-run gates) — text already half-written in E1's port, just needs the conditional removed.
- Stage 8: same — "re-run the preflight if it exists by now" becomes unconditional.
- Stage 12: replace the "if it exists / if it doesn't" branch with the unconditional `ccam lanes sync-base feat/<slug>` call, and add the exit-4 handling this repo's port currently lacks: resolve conflicts, `git add` only the resolved files, `git commit --no-edit`, then `ccam lanes sync-base --continue feat/<slug>` (folds regenerated artifacts in) — carried over from Shipyard's original `ship-feature/SKILL.md` Stage 12, which already has this exact text.
- Stage 12: replace the "if it exists / if it doesn't" branch with the unconditional `agenthub lanes sync-base feat/<slug>` call, and add the exit-4 handling this repo's port currently lacks: resolve conflicts, `git add` only the resolved files, `git commit --no-edit`, then `agenthub lanes sync-base --continue feat/<slug>` (folds regenerated artifacts in) — carried over from Shipyard's original `ship-feature/SKILL.md` Stage 12, which already has this exact text.
## Invariants
1. **CCAM still does not orchestrate.** `sync-base` performs one git operation per call and returns; it never chains into gate re-runs or stage transitions itself — the skill decides what happens next.
1. **AgentHub still does not orchestrate.** `sync-base` performs one git operation per call and returns; it never chains into gate re-runs or stage transitions itself — the skill decides what happens next.
2. **A profile that never declares `MIGRATIONS_DIR` or `GENERATED_MERGE_PATHS` sees no behavior change** — collision checking and the merge driver both degrade to no-ops, same as every other optional declaration.
3. **Never a shell-string git command.** Every git invocation in `lane-sync.js` goes through the shared `git(cwd, args)` execFile helper.
4. **`sync-base` never writes `stage`/`status`/`notes`.** Same boundary as the runtime and the hook routes.
@@ -13,7 +13,7 @@ F4 surfaced `agents install`/`mcp sync`/profile-init as per-lane manual buttons,
2. `AddLaneModal.tsx`: after `api.lanes.worktree()` succeeds, fire `profileInit`, `agentsInstall`, and `mcpSync` in parallel (`Promise.allSettled` — all three are independent of each other: `agentsInstall`/`mcpSync` only need `.git`, not a profile). Each outcome (success/failure/skipped) is collected and shown as a short per-step result list before the modal closes, instead of closing silently the instant the lane record exists.
**Explicitly out of scope:**
- `ccam skills install` — machine-wide, one-time-per-machine, not lane-specific. Running it every time someone adds a lane would be redundant (idempotent, but pointless) and conceptually wrong (it has nothing to do with THIS lane). Stays a manual Settings-page action.
- `agenthub skills install` — machine-wide, one-time-per-machine, not lane-specific. Running it every time someone adds a lane would be redundant (idempotent, but pointless) and conceptually wrong (it has nothing to do with THIS lane). Stays a manual Settings-page action.
- Blocking lane creation on any of the three setup steps failing. A lane with a non-Node.js repo, no MCP servers configured, or a git quirk should still exist and be usable — setup is best-effort enrichment, not a precondition.
- Adopting an EXISTING folder (a second "add lane" mode) — `AddLaneModal` only does managed-worktree creation today; that's unchanged. This auto-setup applies to whatever lane the existing flow just created, regardless of how.
@@ -4,30 +4,30 @@
## Problem
E1 through F3c (this session) built six CLI-only primitives — `ccam lanes sync-base`, `ccam lanes agents install`, `ccam lanes mcp sync`, `ccam lanes integration <name>`, `ccam lanes gc`, `ccam skills install` — with no UI surface. The user wants the lane-scoped, day-to-day-useful ones clickable from the dashboard instead of terminal-only.
E1 through F3c (this session) built six CLI-only primitives — `agenthub lanes sync-base`, `agenthub lanes agents install`, `agenthub lanes mcp sync`, `agenthub lanes integration <name>`, `agenthub lanes gc`, `agenthub skills install` — with no UI surface. The user wants the lane-scoped, day-to-day-useful ones clickable from the dashboard instead of terminal-only.
## Scope
**In scope — LaneCard.tsx (per-lane, gated on `runtime?.available`, same condition the existing up/down button already uses since all four need a `.ccam/profile`):**
**In scope — LaneCard.tsx (per-lane, gated on `runtime?.available`, same condition the existing up/down button already uses since all four need a `.agenthub/profile`):**
1. **"Install agents" button** → `POST /lanes/:id/agents/install` (exists). Shows the installed file list on success.
2. **"Sync MCP" button** → `POST /lanes/:id/mcp/sync` (exists). Shows synced servers + seeded profiles.
3. **Integration status badges** (tracker / dev_qc / ci_wait), read-only, on/off — reads `GET /lanes/:id/integrations/:name` (exists) for each of the three fixed names.
4. **"Check dev sync" button** → `POST /lanes/:id/sync-base` with `{mode: "check"}` (exists — the route already supports `mode`). Shows `DEV_DELTA`/`DEV_OVERLAP` or a collision list. **Read-only preflight only — no merge button.** Merging is a deliberate, evidence-recording action the driving skill/session takes (`ccam lanes sync-base` bare, or `--continue`); a UI click is the wrong place for a git merge with fix-loop implications. This matches the project's own boundary: "the console never writes a lane's stage" — extending that spirit, the console doesn't perform git merges either.
4. **"Check dev sync" button** → `POST /lanes/:id/sync-base` with `{mode: "check"}` (exists — the route already supports `mode`). Shows `DEV_DELTA`/`DEV_OVERLAP` or a collision list. **Read-only preflight only — no merge button.** Merging is a deliberate, evidence-recording action the driving skill/session takes (`agenthub lanes sync-base` bare, or `--continue`); a UI click is the wrong place for a git merge with fix-loop implications. This matches the project's own boundary: "the console never writes a lane's stage" — extending that spirit, the console doesn't perform git merges either.
**In scope — Settings.tsx (machine-wide, new "Lanes" section near the existing Hooks section — modeled on "Reinstall Hooks"/"Cleanup"):**
5. **"Install ship-feature-lane skill" button** → new route `POST /api/skills/install` (doesn't exist yet — needs building; wraps the same `fs.cpSync` logic `ccam skills install`'s CLI already has, factored into `server/lib/lane-agents.js` or a new tiny `server/lib/skills-install.js` so the CLI and route share one implementation instead of duplicating the copy logic).
5. **"Install ship-feature-lane skill" button** → new route `POST /api/skills/install` (doesn't exist yet — needs building; wraps the same `fs.cpSync` logic `agenthub skills install`'s CLI already has, factored into `server/lib/lane-agents.js` or a new tiny `server/lib/skills-install.js` so the CLI and route share one implementation instead of duplicating the copy logic).
6. **"Run housekeeping" button, with a "dry run" checkbox** → new route `POST /api/lanes/gc` (doesn't exist yet — wraps `server/lib/lane-gc.js`'s `reapOrphanMcp`/`capOversizedLogs`, already built and tested in F3c). Shows reaped pids + capped log paths.
**Explicitly out of scope:**
- A merge button for `sync-base` (see #4's reasoning).
- Any UI for `ccam lanes sync-base --continue` (conflict resolution is a session/skill action with real judgment calls about which side to keep — not a button).
- Refactoring the CLI (`bin/ccam.js`) to call the new HTTP routes instead of its current direct `require()` of the lib modules — the CLI's local-only shape (no server round-trip needed when you're already on the machine) stays as-is; only the browser needs an HTTP path, so the routes are additive, not a replacement for the CLI's existing implementation.
- Any UI for `agenthub lanes sync-base --continue` (conflict resolution is a session/skill action with real judgment calls about which side to keep — not a button).
- Refactoring the CLI (`bin/agenthub.js`) to call the new HTTP routes instead of its current direct `require()` of the lib modules — the CLI's local-only shape (no server round-trip needed when you're already on the machine) stays as-is; only the browser needs an HTTP path, so the routes are additive, not a replacement for the CLI's existing implementation.
## Design
### Two new backend routes
- `POST /api/skills/install` — same `sameOriginGuard` as every other mutating route. Extracts the copy logic (`REPO_ROOT/.claude/skills/ship-feature-lane` → `~/.claude/skills/ship-feature-lane`, `fs.cpSync` with `force: true`) out of `bin/ccam.js`'s `cmdSkillsInstall` into a small exported function in `server/lib/skills-install.js`, so `bin/ccam.js` and the route both call the same code (no duplication). Returns `{installed: true, path: string}`.
- `POST /api/skills/install` — same `sameOriginGuard` as every other mutating route. Extracts the copy logic (`REPO_ROOT/.claude/skills/ship-feature-lane` → `~/.claude/skills/ship-feature-lane`, `fs.cpSync` with `force: true`) out of `bin/agenthub.js`'s `cmdSkillsInstall` into a small exported function in `server/lib/skills-install.js`, so `bin/agenthub.js` and the route both call the same code (no duplication). Returns `{installed: true, path: string}`.
- `POST /api/lanes/gc` — same guard. Body `{dryRun?: boolean}`. Calls `reapOrphanMcp`/`capOversizedLogs` from the already-built `server/lib/lane-gc.js` (no changes needed there — Task 2 of F3c already exports both). Returns `{reaped: number[], capped: {path: string, sizeBefore: number}[]}`.
Both routes are machine-wide (no `:id`), registered at the router's top level (not under `/lanes/:id/...`) — `/api/skills/install` sits alongside other non-lane-scoped routes; `/api/lanes/gc` sits in the lanes router but before any `:id` param route (order matters: Express would otherwise try to resolve `gc` as a lane id on a route like `/lanes/:id`).
@@ -60,7 +60,7 @@ gc: (body: { dryRun?: boolean } = {}) =>
### LaneCard.tsx
Four additions, all gated on `runtime?.available` (same condition the up/down button already uses — every one of these four needs a `.ccam/profile`):
Four additions, all gated on `runtime?.available` (same condition the up/down button already uses — every one of these four needs a `.agenthub/profile`):
- **Install agents / Sync MCP buttons**: same visual pattern as the up/down toggle (`rounded px-2 py-1 text-fg-secondary transition-colors hover:bg-surface-2 disabled:opacity-50`), a `useState<"agents" | "mcp" | "sync-check" | null>` busy flag (one shared state covering all three new button actions plus check, mirroring `runtimeBusy`'s shape), result shown as a transient inline line below the button (reuse the `laneActionError`-style banner pattern, but for success text too — a new `actionResult: {label: string; message: string} | null` state scoped to LaneCard).
- **Integration badges**: a small row of three pill-shaped badges (`tracker` / `dev_qc` / `ci_wait`), fetched once when the card mounts and `runtime.available` is true (parallel `Promise.all` of the three `api.lanes.integration` calls, same effect-on-mount shape the existing `git`/`runtime` fetches already use), green/gray styling for on/off, no click handler — read-only.
@@ -1,4 +1,4 @@
# Plugin-first install — CCAM usable straight from a Claude Code plugin install
# Plugin-first install — AgentHub usable straight from a Claude Code plugin install
Date: 2026-08-10
Status: approved (design)
@@ -6,12 +6,12 @@ Status: approved (design)
## Goal
On a machine with nothing but Claude Code installed, these two commands must
leave the user with a working CCAM — hooks flowing, server up, dashboard
reachable, `ccam` CLI on PATH, MCP tools connected:
leave the user with a working AgentHub — hooks flowing, server up, dashboard
reachable, `agenthub` CLI on PATH, MCP tools connected:
```
/plugin marketplace add <ccam repo>
/plugin install ccam@claude-code-agent-monitor-plugins
/plugin marketplace add <agenthub repo>
/plugin install agenthub@claude-code-agent-monitor-plugins
```
No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
@@ -19,8 +19,8 @@ No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
## Why the current state does not do this
- The only plugin with a server connection, `ccam-dashboard`, declares its MCP
server as `../../mcp/build/index.js` (`plugins/ccam-dashboard/.mcp.json`).
- The only plugin with a server connection, `agenthub-dashboard`, declares its MCP
server as `../../mcp/build/index.js` (`plugins/agenthub-dashboard/.mcp.json`).
Plugins installed from a marketplace subdir are cached as **that subdir
only** — verified against `~/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/`,
which contains the plugin directory's contents and nothing above it. The
@@ -32,14 +32,14 @@ No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
## Approach: declare the whole repo as one plugin
Add a **root-level** plugin named `ccam` with `"source": "."` in
Add a **root-level** plugin named `agenthub` with `"source": "."` in
`.claude-plugin/marketplace.json`. Claude Code then caches the *entire repo*
into `~/.claude/plugins/cache/<marketplace>/ccam/<version>/` — verified against
into `~/.claude/plugins/cache/<marketplace>/agenthub/<version>/` — verified against
`~/.claude/plugins/cache/caveman/`, whose cached tree contains the full source
repo (`src/`, `tests/`, `benchmarks/`), because that marketplace declares its
plugin with `"source": "./"`.
So `server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` are all present
So `server/`, `client/`, `mcp/`, `scripts/` and `bin/agenthub.js` are all present
under `${CLAUDE_PLUGIN_ROOT}` the moment the plugin is installed. No second
clone, no credentials beyond the marketplace add itself, no committed copy of
the server, no dependency on undocumented cache internals beyond the documented
@@ -61,7 +61,7 @@ gains no new top-level directories.
`.claude-plugin/marketplace.json` gains one entry:
```json
{ "name": "ccam", "source": ".", "description": "...", "tags": [...] }
{ "name": "agenthub", "source": ".", "description": "...", "tags": [...] }
```
### 2. Hooks — inline in `plugin.json`
@@ -95,7 +95,7 @@ Runs from the `SessionStart` hook. Never blocks a session.
Runtime state lives under `~/.claude/agent-dashboard/runtime/`, reusing the
existing data-dir convention (`server/lib/claude-home.js:37`, where the SQLite
DB already lives) rather than inventing a new directory. `/.ccam/` at the repo
DB already lives) rather than inventing a new directory. `/.agenthub/` at the repo
root is an unrelated lane profile and must not be confused with it.
Sequence:
@@ -118,13 +118,13 @@ Sequence:
fresh version directory, discarding anything installed into the old one.
`--ignore-scripts` is required because `scripts/postinstall.js` otherwise
runs a full `client/` install, pulling the whole Vite dev toolchain.
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries CCAM hook
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries AgentHub hook
entries from a previous `npm run install-hooks`, remove them and print one
line. Without this, every event is POSTed twice and token/cost figures
double. Detection reuses `isOurEntry()` from `scripts/install-hooks.js`.
6. **CLI on PATH.** Symlink `bin/ccam.js` to `~/.local/bin/ccam`. Skills and
commands throughout the repo (`ccam stage`, `ship-feature-lane`) shell out
to `ccam` and fail without it.
6. **CLI on PATH.** Symlink `bin/agenthub.js` to `~/.local/bin/agenthub`. Skills and
commands throughout the repo (`agenthub stage`, `ship-feature-lane`) shell out
to `agenthub` and fail without it.
7. **Start server.** Spawn `server/index.js` detached, `stdio: "ignore"`,
`unref()`, with `NODE_PATH` pointed at `runtime/node_modules`. The hook
returns immediately; the first install (1–3 minutes) proceeds in the
@@ -132,7 +132,7 @@ Sequence:
8. **Record state.** Write `runtime/state.json`.
The client UI bundle is *not* built here. Hooks, the API and MCP do not need
it; it is built lazily by `/ccam-open`.
it; it is built lazily by `/agenthub-open`.
### 4. Client dist relocation — `server/index.js`
@@ -143,7 +143,7 @@ checkout behavior is unchanged.
The bootstrap always starts the server with `DASHBOARD_CLIENT_DIST` set to
`runtime/client-dist/`, whether or not a bundle is there yet — a missing
directory serves the API fine and only the UI route 404s. `/ccam-open` builds
directory serves the API fine and only the UI route 404s. `/agenthub-open` builds
into that same directory, so no server restart is needed to pick the bundle up.
### 5. MCP — commit `mcp/build/`
@@ -152,27 +152,27 @@ Claude Code starts a plugin's MCP servers as the session opens and offers no
"not ready yet, retry" state, so an async bootstrap cannot win that race.
`mcp/build/` is therefore committed (removed from `.gitignore` and `mcp/.gitignore`)
and the root `.mcp.json` points at `${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`
with `CCAM_DASHBOARD_URL` defaulted to `http://localhost:4820`.
with `AGENTHUB_DASHBOARD_URL` defaulted to `http://localhost:4820`.
Cost: the build artifact must stay in sync with `mcp/src`. A pre-commit check
fails when `mcp/src` is newer than `mcp/build`.
The existing `plugins/ccam-dashboard/.mcp.json` keeps its broken relative path
The existing `plugins/agenthub-dashboard/.mcp.json` keeps its broken relative path
fixed to `${CLAUDE_PLUGIN_ROOT}` form as part of this work, so the subdir plugin
is not left in a knowingly broken state.
### 6. Commands — `plugins/ccam/commands/`
### 6. Commands — `plugins/agenthub/commands/`
The plugin root is the repo root, so `plugin.json` declares an explicit
`"commands": "./plugins/ccam/commands"` path. This keeps the new command files
`"commands": "./plugins/agenthub/commands"` path. This keeps the new command files
inside `plugins/`, next to the ten existing plugins, instead of adding a
top-level `commands/` directory to the repo root.
- `/ccam-doctor` — bootstrap state, Node version, server liveness, DB path,
- `/agenthub-doctor` — bootstrap state, Node version, server liveness, DB path,
duplicate-hook detection, CLI symlink, MCP build freshness.
- `/ccam-update` — refresh dependencies and restart the server after a plugin
- `/agenthub-update` — refresh dependencies and restart the server after a plugin
update.
- `/ccam-open` — lazily build the client bundle if missing, then open the
- `/agenthub-open` — lazily build the client bundle if missing, then open the
dashboard.
## Data and lifecycle
@@ -196,7 +196,7 @@ deduplicates hook ingest to a single port.
by full detachment (`detached`, `stdio: "ignore"`, `unref()`) and an
immediate hook return. Verified by timing the hook on a cold `HOME`.
- *Duplicate hooks double-count cost.* Events carry no id, so ingest cannot
deduplicate. Mitigated by step 5 of the bootstrap plus a `/ccam-doctor` check.
deduplicate. Mitigated by step 5 of the bootstrap plus a `/agenthub-doctor` check.
- *MCP unavailable at session start.* Resolved by committing `mcp/build/`.
**Medium severity**
@@ -204,8 +204,8 @@ deduplicates hook ingest to a single port.
- *Two sessions spawn two servers* → `EADDRINUSE`. Mitigated by the lock, with
a stale-lock timeout so a crashed bootstrap does not wedge every later
session.
- *`ccam` not on PATH* when `~/.local/bin` is absent from the user's PATH.
`/ccam-doctor` reports it and prints the line to add.
- *`agenthub` not on PATH* when `~/.local/bin` is absent from the user's PATH.
`/agenthub-doctor` reports it and prints the line to add.
- *Windows.* `npm.cmd` resolution (already handled in `scripts/postinstall.js`
via `shell: true`), detached spawn semantics, and `mkdir` locking are the
least-tested paths.
@@ -222,7 +222,7 @@ deduplicates hook ingest to a single port.
## Verification
1. Install the local marketplace and confirm
`~/.claude/plugins/cache/<mp>/ccam/*/server/` exists — this gates everything
`~/.claude/plugins/cache/<mp>/agenthub/*/server/` exists — this gates everything
else.
2. `npm run test:server` for the `DASHBOARD_CLIENT_DIST` override.
3. Unit tests for bootstrap: fast path, stale-lock reclaim, Node version gate,
@@ -54,14 +54,14 @@ sync — they were never coupled to the run mechanism in the first place.
**In scope:**
- tmux session lifecycle (create/attach/resume/kill) per lane, named
`ccam-lane-<id>`.
`agenthub-lane-<id>`.
- A dedicated WebSocket path streaming a real PTY (`node-pty` running
`tmux attach-session`) to the browser, rendered with `@xterm/xterm`.
- Replacing `RunConsole.tsx` with a `TerminalView.tsx` component.
- Reusing (not rebuilding) `RunSetup.tsx`'s cwd/model/permission-mode/effort
pickers and `RunHistory.tsx`/`ActiveRunsSwitcher`'s multi-run list, both
adjusted to the new data source.
- `ccam lanes shell` — CLI convenience to attach a real terminal to the same
- `agenthub lanes shell` — CLI convenience to attach a real terminal to the same
named tmux session.
- `dashboard_runs` migration: drop `mode` (headless/conversation no longer
applies — a live pane is always interactive), add `tmux_session`.
@@ -81,11 +81,11 @@ sync — they were never coupled to the run mechanism in the first place.
### Session naming and lifecycle
One tmux session per lane, name `ccam-lane-<lane.id>` — stable, collision-free
One tmux session per lane, name `agenthub-lane-<lane.id>` — stable, collision-free
(numeric lane id, not a user-editable slug).
- **Start:** `tmux has-session -t ccam-lane-<id>` (exit code only, no
output). If absent: `tmux new-session -d -s ccam-lane-<id> -c <lane.cwd> --
- **Start:** `tmux has-session -t agenthub-lane-<id>` (exit code only, no
output). If absent: `tmux new-session -d -s agenthub-lane-<id> -c <lane.cwd> --
claude <argv...> [initialPrompt]` — `claude` runs directly as the pane's
command (nothing is "typed"). `argv` carries `--model`, `--permission-mode`,
`--effort` as today; an optional initial prompt is passed as a trailing
@@ -97,14 +97,14 @@ One tmux session per lane, name `ccam-lane-<lane.id>` — stable, collision-free
already-live server instead of double-binding; see `server/index.js`'s
port-adoption logic) applied to tmux sessions.
- **Resume:** identical, `argv` includes `--resume <session_id>`.
- **Kill:** `tmux kill-session -t ccam-lane-<id>`. This sends SIGHUP to the
- **Kill:** `tmux kill-session -t agenthub-lane-<id>`. This sends SIGHUP to the
pane's process group. Whether `claude` treats that as a clean shutdown
(firing `SessionEnd`) is unverified — flag for implementation to check; if
not, this repo's existing dead-session liveness reap (`server/lib/
session-liveness.js`, referenced in `CLAUDE.md`) is the safety net that
already exists for exactly this kind of gap, no new code needed.
- **List (`GET /api/run`):** no longer a Map read. Runs `tmux list-sessions
-F '#{session_name}'`, filters the `ccam-lane-` prefix, and joins against
-F '#{session_name}'`, filters the `agenthub-lane-` prefix, and joins against
`dashboard_runs` rows (started_at, model, lane_id, …) for display. This
makes "which runs are active" a **computed fact from tmux state**, not
cached server memory — the same principle this repo already applies to
@@ -136,7 +136,7 @@ A second server is added for the PTY stream, same `verifyClient` auth guard
(Host allowlist + `DASHBOARD_TOKEN`) reused as-is:
- Path: `/ws-pty/:runId` (runId = tmux session name, validated against the
`ccam-lane-<numeric id>` pattern before any tmux command touches it — this
`agenthub-lane-<numeric id>` pattern before any tmux command touches it — this
is the trust boundary: without validation, a WS client could name an
arbitrary tmux session on the host and attach to something unrelated to
this dashboard).
@@ -179,13 +179,13 @@ A second server is added for the PTY stream, same `verifyClient` auth guard
separate code path — expected to be unaffected, but verify before
deleting).
### `ccam lanes shell`
### `agenthub lanes shell`
`bin/ccam.js`, same dispatch pattern as the other `lanes` subcommands
`bin/agenthub.js`, same dispatch pattern as the other `lanes` subcommands
(`rest[0] === "shell"` → `cmdLanesShell`). Resolves the lane from `cwd` the
same way `ccam stage` does, computes `ccam-lane-<id>`, and `execve`s (replace
same way `agenthub stage` does, computes `agenthub-lane-<id>`, and `execve`s (replace
the current process image, not a child — so Ctrl-C/signals behave like a
normal terminal command) `tmux new-session -A -s ccam-lane-<id> -c
normal terminal command) `tmux new-session -A -s agenthub-lane-<id> -c
<lane.cwd>`. `-A` creates-or-attaches, so this is the exact same idempotent
behavior as clicking Start on the dashboard. The user types `claude`
themselves inside — this command's only job is getting them into the right
@@ -236,7 +236,7 @@ invocation regardless of transport.
`npm run test:server`, `npm run test:client` (including the regenerated
snapshot), and a manual click-through: install `tmux` locally, Start a run
from Workspace, confirm the real Claude Code TUI renders in the browser,
type in the browser and confirm it reaches the pane, run `ccam lanes shell`
type in the browser and confirm it reaches the pane, run `agenthub lanes shell`
from a real terminal for the same lane and confirm it drops into the exact
same live session, kill from the dashboard and confirm the tmux session and
row both clear.
@@ -51,7 +51,7 @@ today.
## Persistence
The chosen layout mode and each pane's selected `laneId` are saved to
`localStorage` (e.g. key `ccam.workspace.splitView`) and restored on next
`localStorage` (e.g. key `agenthub.workspace.splitView`) and restored on next
visit to Workspace. If a persisted lane no longer exists, that pane falls
back to unselected (dropdown placeholder).
@@ -1,4 +1,4 @@
# `ccam ci status` — unblock Stage 13's CI-wait half
# `agenthub ci status` — unblock Stage 13's CI-wait half
**Status:** approved 2026-08-25.
@@ -9,10 +9,10 @@ skipped. Two integrations gate it: dev-CI-wait (watch CI on `development`
after a PR merges, before QCing the deployed site) and dev-QC (QC the
deployed site). Neither exists yet.
This spec covers only the CI-wait half. `ccam lanes integration ci_wait`
This spec covers only the CI-wait half. `agenthub lanes integration ci_wait`
already exists (`server/lib/lane-profile.js:isIntegrationEnabled`,
`bin/ccam.js:cmdLanesIntegration`) and reports whether a profile *wants* the
integration on — but nothing acts on it. `ccam ci` doesn't exist
`bin/agenthub.js:cmdLanesIntegration`) and reports whether a profile *wants* the
integration on — but nothing acts on it. `agenthub ci` doesn't exist
(`skills/ship-feature-lane/SKILL.md:167` notes this explicitly at Stage 10,
where the skill falls back to raw `gh pr checks`/`gh run view`).
@@ -22,21 +22,21 @@ here. Stage 13's dev-QC half stays noted as unavailable until that lands.
## Approach
**A read-only status snapshot, not a wait loop.** Per the project's
orchestration doctrine (`docs/LANES.md` § "Orchestration: what CCAM does NOT
do" — no chaining, no queue, no retry logic), `ccam ci status` reports a
orchestration doctrine (`docs/LANES.md` § "Orchestration: what AgentHub does NOT
do" — no chaining, no queue, no retry logic), `agenthub ci status` reports a
fact once; it does not poll, block, or retry. The driving skill owns the
poll loop and the fix/escalate decision, the same shape Stage 12 already
uses for watching a PR (`sleep 300` backgrounded, re-invoked per iteration).
This also means `ccam ci status` needs no flake classification. Stage 10
This also means `agenthub ci status` needs no flake classification. Stage 10
already does that triage (infra/flake signature vs. real failure) reading
`gh` output directly — Stage 13 reuses the same triage, now reading this
command's structured JSON instead of raw `gh`.
**Server-backed, following the `sync-base` pattern**, not a CLI-local `gh`
spawn. Every existing `ccam` command is a thin HTTP client — no command in
`bin/ccam.js` spawns a child process directly (`worktree.js`'s `git()`
helper is the only child-process boundary CCAM has today, always
spawn. Every existing `agenthub` command is a thin HTTP client — no command in
`bin/agenthub.js` spawns a child process directly (`worktree.js`'s `git()`
helper is the only child-process boundary AgentHub has today, always
server-side). A new `server/lib/gh.js` mirrors that helper:
```js
@@ -56,12 +56,12 @@ New route: `GET /api/lanes/:id/ci/status?ref=<branch-or-sha>`
yet, not an error).
- `gh` missing or not authenticated → `{ error: { message } }`, HTTP 500.
New CLI: `ccam lanes ci status <branch-or-sha> [<lane-id>]`
New CLI: `agenthub lanes ci status <branch-or-sha> [<lane-id>]`
- GETs the route above, prints the JSON, sets `process.exitCode`.
- Resolves the lane the same way every other `ccam lanes <verb>` command
- Resolves the lane the same way every other `agenthub lanes <verb>` command
does (`resolveLaneArg`), so it works with an implicit lane-from-cwd or an
explicit id, matching `ccam lanes integration`.
explicit id, matching `agenthub lanes integration`.
## Response shape
@@ -97,7 +97,7 @@ note with a poll loop mirroring Stage 12's:
- Get the merge SHA: `gh pr view <pr_url> --json mergeCommit -q .mergeCommit.oid`.
- Loop (`sleep 300`, `run_in_background: true`, bump heartbeat each
iteration): `ccam lanes ci status <sha>`.
iteration): `agenthub lanes ci status <sha>`.
- `pending` → keep waiting.
- `failure` → triage like Stage 10 (`gh run view` for job logs on the
same run id returned in `checks[].url`), fix + re-enter from Stage 2,
@@ -107,14 +107,14 @@ note with a poll loop mirroring Stage 12's:
that CI-wait specifically passed.
- Update `docs/LANES.md`: the "Stages currently hardcoded skipped" list
(Stage 13 becomes "CI-wait works; dev-QC still off"), the
`ccam lanes integration` section's claim that `ci_wait`'s consumer isn't
built, and add `ccam ci status` to the command reference.
`agenthub lanes integration` section's claim that `ci_wait`'s consumer isn't
built, and add `agenthub ci status` to the command reference.
## Non-goals
- No flake auto-classification or auto-rerun inside `ccam ci status` —
- No flake auto-classification or auto-rerun inside `agenthub ci status` —
stays a dumb fact-reporter; the skill keeps owning that judgment, per
CCAM's no-retry-logic rule.
AgentHub's no-retry-logic rule.
- No change to Stage 10 (PR CI watch) — it keeps using raw `gh` directly.
Reusing this command there is a plausible future cleanup, not required
now.
@@ -14,7 +14,7 @@ same tmux-backed terminal console, same run history.
1. Scope: run Codex in the lane's tmux pane. No ingestion of `~/.codex` history.
2. The tool belongs to the **lane**, not to the run: one lane = one tool, picked
in the "Add lane" modal, not changeable afterwards. One tmux session per lane
stays `ccam-lane-<id>`.
stays `agenthub-lane-<id>`.
3. Codex run controls: model, sandbox, ask-for-approval, reasoning effort.
4. Codex runs persist in `dashboard_runs` (new `tool` column) so Active runs and
Run history show them next to Claude runs.
@@ -97,7 +97,7 @@ Codex does not emit this dashboard's Claude Code hooks, so for a Codex lane:
- Sessions, Analytics, and cost stay empty — nothing ingests Codex usage.
- `server/lib/stage-detect.js` never paints the amber detected badge.
- Pipeline stages still work when the agent calls `ccam stage` itself (the repo
- Pipeline stages still work when the agent calls `agenthub stage` itself (the repo
already ships `AGENTS.md` and `.codex/`).
- Lane liveness for Codex lanes comes from the tmux pane plus `dashboard_runs`.