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
+174 -174
View File
@@ -14,10 +14,10 @@ A lane persists across session restarts, which is what makes a pipeline view pos
## Creating a lane
Create a lane bound to a working directory with `ccam lanes add`:
Create a lane bound to a working directory with `agenthub lanes add`:
```bash
ccam lanes add --cwd /path/to/repo --title "My Feature"
agenthub lanes add --cwd /path/to/repo --title "My Feature"
```
The lane is now ready to track work. When you run a Claude Code session in that directory (or a subdirectory), the dashboard will bind the session to the lane automatically by matching the cwd.
@@ -27,21 +27,21 @@ The lane is now ready to track work. When you run a Claude Code session in that
To provision a dashboard-managed git worktree instead of adopting an existing directory, use a source repository:
```bash
ccam lanes add --repo /path/to/repo --title "My Feature" --base main --slug my-feature
agenthub lanes add --repo /path/to/repo --title "My Feature" --base main --slug my-feature
```
`--title`, `--base`, and `--slug` are optional. The CLI waits for background provisioning to finish and reports either the ready lane or its failure notes.
Adopting your main repo (`ccam lanes add --cwd $(pwd)`) is also how you get stage detection working for sessions that work directly in it rather than in a worktree — see "Superpowers skill invocations" below. Once adopted, that lane's own card lists every managed-worktree lane whose `source_repo` matches its `cwd`, each a clickable link to jump to that lane.
Adopting your main repo (`agenthub lanes add --cwd $(pwd)`) is also how you get stage detection working for sessions that work directly in it rather than in a worktree — see "Superpowers skill invocations" below. Once adopted, that lane's own card lists every managed-worktree lane whose `source_repo` matches its `cwd`, each a clickable link to jump to that lane.
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `ccam lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `ccam lanes agents install`, and `ccam lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. The modal shows a ✓/✗ summary of the three results and stays open until dismissed (Cancel/X) — it does not auto-close. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `agenthub lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `agenthub lanes agents install`, and `agenthub lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. The modal shows a ✓/✗ summary of the three results and stays open until dismissed (Cancel/X) — it does not auto-close. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
### The "+ Add lane" modal
The modal has a segmented toggle mirroring the CLI's two modes:
- **Repo** — adopts the given directory as-is (maps to `ccam lanes add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
- **Worktree** (default) — provisions a managed worktree (maps to `ccam lanes add --repo`). Unlike the CLI, the modal requires you to type the new branch's name yourself rather than deriving one from the title — the underlying route still derives one when `branch` is omitted, so the CLI's behavior is unchanged.
- **Repo** — adopts the given directory as-is (maps to `agenthub lanes add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
- **Worktree** (default) — provisions a managed worktree (maps to `agenthub lanes add --repo`). Unlike the CLI, the modal requires you to type the new branch's name yourself rather than deriving one from the title — the underlying route still derives one when `branch` is omitted, so the CLI's behavior is unchanged.
Both modes' path field has a **Browse** button next to it, opening a small folder browser (`GET /api/lanes/browse?path=<abs>`) instead of a native OS picker — a browser cannot hand a web page an absolute filesystem path from a native dialog, so this dashboard (local-first, server and browser on the same machine) lists directories server-side instead: click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field. Git repos are marked in the listing.
@@ -70,7 +70,7 @@ Codex does not emit Claude Code hooks (sessions, agent events, tool-execution tr
- **Sessions** and **Analytics** tabs — no session history or aggregated statistics
- **Cost** surface — no per-run or per-model pricing data
**These are not bugs; they reflect Codex's architecture.** The stage you declare via `ccam stage` (or that the driving agent calls itself) still works; liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented.
**These are not bugs; they reflect Codex's architecture.** The stage you declare via `agenthub stage` (or that the driving agent calls itself) still works; liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented.
**Stage detection is not in that list.** It does work for a codex lane, from
Codex's own on-disk rollout journals rather than from hooks, and unlike a claude
@@ -79,18 +79,18 @@ see "Stage detection" below. What a codex lane cannot get for free is
*evidence*: an auto-advanced node renders `passed-no-evidence` (amber), never
`done`, because only the session itself can say what it actually finished. The
run console never pre-fills a Claude slash command into a codex pane (Codex
would read `/ccam:ship-feature-lane` as prose), so a codex session declares
would read `/agenthub:ship-feature-lane` as prose), so a codex session declares
evidence only when something tells it to. Three things do:
- **An opening prompt checklist.** The Run Setup panel pre-fills a codex pane's
first prompt with a plain-text list of `ccam stage <stage> --tool codex`
first prompt with a plain-text list of `agenthub stage <stage> --tool codex`
calls to make, one per node of the lane's own pipeline template (built from
`Lane.pipeline_nodes`, so it always matches whatever template is in use) —
the same up-front priming a Claude lane gets from its pre-filled
`/ccam:ship-feature-lane` skill command, just as plain text since Codex has
`/agenthub:ship-feature-lane` skill command, just as plain text since Codex has
no slash commands. See `LaneConsolePane.tsx`'s `promptPrefill`.
- **An `AGENTS.md` block.** `ccam lanes agents install` on a codex lane writes
a marker-delimited `<!-- ccam:stage:start -->` section into
- **An `AGENTS.md` block.** `agenthub lanes agents install` on a codex lane writes
a marker-delimited `<!-- agenthub:stage:start -->` section into
`<lane cwd>/AGENTS.md` — the one instruction channel Codex actually reads,
since it never loads `.claude/agents/`. Appended, never overwriting your own
prose, and a reinstall replaces exactly that block. (On a `claude` lane the
@@ -98,7 +98,7 @@ evidence only when something tells it to. Three things do:
- **A missing-evidence nudge.** `server/lib/codex-nudge.js` sweeps every 60s
(`DASHBOARD_CODEX_NUDGE_MS` to change, `0` to disable) for a codex lane whose
current stage has been held for at least two minutes with no evidence
recorded, and types a one-line `[ccam] ... run: ccam stage ... --evidence
recorded, and types a one-line `[agenthub] ... run: agenthub stage ... --evidence
"..."` reminder straight into that lane's live tmux pane (`tmux.sendText`,
skipped if the pane is at a bare shell prompt). It never writes `stage`,
`stages` or `detected_stage` itself — the evidence that results is a real
@@ -109,9 +109,9 @@ evidence only when something tells it to. Three things do:
Reset a managed worktree, remove one, or purge the lane's eligible session history with the CLI:
```bash
ccam lanes reset <id> [--force] [--keep-db] --yes
ccam lanes remove <id> [--force] --yes
ccam lanes purge <id> --yes
agenthub lanes reset <id> [--force] [--keep-db] --yes
agenthub lanes remove <id> [--force] --yes
agenthub lanes purge <id> --yes
```
Each command first fetches and prints its preflight counts. `reset` and `remove` show `head`, `dirty`, `untracked`, and `unpushed` (plus `database`, the name a data-isolated lane's reset/remove will drop — see "Data isolation" below); `purge` shows `sessions`, `events`, and `tokenRows`. The action refuses to run without `--yes`, and sends those exact facts back as its confirmation. Use `--force` only when the preflight reports unpushed commits. `--keep-db` (reset only) skips the whole drop-recreate-migrate-reseed sequence, leaving the lane's database exactly as it was. An adopted lane points at a directory you own, so the CLI refuses to `reset` it. `remove` IS allowed for an adopted lane: it drops only the dashboard's record and leaves the directory, its contents, and its database (if any) untouched.
@@ -120,8 +120,8 @@ Each command first fetches and prints its preflight counts. `reset` and `remove`
### The two lane kinds
- **`managed`** — a git worktree the dashboard itself created (`ccam lanes add --repo`), rooted under `LANES_ROOT`. The dashboard owns its full lifecycle: it may reset the worktree to its base branch or remove it entirely.
- **`adopted`** — an existing directory you already had (`ccam lanes add --cwd`). The dashboard only ever tracks it; it never resets or deletes that worktree. "Removing" an adopted lane drops the dashboard's record of it and leaves the directory untouched.
- **`managed`** — a git worktree the dashboard itself created (`agenthub lanes add --repo`), rooted under `LANES_ROOT`. The dashboard owns its full lifecycle: it may reset the worktree to its base branch or remove it entirely.
- **`adopted`** — an existing directory you already had (`agenthub lanes add --cwd`). The dashboard only ever tracks it; it never resets or deletes that worktree. "Removing" an adopted lane drops the dashboard's record of it and leaves the directory untouched.
### The three independent safety checks
@@ -164,16 +164,16 @@ Every destructive `POST /api/lanes/:id/:action` must echo back an `expect` objec
### Environment variables
- **`LANES_ROOT`** (default `~/.claude/ccam-lanes`) — the directory managed worktrees are provisioned under; also the boundary that safety check 2 resolves paths against.
- **`LANES_ROOT`** (default `~/.claude/agenthub-lanes`) — the directory managed worktrees are provisioned under; also the boundary that safety check 2 resolves paths against.
- **`LANE_DEAD_SEC`** (default `300`) — seconds of silence before a lane whose stage matches `/watch|poll/i` flips from `active` to `dead` liveness.
- **`LANE_BASE_BRANCH`** (default `main`) — the base branch a new managed worktree resolves against when a request omits `base`. An explicit `--base <branch>` (or request body `base`) always wins over this default.
- **`LANE_BRANCH_PREFIX`** (default `feat/`) — the prefix used to build a new managed worktree's feature branch name (`<prefix><slug>`).
### Limitation: a fresh worktree has no dependencies
`ccam lanes add --repo` runs `git worktree add` only. It does not run `npm install` (or any other package manager), and it does not copy the source repo's untracked local files (`.env`, IDE config, etc.) into the new worktree. A managed lane is ready for `git` immediately but needs its own dependency install and local env setup before a session can run the project inside it.
`agenthub lanes add --repo` runs `git worktree add` only. It does not run `npm install` (or any other package manager), and it does not copy the source repo's untracked local files (`.env`, IDE config, etc.) into the new worktree. A managed lane is ready for `git` immediately but needs its own dependency install and local env setup before a session can run the project inside it.
A repository can close the dependency half of that gap itself by declaring a `bootstrap` hook in its profile (below) and running `ccam lanes hook bootstrap`. Seeding a lane's `.env` and its database is handled automatically once the profile opts in — see "Data isolation" below.
A repository can close the dependency half of that gap itself by declaring a `bootstrap` hook in its profile (below) and running `agenthub lanes hook bootstrap`. Seeding a lane's `.env` and its database is handled automatically once the profile opts in — see "Data isolation" below.
## Lane runtime: running a lane's own stack
@@ -183,11 +183,11 @@ This is resource namespacing on the host, **not** a container: lanes run as the
### Declaring a profile
Everything stack-specific lives in the repository, at `<repo>/.ccam/profile/`. CCAM is multi-repo, so there is no global profile setting — a profile travels with the repo it describes, and therefore already exists in every worktree cut from it.
Everything stack-specific lives in the repository, at `<repo>/.agenthub/profile/`. AgentHub is multi-repo, so there is no global profile setting — a profile travels with the repo it describes, and therefore already exists in every worktree cut from it.
```
myapp/
.ccam/profile/
.agenthub/profile/
profile.env
hooks/
bootstrap.sh boot.sh health.sh
@@ -198,7 +198,7 @@ myapp/
`profile.env` is a `KEY=VALUE` declaration file. It is **parsed, never sourced** — `$(…)`, backticks and `${VAR}` are kept literally, because sourcing arbitrary shell from a repository into the dashboard process would be a code-execution path. Hooks are executed on purpose; config is only read.
```bash
# .ccam/profile/profile.env
# .agenthub/profile/profile.env
PORTS="api fe worker" # names; each gets a <NAME>_PORT in the hook environment
PORT_BASE_api=8000 # lane in slot 3 prefers :8003
PORT_BASE_fe=3000
@@ -210,18 +210,18 @@ FRONTEND_DIR=frontend
Every key has a default, so a missing one never breaks a lane: `PORTS="api fe"`, `PORT_BASE_api=8000`, `PORT_BASE_fe=3000`, `LANE_DIRS=""`, `BACKEND_DIR=backend`, `FRONTEND_DIR=frontend`, `API_PATH=/api`. A port name with no declared base falls back to `8000`.
CCAM resolves the profile from the **lane's own working copy first**, then its source repository. A branch that changes a boot command must boot with the command it changed; the source-repo fallback exists for a profile kept gitignored, which never reaches a worktree through git.
AgentHub resolves the profile from the **lane's own working copy first**, then its source repository. A branch that changes a boot command must boot with the command it changed; the source-repo fallback exists for a profile kept gitignored, which never reaches a worktree through git.
### Scaffolding a profile automatically
```bash
ccam lanes profile init <repo> [--force] # detect + write .ccam/profile/
ccam lanes profile check [<path>] # validate one (path defaults to cwd)
agenthub lanes profile init <repo> [--force] # detect + write .agenthub/profile/
agenthub lanes profile check [<path>] # validate one (path defaults to cwd)
```
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.ccam/profile/` by hand for those, following the reference above.
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.agenthub/profile/` by hand for those, following the reference above.
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.ccam/secrets.env` doesn't exist yet for a database-declaring profile.
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.agenthub/secrets.env` doesn't exist yet for a database-declaring profile.
`profile check` takes a **path**, not a lane id or `--lane`/`--cwd` flag like every other `lanes` subcommand — it is meant to run against a bare repository right after `init`, before any lane or worktree exists for it.
@@ -236,7 +236,7 @@ If every candidate is busy the boot fails with `EPORTBUSY`, naming the process h
### The hook contract
Before running a hook, CCAM exports:
Before running a hook, AgentHub exports:
| Variable | Value |
|---|---|
@@ -244,7 +244,7 @@ Before running a hook, CCAM exports:
| `LANE_ID` | the lane id |
| `LANE_DIR` | the lane's working copy |
| `SOURCE_REPO` | the repository it was cut from |
| `PROFILE_DIR` | the resolved `.ccam/profile` |
| `PROFILE_DIR` | the resolved `.agenthub/profile` |
| `RUN_DIR` / `LOG_DIR` | pid files and logs, under `$LANES_ROOT/.state/lane<slot>/` |
| `<NAME>_PORT` | one per declared port, upper-cased (`PORTS="api fe"` → `API_PORT`, `FE_PORT`) |
| everything in `profile.env` | verbatim |
@@ -254,7 +254,7 @@ Two shell helpers are injected: `die <msg>`, and `harness_spawn <name> <workdir>
Inherited `GIT_*` variables are scrubbed exactly as `server/lib/worktree.js` scrubs them, so a hook that shells out to git cannot inherit a git context pointing at the dashboard's own repository.
```bash
# .ccam/profile/hooks/boot.sh
# .agenthub/profile/hooks/boot.sh
#!/usr/bin/env bash
set -euo pipefail
harness_spawn api "$LANE_DIR/$BACKEND_DIR" npm run start -- --port "$API_PORT"
@@ -262,7 +262,7 @@ harness_spawn fe "$LANE_DIR/$FRONTEND_DIR" npm run preview -- --port "$FE_PORT"
```
```bash
# .ccam/profile/hooks/health.sh
# .agenthub/profile/hooks/health.sh
#!/usr/bin/env bash
set -euo pipefail
curl -sf --retry 30 --retry-delay 1 --retry-all-errors "http://127.0.0.1:$FE_PORT/" >/dev/null
@@ -273,17 +273,17 @@ Runnable hook names are a fixed allowlist: `bootstrap`, `boot`, `health`, `migra
### Up, down, and surviving a restart
```bash
ccam lanes up # boot the lane owning this directory
ccam lanes up 3 --no-build # boot lane 3, reusing an existing build
ccam lanes runtime # slot, ports, service liveness, last boot error
ccam lanes logs api # tail one service or hook log
ccam lanes down # stop the stack
ccam lanes hook ci-gate # run any allowlisted hook
agenthub lanes up # boot the lane owning this directory
agenthub lanes up 3 --no-build # boot lane 3, reusing an existing build
agenthub lanes runtime # slot, ports, service liveness, last boot error
agenthub lanes logs api # tail one service or hook log
agenthub lanes down # stop the stack
agenthub lanes hook ci-gate # run any allowlisted hook
```
`up` runs `boot` then `health`. It deliberately does **not** run `bootstrap` — installing dependencies on every boot would make a routine restart minutes long. A failing health check **leaves the processes running**: their logs are what tell you which service never came up, and killing them to report a tidy failure destroys the evidence.
Services are fully detached, so **a lane's stack outlives the dashboard**. Restarting or updating CCAM does not touch a running lane. Nothing has to re-adopt them either: whether a stack is up is recomputed from pid files and port probes on every read, never cached, because a process can die to OOM or a stray `kill` without telling anyone.
Services are fully detached, so **a lane's stack outlives the dashboard**. Restarting or updating AgentHub does not touch a running lane. Nothing has to re-adopt them either: whether a stack is up is recomputed from pid files and port probes on every read, never cached, because a process can die to OOM or a stray `kill` without telling anyone.
`down` kills each recorded pid tree bottom-up (a parent killed first reparents its children to init, where nothing knows to look for them), then — **only when there was a pid file** — sweeps any listener still on the lane's ports. That condition matters: a lane whose stack is already down still owns its port numbers, and an unconditional sweep would kill a server you started there yourself.
@@ -293,7 +293,7 @@ While a hook runs, each output line is broadcast as a **`lane_hook_output`** Web
The runtime writes `slot` and `ports` on the lane row, and nothing else. It never writes `stage`, `status` or `notes`.
In CCAM those describe **the agent's work**, not the stack's state: `status=running` means a session is working, and a booted server is not a session. Conflating them would corrupt lane liveness. This is the same boundary as "the console never writes a lane's stage". Boot failures live in `$LANES_ROOT/.state/lane<slot>/last-error.json` and surface through `GET /api/lanes/:id/runtime`.
In AgentHub those describe **the agent's work**, not the stack's state: `status=running` means a session is working, and a booted server is not a session. Conflating them would corrupt lane liveness. This is the same boundary as "the console never writes a lane's stage". Boot failures live in `$LANES_ROOT/.state/lane<slot>/last-error.json` and surface through `GET /api/lanes/:id/runtime`.
Adopted lanes may be brought up and down — `up` only runs what you could run yourself — but every path that writes into their working copy is refused.
@@ -308,7 +308,7 @@ Adopted lanes may be brought up and down — `up` only runs what you could run y
Two lanes running their stack at once need more than separate ports — they need separate data, or one lane's migration corrupts the other's session. This is the other half of runtime isolation, and every piece of it is **off by default**: a profile that never declares `DB_PREFIX` gets no database, `REDIS=1` gets no Redis index, and no `ENV_FILES` gets no `.env` writes at all.
**Profile declarations** (`.ccam/profile/profile.env`, all optional):
**Profile declarations** (`.agenthub/profile/profile.env`, all optional):
```bash
DB_PREFIX="myapp_l" # lane in slot 3 -> myapp_l3 ; empty = no per-lane DB
@@ -320,14 +320,14 @@ REDIS=1 # 1 = allocate a logical Redis index = slot
ENV_FILES="backend/.env" # file(s) to seed, relative to the lane
ENV_SOURCE="backend/.env" # source path in the source repo (defaults to ENV_FILES)
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys CCAM overwrites per lane
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys AgentHub overwrites per lane
ENV_PRESERVE="JWT_SECRET" # keys kept from the lane's OWN file on a --force refresh
UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
```
A ready-to-copy template (including `db-create.sh`/`db-drop.sh` for a Postgres-in-docker-compose stack) lives at `server/data/profile-templates/postgres-compose/`.
**Machine-level credentials** live at `~/.ccam/secrets.env` (mode `0600`), never in the repo:
**Machine-level credentials** live at `~/.agenthub/secrets.env` (mode `0600`), never in the repo:
```bash
PG_HOST=127.0.0.1
@@ -340,7 +340,7 @@ REDIS_PORT=6379
Parsed with the same literal `KEY=VALUE` reader as `profile.env` — never sourced. A missing file falls back to those same defaults (with a one-time warning); a file readable by group or world is refused outright rather than trusted. No route ever returns a value from this file — `GET /api/lanes/:id/runtime` reports a database's *name* and a Redis *index*, never a connection string.
**Who creates the database?** A `db-create.sh` / `db-drop.sh` hook (already in the allowlist) — CCAM stays stack-agnostic, since `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ. **Who rewrites `.env`?** CCAM itself: mechanical and identical across stacks, so pushing it into every repo would duplicate the same ~30 lines.
**Who creates the database?** A `db-create.sh` / `db-drop.sh` hook (already in the allowlist) — AgentHub stays stack-agnostic, since `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ. **Who rewrites `.env`?** AgentHub itself: mechanical and identical across stacks, so pushing it into every repo would duplicate the same ~30 lines.
**Hook environment additions**, present only when their owning declaration is (a `db-create.sh` that forgot to check `DB_PREFIX` fails loudly on an unset variable instead of touching a database named "undefined"):
@@ -355,10 +355,10 @@ Parsed with the same literal `KEY=VALUE` reader as `profile.env` — never sourc
**When each step runs:**
- **Provisioning** a new managed worktree (`ccam lanes add --repo`) — after the worktree is created: seed `.env`, run `bootstrap`, create the database, `migrate`, `seed`. Runs once.
- **Provisioning** a new managed worktree (`agenthub lanes add --repo`) — after the worktree is created: seed `.env`, run `bootstrap`, create the database, `migrate`, `seed`. Runs once.
- **`up`** — repair `.env` (a hand-edited or never-seeded file gets fixed), ensure the database exists (cheap when it already does), `migrate` on every boot (a lane's schema drifts while it sits idle), and `seed` only on the boot that actually created the database.
- **`reset`** — refresh `.env` with `--force` (preserving `ENV_PRESERVE` keys from the lane's own file — swapping in the source's `JWT_SECRET` would 401 a running lane's tokens until reboot), re-run `bootstrap` (a reset can land on a branch with new dependencies), clear the declared `LANE_DIRS`, then drop, recreate, migrate and reseed the database — unless `--keep-db`, which skips that whole block.
- **`remove`** — drops the database and its `_test` sibling before the rest of teardown. Best-effort: a failed drop is logged, never blocks removing the lane's record. **Never runs for an adopted lane** — its data was never CCAM's to create, so it is never CCAM's to destroy, the same invariant that protects an adopted lane's worktree.
- **`remove`** — drops the database and its `_test` sibling before the rest of teardown. Best-effort: a failed drop is logged, never blocks removing the lane's record. **Never runs for an adopted lane** — its data was never AgentHub's to create, so it is never AgentHub's to destroy, the same invariant that protects an adopted lane's worktree.
A missing source `.env` falls back to `.env.example` with a loud warning, never a silent success. And any hook output that echoes a value from `secrets.env` (the password, specifically) is redacted before it reaches `$LOG_DIR/<hook>.log` or the `lane_hook_output` WebSocket message — that stream reaches a browser tab, and a hook debugging its own environment must not publish a database password to everyone watching.
@@ -369,12 +369,12 @@ The dashboard web UI merges lanes and runs into a single **Workspace** page acce
- **Header** — the page title and four counters (`lanes`, `running`, `needs you`, `dead`), plus Add lane. The `needs you` and `dead` counters appear only when they are non-zero, so a quiet header means nothing is waiting on a human.
- **Detail panel** — the selected lane's declared stage, its inferred stage when detection leads, a full-width pipeline map, and a legend naming all five node states plus the dashed-amber inferred treatment.
- **Terminal** — a real interactive terminal (xterm.js) displaying the tmux session's PTY output, with full support for interactive commands, editors, and pagers. A live run keeps its rendered history and scroll position when scrolling.
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2×2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`ccam.workspace.splitView`) across reloads.
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2×2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`agenthub.workspace.splitView`) across reloads.
- **Lane grid** — one card per lane, 1 column, 2 at `md`, 3 at `xl`. Each card carries the lane id, liveness dot and status, title, declared stage with a progress bar and time-on-stage, the `auto:` chip when detection leads, the kind and CI tags, the working-copy facts from `GET /api/lanes/:id/git`, the needs-you banner, and the action row.
Run history is per lane, queryable via `GET /api/run/history?laneId=<n>`.
**A console pane follows the lane it shows.** Selecting another lane — from the lane strip in layout 1, or from a pane's own lane picker in layouts 2 and 4 — swaps that pane's cwd, run history and terminal over to the new lane. If the new lane already has a live run in `GET /api/run`, the pane re-attaches to it immediately, so each lane sticks to its own `ccam-lane-<id>` tmux session; if it has none, the pane shows that lane's setup form. Nothing of the previous lane (a half-typed prompt, its terminal) carries over.
**A console pane follows the lane it shows.** Selecting another lane — from the lane strip in layout 1, or from a pane's own lane picker in layouts 2 and 4 — swaps that pane's cwd, run history and terminal over to the new lane. If the new lane already has a live run in `GET /api/run`, the pane re-attaches to it immediately, so each lane sticks to its own `agenthub-lane-<id>` tmux session; if it has none, the pane shows that lane's setup form. Nothing of the previous lane (a half-typed prompt, its terminal) carries over.
The **working directory** field tracks the selected lane's own `cwd` specifically, and re-syncs as soon as that path is known rather than only when the selection changes — a pane can render before `GET /api/lanes` has answered (split view restores its pane lanes from `localStorage`), and its lane id never changes afterwards. `RunSetup` submits that string verbatim to `POST /api/lanes/:id/start`, so a cwd left over from the previous lane or from the home default would start the run in the wrong folder. The home suggestion is used only while no lane is selected at all.
@@ -392,7 +392,7 @@ An external session is deduped against a dashboard run with the same `session_id
External rows have **no Attach action**: the dashboard owns no tmux session for them, so there is no PTY to bridge. Their action is **Resume**, which does what resuming from history does — `POST /api/lanes/ensure` for the session's `cwd`, then `POST /api/lanes/:id/start` with `resumeSessionId` — spawning a *new* tmux-backed `claude --resume <session>` in that folder. The original terminal keeps running; resuming gives you a second Claude Code process on the same transcript, not a view of the first one.
**Start and Resume are create-or-reuse, and never silently swallow the request.** When the lane's `ccam-lane-<id>` tmux session does not exist, it is created with the full argv. When it exists but its pane is sitting at a **shell prompt** (a `ccam lanes shell` you opened, or a `claude` that has since exited), the argv is typed into that pane — so `--resume` really runs, and an initial prompt really lands, in the session you are already looking at. Only when the pane is running something (a live `claude`, an editor, a build) is the request adopted as-is: attaching shows you what is running rather than typing over it. Before this, an existing session was always adopted silently, so the first Resume after a `ccam lanes shell` answered `200` while doing nothing at all.
**Start and Resume are create-or-reuse, and never silently swallow the request.** When the lane's `agenthub-lane-<id>` tmux session does not exist, it is created with the full argv. When it exists but its pane is sitting at a **shell prompt** (a `agenthub lanes shell` you opened, or a `claude` that has since exited), the argv is typed into that pane — so `--resume` really runs, and an initial prompt really lands, in the session you are already looking at. Only when the pane is running something (a live `claude`, an editor, a build) is the request adopted as-is: attaching shows you what is running rather than typing over it. Before this, an existing session was always adopted silently, so the first Resume after a `agenthub lanes shell` answered `200` while doing nothing at all.
The UI operates on a working directory (`cwd`), not a lane id. Starting a run in a `cwd` that no lane owns calls `POST /api/lanes/ensure` first, to idempotently find or adopt a lane for that path; a `cwd` an existing lane already owns is matched from the loaded lane list without a round trip. Either way the run is then started through `POST /api/lanes/:id/start` rather than directly through `POST /api/run`.
@@ -460,7 +460,7 @@ every 30 seconds, and a failed fetch is silent.
List all lanes with their current status:
```bash
ccam lanes
agenthub lanes
```
This shows each lane's:
@@ -482,37 +482,37 @@ The dashboard web UI shows each lane as a card in a grid, with the selected lane
A lane moves through stages defined in a **pipeline template** (see "Custom pipeline templates" below). The default pipeline has eight stages: `intake`, `plan`, `implement`, `tests`, `review`, `gate`, `ship`, and `done`.
**A lane is created on `default` unless told otherwise.** The "+ Add lane" modal has a *Pipeline template* select listing every template the server reports, so this is chosen at creation; `ccam lanes add --pipeline <id>` is the same choice from the terminal.
**A lane is created on `default` unless told otherwise.** The "+ Add lane" modal has a *Pipeline template* select listing every template the server reports, so this is chosen at creation; `agenthub lanes add --pipeline <id>` is the same choice from the terminal.
For a lane that already exists, switch it from the terminal:
```bash
ccam lanes pipeline # which template this lane uses, and what else exists
ccam lanes pipeline ship-feature # switch it
agenthub lanes pipeline # which template this lane uses, and what else exists
agenthub lanes pipeline ship-feature # switch it
```
or from the dashboard: the pipeline-template select next to the lane's title in the Workspace detail panel does the same `PATCH /api/lanes/:id` the CLI uses, live (disabled while viewing an archived feature snapshot).
**The template names the skill that drives it.** `"skill": "/ccam:ship-feature-lane"` on the 16-node `ship-feature` template, `"skill": "/ccam:ship-feature"` on the 8-node `default` one. It rides along on every lane payload as `pipeline_skill`, and from a terminal there is one command per depth of work — the command you type picks the pipeline, so no `ccam lanes pipeline` step first:
**The template names the skill that drives it.** `"skill": "/agenthub:ship-feature-lane"` on the 16-node `ship-feature` template, `"skill": "/agenthub:ship-feature"` on the 8-node `default` one. It rides along on every lane payload as `pipeline_skill`, and from a terminal there is one command per depth of work — the command you type picks the pipeline, so no `agenthub lanes pipeline` step first:
```bash
cd ~/lanes/myrepo__feat-export
# 16 stages: ends in a published PR, watched until a human merges it
ccam ship-full "add an export button"
# → lane switched to `ship-feature`, then: claude "/ccam:ship-feature-lane add an export button"
agenthub ship-full "add an export button"
# → lane switched to `ship-feature`, then: claude "/agenthub:ship-feature-lane add an export button"
# 8 stages: in-repo feature work, stops at review/gate — no PR, no browser QC
ccam ship-quick "fix the CSV separator"
# → lane switched to `default`, then: claude "/ccam:ship-feature fix the CSV separator"
agenthub ship-quick "fix the CSV separator"
# → lane switched to `default`, then: claude "/agenthub:ship-feature fix the CSV separator"
ccam ship-full "add an export button" --print # show the command, switch nothing, run nothing
agenthub ship-full "add an export button" --print # show the command, switch nothing, run nothing
```
| | `ccam ship-full` | `ccam ship-quick` |
| | `agenthub ship-full` | `agenthub ship-quick` |
|---|---|---|
| Template | `ship-feature` (16 nodes) | `default` (8 nodes) |
| Skill | `/ccam:ship-feature-lane` | `/ccam:ship-feature` |
| Skill | `/agenthub:ship-feature-lane` | `/agenthub:ship-feature` |
| Ends at | PR open on `development`, watched until merged | `ship` in the working copy |
| Includes | unit-test agent, CI gates, e2e, code review, local browser QC, senior GO/NO-GO | plan, implement, tests, review, gate |
| Use when | the work should reach reviewers | the work stays local |
@@ -521,30 +521,30 @@ The lane is resolved from the working directory, so no id is needed (pass one to
### Running ONE node
`ccam ship-full` / `ccam ship-quick` hand the whole pipeline to a skill. When you only want one step — re-run the CI gate, redo the brainstorm, open the PR by hand — run that node's own `command`:
`agenthub ship-full` / `agenthub ship-quick` hand the whole pipeline to a skill. When you only want one step — re-run the CI gate, redo the brainstorm, open the PR by hand — run that node's own `command`:
```bash
ccam lanes node # list every node of this lane's pipeline and its command
ccam lanes node intake # → claude "/superpowers:brainstorming"
ccam lanes node intake the export button # extra words are appended to the prompt
ccam lanes node gates # → ccam lanes hook ci-gate
ccam lanes node pr-open --print # show the argv, run nothing
agenthub lanes node # list every node of this lane's pipeline and its command
agenthub lanes node intake # → claude "/superpowers:brainstorming"
agenthub lanes node intake the export button # extra words are appended to the prompt
agenthub lanes node gates # → agenthub lanes hook ci-gate
agenthub lanes node pr-open --print # show the argv, run nothing
```
Both shipped templates give **every** node a command, so any of the 16 (or 8) is runnable. A node starting with `/` is spawned as `claude "<command> <extra args>"`; anything else is spawned directly. The lane comes from the working directory as usual, and the command always runs in the lane's own `cwd`.
The node is matched by **id or alias**, so one name works on either template: `default` calls the step `implement` and `ship-feature` calls it `implementing`, and `ccam lanes node implement` finds it on both.
The node is matched by **id or alias**, so one name works on either template: `default` calls the step `implement` and `ship-feature` calls it `implementing`, and `agenthub lanes node implement` finds it on both.
**From inside a Claude session, use the slash commands instead.** The plugin ships one per node — `/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/e2e-passed`, `/review`, `/qc-plan`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/reported`, `/watching-pr`, `/merged`, `/done` — plus `/ship-full` and `/ship-quick` for the whole pipeline. Each resolves the node's command through `ccam lanes node <id> --print` and runs it *in the current session*: a slash-command node is invoked directly (never as a nested `claude` process), anything else runs as a shell argv in the lane's cwd. Extra words are appended to the prompt: `/intake the export button`.
**From inside a Claude session, use the slash commands instead.** The plugin ships one per node — `/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/e2e-passed`, `/review`, `/qc-plan`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/reported`, `/watching-pr`, `/merged`, `/done` — plus `/ship-full` and `/ship-quick` for the whole pipeline. Each resolves the node's command through `agenthub lanes node <id> --print` and runs it *in the current session*: a slash-command node is invoked directly (never as a nested `claude` process), anything else runs as a shell argv in the lane's cwd. Extra words are appended to the prompt: `/intake the export button`.
Two deliberate limits:
- **The command is split on whitespace and spawned without a shell.** A template is data — files anyone can drop into `DASHBOARD_PIPELINES_DIR` — so its `command` is never interpreted as shell. A node needing a pipe or a redirect names a script that contains them.
- **Running a node does not declare its stage.** A command executing is not proof the node passed — the same reason the run console never writes a lane's stage. Declare with `ccam stage <id>` when the work is actually done. (The exception is a node whose command *is* `ccam stage <id>`: there the declaration is the whole step.)
- **Running a node does not declare its stage.** A command executing is not proof the node passed — the same reason the run console never writes a lane's stage. Declare with `agenthub stage <id>` when the work is actually done. (The exception is a node whose command *is* `agenthub stage <id>`: there the declaration is the whole step.)
**The template also decides what the Workspace run console pre-fills.** A template may name the slash command that drives it — `"skill": "/ccam:ship-feature-lane"` on the 16-node `ship-feature` template, `"skill": "/ccam:ship-feature"` on the 8-node `default` one. That string rides along on every lane payload as `pipeline_skill`, and the run console starts its prompt box with it (plus a trailing space) so you only type the requirement: pick the 16-node template and Run starts the 16-step skill, pick the 8-node one and it starts the 8-step skill. The dashboard never executes it — it is text in a prompt box, exactly as if you had typed it. A prompt you have already edited is never overwritten by a later template switch, and a template with no `skill` field pre-fills nothing.
**The template also decides what the Workspace run console pre-fills.** A template may name the slash command that drives it — `"skill": "/agenthub:ship-feature-lane"` on the 16-node `ship-feature` template, `"skill": "/agenthub:ship-feature"` on the 8-node `default` one. That string rides along on every lane payload as `pipeline_skill`, and the run console starts its prompt box with it (plus a trailing space) so you only type the requirement: pick the 16-node template and Run starts the 16-step skill, pick the 8-node one and it starts the 8-step skill. The dashboard never executes it — it is text in a prompt box, exactly as if you had typed it. A prompt you have already edited is never overwritten by a later template switch, and a template with no `skill` field pre-fills nothing.
Switching re-resolves the lane's existing declared `stage` against the new node list. A stage the old template knew may resolve to nothing in the new one; the switch warns when that happens (both CLI and UI), and the next `ccam stage` fixes it.
Switching re-resolves the lane's existing declared `stage` against the new node list. A stage the old template knew may resolve to nothing in the new one; the switch warns when that happens (both CLI and UI), and the next `agenthub stage` fixes it.
### Aggregate pass rates across lanes
@@ -561,9 +561,9 @@ by pipeline template:
Archived features matter here: a lane's own `stages` only holds its current
feature, so without them the history would reset every time a lane starts new
work. This is also why new work reported as a bare backwards `ccam stage` (see
work. This is also why new work reported as a bare backwards `agenthub stage` (see
"Declaring an EARLIER stage") contributes less here than the same work reported
under `ccam feature activate`: the backwards declaration prunes the records
under `agenthub feature activate`: the backwards declaration prunes the records
ahead of it, so the previous pass leaves nothing behind to count, while a
feature switch archives them first. Neither ever counted *passes* — one lane row
contributes at most one record per node, however many times it ran — so this
@@ -577,9 +577,9 @@ offering every other lane even while the counts show only one.
## Attaching a real terminal to a lane
`ccam lanes shell` attaches a real terminal to the exact tmux session the dashboard's Start/Resume buttons use for this lane (`ccam-lane-<id>`), creating it if it doesn't exist yet. Type `claude` inside it like any normal terminal session — the dashboard's Workspace terminal view is just another client attached to the same tmux session, so both stay in sync live.
`agenthub lanes shell` attaches a real terminal to the exact tmux session the dashboard's Start/Resume buttons use for this lane (`agenthub-lane-<id>`), creating it if it doesn't exist yet. Type `claude` inside it like any normal terminal session — the dashboard's Workspace terminal view is just another client attached to the same tmux session, so both stay in sync live.
**In practice you never have to pick correctly at creation.** The template only has to match whichever skill is actually driving the lane, and the skill enforces that itself: `ship-feature-lane` runs `ccam lanes pipeline ship-feature` and `ship-feature` runs `ccam lanes pipeline default` before their first `ccam stage` call — a no-op if the lane is already on that template, a self-correction if it isn't. A human chatting with the lane never needs to open the picker; whichever skill gets invoked decides the template.
**In practice you never have to pick correctly at creation.** The template only has to match whichever skill is actually driving the lane, and the skill enforces that itself: `ship-feature-lane` runs `agenthub lanes pipeline ship-feature` and `ship-feature` runs `agenthub lanes pipeline default` before their first `agenthub stage` call — a no-op if the lane is already on that template, a self-correction if it isn't. A human chatting with the lane never needs to open the picker; whichever skill gets invoked decides the template.
The dashboard renders every node in the pipeline in one of five **states**:
@@ -595,19 +595,19 @@ The dashboard renders every node in the pipeline in one of five **states**:
A stage in this state was **claimed but not proven**. Two scenarios:
1. **Declared but no artifact left.** A skill called `ccam stage review` without `--evidence`, claiming the stage happened but leaving no proof that work occurred.
1. **Declared but no artifact left.** A skill called `agenthub stage review` without `--evidence`, claiming the stage happened but leaving no proof that work occurred.
2. **Implicitly skipped.** The lane jumped from `plan` to `implement` (to stage index 2), leaving `intake` (index 0) in `passed-no-evidence` state.
In both cases, the stage is not **done** — it's incomplete or untested. Amber signals that the agent claimed to have done this work but the dashboard has no evidence to show it was actually completed. This matters in a pipeline: if your gate checks `done` not `passed-no-evidence`, a lane stuck in amber will block until evidence is provided.
## Reporting a stage
`ccam stage` is not skill-only. Any Claude session working inside an adopted lane's cwd — including a plain chat session where no skill was ever invoked — should call it on real stage transitions. Tool-event detection (see "Inferred vs. declared" below) can only paint the amber "detected" badge; it never moves the blue `current` ring. A lane worked entirely through plain chat that never calls `ccam stage` will keep showing whatever stage it was last declared at (often `intake`), regardless of how much real work happens.
`agenthub stage` is not skill-only. Any Claude session working inside an adopted lane's cwd — including a plain chat session where no skill was ever invoked — should call it on real stage transitions. Tool-event detection (see "Inferred vs. declared" below) can only paint the amber "detected" badge; it never moves the blue `current` ring. A lane worked entirely through plain chat that never calls `agenthub stage` will keep showing whatever stage it was last declared at (often `intake`), regardless of how much real work happens.
When a skill — or a plain chat session — reaches a significant milestone, it reports the current stage to the dashboard using `ccam stage`:
When a skill — or a plain chat session — reaches a significant milestone, it reports the current stage to the dashboard using `agenthub stage`:
```bash
ccam stage implement --evidence "Schema created, migrations run"
agenthub stage implement --evidence "Schema created, migrations run"
```
This command:
@@ -619,7 +619,7 @@ This command:
### Full command signature
```bash
ccam stage <stage> [--lane <id>] [--cwd <path>] [--tool claude|codex] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]
agenthub stage <stage> [--lane <id>] [--cwd <path>] [--tool claude|codex] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]
```
**Parameters:**
@@ -668,7 +668,7 @@ A declaration may move a lane backwards — that is how a fix-loop re-entry, or
new request landing on a lane that already reached `done`, is reported:
```bash
ccam stage plan --note "new request: export to CSV"
agenthub stage plan --note "new request: export to CSV"
```
Declaring a stage that sits **before** the lane's current one does two extra
@@ -692,17 +692,17 @@ for the same reason, declaring a stage that matches no node prunes nothing.
Going back this way is a *declaration*, not a reset: the lane, its worktree, and
its archived features are untouched. To start a genuinely separate piece of work
with its own archived pipeline, use `ccam feature activate <slug>` instead (see
with its own archived pipeline, use `agenthub feature activate <slug>` instead (see
"Per-feature state and archive").
### Which lane a CLI command addresses
Every cwd-resolving `ccam` command (`stage`, `lanes pipeline`, `feature
Every cwd-resolving `agenthub` command (`stage`, `lanes pipeline`, `feature
activate`, `lanes up/down/logs`, …) shares one resolver, `resolveLaneArg` in
`bin/ccam.js`. It answers in this order:
`bin/agenthub.js`. It answers in this order:
1. `--lane <id>`, or a leading numeric positional.
2. `CCAM_LANE_ID` in the environment — every tmux pane the dashboard starts
2. `AGENTHUB_LANE_ID` in the environment — every tmux pane the dashboard starts
carries it (`tmux new-session -e`, or an assignment prefix when the argv is
typed into a pane already at a shell prompt). A session inside a
dashboard-started pane therefore never has to guess.
@@ -732,7 +732,7 @@ exactly the failure this resolver exists to prevent.
A `build` skill might call:
```bash
ccam stage implement --evidence "Built and ran tests, 42 tests passing" --status running
agenthub stage implement --evidence "Built and ran tests, 42 tests passing" --status running
```
Output:
@@ -743,7 +743,7 @@ lane #5 → implement (66%)
A later call reports completion:
```bash
ccam stage tests --evidence "Pre-push gate: all linters passing" --result pass
agenthub stage tests --evidence "Pre-push gate: all linters passing" --result pass
```
## Custom pipeline templates
@@ -759,7 +759,7 @@ If both exist, **files in the user directory override built-ins** with the same
### Custom template format
Create a JSON file in your custom pipeline directory (e.g., `~/.ccam/pipelines/ml-training.json`):
Create a JSON file in your custom pipeline directory (e.g., `~/.agenthub/pipelines/ml-training.json`):
```json
{
@@ -780,25 +780,25 @@ Create a JSON file in your custom pipeline directory (e.g., `~/.ccam/pipelines/m
- **id** (string, required): unique identifier for this template
- **name** (string, optional): display name shown in the UI
- **skill** (string, optional): the slash command that drives this pipeline (e.g. `"/ccam:ship-feature-lane"`). The Workspace run console pre-fills its prompt with it for lanes on this template; omit it and the prompt starts empty. Never executed by the dashboard.
- **skill** (string, optional): the slash command that drives this pipeline (e.g. `"/agenthub:ship-feature-lane"`). The Workspace run console pre-fills its prompt with it for lanes on this template; omit it and the prompt starts empty. Never executed by the dashboard.
- **nodes** (array, required): list of pipeline stages, in order
- **id** (string): stage identifier (used in `ccam stage`)
- **id** (string): stage identifier (used in `agenthub stage`)
- **label** (string, optional): display name in the UI; defaults to `id` if omitted
- **icon** (string, optional): unicode emoji or single character shown before the stage name
- **gate** (boolean, optional): `true` if this stage blocks unless explicitly passed; defaults to `false`
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/superpowers:writing-plans"`) or a shell one (`"ccam lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; both shipped templates fill every node, using `"ccam stage <id>"` for the nodes whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a lane card and as copyable text per node on the Workflows page, and run by `ccam lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
- **aliases** (array, optional): alternative names for the stage (e.g., `["training", "fit"]` lets you call `ccam stage training`)
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/superpowers:writing-plans"`) or a shell one (`"agenthub lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; both shipped templates fill every node, using `"agenthub stage <id>"` for the nodes whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a lane card and as copyable text per node on the Workflows page, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
- **aliases** (array, optional): alternative names for the stage (e.g., `["training", "fit"]` lets you call `agenthub stage training`)
Then set the environment variable:
```bash
export DASHBOARD_PIPELINES_DIR=~/.ccam/pipelines
export DASHBOARD_PIPELINES_DIR=~/.agenthub/pipelines
```
When you create a new lane, you can optionally specify which pipeline to use:
```bash
ccam lanes add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-training
agenthub lanes add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-training
```
(The pipeline is selected at creation time. Lanes created without `--pipeline` default to the `default` pipeline.)
@@ -808,11 +808,11 @@ ccam lanes add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-tr
A lane can carry many features (identified by slug) across its lifetime. Each feature has its own saved pipeline state (stage, status, notes) that persists separately from the live lane — the one every session and the Workspace console observe.
**Why:** `clearLane` used to erase the entire lane on each new feature. Now a lane can accumulate many features' saved states, letting you switch between them (via `ccam feature activate`) and keep their pipeline histories separate. This matters for proof galleries, parallel task batches, and any flow where one lane runs many intentional phases.
**Why:** `clearLane` used to erase the entire lane on each new feature. Now a lane can accumulate many features' saved states, letting you switch between them (via `agenthub feature activate`) and keep their pipeline histories separate. This matters for proof galleries, parallel task batches, and any flow where one lane runs many intentional phases.
### The opt-in model
Nothing changes for a lane that never calls `ccam feature activate` — `clearLane` still erases the live stage and status rows exactly as before. Only when you opt in to features does a lane start archiving. At that point:
Nothing changes for a lane that never calls `agenthub feature activate` — `clearLane` still erases the live stage and status rows exactly as before. Only when you opt in to features does a lane start archiving. At that point:
- `clearLane` archives the **current** active feature (if any) before clearing the live lane row — its saved stage goes into the archive
- A lane that has never activated a feature is unaffected by this change and works exactly as it did before feature support
@@ -828,11 +828,11 @@ This is a **deliberately different rule** from `worktree.js:slugify`'s branch-na
### Activate semantics
`ccam feature activate <slug>` (or the API's `POST /api/lanes/:id/features/activate`):
`agenthub feature activate <slug>` (or the API's `POST /api/lanes/:id/features/activate`):
1. Archives the current active feature (if any and if different from the target slug) — copies its live stage/status/notes to the archive
2. Restores the target slug's saved pipeline onto the live lane row — so switching back to a past feature resumes its pipeline exactly where it left off
3. Creates a fresh feature row for a never-seen slug (empty stage/status/notes)
4. Clears the live lane's detected columns (`detected_stage`/`detected_signal`/`detected_at`), the same way `ccam stage` and `clear` do
4. Clears the live lane's detected columns (`detected_stage`/`detected_signal`/`detected_at`), the same way `agenthub stage` and `clear` do
This means activating an archived feature twice resumes the same pipeline both times.
@@ -845,9 +845,9 @@ The Workspace page's feature picker (read-only) shows every archived and active
### CLI commands
```bash
ccam feature list [<id>] # List every feature (archived or live)
ccam feature activate <slug> [--title text] [<id>] # Switch to a feature (echoes canonicalized slug)
ccam feature show <slug> [<id>] # Show one feature's saved pipeline
agenthub feature list [<id>] # List every feature (archived or live)
agenthub feature activate <slug> [--title text] [<id>] # Switch to a feature (echoes canonicalized slug)
agenthub feature show <slug> [<id>] # Show one feature's saved pipeline
```
Omit `<id>` to address the lane owning the current directory.
@@ -855,10 +855,10 @@ Omit `<id>` to address the lane owning the current directory.
## Stage detection
Besides the stage a skill explicitly declares with `ccam stage`, the dashboard
Besides the stage a skill explicitly declares with `agenthub stage`, the dashboard
also **infers** a stage from the tool events every hook already delivers
(`POST /api/hooks/event`). This is a best-effort heuristic for
un-instrumented sessions that never call `ccam stage` — it fills in a lane's
un-instrumented sessions that never call `agenthub stage` — it fills in a lane's
pipeline map without requiring every skill to be rewritten.
Two event sources feed the identical matcher, one per tool:
@@ -889,9 +889,9 @@ forward-only/declared-wins rules, the same amber-never-green boundary. Three
codex-only details:
- **A written detection also advances the lane's declared `stage`.** This is
the one place in CCAM where an inference moves the blue `current` ring, and
the one place in AgentHub where an inference moves the blue `current` ring, and
it exists because nothing else can: a codex session has no hook and rarely
calls `ccam stage` on its own, so without it the card sits at `idle` forever
calls `agenthub stage` on its own, so without it the card sits at `idle` forever
while the amber badge is the only thing that ever moves. It is gated on
`recordDetection` having *written*, so it inherits that function's
forward-only, declared-wins and TTL guards unchanged — a detection behind the
@@ -920,12 +920,12 @@ codex-only details:
**A codex lane's *evidence* is a separate matter.** Auto-advance moves the ring
but can never supply evidence, so every node a codex lane passes through stays
amber until the session itself runs `ccam stage <node> --evidence "..."`. CCAM
amber until the session itself runs `agenthub stage <node> --evidence "..."`. AgentHub
cannot inject an instruction into a CLI it merely spawns, so an `AGENTS.md` in
the working copy is the only lever — `ccam lanes agents install` writes the
the working copy is the only lever — `agenthub lanes agents install` writes the
marker-delimited block for you (see "Installing the QC/gate agents"). Note that
from inside a codex session `ccam stage` needs to know which lane it is: a pane
the dashboard started carries `CCAM_LANE_ID`, and anywhere else `--tool codex`
from inside a codex session `agenthub stage` needs to know which lane it is: a pane
the dashboard started carries `AGENTHUB_LANE_ID`, and anywhere else `--tool codex`
disambiguates (see "Which lane a CLI command addresses" below).
### The evidence boundary
@@ -942,14 +942,14 @@ proof would let a lane's pipeline map lie about what actually happened.
The node the agent *declared* itself on is never flagged as detected, so it
keeps its blue `current` ring — including when the declaration came in through
an alias (`ccam stage coding` resolves to the `implement` node, and `stages` is
an alias (`agenthub stage coding` resolves to the `implement` node, and `stages` is
keyed by the raw declared word, not the node id). Past nodes get the same
protection: `stageRecords` (`server/lib/pipelines.js`) resolves every recorded
key back onto its node, so a stage declared by alias keeps its record — and its
`--evidence` — instead of reading as an inference or losing its `done`. Where a
node has both a canonical record and an alias record, the canonical one wins.
The same rule governs `ccam lanes`: the inferred stage is printed only when it
The same rule governs `agenthub lanes`: the inferred stage is printed only when it
leads the declared one (see "Viewing lanes" below), and `LaneStripCard.tsx`'s
`detectionLeads` shows its badge under the identical condition — the terminal
and the browser never disagree about which one is the headline. Both locate the
@@ -1030,7 +1030,7 @@ same as every other detected stage.
Detection only ever attributes to a lane whose `cwd` matches the hook's
session `cwd` (see "Which lane a signal is credited to" above). A session
working directly in a source repo that was never itself adopted as a lane —
run `ccam lanes add --cwd $(pwd)` from that repo to fix that — gets no
run `agenthub lanes add --cwd $(pwd)` from that repo to fix that — gets no
detection at all, because no lane owns that `cwd`.
### Detection expires
@@ -1111,13 +1111,13 @@ actually runs:
| Node | Detect rules |
|---|---|
| `intake` | `Skill` matching `brainstorming`; `Bash` matching `ccam … feature activate`; `Agent` matching subagent `Explore` |
| `intake` | `Skill` matching `brainstorming`; `Bash` matching `agenthub … feature activate`; `Agent` matching subagent `Explore` |
| `plan` | `Skill` matching `writing-plans`; `Write` matching `docs/superpowers/specs/lane-.*\.md`; `Agent` matching `Plan\|Software Architect`; `TaskCreate` (any) |
| `implementing` | `Skill` matching `test-driven-development\|executing-plans\|subagent-driven-development\|systematic-debugging`; `Edit`/`Write` to any path NOT under `docs/superpowers/specs/` (resp. not under `docs/`) |
| `gates` | `Bash` matching `ccam … hook ci-gate` or `ccam … sync-base --check` |
| `e2e-feature` | `Bash` matching `ccam … up --qc` or `ccam … hook e2e` |
| `gates` | `Bash` matching `agenthub … hook ci-gate` or `agenthub … sync-base --check` |
| `e2e-feature` | `Bash` matching `agenthub … up --qc` or `agenthub … hook e2e` |
| `review` | `Skill` matching `code-review\|requesting-code-review\|receiving-code-review`; `Agent` matching a name containing `reviewer`/`Reviewer` (excluding `senior-gate-reviewer`, a lookbehind) |
| `qc` | `Agent` matching `qc-local\|Evidence Collector\|Accessibility Auditor`; `Bash` matching `ccam … lanes proof-link` |
| `qc` | `Agent` matching `qc-local\|Evidence Collector\|Accessibility Auditor`; `Bash` matching `agenthub … lanes proof-link` |
| `gate` | `Agent` matching `senior-gate-reviewer\|Reality Checker`; `Skill` matching `verification-before-completion` |
| `publishing` | `Skill` matching `finishing-a-development-branch`; `Bash` matching a `git push` **invocation** (the same pattern `default.json`'s `ship` uses) |
| `pr-open` | `Bash` matching `gh … pr create` |
@@ -1131,14 +1131,14 @@ below; the `code-review` skill invocation is the honest signal. And `merged` /
state — a test pins that.
Detection here is a **safety net, not the mechanism**: the `ship-feature-lane`
skill declares every one of these stages with `ccam stage` itself. What
skill declares every one of these stages with `agenthub stage` itself. What
detection adds is the stage an agent forgot after a context compaction, the
heartbeat that keeps a working lane from reading STALLED, and coverage for a
lane running Superpowers skills without the driving skill at all. It cannot
substitute for the skill's own declarations: because `recordDetection` is
forward-only AND never overrides a higher declared stage, a fix-loop re-entry
that drops back to `gates` is invisible to detection — only the skill's
`ccam stage gates` moves the lane back down.
`agenthub stage gates` moves the lane back down.
`done` in `default.json` deliberately has no rule — it is explicitly out of
scope for inference (see the task's "Out of scope" list): `done` is the one
@@ -1216,12 +1216,12 @@ Set the environment variable to change when a watcher is considered dead:
export LANE_DEAD_SEC=600 # 10 minutes instead of 5
```
## Housekeeping: ccam lanes gc
## Housekeeping: agenthub lanes gc
Runs machine-wide, not against one lane — safe to run any time, running lanes and live sessions are unaffected:
```bash
ccam lanes gc [--dry-run]
agenthub lanes gc [--dry-run]
```
Two things, both scoped to what's actually stale:
@@ -1229,7 +1229,7 @@ Two things, both scoped to what's actually stale:
- **Reaps orphaned Playwright MCP processes.** A `.playwright-mcp`-scoped process whose owning Claude Code session died gets reparented to pid 1 — a live session's MCP keeps its real parent and is left alone. Kills the whole process tree so a spawned browser doesn't leak too.
- **Caps oversized hook logs.** Any `LANES_ROOT/.state/lane<N>/logs/*.log` over 10MB is truncated in place to its last 2MB (same inode — a concurrent writer's file descriptor stays valid).
`--dry-run` prints what would happen without doing it. This is a deliberately narrower port of Shipyard's `lane-gc.sh` — it does NOT auto-remove stale worktrees by age (that would violate this repo's own never-automatic-destroy rule; use `ccam lanes reset|remove|purge` explicitly instead), archive feature state (CCAM's lane/feature state lives in the database, not flat files), or sweep scratch debris (CCAM doesn't generate the files Shipyard's own scripts did). See `docs/superpowers/specs/2026-08-05-lane-gc-design.md` for the full scoping rationale.
`--dry-run` prints what would happen without doing it. This is a deliberately narrower port of Shipyard's `lane-gc.sh` — it does NOT auto-remove stale worktrees by age (that would violate this repo's own never-automatic-destroy rule; use `agenthub lanes reset|remove|purge` explicitly instead), archive feature state (AgentHub's lane/feature state lives in the database, not flat files), or sweep scratch debris (AgentHub doesn't generate the files Shipyard's own scripts did). See `docs/superpowers/specs/2026-08-05-lane-gc-design.md` for the full scoping rationale.
## Lane actions
@@ -1237,7 +1237,7 @@ The web UI and CLI provide these actions on a lane:
### start
Start a tmux-backed run attached to the lane. The Workspace page's "Start Run" button calls `POST /api/run` with the lane's id; you can also attach from a real terminal using `ccam lanes shell`.
Start a tmux-backed run attached to the lane. The Workspace page's "Start Run" button calls `POST /api/run` with the lane's id; you can also attach from a real terminal using `agenthub lanes shell`.
```bash
curl -X POST http://localhost:4820/api/run \
@@ -1245,7 +1245,7 @@ curl -X POST http://localhost:4820/api/run \
-d '{"laneId": 5, "initialPrompt": "continue the work"}'
```
The `initialPrompt` field is optional — if omitted, the tmux session is created/attached with no initial input, and you type into the terminal directly. Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The same tmux session persists across attach/detach cycles, so you can switch between the browser Workspace and `ccam lanes shell` seamlessly.
The `initialPrompt` field is optional — if omitted, the tmux session is created/attached with no initial input, and you type into the terminal directly. Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The same tmux session persists across attach/detach cycles, so you can switch between the browser Workspace and `agenthub lanes shell` seamlessly.
**Permission mode in a lane run.** `permissionMode` (one of `acceptEdits` — the default — `default`, `plan`, `bypassPermissions`) sets the mode the pane *starts* in. The pane's `claude` also gets `--allow-dangerously-skip-permissions`, which puts **bypass permissions** into the pane's own `shift+tab` cycle **without enabling it** — matching what you get in a real terminal, where the cycle would otherwise be limited to plan / auto / manual / accept-edits and bypass would be unreachable from inside a dashboard-started run. Bypass is still only ever entered by an explicit `shift+tab` in the pane or by asking for `permissionMode: "bypassPermissions"` up front; nothing skips permission checks by default.
@@ -1308,7 +1308,7 @@ Proof files live inside the lane's own `cwd`, not under `LANES_ROOT`:
### Linking proof directories (the `ensure_proof_link` primitive)
An MCP server's `--output-dir` flag may point to different locations across runs. To converge all proof onto one canonical path regardless, CCAM provides the `ensure_proof_link` operation, exposed only as the explicit `ccam lanes proof-link` CLI command — it is **never automatic**. This matches the "CCAM does not orchestrate" rule: a session decides when to link, not the dashboard.
An MCP server's `--output-dir` flag may point to different locations across runs. To converge all proof onto one canonical path regardless, AgentHub provides the `ensure_proof_link` operation, exposed only as the explicit `agenthub lanes proof-link` CLI command — it is **never automatic**. This matches the "AgentHub does not orchestrate" rule: a session decides when to link, not the dashboard.
`ensure_proof_link` works as follows:
@@ -1335,22 +1335,22 @@ Cross-lane named locks serialize work that thrashes a shared machine — builds,
- **Atomicity via `mkdir`.** Lock ownership is declared via an `owner` marker file inside `LANES_ROOT/.locks/<lock-name>/`. `mkdir` atomically creates the lock directory or fails with `EEXIST` — no race between check and create.
- **Owner file format:** `<holder> <acquired-epoch-seconds>` — the holder identity (defaults to `lane<slot>` for the calling lane) and a Unix **seconds** timestamp. When a new acquire finds the directory already exists but the holder is stale (older than `LOCK_MAX_HOLD` seconds, default 2700s / 45 minutes), the old holder is considered dead, its directory is removed, and the acquire is retried once before answering.
- **The `LOCK_MAX_HOLD` floor.** The default is 2700 seconds, but it has a **hard floor of 300 seconds**. No caller can force-break a live holder by setting `LOCK_MAX_HOLD=1` — the floor prevents that mistake. The lowest possible break threshold is 300 seconds, even if `LOCK_MAX_HOLD` is overridden to something smaller.
- **Waiting and polling.** `ccam lock acquire` blocks the calling lane (polling the server every ~2 seconds via a single-shot `POST /:name/acquire` — the server itself never blocks or queues) until the lock is free or `--timeout` expires. While waiting, it prints a status line every ~60 seconds, so a long wait reads as "still waiting", not a hung command — this is terminal output for whoever is watching, not a dashboard liveness signal.
- **Waiting and polling.** `agenthub lock acquire` blocks the calling lane (polling the server every ~2 seconds via a single-shot `POST /:name/acquire` — the server itself never blocks or queues) until the lock is free or `--timeout` expires. While waiting, it prints a status line every ~60 seconds, so a long wait reads as "still waiting", not a hung command — this is terminal output for whoever is watching, not a dashboard liveness signal.
### The CLI
```bash
ccam lock status [<name>]
agenthub lock status [<name>]
```
Show one named lock's current holder, or every currently-held lock. `<name>` is optional — without it, lists all locks.
```bash
ccam lock acquire <name> [--holder X] [--timeout N]
agenthub lock acquire <name> [--holder X] [--timeout N]
```
Acquire a cross-lane named lock, polling until it is free or the timeout expires. `--holder` defaults to `lane<slot>` (the calling lane's identifier based on its slot). `--timeout` is in seconds; without it, the command waits indefinitely.
```bash
ccam lock release <name> [--holder X]
agenthub lock release <name> [--holder X]
```
Release a lock. Refused with status `409` if the `--holder` does not match the current owner. The `--holder` default is the same as `acquire`: `lane<slot>`.
@@ -1368,23 +1368,23 @@ The lane card shows a lock badge when the lane **holds** a named lock (its `lane
## The ship-feature-lane skill (E1)
The **ship-feature-lane** skill is the ported Shipyard driving skill for autonomous end-to-end feature pipelines in a single CCAM lane. It coordinates the entire development flow: implementation (TDD) → independent unit tests → pre-push CI gates + dev preflight → e2e testing → code review → local QC → senior gate authorization → PR push and publication → CI watch → final report.
The **ship-feature-lane** skill is the ported Shipyard driving skill for autonomous end-to-end feature pipelines in a single AgentHub lane. It coordinates the entire development flow: implementation (TDD) → independent unit tests → pre-push CI gates + dev preflight → e2e testing → code review → local QC → senior gate authorization → PR push and publication → CI watch → final report.
**Every stage whose job is to judge the work runs in a fresh-context subagent, never in the driving session.** The plan debate (Stage 0b), unit tests (Stage 1b), the e2e run and its triage (Stage 3), code review (Stage 4), browser QC (Stage 6), the senior gate (Stage 7), and a re-triage of CI that stayed red after a fix (Stage 10) are each dispatched with the Agent tool to a subagent that starts with an empty context and receives only artifacts it can verify itself — working directory, lane spec, diff range, commands, and the other agents' reports verbatim, never the driving session's paraphrase of why a finding was fine. The stages the driving session keeps are the ones that *do* rather than judge: intake, implementation, the mechanical gates, publishing, the CI/PR watch, and applying every fix the judges ask for. `subagent_type: fork` is explicitly forbidden for these: a fork inherits the implementing session's context, so it inherits the belief that the code is already correct. The driving session implements and applies fixes; it never grades its own work. Stages 1b/3 use the generic `general-purpose` type (nothing to install). Stage 4 uses the generic `Code Reviewer` type as its catch-all, plus — in parallel, routed by which of `server/`, `client/`, `mcp/` the diff touches — the plugin-qualified `ccam:backend-reviewer` / `ccam:frontend-reviewer` / `ccam:mcp-reviewer` domain reviewers (nothing to install; `ccam:` addressing works from any lane). Stages 6/7 use the lane-installed `qc-local`/`senior-gate-reviewer` agents.
**Every stage whose job is to judge the work runs in a fresh-context subagent, never in the driving session.** The plan debate (Stage 0b), unit tests (Stage 1b), the e2e run and its triage (Stage 3), code review (Stage 4), browser QC (Stage 6), the senior gate (Stage 7), and a re-triage of CI that stayed red after a fix (Stage 10) are each dispatched with the Agent tool to a subagent that starts with an empty context and receives only artifacts it can verify itself — working directory, lane spec, diff range, commands, and the other agents' reports verbatim, never the driving session's paraphrase of why a finding was fine. The stages the driving session keeps are the ones that *do* rather than judge: intake, implementation, the mechanical gates, publishing, the CI/PR watch, and applying every fix the judges ask for. `subagent_type: fork` is explicitly forbidden for these: a fork inherits the implementing session's context, so it inherits the belief that the code is already correct. The driving session implements and applies fixes; it never grades its own work. Stages 1b/3 use the generic `general-purpose` type (nothing to install). Stage 4 uses the generic `Code Reviewer` type as its catch-all, plus — in parallel, routed by which of `server/`, `client/`, `mcp/` the diff touches — the plugin-qualified `agenthub:backend-reviewer` / `agenthub:frontend-reviewer` / `agenthub:mcp-reviewer` domain reviewers (nothing to install; `agenthub:` addressing works from any lane). Stages 6/7 use the lane-installed `qc-local`/`senior-gate-reviewer` agents.
### Getting the skill (nothing to do, if you run the plugin)
The skill lives in `skills/ship-feature-lane/`, and the repo root **is** the `ccam` plugin root (`"source": "./"`), so `skills/` is the plugin's own skills directory. Anyone with the `ccam` plugin installed already has it in every session, from any lane's working directory, as **`/ccam:ship-feature-lane`** — with the bare `/ship-feature-lane` working as an alias unless another skill or command claims that name.
The skill lives in `skills/ship-feature-lane/`, and the repo root **is** the `agenthub` plugin root (`"source": "./"`), so `skills/` is the plugin's own skills directory. Anyone with the `agenthub` plugin installed already has it in every session, from any lane's working directory, as **`/agenthub:ship-feature-lane`** — with the bare `/ship-feature-lane` working as an alias unless another skill or command claims that name.
Only a plain checkout without the plugin needs the fallback:
```bash
ccam skills install
agenthub skills install
```
Copies `skills/ship-feature-lane/` into `~/.claude/skills/ship-feature-lane/` (overwrites on reinstall — run it again after pulling an update to the skill), which restores the bare `/ship-feature-lane` name. One-time per machine, same as `ccam lanes agents install` is one-time per lane.
Copies `skills/ship-feature-lane/` into `~/.claude/skills/ship-feature-lane/` (overwrites on reinstall — run it again after pulling an update to the skill), which restores the bare `/ship-feature-lane` name. One-time per machine, same as `agenthub lanes agents install` is one-time per lane.
**Agents come from the same plugin, under two names.** `qc-local` and `senior-gate-reviewer` live in `agents/` at the repo root, so the plugin exposes them as `ccam:qc-local` / `ccam:senior-gate-reviewer`. `ccam lanes agents install` copies those same files into the lane's own `.claude/agents/` — the highest-priority agent source — which is what makes the bare `subagent_type: qc-local` the skill prefers resolve inside the lane. Either name reaches the same definition; the skill falls back to the `ccam:` form when the lane copy is missing.
**Agents come from the same plugin, under two names.** `qc-local` and `senior-gate-reviewer` live in `agents/` at the repo root, so the plugin exposes them as `agenthub:qc-local` / `agenthub:senior-gate-reviewer`. `agenthub lanes agents install` copies those same files into the lane's own `.claude/agents/` — the highest-priority agent source — which is what makes the bare `subagent_type: qc-local` the skill prefers resolve inside the lane. Either name reaches the same definition; the skill falls back to the `agenthub:` form when the lane copy is missing.
### Invoking the skill
@@ -1393,7 +1393,7 @@ Copies `skills/ship-feature-lane/` into `~/.claude/skills/ship-feature-lane/` (o
/ship-feature-lane <requirement>
```
The skill frontloads all clarifying questions once, then runs unattended. The only interactive touchpoints are **Stage 0 (Q&A intake)** and **merging the PR on GitHub**; everything else runs to completion or escalates with a `blocked` status visible on the dashboard. Progress is declared via `ccam stage` commands at every step.
The skill frontloads all clarifying questions once, then runs unattended. The only interactive touchpoints are **Stage 0 (Q&A intake)** and **merging the PR on GitHub**; everything else runs to completion or escalates with a `blocked` status visible on the dashboard. Progress is declared via `agenthub stage` commands at every step.
### Current status: stages 0–8, 10–12, 14 work; stage 9 deferred, stage 13 partially wired (CI-wait works, dev-QC doesn't); stages 6–7 agent-gated
@@ -1406,9 +1406,9 @@ The skill frontloads all clarifying questions once, then runs unattended. The on
- **9 (Ticket)**: Ticket-filing integration not yet built — this stage is skipped entirely.
**Partially wired:**
- **13 (Post-merge verification)**: CI-wait half now runs for real via `ccam lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
- **13 (Post-merge verification)**: CI-wait half now runs for real via `agenthub lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
**Stages 6 and 7 are now unblocked** — run `ccam lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
**Stages 6 and 7 are now unblocked** — run `agenthub lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
See [the skill text](../skills/ship-feature-lane/SKILL.md) for the full pipeline definition.
@@ -1417,28 +1417,28 @@ See [the skill text](../skills/ship-feature-lane/SKILL.md) for the full pipeline
When the lane stack needs to run under QC conditions (deterministic, mocked externals), use the `--qc` flag:
```bash
ccam lanes up --qc
agenthub lanes up --qc
```
This flag activates the lane's `QC_BOOT_ENV` profile declaration. Define it in the profile's `profile.env` file as space-separated `KEY=value` pairs (the same format as the `PORTS` declaration):
```bash
# .ccam/profile/profile.env
# .agenthub/profile/profile.env
PORTS="app"
PORT_BASE_app=3000
QC_BOOT_ENV="MOCK_PAYMENTS=1 STUB_EMAIL=1"
```
These environment variables are passed to the profile's `boot` hook during `ccam lanes up --qc`, ensuring QC test runs are deterministic.
These environment variables are passed to the profile's `boot` hook during `agenthub lanes up --qc`, ensuring QC test runs are deterministic.
### Dev preflight and merge safety: sync-base
`ccam lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
`agenthub lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
```bash
ccam lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
ccam lanes sync-base feat/<slug> # merge origin/development into the feature branch
ccam lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
agenthub lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
agenthub lanes sync-base feat/<slug> # merge origin/development into the feature branch
agenthub lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
```
Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, then `--continue`), `5` migration-number collision (nothing merged — rename the printed file, re-run).
@@ -1446,7 +1446,7 @@ Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, th
Two profile declarations control it, both empty (off) by default:
```bash
# .ccam/profile/profile.env
# .agenthub/profile/profile.env
MIGRATIONS_DIR="db/migrations"
GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
```
@@ -1455,19 +1455,19 @@ GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
### Installing the QC/gate agents: agents install
Stage 6 (browser QC) and Stage 7 (senior GO/NO-GO gate) run as subagents — `qc-local` and `senior-gate-reviewer`. The plugin already ships them as `ccam:qc-local` / `ccam:senior-gate-reviewer`; installing them into the lane's own `.claude/agents/` is what makes the bare names work there:
Stage 6 (browser QC) and Stage 7 (senior GO/NO-GO gate) run as subagents — `qc-local` and `senior-gate-reviewer`. The plugin already ships them as `agenthub:qc-local` / `agenthub:senior-gate-reviewer`; installing them into the lane's own `.claude/agents/` is what makes the bare names work there:
```bash
ccam lanes agents install
agenthub lanes agents install
```
Writes both agent definitions (from the repo's own `agents/`, the same files the plugin ships) into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `ccam lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a CCAM upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
Writes both agent definitions (from the repo's own `agents/`, the same files the plugin ships) into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `agenthub lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a AgentHub upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
**On a `codex` lane the same command installs something else.** Codex never
reads `.claude/agents/`, so copying agent definitions there would be dead
files. Instead it writes the stage-declaration block into
`<lane>/AGENTS.md` — the project doc Codex *does* load from the working
directory — delimited by `<!-- ccam:stage:start -->` / `<!-- ccam:stage:end -->`
directory — delimited by `<!-- agenthub:stage:start -->` / `<!-- agenthub:stage:end -->`
so a reinstall replaces exactly that block and nothing else. The file is
appended to when it already exists (your own prose is preserved) and created
when it does not; `installed` comes back as `["AGENTS.md"]`. This is the lever
@@ -1480,10 +1480,10 @@ that gets a codex session to supply the *evidence* auto-advance cannot — see
### Syncing MCP servers: mcp sync
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `ccam lanes mcp sync` gives it those:
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `agenthub lanes mcp sync` gives it those:
```bash
ccam lanes mcp sync
agenthub lanes mcp sync
```
Reads the source repo's already-configured `mcpServers` from `~/.claude.json` (normal Claude Code project-scope config — set this up for the source repo once, the same way you would for any project), relocates any absolute path under the source repo to the lane's own directory, pins a `@playwright/mcp` server's `--output-dir` to the lane's `.playwright-mcp` (so proof screenshots land where the proof gallery reads them), and writes `<lane>/.mcp.json`. Also seeds the lane's Chromium browser profiles from the source repo's own — preserves saved logins, and never overwrites a profile that already exists at the destination.
@@ -1494,22 +1494,22 @@ A lane whose source repo has no `mcpServers` configured gets a clear `ENOMCPCONF
Restart the lane's Claude session after syncing — MCP config is read at session start.
### Checking integration toggles: ccam lanes integration
### Checking integration toggles: agenthub lanes integration
A profile can declare `.ccam/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
```bash
ccam lanes integration tracker # exit 0 = on, 1 = off
agenthub lanes integration tracker # exit 0 = on, 1 = off
```
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `ccam lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `agenthub lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.
### Post-merge CI status: ccam lanes ci status
### Post-merge CI status: agenthub lanes ci status
Check GitHub Actions status for a commit SHA or branch ref — used by Stage 13's CI-wait loop:
```bash
ccam lanes ci status <branch-or-sha> [<id>]
agenthub lanes ci status <branch-or-sha> [<id>]
```
Exit codes: `0` = all checks passed, `1` = at least one check failed, `3` = checks pending or in progress. The `<id>` parameter is optional and filters results to a specific lane if provided; if omitted, it's not needed.
@@ -1572,7 +1572,7 @@ is read one lane at a time, so it can be specific.
it, or `default.json` uses it (an agent moving between pipelines will type
what the other one taught it), or Shipyard's `PHASES` lists it. An alias with
no source is not "flexibility" — it is a synonym someone imagined, and the
`ccam stage` warning already catches a name that resolves to nothing, which is
`agenthub stage` warning already catches a name that resolves to nothing, which is
better feedback than silently absorbing every plausible spelling. Twelve
sourceless aliases were written and then cut for exactly this reason.
@@ -1590,29 +1590,29 @@ Two constraints on this:
`integrate`, `dev-gates`, `e2e-on-dev` and `push-dev` are deliberately **not**
nodes here. Shipyard had them and retired them on 2026-07-16 (see the comment
above `PHASES` in its `dashboard/src/lib/constants.js`); it now folds those
names into the surviving phases so old feature cards still render. CCAM never
names into the surviving phases so old feature cards still render. AgentHub never
shipped them, so there is nothing to fold.
These nodes also carry `detect` rules; see "Stage detection → Where the rules live".
## Orchestration: what CCAM does NOT do
## Orchestration: what AgentHub does NOT do
**CCAM does not chain, queue, retry, or evaluate gates.**
**AgentHub does not chain, queue, retry, or evaluate gates.**
The driving Claude Code session is in control. Here's what that means:
- **No automatic chaining:** The lane doesn't automatically move from one stage to the next. A skill declares the stage explicitly with `ccam stage`.
- **No automatic chaining:** The lane doesn't automatically move from one stage to the next. A skill declares the stage explicitly with `agenthub stage`.
- **No queue:** There is no work queue. When you run a session, it runs immediately; no waiting for dependencies or prior stages.
- **No retry logic:** If a stage fails (marked with `--result fail`), the lane stays at that stage. The next session must be told to reattempt it (the skill logic decides).
- **No gate evaluation:** A gate stage (marked `gate: true` in the template) doesn't block the UI, stop a session, or prevent a transition. It's just a visual marker. The skill must check the gate condition and decide whether to proceed.
**Example:** In a feature pipeline, if the `tests` stage is a gate, and tests fail:
1. A skill calls `ccam stage tests --result fail --evidence "5 tests failing"`
1. A skill calls `agenthub stage tests --result fail --evidence "5 tests failing"`
2. The UI shows `tests` in red (failed state)
3. The next session runs and a human/skill must explicitly decide: re-run tests, or skip to ship?
4. If re-running: `ccam stage tests --evidence "2 tests still failing"`
5. If skipping: `ccam stage ship --evidence "manual gate override"`
4. If re-running: `agenthub stage tests --evidence "2 tests still failing"`
5. If skipping: `agenthub stage ship --evidence "manual gate override"`
The **dashboard shows the history; the driving session makes the decisions.**
@@ -1629,11 +1629,11 @@ The lane card UI currently has no prompt/message input field. When you click `st
## Related commands
```bash
ccam lanes # List all lanes
ccam lanes add --cwd <path> --title <text> # Adopt an existing directory
ccam lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]
ccam lanes reset|remove|purge <id> [--force] --yes
ccam stage <stage> [--evidence "..."] # Report the current stage
agenthub lanes # List all lanes
agenthub lanes add --cwd <path> --title <text> # Adopt an existing directory
agenthub lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]
agenthub lanes reset|remove|purge <id> [--force] --yes
agenthub stage <stage> [--evidence "..."] # Report the current stage
```
## See also
@@ -1644,5 +1644,5 @@ ccam stage <stage> [--evidence "..."] # Report the current stage
- `server/lib/pipelines.js` — pipeline templates and node state derivation
- `server/lib/stage-detect.js` — the stage-detection matcher, field allowlist, and signal cap
- `server/routes/lanes.js` — REST API surface
- `bin/ccam.js` — CLI commands for lanes and stage reporting
- `bin/agenthub.js` — CLI commands for lanes and stage reporting
- `server/routes/hooks.js` — hook-to-lane binding by cwd, and where detection is invoked