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:
+29
-29
@@ -69,7 +69,7 @@ Authentication is **off by default** (the loopback bind is the trust boundary).
|
||||
|
||||
These paths stay exempt even when a token is configured: `/api/health`, `/api/openapi.json`, `/api/docs`, and `/api/hooks` (local Claude Code hook ingestion). Requests that fail the check get `401` with error code `EUNAUTHORIZED`.
|
||||
|
||||
`GET /api/settings/info` includes `server.version` (the running dashboard release). Pair with `ccam version` or the Settings About panel to confirm client and server builds match after deploy.
|
||||
`GET /api/settings/info` includes `server.version` (the running dashboard release). Pair with `agenthub version` or the Settings About panel to confirm client and server builds match after deploy.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -175,14 +175,14 @@ Partially updates a lane. This route is same-origin guarded because its patch
|
||||
can set `run_id`.
|
||||
|
||||
`pipeline` is patchable, which is how a lane moves between templates after
|
||||
creation (`ccam lanes pipeline <template-id>`). An id no template defines is
|
||||
creation (`agenthub lanes pipeline <template-id>`). An id no template defines is
|
||||
rejected with `400 EBADPIPELINE` rather than stored: `getPipeline` falls back
|
||||
to the default template when READING, so an unvalidated write would be
|
||||
accepted and then silently render the wrong map forever. Same 400 treatment as
|
||||
`EBADKIND`. Switching a template re-resolves the lane's existing declared
|
||||
`stage` string against the new node list — a stage the old template knew may
|
||||
resolve to nothing in the new one, leaving `progress: 0` and no `current` node
|
||||
until the next `ccam stage`.
|
||||
until the next `agenthub stage`.
|
||||
|
||||
#### Provision a managed worktree
|
||||
|
||||
@@ -364,7 +364,7 @@ anyone. See `docs/LANES.md#lane-runtime-running-a-lanes-own-stack`.
|
||||
"slot": 3,
|
||||
"kind": "managed",
|
||||
"hooks": ["bootstrap", "boot", "health"],
|
||||
"profileDir": "/work/myapp/.ccam/profile",
|
||||
"profileDir": "/work/myapp/.agenthub/profile",
|
||||
"services": [{ "name": "api", "pid": 40213, "alive": true }],
|
||||
"ports": { "api": { "port": 8103, "expected": 8003, "listening": true } },
|
||||
"database": { "name": "myapp_l3", "testName": "myapp_l3_test" },
|
||||
@@ -373,16 +373,16 @@ anyone. See `docs/LANES.md#lane-runtime-running-a-lanes-own-stack`.
|
||||
"up": true,
|
||||
"healthy": true,
|
||||
"logs": ["boot.log", "health.log", "api.log"],
|
||||
"logDir": "/home/you/.claude/ccam-lanes/.state/lane3/logs",
|
||||
"logDir": "/home/you/.claude/agenthub-lanes/.state/lane3/logs",
|
||||
"lastError": null
|
||||
}
|
||||
```
|
||||
|
||||
- `database` is `null` when the profile declares no `DB_PREFIX`; `redisIndex`
|
||||
is `null` when `REDIS` is not `1`. Both are names/indices only — never a
|
||||
connection string, so this endpoint never leaks the `~/.ccam/secrets.env`
|
||||
connection string, so this endpoint never leaks the `~/.agenthub/secrets.env`
|
||||
password even though it derives `DATABASE_URL` internally to run hooks.
|
||||
- A lane whose repository declares no `.ccam/profile` returns
|
||||
- A lane whose repository declares no `.agenthub/profile` returns
|
||||
`{"available": false, "searched": [...]}` with HTTP **200** — the same
|
||||
contract as `GET /:id/git`. Most lanes never run a stack; that is a normal
|
||||
state, not a fault.
|
||||
@@ -417,7 +417,7 @@ remain readable, and records `EUNHEALTHY` in the runtime's `lastError`.
|
||||
```
|
||||
|
||||
Both write only `slot` and `ports` on the lane row. Neither writes `stage`,
|
||||
`status`, or `notes` — in CCAM those describe the agent's work, not the stack's
|
||||
`status`, or `notes` — in AgentHub those describe the agent's work, not the stack's
|
||||
state. Adopted lanes may be brought up and down.
|
||||
|
||||
Errors: `400 ENOPROFILE` (with the paths searched), `409 ESLOTS` (every slot
|
||||
@@ -591,7 +591,7 @@ Delete proof at one of three granularities: request body `{ "group": "phase-name
|
||||
POST /api/lanes/:id/proof-link
|
||||
```
|
||||
|
||||
Converge the clone-root `proof/` directory (if it exists at `<lane.cwd>/proof`) onto the canonical `.playwright-mcp/proof`. This is an explicit operation, never automatic — exposed only as `ccam lanes proof-link`. Idempotent.
|
||||
Converge the clone-root `proof/` directory (if it exists at `<lane.cwd>/proof`) onto the canonical `.playwright-mcp/proof`. This is an explicit operation, never automatic — exposed only as `agenthub lanes proof-link`. Idempotent.
|
||||
|
||||
**Response:**
|
||||
|
||||
@@ -668,11 +668,11 @@ every number. `status` filters by session lifecycle, same convention as
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{ "name": "ccam:ship-feature", "plugin": "ccam", "count": 12, "sessions": 5 },
|
||||
{ "name": "agenthub:ship-feature", "plugin": "agenthub", "count": 12, "sessions": 5 },
|
||||
{ "name": "daily-report", "plugin": null, "count": 3, "sessions": 3 }
|
||||
],
|
||||
"plugins": [
|
||||
{ "name": "ccam", "skills": 1, "skillCalls": 12, "mcpCalls": 4 }
|
||||
{ "name": "agenthub", "skills": 1, "skillCalls": 12, "mcpCalls": 4 }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -694,7 +694,7 @@ lane's own internal serialization: a name held by one lane at a time, across
|
||||
ALL lanes, for a step that thrashes the shared machine (a build, an e2e run).
|
||||
Every route here is **single-shot** — it never blocks waiting for a lock to
|
||||
free up. A caller that wants to wait polls `POST /:name/acquire` itself (the
|
||||
`ccam lock acquire` CLI does exactly this, printing a status line every ~60s
|
||||
`agenthub lock acquire` CLI does exactly this, printing a status line every ~60s
|
||||
of continued waiting); the server never orchestrates that loop.
|
||||
|
||||
#### List locks
|
||||
@@ -721,7 +721,7 @@ POST /api/locks/:name/acquire
|
||||
{ "holder": "lane3" }
|
||||
```
|
||||
|
||||
`holder` is **required** — this route does not default it; `ccam lock
|
||||
`holder` is **required** — this route does not default it; `agenthub lock
|
||||
acquire` resolves `lane<slot>` for the calling lane and sends it explicitly.
|
||||
A holder older than `LOCK_MAX_HOLD` (default 2700s, floored at 300s — see
|
||||
`docs/LANES.md#cross-lane-named-locks`) is broken automatically before the
|
||||
@@ -1157,16 +1157,16 @@ Response `Content-Type: text/plain; version=0.0.4; charset=utf-8`.
|
||||
|
||||
| Metric | Type | Labels | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `ccam_up` | gauge | — | `1` when the API served the scrape |
|
||||
| `ccam_build_info` | gauge | `version` | Always `1`; dashboard version rides on the label |
|
||||
| `ccam_process_uptime_seconds` | gauge | — | Server process uptime |
|
||||
| `ccam_process_resident_memory_bytes` | gauge | — | Server process RSS |
|
||||
| `ccam_sessions` | gauge | `status` (`active`/`completed`/`error`/`abandoned`) | Sessions by status |
|
||||
| `ccam_agents` | gauge | `status` (`working`/`waiting`/`completed`/`error`) | Agents by status |
|
||||
| `ccam_events_total` | counter | — | Total events recorded |
|
||||
| `ccam_websocket_clients` | gauge | — | Connected realtime clients |
|
||||
| `ccam_remote_sources` | gauge | `enabled` (`true`/`false`) | Configured Remote Data Sources |
|
||||
| `ccam_tokens_total` | counter | `kind` (`input`/`output`/`cache_read`/`cache_write`) | Cumulative token usage |
|
||||
| `agenthub_up` | gauge | — | `1` when the API served the scrape |
|
||||
| `agenthub_build_info` | gauge | `version` | Always `1`; dashboard version rides on the label |
|
||||
| `agenthub_process_uptime_seconds` | gauge | — | Server process uptime |
|
||||
| `agenthub_process_resident_memory_bytes` | gauge | — | Server process RSS |
|
||||
| `agenthub_sessions` | gauge | `status` (`active`/`completed`/`error`/`abandoned`) | Sessions by status |
|
||||
| `agenthub_agents` | gauge | `status` (`working`/`waiting`/`completed`/`error`) | Agents by status |
|
||||
| `agenthub_events_total` | counter | — | Total events recorded |
|
||||
| `agenthub_websocket_clients` | gauge | — | Connected realtime clients |
|
||||
| `agenthub_remote_sources` | gauge | `enabled` (`true`/`false`) | Configured Remote Data Sources |
|
||||
| `agenthub_tokens_total` | counter | `kind` (`input`/`output`/`cache_read`/`cache_write`) | Cumulative token usage |
|
||||
|
||||
Status series are always emitted (even at `0`) so a series never disappears from the exposition. The endpoint is mounted under `/api`, so it sits behind the same two guards as every other route: the **Host-header (DNS-rebinding) guard** and the optional **`DASHBOARD_TOKEN`** guard. A scraper that reaches the server as anything other than loopback (e.g. Prometheus in Docker hitting `host.docker.internal`) must be allowlisted with `DASHBOARD_ALLOWED_HOSTS`, or the scrape returns `403 EBADHOST`; if a token is set, the scrape must also send it.
|
||||
|
||||
@@ -1174,7 +1174,7 @@ Example scrape config (start the server with `DASHBOARD_ALLOWED_HOSTS=host.docke
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: ccam
|
||||
- job_name: agenthub
|
||||
metrics_path: /api/metrics
|
||||
static_configs:
|
||||
- targets: ["host.docker.internal:4820"]
|
||||
@@ -1464,7 +1464,7 @@ POST /api/remote-sources
|
||||
| `identity_file` | string | No | Private-key path passed to ssh (`-i`) |
|
||||
| `remote_home` | string | No | Remote Claude home (defaults to remote `~/.claude`) |
|
||||
|
||||
> **Cursor (informational):** Sessions imported from `~/.claude` include **Cursor** agent usage on that machine too — Cursor happens to use the same paths as Claude Code. CCAM does not tag which app created a session.
|
||||
> **Cursor (informational):** Sessions imported from `~/.claude` include **Cursor** agent usage on that machine too — Cursor happens to use the same paths as Claude Code. AgentHub does not tag which app created a session.
|
||||
|
||||
| `enabled` | boolean | No | Whether the source is eligible for syncs (default `true`) |
|
||||
|
||||
@@ -1592,7 +1592,7 @@ POST /api/cc-config/marketplaces/action Body: { name, action: update|remove }
|
||||
|
||||
`scope` is `"user"`, `"project"`, or `"auto-memory"`. `type` is one of `skills`, `agents`, `commands`, `output-styles`, `memory`, `auto-memory`. `name` is required for everything except `memory` (which is `CLAUDE.md` itself). On `PUT`, `name` is validated against `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` (for `auto-memory` it must instead be a flat `*.md` filename). Settings are returned with secret-like keys (matching `/token|secret|password|api[_-]?key|auth/i`) replaced by `"<redacted>"`.
|
||||
|
||||
`GET /api/cc-config/plugins` returns each installed plugin's `contributes` block: the `skills` / `agents` / `commands` / `outputStyles` / `hooks` counts, plus `contributes.items` listing the first four surfaces **by name** (`[{ name, description, file }]`) so the UI can show *which* artifacts a plugin ships, not only how many. `name` is the file's frontmatter `name` when it declares one — the string Claude Code actually addresses it by — otherwise the filename. Each surface covers both the conventional directory (`skills/`, `agents/`, `commands/`, `output-styles/`) **and** any path `plugin.json` declares under that key: a plugin that lists its commands explicitly and keeps nothing in `commands/` (this repo's own `ccam` does) previously reported zero. A declared path that resolves outside the plugin's install directory is ignored — a third-party manifest must not make the dashboard read arbitrary files. The counts are always the matching list's length. `hooks` has no `items` list: a plugin's hooks are matcher/command pairs declared inline in `plugin.json`, not named artifacts.
|
||||
`GET /api/cc-config/plugins` returns each installed plugin's `contributes` block: the `skills` / `agents` / `commands` / `outputStyles` / `hooks` counts, plus `contributes.items` listing the first four surfaces **by name** (`[{ name, description, file }]`) so the UI can show *which* artifacts a plugin ships, not only how many. `name` is the file's frontmatter `name` when it declares one — the string Claude Code actually addresses it by — otherwise the filename. Each surface covers both the conventional directory (`skills/`, `agents/`, `commands/`, `output-styles/`) **and** any path `plugin.json` declares under that key: a plugin that lists its commands explicitly and keeps nothing in `commands/` (this repo's own `agenthub` does) previously reported zero. A declared path that resolves outside the plugin's install directory is ignored — a third-party manifest must not make the dashboard read arbitrary files. The counts are always the matching list's length. `hooks` has no `items` list: a plugin's hooks are matcher/command pairs declared inline in `plugin.json`, not named artifacts.
|
||||
|
||||
`GET /api/cc-config/memory` also surfaces the per-project file-based memory store — every `*.md` under `~/.claude/projects/<slug>/memory/` (the common pattern of a `MEMORY.md` index plus one file per remembered fact). Those items have `scope: "auto-memory"` and carry `project` (the `projects/<slug>` dir name), `name` (filename), `isIndex` (true for `MEMORY.md` / `INDEX-*.md`, which sort first), and parsed `frontmatter`. They are **editable**: `PUT`/`DELETE /api/cc-config/file` accept `{ scope: "auto-memory", type: "auto-memory", project, name, content? }` and create a timestamped backup under `<memory-dir>/.cc-config-backups/auto-memory/` before mutating (an invalid `project` slug returns `EBADPROJECT`). `GET /api/cc-config/backups` lists these with `scope: "auto-memory"` and `project` set. Bodies are also readable via `GET /api/cc-config/file` (they live under `CLAUDE_HOME`).
|
||||
|
||||
@@ -1623,11 +1623,11 @@ DELETE /api/run/:id Kill (SIGTERM → SIGKILL after 5 s)
|
||||
|
||||
**`GET /api/run/binary`:** Probes for the binary (`claude` or `codex`). Optional query parameter `?tool=codex` probes for Codex; omitted or `?tool=claude` probes for Claude. Returns `{ found: bool, path: string | null }`.
|
||||
|
||||
**`POST /api/run` (start/attach):** Requires `laneId` (the lane this run belongs to). Creates or attaches an existing tmux session named `ccam-lane-<id>` in the lane's working directory. If that session already exists and its pane is idling at a shell prompt, the appropriate command line (including `--resume` and any initial prompt) is typed into that pane instead of being dropped; if the pane is running a program, the request adopts the session unchanged. Optionally accepts `initialPrompt` to immediately type/send into the session (if empty or omitted, the session is created/attached with no initial input). Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The dashboard self-heals a lane's `run_id`/`status` on every read if the tmux session has been killed externally.
|
||||
**`POST /api/run` (start/attach):** Requires `laneId` (the lane this run belongs to). Creates or attaches an existing tmux session named `agenthub-lane-<id>` in the lane's working directory. If that session already exists and its pane is idling at a shell prompt, the appropriate command line (including `--resume` and any initial prompt) is typed into that pane instead of being dropped; if the pane is running a program, the request adopts the session unchanged. Optionally accepts `initialPrompt` to immediately type/send into the session (if empty or omitted, the session is created/attached with no initial input). Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The dashboard self-heals a lane's `run_id`/`status` on every read if the tmux session has been killed externally.
|
||||
|
||||
`permissionMode` (`acceptEdits` default, or `default` / `plan` / `bypassPermissions`) is the mode the pane starts in. The pane's argv also carries `--allow-dangerously-skip-permissions`, which only makes **bypass permissions** selectable in the pane's `shift+tab` cycle — it does not enable it.
|
||||
|
||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `ccam lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||
|
||||
Every history row carries `lane_id` (the lane whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=<n>` returns only that lane's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, the Dashboard's Analytics tab, and the Workflows page automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
||||
|
||||
@@ -1811,7 +1811,7 @@ Sent when a notification is created.
|
||||
|
||||
#### /ws-pty/:runId — PTY frames
|
||||
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on lane load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `ccam lanes shell`, other tools), all receiving the same frames live-synced. Resize events: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and re-asserts it whenever the terminal regains focus, because `window-size latest` lets a second attach client (another tab, `ccam lanes shell`) resize the tmux window out from under this one. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on lane load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub lanes shell`, other tools), all receiving the same frames live-synced. Resize events: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and re-asserts it whenever the terminal regains focus, because `window-size latest` lets a second attach client (another tab, `agenthub lanes shell`) resize the tmux window out from under this one. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
|
||||
#### cc_config_changed
|
||||
|
||||
|
||||
+103
-103
@@ -1,6 +1,6 @@
|
||||
# `ccam` CLI Reference
|
||||
# `agenthub` CLI Reference
|
||||
|
||||
The complete guide to `ccam`, the Claude Code Agent Monitor command-line interface — the full dashboard feature surface, in your terminal.
|
||||
The complete guide to `agenthub`, the command-line interface to AgentHub — the local control surface for Claude Code agents. Monitoring, lanes, terminal runs and Claude Code's own configuration, all from your terminal.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,15 +30,15 @@ The complete guide to `ccam`, the Claude Code Agent Monitor command-line interfa
|
||||
|
||||
## Overview
|
||||
|
||||
`ccam` (`bin/ccam.js`) is a **dependency-free** Node.js CLI over the local dashboard API. Everything the web app can do — monitoring, browsing, analytics, alerting, pricing, imports, administration — is available as a terminal command. It ships with the repository, requires no additional install step beyond the normal project setup, and talks only to your local dashboard server.
|
||||
`agenthub` (`bin/agenthub.js`) is a **dependency-free** Node.js CLI over the local dashboard API. Everything the web app can do — monitoring, browsing, analytics, alerting, pricing, imports, administration — is available as a terminal command. It ships with the repository, requires no additional install step beyond the normal project setup, and talks only to your local dashboard server.
|
||||
|
||||
```
|
||||
ccam <command> [options]
|
||||
agenthub <command> [options]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["Terminal\nccam <command>"] --> CLI["bin/ccam.js\n(zero dependencies)"]
|
||||
U["Terminal\nagenthub <command>"] --> CLI["bin/agenthub.js\n(zero dependencies)"]
|
||||
CLI -->|"env override"| ENV["CLAUDE_DASHBOARD_PORT /\nDASHBOARD_PORT"]
|
||||
CLI -->|"else discovery"| REG["~/.claude/.agent-dashboard.json\n(PID-liveness-checked)"]
|
||||
CLI -->|"else fallback"| DEF["http://127.0.0.1:4820"]
|
||||
@@ -50,24 +50,24 @@ flowchart LR
|
||||
|
||||
## Installation & Linking
|
||||
|
||||
`npm run setup` ends with a fail-soft `npm link` (the `link-cli` script), so after a normal local setup `ccam` is on your PATH from any directory:
|
||||
`npm run setup` ends with a fail-soft `npm link` (the `link-cli` script), so after a normal local setup `agenthub` is on your PATH from any directory:
|
||||
|
||||
```bash
|
||||
git clone https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git
|
||||
cd Claude-Code-Agent-Monitor
|
||||
npm run setup # installs deps AND links ccam globally
|
||||
ccam help
|
||||
npm run setup # installs deps AND links agenthub globally
|
||||
agenthub help
|
||||
```
|
||||
|
||||
If linking needed elevated permissions in your environment, setup still succeeds and prints a hint — run `npm link` once from the repo root yourself, or invoke the CLI directly with `node bin/ccam.js <command>`.
|
||||
If linking needed elevated permissions in your environment, setup still succeeds and prints a hint — run `npm link` once from the repo root yourself, or invoke the CLI directly with `node bin/agenthub.js <command>`.
|
||||
|
||||
### With a plugin install (no checkout)
|
||||
|
||||
The `ccam` plugin's bootstrap writes a launcher to `~/.local/bin/ccam` on
|
||||
The `agenthub` plugin's bootstrap writes a launcher to `~/.local/bin/agenthub` on
|
||||
session start — a small script rather than a symlink into the plugin cache,
|
||||
which is replaced on every plugin update. It never overwrites a `ccam` it did
|
||||
which is replaced on every plugin update. It never overwrites a `agenthub` it did
|
||||
not write, so a linked checkout keeps winning. If `~/.local/bin` is not on your
|
||||
PATH, `/ccam-doctor` says so and prints the exact `export PATH=...` line to add.
|
||||
PATH, `/agenthub-doctor` says so and prints the exact `export PATH=...` line to add.
|
||||
|
||||
## Server Discovery
|
||||
|
||||
@@ -90,39 +90,39 @@ The CLI talks to the local dashboard server — **API-backed commands require it
|
||||
```
|
||||
○ Dashboard server is NOT running (tried http://127.0.0.1:4820)
|
||||
This command needs the server. Start it with one of:
|
||||
ccam start # production server in the background
|
||||
agenthub start # production server in the background
|
||||
npm run dev # dev mode (hot reload), foreground
|
||||
npm start # production mode, foreground
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam status` | At-a-glance up/down indicator (`●` running / `○` not running); exits `1` when down |
|
||||
| `ccam start [--port N]` | Start the production server **in the background** (detached; survives closing the terminal), wait up to 30 s for `/api/health`, print the URL + PID and the `kill <pid>` stop command. Logs append to `data/ccam-server.log`. No-ops with a pointer when a server is already up. Requires a built client (`npm run build` once) |
|
||||
| `ccam repl` (aliases `shell`, `i`) | Open the **interactive shell** — see [Interactive REPL](#interactive-repl) |
|
||||
| `agenthub status` | At-a-glance up/down indicator (`●` running / `○` not running); exits `1` when down |
|
||||
| `agenthub start [--port N]` | Start the production server **in the background** (detached; survives closing the terminal), wait up to 30 s for `/api/health`, print the URL + PID and the `kill <pid>` stop command. Logs append to `data/agenthub-server.log`. No-ops with a pointer when a server is already up. Requires a built client (`npm run build` once) |
|
||||
| `agenthub repl` (aliases `shell`, `i`) | Open the **interactive shell** — see [Interactive REPL](#interactive-repl) |
|
||||
|
||||
### Interactive REPL
|
||||
|
||||
`ccam repl` (also `ccam shell` / `ccam i`) opens a persistent prompt where you type commands **without the `ccam` prefix** — ideal for a monitoring session where you run `sessions`, drill into a `session <id>`, check `kanban`, then `cost`, without re-typing `ccam` each time. On entry it prints a **CCAM word-mark welcome banner** with the version and live server status.
|
||||
`agenthub repl` (also `agenthub shell` / `agenthub i`) opens a persistent prompt where you type commands **without the `agenthub` prefix** — ideal for a monitoring session where you run `sessions`, drill into a `session <id>`, check `kanban`, then `cost`, without re-typing `agenthub` each time. On entry it prints a **AgentHub word-mark welcome banner** with the version and live server status.
|
||||
|
||||
```
|
||||
_____ _____ _____ _____
|
||||
/\ \ /\ \ /\ \ /\ \
|
||||
/::\ \ /::\ \ /::\ \ /::\____\
|
||||
… (CCAM word-mark) …
|
||||
Claude Code Agent Monitor · interactive shell · v1.3.0 ● 127.0.0.1:4820
|
||||
Type commands without the 'ccam' prefix — e.g. sessions --limit 5
|
||||
… (AgentHub word-mark) …
|
||||
AgentHub · interactive shell · v1.4.6 ● 127.0.0.1:4820
|
||||
Type commands without the 'agenthub' prefix — e.g. sessions --limit 5
|
||||
help all commands · help <cmd> details · Tab completes · ↑/↓ history · exit to quit
|
||||
|
||||
● ccam 127.0.0.1:4820 › sessions --limit 3
|
||||
● agenthub 127.0.0.1:4820 › sessions --limit 3
|
||||
… table …
|
||||
○ ccam offline › stats # prompt dot turns red when the server is down
|
||||
○ agenthub offline › stats # prompt dot turns red when the server is down
|
||||
```
|
||||
|
||||
- **Live status prompt** — a green `●` + resolved host when the server is up, a red `○` + `offline` when it isn't (probed with a short, cached health check).
|
||||
- **Tab completion** for commands, subcommands (`alerts ack`, `pricing set`, …), and flags (`--limit`, `--status`, …).
|
||||
- **Arrow-key history**, persisted across sessions to `data/.ccam_repl_history`.
|
||||
- **Full command surface** — every command in this reference works inside the shell exactly as on the one-shot CLI (they are dispatched as child `ccam` processes).
|
||||
- **Arrow-key history**, persisted across sessions to `data/.agenthub_repl_history`.
|
||||
- **Full command surface** — every command in this reference works inside the shell exactly as on the one-shot CLI (they are dispatched as child `agenthub` processes).
|
||||
- **Shell built-ins:**
|
||||
|
||||
| Built-in | Description |
|
||||
@@ -136,8 +136,8 @@ The CLI talks to the local dashboard server — **API-backed commands require it
|
||||
| `clear` / `cls` | Clear the screen |
|
||||
| `exit` / `quit` / `q` | Leave the shell (also `Ctrl+D`) |
|
||||
|
||||
- **Robust isolation** — each entered line runs as a short-lived child `ccam` process, so a non-zero exit, an offline refusal, or a blocking `tail` / `watch` (both stop on `Ctrl+C`) can **never** take the shell down with it. Offline reads and server-only refusals behave exactly as they do on the one-shot CLI.
|
||||
- Works with piped input too (`printf 'stats\nexit\n' | ccam repl`) for scripting, running each line in order and exiting at EOF.
|
||||
- **Robust isolation** — each entered line runs as a short-lived child `agenthub` process, so a non-zero exit, an offline refusal, or a blocking `tail` / `watch` (both stop on `Ctrl+C`) can **never** take the shell down with it. Offline reads and server-only refusals behave exactly as they do on the one-shot CLI.
|
||||
- Works with piped input too (`printf 'stats\nexit\n' | agenthub repl`) for scripting, running each line in order and exiting at EOF.
|
||||
|
||||
### Offline Mode
|
||||
|
||||
@@ -145,7 +145,7 @@ When the server is down, **read-only commands automatically fall back to reading
|
||||
|
||||
```
|
||||
⚠ Offline mode — server not running; reading data/dashboard.db directly.
|
||||
Data is as of the last capture — live capture and full features need the server: ccam start
|
||||
Data is as of the last capture — live capture and full features need the server: agenthub start
|
||||
```
|
||||
|
||||
| Works offline | Server required (with the printed reason) |
|
||||
@@ -160,58 +160,58 @@ When the server is down, **read-only commands automatically fall back to reading
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam health` | One-line reachability check with the resolved URL and server timestamp |
|
||||
| `ccam stats` | Totals (sessions, agents, events), today's event count, WS connections, and the sessions-by-status distribution |
|
||||
| `ccam kanban` | The Kanban board as text: sessions grouped into Active / Waiting / Completed / Error / Abandoned and agents into Working / Waiting / Completed / Error, with current tools |
|
||||
| `ccam tail [--session <id>]` | Live event feed — polls `/api/events` every 2 s and prints only new rows (the Activity Feed without a WebSocket client). `Ctrl+C` stops |
|
||||
| `agenthub health` | One-line reachability check with the resolved URL and server timestamp |
|
||||
| `agenthub stats` | Totals (sessions, agents, events), today's event count, WS connections, and the sessions-by-status distribution |
|
||||
| `agenthub kanban` | The Kanban board as text: sessions grouped into Active / Waiting / Completed / Error / Abandoned and agents into Working / Waiting / Completed / Error, with current tools |
|
||||
| `agenthub tail [--session <id>]` | Live event feed — polls `/api/events` every 2 s and prints only new rows (the Activity Feed without a WebSocket client). `Ctrl+C` stops |
|
||||
|
||||
### Data Browsing
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam sessions [--status s] [--q text] [--limit n]` | Server-filtered session table: short ID, status, name, agent count, duration, model, relative last-update |
|
||||
| `ccam session <id>` | Deep dive: metadata card, per-session cost, a parent→child **agent tree** (`├─`/`└─`) with live tools, and the most recent events |
|
||||
| `ccam agents [--status s] [--session id] [--limit n]` | Agent table with type, current tool, and duration |
|
||||
| `ccam events [--session id] [--limit n]` | Newest-first event log with type, tool, and summary |
|
||||
| `agenthub sessions [--status s] [--q text] [--limit n]` | Server-filtered session table: short ID, status, name, agent count, duration, model, relative last-update |
|
||||
| `agenthub session <id>` | Deep dive: metadata card, per-session cost, a parent→child **agent tree** (`├─`/`└─`) with live tools, and the most recent events |
|
||||
| `agenthub agents [--status s] [--session id] [--limit n]` | Agent table with type, current tool, and duration |
|
||||
| `agenthub events [--session id] [--limit n]` | Newest-first event log with type, tool, and summary |
|
||||
|
||||
### Insights
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam analytics` | Token totals (input / output / cache read / cache write), top tools by call count, agent-type distribution, average events per session |
|
||||
| `ccam workflows [--session id]` | Workflow-intelligence stats (sessions analyzed, subagents, success rate, depth, compactions) and the top detected patterns; `--session` drills into one session |
|
||||
| `ccam runs [--session id]` | Dynamic Workflow-tool runs: status, agent count, tokens, tool calls, duration |
|
||||
| `ccam cost [--session <id>]` | Total estimated cost with a per-model bar-chart breakdown; `--session` scopes it to one session (mirrors `/api/pricing/cost/:sessionId`). Any billed **server-tool surcharges** (web search $/1k, code-execution container-time) are shown on a surcharges line. Models with usage but **no matching pricing rule** (priced at $0 and excluded from the total) are listed in a warning with their token volume and the `ccam pricing set` invocation that fixes it |
|
||||
| `agenthub analytics` | Token totals (input / output / cache read / cache write), top tools by call count, agent-type distribution, average events per session |
|
||||
| `agenthub workflows [--session id]` | Workflow-intelligence stats (sessions analyzed, subagents, success rate, depth, compactions) and the top detected patterns; `--session` drills into one session |
|
||||
| `agenthub runs [--session id]` | Dynamic Workflow-tool runs: status, agent count, tokens, tool calls, duration |
|
||||
| `agenthub cost [--session <id>]` | Total estimated cost with a per-model bar-chart breakdown; `--session` scopes it to one session (mirrors `/api/pricing/cost/:sessionId`). Any billed **server-tool surcharges** (web search $/1k, code-execution container-time) are shown on a surcharges line. Models with usage but **no matching pricing rule** (priced at $0 and excluded from the total) are listed in a warning with their token volume and the `agenthub pricing set` invocation that fixes it |
|
||||
|
||||
### Alerts & Webhooks
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam alerts [--unacked] [--limit n]` | Fired-alert feed with state, trigger time, rule, and message |
|
||||
| `ccam alerts ack <id>` | Acknowledge one alert |
|
||||
| `ccam alerts ack-all` | Acknowledge every unacknowledged alert |
|
||||
| `ccam rules` | Alert rules with enabled state, type, and cooldown |
|
||||
| `ccam webhooks` | Webhook targets (URLs masked server-side, secrets never returned) |
|
||||
| `ccam webhooks test <id>` | Fire a synthetic test alert at a target and report the delivery result; exits non-zero on failure |
|
||||
| `agenthub alerts [--unacked] [--limit n]` | Fired-alert feed with state, trigger time, rule, and message |
|
||||
| `agenthub alerts ack <id>` | Acknowledge one alert |
|
||||
| `agenthub alerts ack-all` | Acknowledge every unacknowledged alert |
|
||||
| `agenthub rules` | Alert rules with enabled state, type, and cooldown |
|
||||
| `agenthub webhooks` | Webhook targets (URLs masked server-side, secrets never returned) |
|
||||
| `agenthub webhooks test <id>` | Fire a synthetic test alert at a target and report the delivery result; exits non-zero on failure |
|
||||
|
||||
### Pricing
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam pricing` | All model pricing rules with per-mtok rates, including **Fast In/Out** and **Intro In/Out** columns for fast-mode premiums and time-limited promo pricing |
|
||||
| `ccam pricing set <pattern> --input N --output N [--cache-read N] [--cache-write N] [--cache-write-1h N] [--name label]` | Create or update a rule (SQL `LIKE` pattern, e.g. `claude-opus-4-6%`) |
|
||||
| `ccam pricing set <pattern> … [--fast-input N] [--fast-output N]` | Also set **fast-mode** premium rates on the rule |
|
||||
| `ccam pricing set <pattern> … [--intro-input N] [--intro-output N] [--intro-cache-read N] [--intro-cache-write N] [--intro-cache-write-1h N] --intro-until YYYY-MM-DD` | Set a **time-limited introductory (promo) rate block**. The intro fields are only sent when an `--intro-*` flag is present, so a plain rate edit never clobbers an existing promo; a bare `--intro-until` (no date) clears it |
|
||||
| `ccam pricing delete <pattern>` | Delete a rule |
|
||||
| `ccam pricing reset` | Restore the default rate table |
|
||||
| `agenthub pricing` | All model pricing rules with per-mtok rates, including **Fast In/Out** and **Intro In/Out** columns for fast-mode premiums and time-limited promo pricing |
|
||||
| `agenthub pricing set <pattern> --input N --output N [--cache-read N] [--cache-write N] [--cache-write-1h N] [--name label]` | Create or update a rule (SQL `LIKE` pattern, e.g. `claude-opus-4-6%`) |
|
||||
| `agenthub pricing set <pattern> … [--fast-input N] [--fast-output N]` | Also set **fast-mode** premium rates on the rule |
|
||||
| `agenthub pricing set <pattern> … [--intro-input N] [--intro-output N] [--intro-cache-read N] [--intro-cache-write N] [--intro-cache-write-1h N] --intro-until YYYY-MM-DD` | Set a **time-limited introductory (promo) rate block**. The intro fields are only sent when an `--intro-*` flag is present, so a plain rate edit never clobbers an existing promo; a bare `--intro-until` (no date) clears it |
|
||||
| `agenthub pricing delete <pattern>` | Delete a rule |
|
||||
| `agenthub pricing reset` | Restore the default rate table |
|
||||
|
||||
### Import
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam import rescan` | Re-scan the default `~/.claude/projects` tree (idempotent; prints imported / backfilled / skipped / errors) |
|
||||
| `ccam import path <dir>` | Recursively import every `.jsonl` under an absolute directory (`~` is expanded server-side) |
|
||||
| `ccam import-data <file.json>` | Restore a full dashboard export produced by `ccam export` (or **Settings → Export data**). Idempotent and non-destructive — sessions already present are skipped whole, so it safely **consolidates several machines** into one dashboard. The file path is resolved to absolute and read server-side |
|
||||
| `agenthub import rescan` | Re-scan the default `~/.claude/projects` tree (idempotent; prints imported / backfilled / skipped / errors) |
|
||||
| `agenthub import path <dir>` | Recursively import every `.jsonl` under an absolute directory (`~` is expanded server-side) |
|
||||
| `agenthub import-data <file.json>` | Restore a full dashboard export produced by `agenthub export` (or **Settings → Export data**). Idempotent and non-destructive — sessions already present are skipped whole, so it safely **consolidates several machines** into one dashboard. The file path is resolved to absolute and read server-side |
|
||||
|
||||
### Remote Sources
|
||||
|
||||
@@ -219,11 +219,11 @@ Manage the remote (SSH) machines this dashboard pulls Claude Code history from
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam remote-sources` (alias `remotes`) | List configured sources with id, auto-sync on/off, status, label, host, **session count**, and last-sync time, followed by a totals line (sources / auto-syncing / sessions collected) |
|
||||
| `ccam remote-sources add --label <name> --host <user@host> [--port N] [--identity <path>] [--remote-home <path>] [--disabled]` | Add a source. `--host` is an ssh destination (`user@host`) or a `~/.ssh/config` alias; `--disabled` skips it in the background poller |
|
||||
| `ccam remote-sources test <id>` | Probe SSH connectivity and check the remote `~/.claude/projects` exists; exits non-zero on failure |
|
||||
| `ccam remote-sources sync [id]` | Pull history now — one source by id, or **all** sources when the id is omitted. Prints imported / tagged counts |
|
||||
| `ccam remote-sources rm <id> [--purge]` | Remove a source (its imported sessions are detached back to `local` by default; `--purge` also **deletes** them) |
|
||||
| `agenthub remote-sources` (alias `remotes`) | List configured sources with id, auto-sync on/off, status, label, host, **session count**, and last-sync time, followed by a totals line (sources / auto-syncing / sessions collected) |
|
||||
| `agenthub remote-sources add --label <name> --host <user@host> [--port N] [--identity <path>] [--remote-home <path>] [--disabled]` | Add a source. `--host` is an ssh destination (`user@host`) or a `~/.ssh/config` alias; `--disabled` skips it in the background poller |
|
||||
| `agenthub remote-sources test <id>` | Probe SSH connectivity and check the remote `~/.claude/projects` exists; exits non-zero on failure |
|
||||
| `agenthub remote-sources sync [id]` | Pull history now — one source by id, or **all** sources when the id is omitted. Prints imported / tagged counts |
|
||||
| `agenthub remote-sources rm <id> [--purge]` | Remove a source (its imported sessions are detached back to `local` by default; `--purge` also **deletes** them) |
|
||||
|
||||
### Lanes
|
||||
|
||||
@@ -231,57 +231,57 @@ A lane is a durable unit of parallel agent work — one working directory, many
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam lanes` | List lanes with stage, status, liveness and progress |
|
||||
| `ccam lanes add --cwd <path> --title <text>` | Adopt an existing directory as a lane |
|
||||
| `ccam lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]` | Provision a dashboard-managed git worktree as a new lane |
|
||||
| `ccam lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.ccam/profile/`. Refuses to overwrite an existing one without `--force` |
|
||||
| `ccam lanes pipeline [<template-id>] [<id>]` | Show which pipeline template a lane renders against, plus the available templates and their node counts; pass a template id to switch. An unknown id is refused (`400 EBADPIPELINE`), and a switch that leaves the lane's current stage unresolvable warns |
|
||||
| `ccam lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
|
||||
| `ccam lanes reset\|remove\|purge <id> [--force] [--keep-db] --yes` | Show preflight facts, then perform a destructive action. Refuses without `--yes`; `--force` is required when commits are unpushed; `--keep-db` (`reset` only) skips dropping/recreating a data-isolated lane's database |
|
||||
| `ccam ship-full "<requirement>" [<id>] [--print]` | **The 16-stage lane pipeline.** Puts the lane on the `ship-feature` template and starts a `claude` session running its skill (`/ccam:ship-feature-lane`): implement → gates → e2e → review → local QC → senior GO/NO-GO → push branch + open PR, then watches the PR until a human merges it. Use when the work should end in a reviewable PR |
|
||||
| `ccam ship-quick "<requirement>" [<id>] [--print]` | **The 8-stage in-repo pipeline.** Puts the lane on the `default` template and runs its skill (`/ccam:ship-feature`): plan → implement → tests → review → gate → ship. No PR, no browser QC. Use for work that stays in the working copy |
|
||||
| `ccam stage <stage> [--tool claude\|codex] [--evidence <text>] [--note <text>] [--result pass\|fail]` | Declare the lane's current pipeline stage. Called by a skill at each phase boundary. `<stage>` may be a node id or one of its template's aliases; a name matching neither is still recorded but warns on stderr (exit stays `0`) that the pipeline map won't show it. `--tool` picks between a `claude` and a `codex` lane sharing one directory |
|
||||
| `ccam feature list [<id>]` | List every feature this lane has activated, archived or live |
|
||||
| `ccam feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
|
||||
| `ccam feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
|
||||
| `agenthub lanes` | List lanes with stage, status, liveness and progress |
|
||||
| `agenthub lanes add --cwd <path> --title <text>` | Adopt an existing directory as a lane |
|
||||
| `agenthub lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]` | Provision a dashboard-managed git worktree as a new lane |
|
||||
| `agenthub lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.agenthub/profile/`. Refuses to overwrite an existing one without `--force` |
|
||||
| `agenthub lanes pipeline [<template-id>] [<id>]` | Show which pipeline template a lane renders against, plus the available templates and their node counts; pass a template id to switch. An unknown id is refused (`400 EBADPIPELINE`), and a switch that leaves the lane's current stage unresolvable warns |
|
||||
| `agenthub lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
|
||||
| `agenthub lanes reset\|remove\|purge <id> [--force] [--keep-db] --yes` | Show preflight facts, then perform a destructive action. Refuses without `--yes`; `--force` is required when commits are unpushed; `--keep-db` (`reset` only) skips dropping/recreating a data-isolated lane's database |
|
||||
| `agenthub ship-full "<requirement>" [<id>] [--print]` | **The 16-stage lane pipeline.** Puts the lane on the `ship-feature` template and starts a `claude` session running its skill (`/agenthub:ship-feature-lane`): implement → gates → e2e → review → local QC → senior GO/NO-GO → push branch + open PR, then watches the PR until a human merges it. Use when the work should end in a reviewable PR |
|
||||
| `agenthub ship-quick "<requirement>" [<id>] [--print]` | **The 8-stage in-repo pipeline.** Puts the lane on the `default` template and runs its skill (`/agenthub:ship-feature`): plan → implement → tests → review → gate → ship. No PR, no browser QC. Use for work that stays in the working copy |
|
||||
| `agenthub stage <stage> [--tool claude\|codex] [--evidence <text>] [--note <text>] [--result pass\|fail]` | Declare the lane's current pipeline stage. Called by a skill at each phase boundary. `<stage>` may be a node id or one of its template's aliases; a name matching neither is still recorded but warns on stderr (exit stays `0`) that the pipeline map won't show it. `--tool` picks between a `claude` and a `codex` lane sharing one directory |
|
||||
| `agenthub feature list [<id>]` | List every feature this lane has activated, archived or live |
|
||||
| `agenthub feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
|
||||
| `agenthub feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
|
||||
|
||||
`ccam ship-full` / `ccam ship-quick` both resolve the lane from the working directory (or take a lane id), pass the requirement to `claude` as one argv element — never through a shell — and accept `--print`, which shows the exact command, switches no template and runs nothing. The skill string itself lives in the template's `skill` field; a template without one makes the command refuse rather than guess.
|
||||
`agenthub ship-full` / `agenthub ship-quick` both resolve the lane from the working directory (or take a lane id), pass the requirement to `claude` as one argv element — never through a shell — and accept `--print`, which shows the exact command, switches no template and runs nothing. The skill string itself lives in the template's `skill` field; a template without one makes the command refuse rather than guess.
|
||||
|
||||
|
||||
**Runtime** — the lane's own application stack, as opposed to `start`/`stop`, which drive its Claude run. Two lifecycles, one lane id. Each requires the repository to declare a profile at `<repo>/.ccam/profile/` ([contract](LANES.md#lane-runtime-running-a-lanes-own-stack)); without one they report that nothing is configured rather than failing.
|
||||
**Runtime** — the lane's own application stack, as opposed to `start`/`stop`, which drive its Claude run. Two lifecycles, one lane id. Each requires the repository to declare a profile at `<repo>/.agenthub/profile/` ([contract](LANES.md#lane-runtime-running-a-lanes-own-stack)); without one they report that nothing is configured rather than failing.
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam lanes up [<id>] [--no-build]` | Boot the stack through the profile's `boot` + `health` hooks, then poll until it is healthy or the boot fails. `--no-build` reuses an existing build |
|
||||
| `ccam lanes down [<id>]` | Stop the stack. Idempotent, and a no-op for a lane that was never up |
|
||||
| `ccam lanes runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the lane's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
||||
| `ccam lanes logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
||||
| `ccam lanes hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
||||
| `ccam lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
| `ccam lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||
| `ccam lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
||||
| `ccam lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||
| `agenthub lanes up [<id>] [--no-build]` | Boot the stack through the profile's `boot` + `health` hooks, then poll until it is healthy or the boot fails. `--no-build` reuses an existing build |
|
||||
| `agenthub lanes down [<id>]` | Stop the stack. Idempotent, and a no-op for a lane that was never up |
|
||||
| `agenthub lanes runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the lane's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
||||
| `agenthub lanes logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
||||
| `agenthub lanes hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
||||
| `agenthub lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
| `agenthub lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||
| `agenthub lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
||||
| `agenthub lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||
|
||||
Omit `<id>` and the command addresses the lane owning the current directory, so a session running inside a lane never needs to know its own id. Only a leading all-digits argument is read as an id — `ccam lanes logs web --tail 4096` addresses the lane by directory, not lane 4096.
|
||||
Omit `<id>` and the command addresses the lane owning the current directory, so a session running inside a lane never needs to know its own id. Only a leading all-digits argument is read as an id — `agenthub lanes logs web --tail 4096` addresses the lane by directory, not lane 4096.
|
||||
|
||||
Resolution order, shared by every cwd-resolving command: `--lane`/leading id → `CCAM_LANE_ID` (exported into every tmux pane the dashboard starts) → `--tool claude|codex`, else the harness's marker (`CLAUDECODE` means claude; Codex has no equivalent marker, so a codex session outside a dashboard pane passes `--tool codex`) → the lane owning the directory. One directory can back a `claude` lane and a `codex` lane at once, so when two candidates remain the command fails and names both instead of picking one — see [Which lane a CLI command addresses](LANES.md#which-lane-a-cli-command-addresses).
|
||||
Resolution order, shared by every cwd-resolving command: `--lane`/leading id → `AGENTHUB_LANE_ID` (exported into every tmux pane the dashboard starts) → `--tool claude|codex`, else the harness's marker (`CLAUDECODE` means claude; Codex has no equivalent marker, so a codex session outside a dashboard pane passes `--tool codex`) → the lane owning the directory. One directory can back a `claude` lane and a `codex` lane at once, so when two candidates remain the command fails and names both instead of picking one — see [Which lane a CLI command addresses](LANES.md#which-lane-a-cli-command-addresses).
|
||||
|
||||
Because a lane's services are fully detached, restarting the dashboard (or `ccam update`) never stops a running stack.
|
||||
Because a lane's services are fully detached, restarting the dashboard (or `agenthub update`) never stops a running stack.
|
||||
|
||||
### Administration
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `ccam doctor` | Diagnosis: API reachability, hook installation status + path, database path/size/row counts, server uptime and Node version, WS connections |
|
||||
| `ccam info` | The raw `/api/settings/info` JSON (pipe it to `jq`) |
|
||||
| `ccam export [file.json]` | Full JSON data export (sessions, agents, events, tokens, workflows, dashboard runs, alert rules, pricing) — defaults to a dated filename. Re-importable via `ccam import-data` |
|
||||
| `ccam cleanup --hours N --days M` | Abandon active sessions idle for `N` hours and/or purge completed sessions older than `M` days |
|
||||
| `ccam reinstall-hooks` | Rewrite the Claude Code hook entries in `~/.claude/settings.json` |
|
||||
| `ccam update-check` | Ask the server whether the dashboard checkout is behind the canonical remote (branch- and fork-aware). Prints the behind-by count, a situation note for fork/feature-branch checkouts, and the **copy-paste update command**. When the checkout is fast-forwardable, the dashboard UI can also self-apply (`POST /api/updates/apply`: pull, rebuild, self-restart) instead of running the command by hand. Also refreshes the update banner in any open dashboard tab (same `update_status` broadcast) |
|
||||
| `ccam clear-data --yes` | Delete **all** data (schema preserved). Refuses to run without `--yes` |
|
||||
| `ccam open` | Open the dashboard in your default browser (`open` / `xdg-open` / `start`) |
|
||||
| `ccam version` | Print the ccam version (also `--version` / `-v`) |
|
||||
| `ccam help` | Full command reference (also shown with no arguments) |
|
||||
| `agenthub doctor` | Diagnosis: API reachability, hook installation status + path, database path/size/row counts, server uptime and Node version, WS connections |
|
||||
| `agenthub info` | The raw `/api/settings/info` JSON (pipe it to `jq`) |
|
||||
| `agenthub export [file.json]` | Full JSON data export (sessions, agents, events, tokens, workflows, dashboard runs, alert rules, pricing) — defaults to a dated filename. Re-importable via `agenthub import-data` |
|
||||
| `agenthub cleanup --hours N --days M` | Abandon active sessions idle for `N` hours and/or purge completed sessions older than `M` days |
|
||||
| `agenthub reinstall-hooks` | Rewrite the Claude Code hook entries in `~/.claude/settings.json` |
|
||||
| `agenthub update-check` | Ask the server whether the dashboard checkout is behind the canonical remote (branch- and fork-aware). Prints the behind-by count, a situation note for fork/feature-branch checkouts, and the **copy-paste update command**. When the checkout is fast-forwardable, the dashboard UI can also self-apply (`POST /api/updates/apply`: pull, rebuild, self-restart) instead of running the command by hand. Also refreshes the update banner in any open dashboard tab (same `update_status` broadcast) |
|
||||
| `agenthub clear-data --yes` | Delete **all** data (schema preserved). Refuses to run without `--yes` |
|
||||
| `agenthub open` | Open the dashboard in your default browser (`open` / `xdg-open` / `start`) |
|
||||
| `agenthub version` | Print the agenthub version (also `--version` / `-v`) |
|
||||
| `agenthub help` | Full command reference (also shown with no arguments) |
|
||||
|
||||
## Safety Model
|
||||
|
||||
@@ -298,25 +298,25 @@ The CLI renders a full terminal UI while staying 100% script-friendly:
|
||||
- **Inline bar charts** for the sessions-by-status distribution (`stats`), top tools and agent types (`analytics`), and the per-model cost breakdown (`cost`).
|
||||
- **Real tree rendering** (`├─`/`└─` with continuation rails) for the agent hierarchy in `session <id>`, and status lanes with branch rows in `kanban`.
|
||||
- Session tables include a relative **Updated** column (`4m ago`) so freshness is visible at a glance; event types are color-coded consistently across `events`, `tail`, and `session <id>`.
|
||||
- `ccam start` animates a spinner on a TTY (dot-trail when piped).
|
||||
- `agenthub start` animates a spinner on a TTY (dot-trail when piped).
|
||||
|
||||
Color rules (informal CLI conventions):
|
||||
|
||||
| Condition | Effect |
|
||||
| --------- | ------ |
|
||||
| stdout is a TTY | Colors **on** |
|
||||
| Output piped / redirected | Colors **off** automatically — `ccam sessions \| grep error` and `ccam info \| jq .db.counts` see plain text |
|
||||
| Output piped / redirected | Colors **off** automatically — `agenthub sessions \| grep error` and `agenthub info \| jq .db.counts` see plain text |
|
||||
| `NO_COLOR=1` env or `--no-color` anywhere on the command line | Colors **off** |
|
||||
| `FORCE_COLOR=1` or `CCAM_COLOR=1` | Colors **on** even when piped (useful under `watch`/CI) |
|
||||
| `FORCE_COLOR=1` or `AGENTHUB_COLOR=1` | Colors **on** even when piped (useful under `watch`/CI) |
|
||||
|
||||
- `ccam version` (also `--version` / `-v`) prints the package version.
|
||||
- `agenthub version` (also `--version` / `-v`) prints the package version.
|
||||
- Exit codes: `0` success, `1` for unreachable server, API errors, usage errors, unknown commands, or a failed `webhooks test` — safe to use in scripts and CI.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
| ------- | --- |
|
||||
| `○ Dashboard server is NOT running` | Start it: `ccam start` (background), `npm run dev`, or `npm start`. If it runs on a custom port, set `DASHBOARD_PORT` or rely on the discovery file |
|
||||
| `ccam: command not found` | Run `npm link` from the repo root (setup's fail-soft link may have skipped on permissions), or use `node bin/ccam.js …` |
|
||||
| `○ Dashboard server is NOT running` | Start it: `agenthub start` (background), `npm run dev`, or `npm start`. If it runs on a custom port, set `DASHBOARD_PORT` or rely on the discovery file |
|
||||
| `agenthub: command not found` | Run `npm link` from the repo root (setup's fail-soft link may have skipped on permissions), or use `node bin/agenthub.js …` |
|
||||
| Wrong server answers (multiple dashboards) | Set `CLAUDE_DASHBOARD_PORT` explicitly — env overrides always beat discovery |
|
||||
| `tail` shows nothing | Events only flow while hooks are installed and a Claude Code session is active — check `ccam doctor` |
|
||||
| `tail` shows nothing | Events only flow while hooks are installed and a Claude Code session is active — check `agenthub doctor` |
|
||||
|
||||
+4
-4
@@ -156,14 +156,14 @@ npm run install-hooks
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Skip this entirely when the `ccam` plugin is installed.** The plugin declares
|
||||
> **Skip this entirely when the `agenthub` plugin is installed.** The plugin declares
|
||||
> the same eight hooks itself (inline in `.claude-plugin/plugin.json`, each
|
||||
> running `${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js`). Running both means
|
||||
> every event is POSTed twice — events carry no id, so ingest cannot deduplicate
|
||||
> them and every token and cost figure doubles. `scripts/plugin-bootstrap.js`
|
||||
> removes checkout-installed entries on session start (backup:
|
||||
> `~/.claude/settings.json.ccam-bak`), `install-hooks.js` warns when it detects
|
||||
> a plugin install, and `/ccam-doctor` reports any duplicates that remain. See
|
||||
> `~/.claude/settings.json.agenthub-bak`), `install-hooks.js` warns when it detects
|
||||
> a plugin install, and `/agenthub-doctor` reports any duplicates that remain. See
|
||||
> [PLUGINS.md](PLUGINS.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -175,7 +175,7 @@ npm run install-hooks
|
||||
> break every host hook with `MODULE_NOT_FOUND`. The host handler POSTs to
|
||||
> `http://localhost:4820`, which a containerized dashboard already publishes.
|
||||
> (Escape hatch for running Claude Code *inside* the same container:
|
||||
> `CCAM_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.)
|
||||
> `AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.)
|
||||
|
||||
This copies hook scripts from `scripts/hooks/` to `.githooks/`:
|
||||
|
||||
|
||||
+174
-174
@@ -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
|
||||
|
||||
+3
-3
@@ -142,14 +142,14 @@ npm run mcp:build
|
||||
npm run mcp:start
|
||||
```
|
||||
|
||||
Installing the `ccam` plugin needs none of this: it ships the built server and
|
||||
Installing the `agenthub` plugin needs none of this: it ships the built server and
|
||||
wires it up itself (see [PLUGINS.md](PLUGINS.md)).
|
||||
|
||||
### Why `mcp/build/` is committed
|
||||
|
||||
Claude Code starts a plugin's MCP servers the moment a session opens and offers
|
||||
no "not ready yet, retry" state, so an async bootstrap cannot win that race. The
|
||||
build artifact is therefore committed, and `plugins/ccam/.mcp.json` points at
|
||||
build artifact is therefore committed, and `plugins/agenthub/.mcp.json` points at
|
||||
`${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`.
|
||||
|
||||
`npm run mcp:build` bundles `mcp/src` and its runtime dependencies
|
||||
@@ -169,7 +169,7 @@ npm run mcp:check-build # fails when mcp/build is stale or unstamped
|
||||
```
|
||||
|
||||
`npm run mcp:build` re-stamps it, the pre-commit hook runs the check whenever
|
||||
`mcp/src` is part of the commit, and `/ccam-doctor` reports it. Modification
|
||||
`mcp/src` is part of the commit, and `/agenthub-doctor` reports it. Modification
|
||||
times are deliberately not used: a fresh clone stamps every file at checkout
|
||||
time in arbitrary order.
|
||||
|
||||
|
||||
+134
-134
@@ -1,6 +1,6 @@
|
||||
# Claude Code Agent Monitor — Plugin Marketplace
|
||||
# AgentHub — Plugin Marketplace
|
||||
|
||||
Official Claude Code plugins for the Agent Monitor dashboard. The **`ccam`** plugin _is_ the dashboard — hooks, server, `ccam` CLI and MCP tools, no checkout required. On top of it, **10 focused plugins** extend Claude Code with skills, agents, slash commands, hooks, and CLI tools for deep analytics, cost guardrails, productivity automation, developer tools, AI-powered insights, session forensics, workflow/fleet intelligence, reliability & SLOs, config & memory governance, and dashboard connectivity.
|
||||
Official Claude Code plugins for the Agent Monitor dashboard. The **`agenthub`** plugin _is_ the dashboard — hooks, server, `agenthub` CLI and MCP tools, no checkout required. On top of it, **10 focused plugins** extend Claude Code with skills, agents, slash commands, hooks, and CLI tools for deep analytics, cost guardrails, productivity automation, developer tools, AI-powered insights, session forensics, workflow/fleet intelligence, reliability & SLOs, config & memory governance, and dashboard connectivity.
|
||||
|
||||
Every plugin is powered by the local Agent Monitor REST API at `http://localhost:4820`. They are read-only advisors unless a skill explicitly documents a mutating endpoint (and those preview + confirm before acting).
|
||||
|
||||
@@ -15,66 +15,66 @@ claude plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
### Install the dashboard itself
|
||||
|
||||
```bash
|
||||
claude plugin install ccam@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
```
|
||||
|
||||
That is the whole install: no clone, no `npm run setup`, no `npm run install-hooks`, no manual `npm start`. See [The `ccam` plugin](#the-ccam-plugin) below for what it does on first session start.
|
||||
That is the whole install: no clone, no `npm run setup`, no `npm run install-hooks`, no manual `npm start`. See [The `agenthub` plugin](#the-agenthub-plugin) below for what it does on first session start.
|
||||
|
||||
### Or via the Smartgift skills marketplace
|
||||
|
||||
`ccam` is also listed as a standalone entry in
|
||||
`agenthub` is also listed as a standalone entry in
|
||||
[`smartgift-claude-skills`](https://git.smartgift.io.vn/Smartgift-AI/smartgift-claude-skills)
|
||||
— its `source` still points at this repo's `main` branch, so the two entries
|
||||
install identically:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/smartgift-claude-skills.git
|
||||
claude plugin install ccam@sg
|
||||
claude plugin install agenthub@sg
|
||||
```
|
||||
|
||||
Pick whichever marketplace you already have added; installing `ccam` from both
|
||||
Pick whichever marketplace you already have added; installing `agenthub` from both
|
||||
at once is redundant but harmless (Claude Code treats it as one plugin per
|
||||
marketplace name, not per source).
|
||||
|
||||
### Install a focused plugin
|
||||
|
||||
```bash
|
||||
claude plugin install ccam-analytics@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-cost-guard@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-productivity@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-devtools@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-insights@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-sessions@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-workflows@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-quality@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-config@claude-code-agent-monitor-plugins
|
||||
claude plugin install ccam-dashboard@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-analytics@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-cost-guard@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-productivity@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-devtools@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-insights@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-sessions@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-workflows@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-quality@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-config@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-dashboard@claude-code-agent-monitor-plugins
|
||||
```
|
||||
|
||||
### Or install locally during development
|
||||
|
||||
```bash
|
||||
# From the repo root, test a plugin locally
|
||||
claude --plugin-dir plugins/ccam-analytics
|
||||
claude --plugin-dir plugins/agenthub-analytics
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
||||
- Node **>= 22.5** when installing via the `ccam` plugin. A plugin install has no
|
||||
- Node **>= 22.5** when installing via the `agenthub` plugin. A plugin install has no
|
||||
native `better-sqlite3`, so the server stores data through `node:sqlite`, which
|
||||
landed in 22.5. The bootstrap refuses with one line on anything older instead
|
||||
of letting the server crash. (A checkout install still works on Node >= 20.)
|
||||
- The Agent Monitor dashboard reachable at `http://localhost:4820` — the `ccam`
|
||||
- The Agent Monitor dashboard reachable at `http://localhost:4820` — the `agenthub`
|
||||
plugin starts it for you; a checkout starts it with `npm start` (see
|
||||
[SETUP.md](../SETUP.md))
|
||||
|
||||
Skills and commands are invoked as `/ccam-<plugin>:<name>`. Agents are dispatched automatically by Claude Code (or named explicitly).
|
||||
Skills and commands are invoked as `/agenthub-<plugin>:<name>`. Agents are dispatched automatically by Claude Code (or named explicitly).
|
||||
|
||||
## The `ccam` plugin
|
||||
## The `agenthub` plugin
|
||||
|
||||
The marketplace's root entry (`"source": "./"`) is the entire repository, so
|
||||
`server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` all land under
|
||||
`server/`, `client/`, `mcp/`, `scripts/` and `bin/agenthub.js` all land under
|
||||
`${CLAUDE_PLUGIN_ROOT}` when Claude Code caches it. That is what makes a
|
||||
checkout unnecessary.
|
||||
|
||||
@@ -84,31 +84,31 @@ checkout unnecessary.
|
||||
|---|---|
|
||||
| The eight event hooks | inline `hooks` in `.claude-plugin/plugin.json`, each running `scripts/hook-handler.js` |
|
||||
| The dashboard server | started detached by the bootstrap, from the plugin cache |
|
||||
| The `ccam` CLI | a launcher written to `~/.local/bin/ccam` |
|
||||
| The MCP tools | `plugins/ccam/.mcp.json`, pointing at the committed `mcp/build/index.js` |
|
||||
| The `agenthub` CLI | a launcher written to `~/.local/bin/agenthub` |
|
||||
| The MCP tools | `plugins/agenthub/.mcp.json`, pointing at the committed `mcp/build/index.js` |
|
||||
| The dashboard UI (`/run` and every other client route) | built into the runtime dir by the bootstrap, so it works the moment `claude` starts |
|
||||
| `/ccam-doctor`, `/ccam-update`, `/ccam-open` | `plugins/ccam/commands/` |
|
||||
| `/ship-full`, `/ship-quick` and one command per pipeline node (`/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/review`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/done`, …) | `plugins/ccam/commands/`, one file per name |
|
||||
| `/agenthub-doctor`, `/agenthub-update`, `/agenthub-open` | `plugins/agenthub/commands/` |
|
||||
| `/ship-full`, `/ship-quick` and one command per pipeline node (`/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/review`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/done`, …) | `plugins/agenthub/commands/`, one file per name |
|
||||
| The skills (`ship-feature-lane`, `ship-feature`, `repo-onboarding`, `debug-live-issue`, `mcp-operations`, `update-project-docs`, `file-headers`) | `skills/` at the repo root — which IS the plugin root, so they are auto-discovered as plugin skills |
|
||||
| The agents (`qc-local`, `senior-gate-reviewer`, `backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`) | `agents/` at the repo root, auto-discovered the same way — exposed as `ccam:<name>` |
|
||||
| The agents (`qc-local`, `senior-gate-reviewer`, `backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`) | `agents/` at the repo root, auto-discovered the same way — exposed as `agenthub:<name>` |
|
||||
|
||||
Plugin skills are namespaced: they are invoked as `/ccam:<name>`, with the bare
|
||||
Plugin skills are namespaced: they are invoked as `/agenthub:<name>`, with the bare
|
||||
`/<name>` working as an alias unless another skill or command claims that name.
|
||||
This is why `skills/` sits at the repo root instead of `.claude/skills/` — the
|
||||
latter only loads for a session whose cwd is inside this checkout, which is
|
||||
useless for a lane working in a different repo. `ccam skills install` survives
|
||||
useless for a lane working in a different repo. `agenthub skills install` survives
|
||||
as the fallback for a plain checkout without the plugin.
|
||||
|
||||
**The agents ship the same way, from the same files.** `agents/` at the repo
|
||||
root holds all five (`qc-local`, `senior-gate-reviewer`, `backend-reviewer`,
|
||||
`frontend-reviewer`, `mcp-reviewer`), so the plugin exposes them as
|
||||
`ccam:<name>`. `ccam lanes agents install` copies `qc-local` +
|
||||
`agenthub:<name>`. `agenthub lanes agents install` copies `qc-local` +
|
||||
`senior-gate-reviewer` from that same directory into a lane's own
|
||||
`.claude/agents/` — the highest-priority agent source — which is what makes the
|
||||
bare `subagent_type: qc-local` in the pipeline skill resolve there. One source
|
||||
file, two ways to reach it; the skill names the bare form first and falls back
|
||||
to the `ccam:` form. On a `codex` lane the same command installs the
|
||||
`<!-- ccam:stage:start -->` block in the lane's `AGENTS.md` instead — Codex
|
||||
to the `agenthub:` form. On a `codex` lane the same command installs the
|
||||
`<!-- agenthub:stage:start -->` block in the lane's `AGENTS.md` instead — Codex
|
||||
never reads `.claude/agents/`.
|
||||
|
||||
Because the plugin ships the hooks itself, `npm run install-hooks` is not needed
|
||||
@@ -119,8 +119,8 @@ too (`${CLAUDE_PLUGIN_ROOT}` resolved to the actual cache path) — confirmed
|
||||
against a real install — so those entries also contain `hook-handler.js`, same
|
||||
as a leftover checkout install. The bootstrap tells them apart by whether the
|
||||
command resolves under `~/.claude/plugins/cache/`: only genuine checkout paths
|
||||
are removed (backing `~/.claude/settings.json` up as `settings.json.ccam-bak`
|
||||
first), never the plugin's own. `/ccam-doctor` reports the state using the same
|
||||
are removed (backing `~/.claude/settings.json` up as `settings.json.agenthub-bak`
|
||||
first), never the plugin's own. `/agenthub-doctor` reports the state using the same
|
||||
check.
|
||||
|
||||
### First session start
|
||||
@@ -134,10 +134,10 @@ waits on an install:
|
||||
3. **Lock** — atomic `mkdir` lock holding the PID; reclaimed when the owner is dead or the lock is older than 10 minutes, so two sessions cannot race the install or spawn two servers.
|
||||
4. **Install** — `npm install --omit=dev --ignore-scripts` into the runtime dir (never into the plugin cache, which is garbage-collected and replaced on every update). `--ignore-scripts` keeps the root `postinstall` from pulling the whole Vite client toolchain.
|
||||
5. **Legacy hook cleanup** — see above.
|
||||
6. **CLI** — write the `~/.local/bin/ccam` launcher, never clobbering a `ccam` the bootstrap did not write.
|
||||
7. **UI build** — `client/` is copied into the runtime dir and built there (`npm install && npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (API and MCP still work, and `/ccam-open` can retry) but does mean `/run` 404s until it's fixed.
|
||||
6. **CLI** — write the `~/.local/bin/agenthub` launcher, never clobbering a `agenthub` the bootstrap did not write.
|
||||
7. **UI build** — `client/` is copied into the runtime dir and built there (`npm install && npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (API and MCP still work, and `/agenthub-open` can retry) but does mean `/run` 404s until it's fixed.
|
||||
8. **Server** — on Linux with `systemd --user` available, install/refresh a
|
||||
`ccam.service` unit (see [Auto-start on boot](#auto-start-on-boot)) instead
|
||||
`agenthub.service` unit (see [Auto-start on boot](#auto-start-on-boot)) instead
|
||||
of a bare spawn, so the server is supervised and comes back after a reboot
|
||||
with no Claude Code session involved. Everywhere else (macOS, Windows, or
|
||||
no systemd), spawn `server/index.js` detached, with `NODE_PATH` at the
|
||||
@@ -148,7 +148,7 @@ waits on an install:
|
||||
|
||||
The first run takes a few minutes — most of it is the UI build, which is what
|
||||
makes client-only routes (`http://localhost:4820/run`, and every other page)
|
||||
work the moment `claude` starts, with no manual `/ccam-open` step. Progress
|
||||
work the moment `claude` starts, with no manual `/agenthub-open` step. Progress
|
||||
goes to `~/.claude/agent-dashboard/runtime/bootstrap.log`; `npm run build`'s own
|
||||
output goes to `client-build.log` next to it; the server's own output goes to
|
||||
`server.log`.
|
||||
@@ -156,9 +156,9 @@ output goes to `client-build.log` next to it; the server's own output goes to
|
||||
### Auto-start on boot
|
||||
|
||||
On Linux, every plugin bootstrap (not just the first) installs/refreshes a
|
||||
systemd `--user` service (`~/.config/systemd/user/ccam.service`, managed via
|
||||
systemd `--user` service (`~/.config/systemd/user/agenthub.service`, managed via
|
||||
`scripts/systemd-service.js` — shared with the checkout path's
|
||||
`ccam service install`, see the README). Its `ExecStart`/`WorkingDirectory`
|
||||
`agenthub service install`, see the README). Its `ExecStart`/`WorkingDirectory`
|
||||
point at the current plugin cache path, so a plugin update that moves the
|
||||
cache directory rewrites and restarts the unit on the next session start, the
|
||||
same self-healing pattern the CLI launcher already uses. The bootstrap also
|
||||
@@ -166,7 +166,7 @@ tries `loginctl enable-linger` so the service starts on a cold boot before
|
||||
anyone logs in; when that needs elevated rights, it's skipped silently (check
|
||||
`~/.claude/agent-dashboard/runtime/bootstrap.log`, or run
|
||||
`sudo loginctl enable-linger <user>` once by hand). A pre-existing
|
||||
`ccam.service` unit not written by this bootstrap is left untouched, and the
|
||||
`agenthub.service` unit not written by this bootstrap is left untouched, and the
|
||||
server falls back to the old per-session detached spawn instead.
|
||||
|
||||
On macOS, Windows, or a Linux box without systemd, nothing changes: the
|
||||
@@ -177,19 +177,19 @@ reboot, same as always.
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `/ccam-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, **skill plugins** (see below), MCP build freshness, UI bundle |
|
||||
| `/ccam-update` | Pull the latest `ccam` release via `claude plugin marketplace update` + `claude plugin update ccam@<marketplace> -y`, then reinstall dependencies and restart the server (`plugin-bootstrap.js --force`). The plugin pull needs a manual `/reload-plugins` afterward to take effect in the current session — not scriptable from a command. |
|
||||
| `/ccam-open` | Rebuild the UI bundle and print the dashboard URL. Re-runs the bootstrap first when the server is down or still executing a plugin root that a mid-session `/plugin` update retired, so a fresh bundle never talks to an older server's routes |
|
||||
| `/ship-full <requirement>` | Put the lane on the 16-node `ship-feature` template, then run `/ccam:ship-feature-lane` **in this session**. The in-session twin of `ccam ship-full` (which spawns its own `claude` from a terminal) |
|
||||
| `/ship-quick <requirement>` | The same for the 8-node `default` template and `/ccam:ship-feature` — stops at review/gate, no PR, no browser QC |
|
||||
| One per pipeline node: `/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/e2e-passed`, `/review`, `/qc-plan`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/reported`, `/watching-pr`, `/merged`, `/done` | Run just that node's own `command`, resolved through `ccam lanes node <id> --print` against whichever template the lane is on (matched by id **or alias**, so `/implement` works on `default`'s `implement` and `ship-feature`'s `implementing`). A slash-command node is invoked in the current session, never as a nested `claude`; anything else runs as a shell argv in the lane's cwd. None of them declares a stage |
|
||||
| `/agenthub-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, **skill plugins** (see below), MCP build freshness, UI bundle |
|
||||
| `/agenthub-update` | Pull the latest `agenthub` release via `claude plugin marketplace update` + `claude plugin update agenthub@<marketplace> -y`, then reinstall dependencies and restart the server (`plugin-bootstrap.js --force`). The plugin pull needs a manual `/reload-plugins` afterward to take effect in the current session — not scriptable from a command. |
|
||||
| `/agenthub-open` | Rebuild the UI bundle and print the dashboard URL. Re-runs the bootstrap first when the server is down or still executing a plugin root that a mid-session `/plugin` update retired, so a fresh bundle never talks to an older server's routes |
|
||||
| `/ship-full <requirement>` | Put the lane on the 16-node `ship-feature` template, then run `/agenthub:ship-feature-lane` **in this session**. The in-session twin of `agenthub ship-full` (which spawns its own `claude` from a terminal) |
|
||||
| `/ship-quick <requirement>` | The same for the 8-node `default` template and `/agenthub:ship-feature` — stops at review/gate, no PR, no browser QC |
|
||||
| One per pipeline node: `/intake`, `/plan`, `/implement`, `/tests`, `/e2e`, `/e2e-passed`, `/review`, `/qc-plan`, `/qc`, `/gate`, `/publishing`, `/pr-open`, `/reported`, `/watching-pr`, `/merged`, `/done` | Run just that node's own `command`, resolved through `agenthub lanes node <id> --print` against whichever template the lane is on (matched by id **or alias**, so `/implement` works on `default`'s `implement` and `ship-feature`'s `implementing`). A slash-command node is invoked in the current session, never as a nested `claude`; anything else runs as a shell argv in the lane's cwd. None of them declares a stage |
|
||||
|
||||
#### The `Skill plugins` row
|
||||
|
||||
Derived from the loaded pipeline templates, not hardcoded: every `/plugin:skill`
|
||||
string a template names — as the pipeline's own `skill` or as one node's
|
||||
`command` — has to come from an installed, enabled plugin. Today that means
|
||||
**`superpowers`**, which `/ccam:ship-feature` and `/ccam:ship-feature-lane`
|
||||
**`superpowers`**, which `/agenthub:ship-feature` and `/agenthub:ship-feature-lane`
|
||||
drive step by step (`brainstorming`, `writing-plans`,
|
||||
`test-driven-development`, `requesting-code-review`,
|
||||
`verification-before-completion`, `finishing-a-development-branch`). Install it
|
||||
@@ -219,8 +219,8 @@ throws — the dashboard, hooks, CLI and MCP do not need these plugins.
|
||||
|
||||
The result is recorded as `skillPluginsSettled` in the bootstrap's `state.json`,
|
||||
which is also part of `fastPathOk()`: the check runs **once** per install, then
|
||||
the hot path reads one flag. `/ccam-update` (`--force`) is the retry.
|
||||
`CCAM_AUTO_INSTALL_PLUGINS=0` (or `false`/`off`) turns it off entirely.
|
||||
the hot path reads one flag. `/agenthub-update` (`--force`) is the retry.
|
||||
`AGENTHUB_AUTO_INSTALL_PLUGINS=0` (or `false`/`off`) turns it off entirely.
|
||||
|
||||
An installed plugin only resolves in the **current** session after
|
||||
`/reload-plugins`; a new session picks it up on its own. The log says so, since
|
||||
@@ -251,34 +251,34 @@ single port.
|
||||
|
||||
### Uninstalling
|
||||
|
||||
`claude plugin uninstall ccam@claude-code-agent-monitor-plugins` only removes
|
||||
`claude plugin uninstall agenthub@claude-code-agent-monitor-plugins` only removes
|
||||
the plugin from the enabled list — verified against a real install/uninstall,
|
||||
not assumed. It does **not**:
|
||||
|
||||
- stop the running server (it keeps serving from the now-uninstalled cache dir)
|
||||
- disable or remove the `ccam.service` systemd `--user` unit (Linux) — it
|
||||
- disable or remove the `agenthub.service` systemd `--user` unit (Linux) — it
|
||||
will keep restarting the old server on every reboot, since `systemctl`
|
||||
knows nothing about `claude plugin uninstall`
|
||||
- remove the cached source under `~/.claude/plugins/cache/.../ccam/<version>/`
|
||||
- remove the cached source under `~/.claude/plugins/cache/.../agenthub/<version>/`
|
||||
- remove the hook entries `claude plugin install` wrote into
|
||||
`~/.claude/settings.json` (still pointing at that cache dir — they keep
|
||||
firing, and keep working, until the cache directory is eventually GC'd by
|
||||
Claude Code, at which point they silently start failing)
|
||||
- remove `~/.local/bin/ccam` or `~/.claude/agent-dashboard/runtime/`
|
||||
- remove `~/.local/bin/agenthub` or `~/.claude/agent-dashboard/runtime/`
|
||||
|
||||
For a clean machine, do all of this yourself:
|
||||
|
||||
```bash
|
||||
# 1. Stop the server. On Linux, check for the systemd unit first — a bare
|
||||
# `kill` just gets restarted by it (Restart=on-failure).
|
||||
systemctl --user disable --now ccam.service 2>/dev/null
|
||||
rm -f ~/.config/systemd/user/ccam.service && systemctl --user daemon-reload 2>/dev/null
|
||||
systemctl --user disable --now agenthub.service 2>/dev/null
|
||||
rm -f ~/.config/systemd/user/agenthub.service && systemctl --user daemon-reload 2>/dev/null
|
||||
# Fallback for a detached (non-systemd) server, or if the unit above was absent:
|
||||
kill "$(node -e 'console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.claude/.agent-dashboard.json","utf8")).pid)')" 2>/dev/null
|
||||
|
||||
# 2. Runtime state and the CLI launcher
|
||||
rm -rf ~/.claude/agent-dashboard/runtime # deps, UI bundle, logs, state
|
||||
rm -f ~/.local/bin/ccam
|
||||
rm -f ~/.local/bin/agenthub
|
||||
|
||||
# 3. The hook entries claude plugin install wrote (settings.json is not
|
||||
# ours to fully own — back it up first and only drop entries mentioning
|
||||
@@ -287,7 +287,7 @@ node -e '
|
||||
const fs = require("fs");
|
||||
const p = require("os").homedir() + "/.claude/settings.json";
|
||||
const d = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
fs.copyFileSync(p, p + ".ccam-bak2");
|
||||
fs.copyFileSync(p, p + ".agenthub-bak2");
|
||||
for (const [type, entries] of Object.entries(d.hooks || {})) {
|
||||
const kept = entries.filter((e) => !JSON.stringify(e).includes("hook-handler.js"));
|
||||
if (kept.length) d.hooks[type] = kept; else delete d.hooks[type];
|
||||
@@ -301,169 +301,169 @@ above — delete that directory too only if you want the recorded history gone.
|
||||
|
||||
## Available Plugins
|
||||
|
||||
### 1. `ccam-analytics` — Analytics & Monitoring
|
||||
### 1. `agenthub-analytics` — Analytics & Monitoring
|
||||
|
||||
Deep analytics on sessions, token usage, costs, cache efficiency, model mix, and productivity.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Session Report | `/ccam-analytics:session-report` | Per-model tokens (input/output/cache_read/cache_write + baselines), cost, agent hierarchy, tool activity, timeline |
|
||||
| Cost Breakdown | `/ccam-analytics:cost-breakdown` | Per-model cost via the pricing engine, daily trends, cache efficiency, optimization opportunities |
|
||||
| Usage Trends | `/ccam-analytics:usage-trends` | 365-day session/event trends, token volume, tool rankings, model distribution, event-type ratios |
|
||||
| Productivity Score | `/ccam-analytics:productivity-score` | Weighted scorecard: completion, token efficiency, tool effectiveness, velocity, cost efficiency |
|
||||
| Cache Efficiency | `/ccam-analytics:cache-efficiency` | Cache hit rate, write-vs-read reuse, sessions with poor cache reuse |
|
||||
| Model Mix | `/ccam-analytics:model-mix` | Share of tokens and cost per model family; expensive models doing cheap work |
|
||||
| Session Report | `/agenthub-analytics:session-report` | Per-model tokens (input/output/cache_read/cache_write + baselines), cost, agent hierarchy, tool activity, timeline |
|
||||
| Cost Breakdown | `/agenthub-analytics:cost-breakdown` | Per-model cost via the pricing engine, daily trends, cache efficiency, optimization opportunities |
|
||||
| Usage Trends | `/agenthub-analytics:usage-trends` | 365-day session/event trends, token volume, tool rankings, model distribution, event-type ratios |
|
||||
| Productivity Score | `/agenthub-analytics:productivity-score` | Weighted scorecard: completion, token efficiency, tool effectiveness, velocity, cost efficiency |
|
||||
| Cache Efficiency | `/agenthub-analytics:cache-efficiency` | Cache hit rate, write-vs-read reuse, sessions with poor cache reuse |
|
||||
| Model Mix | `/agenthub-analytics:model-mix` | Share of tokens and cost per model family; expensive models doing cheap work |
|
||||
|
||||
**Commands:** `/ccam-analytics:cost-today` · `/ccam-analytics:top-spenders` · `/ccam-analytics:burn-rate`
|
||||
**Commands:** `/agenthub-analytics:cost-today` · `/agenthub-analytics:top-spenders` · `/agenthub-analytics:burn-rate`
|
||||
|
||||
**Agents:** `analytics-advisor` (full advisor incl. workflow intelligence) · `token-economist` (token economics & reduction tactics)
|
||||
|
||||
**Hooks:** Logs `Stop` / `SubagentStop` events. **CLI:** `ccam-stats` — terminal stats (sessions, cost, tokens).
|
||||
**Hooks:** Logs `Stop` / `SubagentStop` events. **CLI:** `agenthub-stats` — terminal stats (sessions, cost, tokens).
|
||||
|
||||
---
|
||||
|
||||
### 2. `ccam-cost-guard` — Budget Guardrails
|
||||
### 2. `agenthub-cost-guard` — Budget Guardrails
|
||||
|
||||
Spend limits, forecasting, cost alerts, and model-routing savings.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Budget Set | `/ccam-cost-guard:budget-set` | Define a budget and (optionally) arm a `token_threshold` alert rule; explains the $→token conversion |
|
||||
| Spend Forecast | `/ccam-cost-guard:spend-forecast` | Project week/month-end spend from the daily trend (moving average × remaining days) |
|
||||
| Cost Alert | `/ccam-cost-guard:cost-alert` | Review alert rules and fired alerts; explain exactly what tripped |
|
||||
| Model Savings | `/ccam-cost-guard:model-savings` | Estimate $ saved by routing eligible work to a cheaper model family |
|
||||
| Daily Budget Check | `/ccam-cost-guard:daily-budget-check` | Today's spend vs a daily budget, pace vs target, projected overage |
|
||||
| Budget Set | `/agenthub-cost-guard:budget-set` | Define a budget and (optionally) arm a `token_threshold` alert rule; explains the $→token conversion |
|
||||
| Spend Forecast | `/agenthub-cost-guard:spend-forecast` | Project week/month-end spend from the daily trend (moving average × remaining days) |
|
||||
| Cost Alert | `/agenthub-cost-guard:cost-alert` | Review alert rules and fired alerts; explain exactly what tripped |
|
||||
| Model Savings | `/agenthub-cost-guard:model-savings` | Estimate $ saved by routing eligible work to a cheaper model family |
|
||||
| Daily Budget Check | `/agenthub-cost-guard:daily-budget-check` | Today's spend vs a daily budget, pace vs target, projected overage |
|
||||
|
||||
**Commands:** `/ccam-cost-guard:budget` · `/ccam-cost-guard:forecast` · `/ccam-cost-guard:overspend`
|
||||
**Commands:** `/agenthub-cost-guard:budget` · `/agenthub-cost-guard:forecast` · `/agenthub-cost-guard:overspend`
|
||||
|
||||
**Agent:** `budget-sentinel` — watches spend vs target, projects month-end, recommends cuts. **Hooks:** fail-safe `Stop` event POST so budget tracking sees session ends.
|
||||
|
||||
---
|
||||
|
||||
### 3. `ccam-productivity` — Productivity & Workflows
|
||||
### 3. `agenthub-productivity` — Productivity & Workflows
|
||||
|
||||
Standups, weekly/monthly reviews, sprint tracking, focus analysis, and workflow optimization.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Daily Standup | `/ccam-productivity:daily-standup` | Standup from recent sessions — work by project (cwd), costs, tools, errors, velocity |
|
||||
| Weekly Report | `/ccam-productivity:weekly-report` | Daily session/event trends, per-session costs, token volumes, tool top-20, completion rates |
|
||||
| Sprint Summary | `/ccam-productivity:sprint-summary` | Per-project + per-model costs, token efficiency, subagent effectiveness, retrospective data |
|
||||
| Workflow Optimizer | `/ccam-productivity:workflow-optimizer` | Tool-flow transitions, effectiveness, delegation, error propagation, concurrency, compaction |
|
||||
| Monthly Review | `/ccam-productivity:monthly-review` | Month-over-month sessions, cost, tokens, completion, top projects, notable shifts |
|
||||
| Time of Day | `/ccam-productivity:time-of-day` | Activity/productivity bucketed by hour and day-of-week; peak vs low-output windows |
|
||||
| Daily Standup | `/agenthub-productivity:daily-standup` | Standup from recent sessions — work by project (cwd), costs, tools, errors, velocity |
|
||||
| Weekly Report | `/agenthub-productivity:weekly-report` | Daily session/event trends, per-session costs, token volumes, tool top-20, completion rates |
|
||||
| Sprint Summary | `/agenthub-productivity:sprint-summary` | Per-project + per-model costs, token efficiency, subagent effectiveness, retrospective data |
|
||||
| Workflow Optimizer | `/agenthub-productivity:workflow-optimizer` | Tool-flow transitions, effectiveness, delegation, error propagation, concurrency, compaction |
|
||||
| Monthly Review | `/agenthub-productivity:monthly-review` | Month-over-month sessions, cost, tokens, completion, top projects, notable shifts |
|
||||
| Time of Day | `/agenthub-productivity:time-of-day` | Activity/productivity bucketed by hour and day-of-week; peak vs low-output windows |
|
||||
|
||||
**Commands:** `/ccam-productivity:standup` · `/ccam-productivity:whats-next` · `/ccam-productivity:focus-report`
|
||||
**Commands:** `/agenthub-productivity:standup` · `/agenthub-productivity:whats-next` · `/agenthub-productivity:focus-report`
|
||||
|
||||
**Agents:** `productivity-coach` (work-pattern review) · `focus-analyst` (deep-work / focus blocks). **Hooks:** session start/end timing.
|
||||
|
||||
---
|
||||
|
||||
### 4. `ccam-devtools` — Developer Tools
|
||||
### 4. `agenthub-devtools` — Developer Tools
|
||||
|
||||
Debugging, data-integrity inspection, event tracing, transcript search, diagnostics, export, and health checks.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Session Debug | `/ccam-devtools:session-debug` | Full event chain, agent hierarchy, token usage with baselines, workflow intelligence |
|
||||
| Hook Diagnostics | `/ccam-devtools:hook-diagnostics` | Hook install, connectivity, handler validation, event delivery, data freshness |
|
||||
| Data Export | `/ccam-devtools:data-export` | Export sessions/events/analytics/costs as JSON/CSV/Markdown |
|
||||
| Health Check | `/ccam-devtools:health-check` | API, SQLite (WAL), WebSocket, endpoints, hooks, disk, data freshness |
|
||||
| Event Trace | `/ccam-devtools:event-trace` | Ordered event timeline for a session, highlighting gaps/failures |
|
||||
| Transcript Grep | `/ccam-devtools:transcript-grep` | Search a session transcript for a string/pattern with context |
|
||||
| Session Debug | `/agenthub-devtools:session-debug` | Full event chain, agent hierarchy, token usage with baselines, workflow intelligence |
|
||||
| Hook Diagnostics | `/agenthub-devtools:hook-diagnostics` | Hook install, connectivity, handler validation, event delivery, data freshness |
|
||||
| Data Export | `/agenthub-devtools:data-export` | Export sessions/events/analytics/costs as JSON/CSV/Markdown |
|
||||
| Health Check | `/agenthub-devtools:health-check` | API, SQLite (WAL), WebSocket, endpoints, hooks, disk, data freshness |
|
||||
| Event Trace | `/agenthub-devtools:event-trace` | Ordered event timeline for a session, highlighting gaps/failures |
|
||||
| Transcript Grep | `/agenthub-devtools:transcript-grep` | Search a session transcript for a string/pattern with context |
|
||||
|
||||
**Commands:** `/ccam-devtools:doctor` · `/ccam-devtools:export` · `/ccam-devtools:tail-events`
|
||||
**Commands:** `/agenthub-devtools:doctor` · `/agenthub-devtools:export` · `/agenthub-devtools:tail-events`
|
||||
|
||||
**Agents:** `issue-triager` (cross-component triage) · `db-inspector` (data-integrity inspection). **CLI:** `ccam-doctor`, `ccam-export`.
|
||||
**Agents:** `issue-triager` (cross-component triage) · `db-inspector` (data-integrity inspection). **CLI:** `agenthub-doctor`, `agenthub-export`.
|
||||
|
||||
---
|
||||
|
||||
### 5. `ccam-insights` — AI-Powered Insights
|
||||
### 5. `agenthub-insights` — AI-Powered Insights
|
||||
|
||||
Pattern detection, anomaly alerting, forecasting, regression watch, benchmarking, optimization, and comparison.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Pattern Detect | `/ccam-insights:pattern-detect` | Tool-flow transitions, recurring sequences, agent co-occurrence, delegation habits |
|
||||
| Anomaly Alert | `/ccam-insights:anomaly-alert` | Cost/token/event-ratio/complexity outliers (statistical) |
|
||||
| Optimization Suggest | `/ccam-insights:optimization-suggest` | Model downgrades, cache optimization, compaction reduction, tool reliability |
|
||||
| Session Compare | `/ccam-insights:session-compare` | Side-by-side tokens, costs, complexity, tool-flow, metadata deltas |
|
||||
| Regression Watch | `/ccam-insights:regression-watch` | Rising error rate, falling cache hits, growing compaction, climbing cost/session |
|
||||
| Benchmark | `/ccam-insights:benchmark` | Benchmark a session vs the rolling average; show percentile |
|
||||
| Pattern Detect | `/agenthub-insights:pattern-detect` | Tool-flow transitions, recurring sequences, agent co-occurrence, delegation habits |
|
||||
| Anomaly Alert | `/agenthub-insights:anomaly-alert` | Cost/token/event-ratio/complexity outliers (statistical) |
|
||||
| Optimization Suggest | `/agenthub-insights:optimization-suggest` | Model downgrades, cache optimization, compaction reduction, tool reliability |
|
||||
| Session Compare | `/agenthub-insights:session-compare` | Side-by-side tokens, costs, complexity, tool-flow, metadata deltas |
|
||||
| Regression Watch | `/agenthub-insights:regression-watch` | Rising error rate, falling cache hits, growing compaction, climbing cost/session |
|
||||
| Benchmark | `/agenthub-insights:benchmark` | Benchmark a session vs the rolling average; show percentile |
|
||||
|
||||
**Commands:** `/ccam-insights:insights` · `/ccam-insights:compare` · `/ccam-insights:anomalies`
|
||||
**Commands:** `/agenthub-insights:insights` · `/agenthub-insights:compare` · `/agenthub-insights:anomalies`
|
||||
|
||||
**Agents:** `insights-advisor` (strategic analysis) · `trend-forecaster` (near-future cost/usage projection).
|
||||
|
||||
---
|
||||
|
||||
### 6. `ccam-sessions` — Session Forensics
|
||||
### 6. `agenthub-sessions` — Session Forensics
|
||||
|
||||
Search, timeline, transcript replay, per-project rollups, and lifecycle management.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Session Search | `/ccam-sessions:session-search` | Find sessions by project/model/status/date; rank by cost or recency |
|
||||
| Session Timeline | `/ccam-sessions:session-timeline` | Ordered timeline of one session's events with durations and tool names |
|
||||
| Transcript Replay | `/ccam-sessions:transcript-replay` | Walk a transcript turn-by-turn, summarizing each message |
|
||||
| CWD Rollup | `/ccam-sessions:cwd-rollup` | Roll up sessions by working directory: counts, cost, tokens, last-active |
|
||||
| Session Cleanup | `/ccam-sessions:session-cleanup` | Identify stale/empty sessions; preview before the cleanup endpoint deletes (confirm required) |
|
||||
| Session Search | `/agenthub-sessions:session-search` | Find sessions by project/model/status/date; rank by cost or recency |
|
||||
| Session Timeline | `/agenthub-sessions:session-timeline` | Ordered timeline of one session's events with durations and tool names |
|
||||
| Transcript Replay | `/agenthub-sessions:transcript-replay` | Walk a transcript turn-by-turn, summarizing each message |
|
||||
| CWD Rollup | `/agenthub-sessions:cwd-rollup` | Roll up sessions by working directory: counts, cost, tokens, last-active |
|
||||
| Session Cleanup | `/agenthub-sessions:session-cleanup` | Identify stale/empty sessions; preview before the cleanup endpoint deletes (confirm required) |
|
||||
|
||||
**Commands:** `/ccam-sessions:find-session` · `/ccam-sessions:replay` · `/ccam-sessions:recent`
|
||||
**Commands:** `/agenthub-sessions:find-session` · `/agenthub-sessions:replay` · `/agenthub-sessions:recent`
|
||||
|
||||
**Agent:** `session-investigator` — end-to-end investigation of a single session.
|
||||
|
||||
---
|
||||
|
||||
### 7. `ccam-workflows` — Orchestration & Fleet Intelligence
|
||||
### 7. `agenthub-workflows` — Orchestration & Fleet Intelligence
|
||||
|
||||
Multi-agent structure analysis using the workflow intelligence API and Workflow-tool run journals.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| DAG Map | `/ccam-workflows:dag-map` | Orchestration DAG: parent→child subagent edges, depth, fan-out |
|
||||
| Delegation Audit | `/ccam-workflows:delegation-audit` | Model delegation + subagent effectiveness; wasted delegations |
|
||||
| Concurrency Report | `/ccam-workflows:concurrency-report` | Concurrency lanes, parallelism, serialization bottlenecks |
|
||||
| Error Propagation | `/ccam-workflows:error-propagation` | Trace failures by depth and how they cascade across subagents |
|
||||
| Fleet Runs | `/ccam-workflows:fleet-runs` | Summarize Workflow-tool fleet runs (no-hook fleets ingested from run journals) |
|
||||
| DAG Map | `/agenthub-workflows:dag-map` | Orchestration DAG: parent→child subagent edges, depth, fan-out |
|
||||
| Delegation Audit | `/agenthub-workflows:delegation-audit` | Model delegation + subagent effectiveness; wasted delegations |
|
||||
| Concurrency Report | `/agenthub-workflows:concurrency-report` | Concurrency lanes, parallelism, serialization bottlenecks |
|
||||
| Error Propagation | `/agenthub-workflows:error-propagation` | Trace failures by depth and how they cascade across subagents |
|
||||
| Fleet Runs | `/agenthub-workflows:fleet-runs` | Summarize Workflow-tool fleet runs (no-hook fleets ingested from run journals) |
|
||||
|
||||
**Commands:** `/ccam-workflows:workflow` · `/ccam-workflows:dag` · `/ccam-workflows:runs`
|
||||
**Commands:** `/agenthub-workflows:workflow` · `/agenthub-workflows:dag` · `/agenthub-workflows:runs`
|
||||
|
||||
**Agent:** `orchestration-analyst` — analyzes the 11 workflow datasets + fleet runs.
|
||||
|
||||
---
|
||||
|
||||
### 8. `ccam-quality` — Reliability & SLOs
|
||||
### 8. `agenthub-quality` — Reliability & SLOs
|
||||
|
||||
Error monitoring, hook-delivery health, SLO tracking with error budgets, and regression alerts.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Error Scan | `/ccam-quality:error-scan` | Scan events for APIError + failure signals; group by tool/model; rank by frequency |
|
||||
| API Error Report | `/ccam-quality:api-error-report` | APIError detail: counts over time, affected sessions/models, likely causes |
|
||||
| Hook Failure Audit | `/ccam-quality:hook-failure-audit` | PreToolUse/PostToolUse balance, missing terminators, stale ingestion |
|
||||
| SLO Check | `/ccam-quality:slo-check` | Completion rate, tool success rate, error rate; error budget remaining |
|
||||
| Regression Alert | `/ccam-quality:regression-alert` | Compare this period's error/failure rates to the prior period; optional alert rule |
|
||||
| Error Scan | `/agenthub-quality:error-scan` | Scan events for APIError + failure signals; group by tool/model; rank by frequency |
|
||||
| API Error Report | `/agenthub-quality:api-error-report` | APIError detail: counts over time, affected sessions/models, likely causes |
|
||||
| Hook Failure Audit | `/agenthub-quality:hook-failure-audit` | PreToolUse/PostToolUse balance, missing terminators, stale ingestion |
|
||||
| SLO Check | `/agenthub-quality:slo-check` | Completion rate, tool success rate, error rate; error budget remaining |
|
||||
| Regression Alert | `/agenthub-quality:regression-alert` | Compare this period's error/failure rates to the prior period; optional alert rule |
|
||||
|
||||
**Commands:** `/ccam-quality:errors` · `/ccam-quality:slo` · `/ccam-quality:health`
|
||||
**Commands:** `/agenthub-quality:errors` · `/agenthub-quality:slo` · `/agenthub-quality:health`
|
||||
|
||||
**Agent:** `reliability-engineer` — treats Claude Code usage as a service with an error budget.
|
||||
|
||||
---
|
||||
|
||||
### 9. `ccam-config` — Config & Memory Governance
|
||||
### 9. `agenthub-config` — Config & Memory Governance
|
||||
|
||||
Audit your Claude Code configuration and curate the file-based memory store via the Config Explorer API.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Config Audit | `/ccam-config:config-audit` | Counts per surface (user vs project), duplicate skills/agents, shell-running hooks |
|
||||
| Memory Review | `/ccam-config:memory-review` | CLAUDE.md + per-project auto-memory files grouped by project; flag stale/oversized facts |
|
||||
| Skill Inventory | `/ccam-config:skill-inventory` | Installed skills + contributing plugins; overlap with your own skills |
|
||||
| MCP Audit | `/ccam-config:mcp-audit` | MCP servers (user + project): transport, command/args/env names, source file |
|
||||
| Hook Inventory | `/ccam-config:hook-inventory` | Hooks across settings + the hooks scripts dir; flag network/arbitrary-command hooks |
|
||||
| Config Audit | `/agenthub-config:config-audit` | Counts per surface (user vs project), duplicate skills/agents, shell-running hooks |
|
||||
| Memory Review | `/agenthub-config:memory-review` | CLAUDE.md + per-project auto-memory files grouped by project; flag stale/oversized facts |
|
||||
| Skill Inventory | `/agenthub-config:skill-inventory` | Installed skills + contributing plugins; overlap with your own skills |
|
||||
| MCP Audit | `/agenthub-config:mcp-audit` | MCP servers (user + project): transport, command/args/env names, source file |
|
||||
| Hook Inventory | `/agenthub-config:hook-inventory` | Hooks across settings + the hooks scripts dir; flag network/arbitrary-command hooks |
|
||||
|
||||
**Commands:** `/ccam-config:audit-config` · `/ccam-config:memory` · `/ccam-config:inventory`
|
||||
**Commands:** `/agenthub-config:audit-config` · `/agenthub-config:memory` · `/agenthub-config:inventory`
|
||||
|
||||
**Agent:** `config-auditor` — audits config sprawl, duplication, risky hooks, and stale memory.
|
||||
|
||||
@@ -471,18 +471,18 @@ Audit your Claude Code configuration and curate the file-based memory store via
|
||||
|
||||
---
|
||||
|
||||
### 10. `ccam-dashboard` — Dashboard Connector
|
||||
### 10. `agenthub-dashboard` — Dashboard Connector
|
||||
|
||||
Direct MCP integration, quick status, live watch, and endpoint probing.
|
||||
|
||||
| Skill | Command | Purpose |
|
||||
|-------|---------|---------|
|
||||
| Dashboard Status | `/ccam-dashboard:dashboard-status` | Health: API connectivity, session/event counts, hook status, data freshness |
|
||||
| Quick Stats | `/ccam-dashboard:quick-stats` | One-line metrics: active sessions, total cost, events, top tool, cache efficiency |
|
||||
| Live Watch | `/ccam-dashboard:live-watch` | Poll a few times to show live deltas (active sessions/agents, events, ws connections) |
|
||||
| Endpoint Probe | `/ccam-dashboard:endpoint-probe` | Probe each major API route and report reachability/shape |
|
||||
| Dashboard Status | `/agenthub-dashboard:dashboard-status` | Health: API connectivity, session/event counts, hook status, data freshness |
|
||||
| Quick Stats | `/agenthub-dashboard:quick-stats` | One-line metrics: active sessions, total cost, events, top tool, cache efficiency |
|
||||
| Live Watch | `/agenthub-dashboard:live-watch` | Poll a few times to show live deltas (active sessions/agents, events, ws connections) |
|
||||
| Endpoint Probe | `/agenthub-dashboard:endpoint-probe` | Probe each major API route and report reachability/shape |
|
||||
|
||||
**Commands:** `/ccam-dashboard:status` · `/ccam-dashboard:ping` · `/ccam-dashboard:open-dashboard`
|
||||
**Commands:** `/agenthub-dashboard:status` · `/agenthub-dashboard:ping` · `/agenthub-dashboard:open-dashboard`
|
||||
|
||||
**Agent:** `dashboard-operator` — verifies the dashboard is up and guides start/restart/import. **MCP Server:** direct tool access to the Agent Monitor API. **Settings:** default agent model.
|
||||
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ Comprehensive documentation for the Agent Dashboard project.
|
||||
|
||||
- [Architecture Overview](../ARCHITECTURE.md) - System design and technical reference
|
||||
- [I18N Architecture](./I18N.md) - Internationalization architecture and usage guide
|
||||
- [CLI Reference](./CLI.md) - The `ccam` terminal CLI: every command, discovery, safety model
|
||||
- [CLI Reference](./CLI.md) - The `agenthub` terminal CLI: every command, discovery, safety model
|
||||
- [Setup Guide](../SETUP.md) - Installation and configuration
|
||||
- [Installation](../INSTALL.md) - Detailed installation instructions
|
||||
|
||||
@@ -49,7 +49,7 @@ graph TB
|
||||
| [HOOKS.md](./HOOKS.md) | Claude Code hook system integration | Hook developers |
|
||||
| [MCP.md](./MCP.md) | MCP server setup and tool reference | MCP integrators |
|
||||
| [I18N.md](./I18N.md) | Language architecture, locale strategy, and rollout checklist | Frontend and product teams |
|
||||
| [CLI.md](./CLI.md) | `ccam` command reference — monitoring, browsing, insights, alerts, pricing, import, administration | Terminal users and CI scripting |
|
||||
| [CLI.md](./CLI.md) | `agenthub` command reference — monitoring, browsing, insights, alerts, pricing, import, administration | Terminal users and CI scripting |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ Task 4: fix round 1/5 (3 addressed, 0 open — clear now gated on the pre-existi
|
||||
Task 4: complete (commits 76c6f26..3f7121d, review clean) — 780 server tests pass
|
||||
|
||||
Task 5: first attempt reported DONE_WITH_CONCERNS claiming the sandbox blocks loopback — WRONG, and nothing was committed (pre-commit test gate held). Real cause: the plan's own test harness used blocking spawnSync while the test HTTP server ran in the same process, so the event loop stalled and the CLI child's request was never served. Verified loopback works between processes (detached node server + curl + separate node fetch, all exit 0). Plan's code block corrected to async spawn.
|
||||
Task 5: review — spec PASS, 2 Important (ccam lanes add untested; temp DB leaked), 3 Minor.
|
||||
Task 5: review — spec PASS, 2 Important (agenthub lanes add untested; temp DB leaked), 3 Minor.
|
||||
Task 5: fix round 1/5 (2 addressed, 1 open — lanes-add test added, health poll replaced the 100ms sleep, DB cleanup added but not exception-safe; commits 19dd31b..3deb420)
|
||||
Task 5: fix round 2/5 (1 addressed, 0 open — after() teardown wrapped in try/finally; commit 35138b4; verified inline)
|
||||
Task 5: complete (commits 3f7121d..35138b4, review clean) — 783 server tests pass. `ccam lanes add` was an authorised addition so the CLI's empty-state hint names a command that exists.
|
||||
Task 5: complete (commits 3f7121d..35138b4, review clean) — 783 server tests pass. `agenthub lanes add` was an authorised addition so the CLI's empty-state hint names a command that exists.
|
||||
|
||||
Task 6: review — spec MET; security verified adversarially (cross-origin POST to /:id/start returns 403; prompt travels via stdin, model/effort/resumeSessionId are separate argv with no shell; /:id/stage not captured by /:id/:action; unknown action rejected before any lookup or mutation). 1 Important, 2 Minor.
|
||||
Task 6: fix round 1/5 (2 addressed, 0 open — message now consults getRun and returns 409 for a recorded-but-dead run instead of 500; unknown-action test asserts stage/status unchanged; new 409 test; commits f225fe8..b3f9e2f)
|
||||
@@ -42,12 +42,12 @@ Task 7: minor (deferred): the Lanes screen snapshot captures the empty state onl
|
||||
Task 7: minor (deferred): the card cannot send a prompt — driving a lane from the UI needs a prompt/message input; today `start` opens a promptless conversation run and `message` has no input field. Follow-up feature, not a defect.
|
||||
Task 7: complete (commits b3f9e2f..a05065d, review clean) — verified by controller: 279/279 client tests, `npm run build` clean
|
||||
|
||||
Task 8: review — every documented command/flag/env var/endpoint/state-rule fact-checked against the shipped code and correct, EXCEPT one Critical: docs advertised `ccam lanes add --pipeline <id>` which the CLI never parsed.
|
||||
Task 8: review — every documented command/flag/env var/endpoint/state-rule fact-checked against the shipped code and correct, EXCEPT one Critical: docs advertised `agenthub lanes add --pipeline <id>` which the CLI never parsed.
|
||||
Task 8: ruled — fix the CODE, not the docs: a pipeline template you cannot select from the CLI is a template nobody uses, and the server already accepted the field. Authorised a scoped code change inside the docs task.
|
||||
Task 8: fix round 1/5 (1 addressed, 0 open — --pipeline parsed and forwarded only when provided, usage string updated, two tests via a DASHBOARD_PIPELINES_DIR fixture with cleanup, docs parenthetical corrected; commits 6cca59c..3b196ef)
|
||||
Task 8: minor (deferred): README VN/CN/KO mirrors are now behind the English README; the repo's update-project-docs convention expects them synced.
|
||||
Task 8: complete (commits a05065d..3b196ef, review clean) — 790 server tests pass
|
||||
|
||||
Final whole-branch review (Opus): READY — no Critical/Important/Minor findings. Cross-cutting types, CLI-vs-server cwd resolution, WS payload branches, migration safety on an existing DB, the same-origin guard on the spawning routes, and both confirmation gates all verified. All 8 deferred/parked items triaged acceptable-to-defer, none load-bearing.
|
||||
Controller end-to-end smoke test (real server, real CLI, real hook POSTs) — lane created, `ccam stage plan --evidence` then `ccam stage review` reported: node states came back intake=passed-no-evidence, plan=done, implement/tests=passed-no-evidence, review=current, rest=pending; progress 57%; stage_seconds live; `ccam lanes` table and counters correct; a Notification hook bound the lane and a later hook from the newly-bound session cleared needs_action exactly as the amended contract specifies.
|
||||
Side effect found and REVERTED: starting the fork's server auto-configured Claude Code hooks in ~/.claude/settings.json (8 entries pointing at this fork). Removed surgically; the pre-existing `rtk hook claude` PreToolUse entry was left untouched. Polluted copy kept at /tmp/settings.before-ccam-cleanup.json.
|
||||
Controller end-to-end smoke test (real server, real CLI, real hook POSTs) — lane created, `agenthub stage plan --evidence` then `agenthub stage review` reported: node states came back intake=passed-no-evidence, plan=done, implement/tests=passed-no-evidence, review=current, rest=pending; progress 57%; stage_seconds live; `agenthub lanes` table and counters correct; a Notification hook bound the lane and a later hook from the newly-bound session cleared needs_action exactly as the amended contract specifies.
|
||||
Side effect found and REVERTED: starting the fork's server auto-configured Claude Code hooks in ~/.claude/settings.json (8 entries pointing at this fork). Removed surgically; the pre-existing `rtk hook claude` PreToolUse entry was left untouched. Polluted copy kept at /tmp/settings.before-agenthub-cleanup.json.
|
||||
|
||||
@@ -18,7 +18,7 @@ Task 3: complete (883 server tests)
|
||||
|
||||
Task 4 (B4): detected nodes render amber-dashed via a class that REPLACES the state-driven one, so even a hypothetical {detected:true, state:"done"} payload renders amber and never green — the safe direction. `auto:` chip only when the detection leads the declaration. tsc (not vitest) caught a missing field in an unrelated test fixture, which is why `npm run build` is in every client task's verification list.
|
||||
Task 4: complete (308 client tests)
|
||||
Task 5 (B5): `ccam lanes` gains a detected suffix using the same lead-comparison as the card, so terminal and browser never disagree; docs/LANES.md documents the signals, the field allowlist, the 120-char cap, the rules node by node, the deliberate blanks on intake/gate/done, forward-only + write-on-change, and the evidence boundary with its reason. The implementer flagged rather than papered over the fact that GET /api/lanes has NO OpenAPI path docs at all and no Lane schema — it added a standalone schema for the three fields instead of inventing either.
|
||||
Task 5 (B5): `agenthub lanes` gains a detected suffix using the same lead-comparison as the card, so terminal and browser never disagree; docs/LANES.md documents the signals, the field allowlist, the 120-char cap, the rules node by node, the deliberate blanks on intake/gate/done, forward-only + write-on-change, and the evidence boundary with its reason. The implementer flagged rather than papered over the fact that GET /api/lanes has NO OpenAPI path docs at all and no Lane schema — it added a standalone schema for the three fields instead of inventing either.
|
||||
Task 5: complete (885 server tests)
|
||||
|
||||
FINAL whole-branch review (Opus): READY WITH FIXES — 1 Critical, 5 Important, 7 Minor. The evidence boundary itself held: no lane state could be constructed in which a detection renders green or writes lanes.stage, proved by mutation and live probing rather than by reading, with per-event cost measured (1 µs for an ordinary Bash event, zero queries when nothing changes).
|
||||
|
||||
@@ -7,7 +7,7 @@ Task 1: review — spec MET (one accepted deviation: reset-in-place instead of d
|
||||
Task 1: fix round 1/5 (2 addressed, 0 open — ERESETBRANCH verifies the worktree really landed on the feature branch; ENOBASE verifies the base ref before ANY mutation, with a test proving the worktree and its dirty files are untouched on that path; commits 436adfa..03a6e63)
|
||||
Task 1: complete (commits 7195741..03a6e63, review clean) — 801 server tests pass, every commit through the pre-commit gate
|
||||
|
||||
Task 2: review — spec met on the surface, 2 Critical underneath. (a) the migration probed only `kind` while adding four columns, so a crash after the first ALTER left three columns permanently missing on a real install — the plan's own fault; (b) `kind` was validated in createLane only, so updateLane could silently corrupt the boundary that decides whether CCAM may delete a directory. Plus 3 Important (lock Map never pruned, two missing tests).
|
||||
Task 2: review — spec met on the surface, 2 Critical underneath. (a) the migration probed only `kind` while adding four columns, so a crash after the first ALTER left three columns permanently missing on a real install — the plan's own fault; (b) `kind` was validated in createLane only, so updateLane could silently corrupt the boundary that decides whether AgentHub may delete a directory. Plus 3 Important (lock Map never pruned, two missing tests).
|
||||
Task 2: fix round 1/5 (5 addressed, 0 open — per-column independent probes that self-heal a partial migration; shared validateKind() used by both create and update, rejecting before any write so a mixed patch cannot half-apply; lock entry deleted when its chain settles if still current; regression tests for update-kind, per-lane (not global) locking, and an old-schema database plus a simulated mid-migration crash; commits fdf4da4..6138944)
|
||||
Task 2: complete (commits 03a6e63..6138944, review clean) — 808 server tests pass
|
||||
|
||||
@@ -33,7 +33,7 @@ Task 5: complete (commits cf8ebbf..cac0d2a, review clean) — 834 server tests,
|
||||
|
||||
Task 6: written by Codex terra, whose process was killed by a wall-clock limit TWICE before it could run a single test or commit — the work survived staged. A Claude subagent then read the staged diff, verified it and committed it unchanged (00fc9fa). So the code reached review having never been run by its author; the reviewer was told that and given permission to run the CLI test file itself.
|
||||
Task 6: review — no Critical, no Important. Verified field-by-field that the CLI's LANE_PREFLIGHT_FIELDS matches the server's expectedFields exactly and that `expect` is built from the freshly fetched preflight (not client-guessed); `--yes` cannot be bypassed (returns with exit 1 before any POST is constructed); `--force` only ever unlocks the unpushed gate; the adopted-lane refusal is plain language and its test asserts the file's CONTENTS survive; no spawnSync, no bare sleeps, no vacuous tests. Reviewer independently ran the CLI suite: 10/10.
|
||||
Task 6: minor (deferred): `ccam help` shows `[--yes]` while the prose shows it unbracketed; no test covers the ESTALE 409 or the provisioning failed/timeout print paths; the destructive tests share a mutable `managedLane` across describe blocks.
|
||||
Task 6: minor (deferred): `agenthub help` shows `[--yes]` while the prose shows it unbracketed; no test covers the ESTALE 409 or the provisioning failed/timeout print paths; the destructive tests share a mutable `managedLane` across describe blocks.
|
||||
Task 6: complete (commits cac0d2a..00fc9fa, review clean) — 839 server tests
|
||||
|
||||
Task 7: Codex wrote the component work and was killed by a wall-clock limit a third time, leaving it uncommitted and missing both the modal tests and all the documentation; a Claude agent audited it, wrote those, and committed (2a58f5a).
|
||||
@@ -45,13 +45,13 @@ Task 7: complete (commits 00fc9fa..2a58f5a, 1 Critical carried into C8) — 287
|
||||
|
||||
Task 8 (added after Task 7's Critical): implemented by a Claude subagent. Root-cause fix rather than a patch — `blocked[]` had been conflating three kinds of thing, so it now carries only the action-preventing conditions (adopted, missing, unreadable) plus `unpushed-commits` (the one Force overrides), while purely informational facts (`no-remote`) moved to a new `warnings[]`. The modal needed no logic change at all: it was locked out purely because the server mislabelled `no-remote` as blocking. Also landed: status.* i18n keys for all four locales, real support for LANE_BASE_BRANCH and LANE_BRANCH_PREFIX (the spec had promised them, the code had hardcoded main and feat/), CLI tests for the ESTALE print path and for failed provisioning, and `--yes` presented as mandatory everywhere.
|
||||
Task 8: review — APPROVE, no Critical or Important. Reviewer enumerated every blocked[] combination the server can emit and confirmed the modal and the server now agree on all eight; ran the ESTALE test three times (12/12 each, ~300 ms) and confirmed it is structural rather than timing-based, since GET /preflight takes no lock while POST does; verified the env-var tests actually set the variables and observe their values rather than asserting a default that would pass anyway; fact-checked the docs against the code and found no inaccurate sentence.
|
||||
Task 8: minor (deferred, for final-review triage): `lanes.status` is not validated at the API layer — `POST /:id/stage` and `ccam stage --status <s>` accept an arbitrary string, so a rogue value would reproduce the raw-key badge bug this task fixed. The robust fix is a t() defaultValue in LaneCard rather than restricting what an agent may declare.
|
||||
Task 8: minor (deferred, for final-review triage): `lanes.status` is not validated at the API layer — `POST /:id/stage` and `agenthub stage --status <s>` accept an arbitrary string, so a rogue value would reproduce the raw-key badge bug this task fixed. The robust fix is a t() defaultValue in LaneCard rather than restricting what an agent may declare.
|
||||
Task 8: complete (commits 2a58f5a..3139de9, review clean) — 844 server tests, 292 client tests
|
||||
|
||||
FINAL whole-branch review (Opus): READY WITH FIXES — 2 Critical, 6 Important, 9 Minor. Safety core sound: every caller of resetWorktree/removeWorktree/purgeLaneSessions/deleteLane enumerated across server/, bin/, scripts/, mcp/ (the CLI goes over HTTP, so nothing outside the routes reaches the git layer), the guard sits INSIDE both destructive functions rather than only upstream, and the migration was verified against real SQLite including a simulated mid-migration crash.
|
||||
FINAL: the reviewer earned its verdict by mutation — it deleted `await assertDestroyable(lane)` from removeWorktree and all 844 tests stayed green, proving the remove-path guard was pinned by nothing. The same deletion in resetWorktree failed a test.
|
||||
FINAL: controller raised the reviewer's Important I3 to Critical — `cwd LIKE ? || '/%'` was unescaped and `_` is a LIKE wildcard, while every managed lane directory this branch creates is named `<repo>__<slug>`. A lane at /root/myrepo__feat-foo purged sessions belonging to /root/myrepoXXfeat-foo, and the preflight counted the victims too, so the confirmation was consistently wrong rather than detectably wrong. The old fixture (/tmp/wt-purge) had no underscore, so nothing could have caught it.
|
||||
Fix wave (one commit, 513235a, Opus): all 16 findings addressed, +997/-151 across 28 files, 844→857 server tests, 292→297 client tests. Deletion experiments run and reported for C2, C3, I1 and I2. The fixer also found a FOURTH instance of "the UI refusing what the server permits" in bin/ccam.js and disclosed it rather than fixing it silently.
|
||||
Fix wave (one commit, 513235a, Opus): all 16 findings addressed, +997/-151 across 28 files, 844→857 server tests, 292→297 client tests. Deletion experiments run and reported for C2, C3, I1 and I2. The fixer also found a FOURTH instance of "the UI refusing what the server permits" in bin/agenthub.js and disclosed it rather than fixing it silently.
|
||||
Fix-wave re-review (Opus): READY. Verified 857/857 itself, re-ran the C2 mutation in a throwaway copy, and built the four-way table (adopted/missing/unreadable/no-remote × reset/remove/purge × modal/server/CLI) — no square where the UI is stricter than the server, no third instance of the defect.
|
||||
FINAL: the re-reviewer found what neither the fix wave nor I had: a FOURTH refusal shape for C2 that git does NOT incidentally refuse — an adopted lane legitimately pointing at a worktree the user registered themselves. With the guard removed it destroyed the directory silently with no error at all. Check 1 is the sole protection for that case, so the guard was load-bearing well beyond the three shapes the tests cover.
|
||||
FINAL: it also disproved the fix wave's own reasoning on `unreadable` + `remove` by reproducing a corrupt-.git worktree: `git worktree remove --force` and even `--force --force` both refuse (code 128), so the promise written into `destructive.notice.unreadable` in all four locales and into two doc lines — that removal is forced and git's entry cleared — is FALSE for that shape. Decision to unblock stands (blocking recreates the defect class), but the copy over-promises.
|
||||
|
||||
@@ -147,12 +147,12 @@ produces zero diff against the currently committed file.
|
||||
previously the only two mutating lane routes without it.
|
||||
|
||||
Checked both callers before changing anything:
|
||||
- `bin/ccam.js`'s `post()` helper (backing `ccam lanes add` and `ccam stage`)
|
||||
- `bin/agenthub.js`'s `post()` helper (backing `agenthub lanes add` and `agenthub stage`)
|
||||
only ever sends a `Content-Type` header, never `Origin` or `Referer` — the
|
||||
guard passes any request with no Origin header through unconditionally
|
||||
(same rule already relied on by every other guarded lane route). Verified
|
||||
end to end by re-running `server/__tests__/lanes-cli.test.js` (13/13) after
|
||||
the change — both `ccam lanes add --repo` and the destructive-lifecycle
|
||||
the change — both `agenthub lanes add --repo` and the destructive-lifecycle
|
||||
CLI flows (which call `stage` indirectly via `clear`) still pass.
|
||||
- `grep -rn "api/lanes" mcp/ scripts/` — no hits. Nothing else calls these
|
||||
routes.
|
||||
@@ -191,7 +191,7 @@ changed).
|
||||
corrupt-`.git` case, so it was not inaccurate and editing it would have
|
||||
been unrequested scope.
|
||||
- Did not add a CLI-level test reproducing the corrupt-`.git` removal
|
||||
through `ccam lanes remove` — the brief asked for "a test proving (a)"
|
||||
through `agenthub lanes remove` — the brief asked for "a test proving (a)"
|
||||
which the new `worktree.test.js` case already does directly against
|
||||
`removeWorktree` (the same function the route and CLI both call); adding a
|
||||
second, slower end-to-end CLI version would duplicate coverage without
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Give CCAM a durable "lane" entity (one per working directory / agent) plus a Shipyard-style pipeline map that shows which stage and which gates each Claude Code agent has finished, with per-lane liveness and start/stop/resume control.
|
||||
**Goal:** Give AgentHub a durable "lane" entity (one per working directory / agent) plus a Shipyard-style pipeline map that shows which stage and which gates each Claude Code agent has finished, with per-lane liveness and start/stop/resume control.
|
||||
|
||||
**Architecture:** Approach A — CCAM stays a monitor + control panel; it does NOT orchestrate. The Claude session itself drives its pipeline and *declares* its stage through `ccam stage <name>` (a new CLI subcommand hitting `POST /api/lanes/:id/stage`), exactly the way Shipyard's skills call `bin/state.sh N set stage=…`. A lane is bound to a session automatically by longest-prefix match on the hook payload's `cwd`, so un-instrumented sessions still appear (stage inferred from `workflows.phases` / TodoWrite, never invented). Pipeline shape is a JSON template on disk, not hardcoded coordinates.
|
||||
**Architecture:** Approach A — AgentHub stays a monitor + control panel; it does NOT orchestrate. The Claude session itself drives its pipeline and *declares* its stage through `agenthub stage <name>` (a new CLI subcommand hitting `POST /api/lanes/:id/stage`), exactly the way Shipyard's skills call `bin/state.sh N set stage=…`. A lane is bound to a session automatically by longest-prefix match on the hook payload's `cwd`, so un-instrumented sessions still appear (stage inferred from `workflows.phases` / TodoWrite, never invented). Pipeline shape is a JSON template on disk, not hardcoded coordinates.
|
||||
|
||||
**Tech Stack:** Node 18+, Express, better-sqlite3, `node:test` (server), React 18 + TypeScript + Vite + Tailwind, Vitest + Testing Library (client), existing `server/websocket.js` broadcast.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Fork lives at `~/MyDrive/Projects/ResearchAndDevelopment/ccam-lanes`, remote `upstream` = `https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor`, forked at `94bfdde` (2026-07-26). Keep the fork private; never push to `upstream`.
|
||||
- Fork lives at `~/MyDrive/Projects/ResearchAndDevelopment/agenthub-lanes`, remote `upstream` = `https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor`, forked at `94bfdde` (2026-07-26). Keep the fork private; never push to `upstream`.
|
||||
- Every `.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css` file created or modified MUST start with a file overview block plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. This is enforced by `bash .claude/skills/file-headers/scripts/check-headers.sh` whose `AUTHOR_MARK` was rewritten to this name on 2026-07-27 (fork is internal, never published). Do not substitute a different author line. - Preserve existing behavior. Additive schema only: new tables and `ALTER TABLE … ADD COLUMN` guarded by a `try { SELECT col } catch { ALTER }` probe, matching `server/db.js:412-418`.
|
||||
- WebSocket message types are append-only. Add `lane_update`; do not rename or repurpose existing types.
|
||||
- The hook ingest path must stay fail-safe and non-blocking: any lane logic added to `server/routes/hooks.js` runs inside `try { … } catch { /* never block a hook */ }`.
|
||||
@@ -40,7 +40,7 @@
|
||||
- `server/index.js` — mount `app.use("/api/lanes", lanesRouter)`.
|
||||
- `server/routes/hooks.js` — bind session→lane by `cwd`; set `needs_action` on `Notification`.
|
||||
- `server/routes/run.js` — export the same-origin guard for reuse (no behavior change).
|
||||
- `bin/ccam.js` — `ccam stage` and `ccam lanes` subcommands.
|
||||
- `bin/agenthub.js` — `agenthub stage` and `agenthub lanes` subcommands.
|
||||
- `client/src/lib/api.ts` — `api.lanes.*` + `Lane` / `PipelineNode` / `LaneNodeState` types.
|
||||
- `client/src/App.tsx` — `/lanes` route.
|
||||
- `client/src/components/Sidebar.tsx` — nav entry.
|
||||
@@ -1054,19 +1054,19 @@ git commit -m "feat(lanes): bind sessions to lanes by cwd and surface needs-you"
|
||||
|
||||
---
|
||||
|
||||
## Task 5: `ccam stage` / `ccam lanes` CLI
|
||||
## Task 5: `agenthub stage` / `agenthub lanes` CLI
|
||||
|
||||
This is what a skill or a `CLAUDE.md` rule actually calls. A CLI subcommand — not an MCP tool — because `bin/ccam.js` already exists, needs no TypeScript build, and any agent can run it with Bash.
|
||||
This is what a skill or a `CLAUDE.md` rule actually calls. A CLI subcommand — not an MCP tool — because `bin/agenthub.js` already exists, needs no TypeScript build, and any agent can run it with Bash.
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js` (command switch at `bin/ccam.js:2272-2330`, help text at `bin/ccam.js:1589`)
|
||||
- Modify: `bin/agenthub.js` (command switch at `bin/agenthub.js:2272-2330`, help text at `bin/agenthub.js:1589`)
|
||||
- Test: `server/__tests__/lanes-cli.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GET/POST /api/lanes` from Task 3, through the helpers `bin/ccam.js` already defines at `bin/ccam.js:191-192` — `get(pathname)` and `post(pathname, body)`, both wrapping `api()` (`bin/ccam.js:166`). Do not add a second HTTP client. `api()` already exits 1 with a red `✖ METHOD path → message` line on a non-2xx, so error paths below only handle the cases it does not cover.
|
||||
- Consumes: `GET/POST /api/lanes` from Task 3, through the helpers `bin/agenthub.js` already defines at `bin/agenthub.js:191-192` — `get(pathname)` and `post(pathname, body)`, both wrapping `api()` (`bin/agenthub.js:166`). Do not add a second HTTP client. `api()` already exits 1 with a red `✖ METHOD path → message` line on a non-2xx, so error paths below only handle the cases it does not cover.
|
||||
- Produces:
|
||||
- `ccam lanes` — table of lanes: id, title, stage, status, liveness, progress.
|
||||
- `ccam stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]` — resolves the lane from `--lane`, else `--cwd`, else `process.cwd()`; exits 1 with a clear message when no lane owns that path.
|
||||
- `agenthub lanes` — table of lanes: id, title, stage, status, liveness, progress.
|
||||
- `agenthub stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]` — resolves the lane from `--lane`, else `--cwd`, else `process.cwd()`; exits 1 with a clear message when no lane owns that path.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
@@ -1074,7 +1074,7 @@ Create `server/__tests__/lanes-cli.test.js`:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @file Tests for the `ccam stage` / `ccam lanes` CLI subcommands: lane
|
||||
* @file Tests for the `agenthub stage` / `agenthub lanes` CLI subcommands: lane
|
||||
* resolution from the current directory, the stage round-trip through the HTTP
|
||||
* API, and the non-zero exit when the cwd belongs to no lane.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -1095,8 +1095,8 @@ process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "ccam.js");
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "agenthub.js");
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
@@ -1155,7 +1155,7 @@ after(() => {
|
||||
fs.rmSync(LANE_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("ccam stage", () => {
|
||||
describe("agenthub stage", () => {
|
||||
it("reports a stage for the lane owning the current directory", () => {
|
||||
const r = cli(["stage", "review", "--evidence", "3 findings"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -1175,21 +1175,21 @@ describe("ccam stage", () => {
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `node --test server/__tests__/lanes-cli.test.js`
|
||||
Expected: FAIL — `ccam stage` is an unknown command.
|
||||
Expected: FAIL — `agenthub stage` is an unknown command.
|
||||
|
||||
- [ ] **Step 3: Implement the subcommands**
|
||||
|
||||
In `bin/ccam.js`, add two command functions alongside `cmdOpen` (`bin/ccam.js:1450`), using the file's existing request helper and output formatter rather than a new one:
|
||||
In `bin/agenthub.js`, add two command functions alongside `cmdOpen` (`bin/agenthub.js:1450`), using the file's existing request helper and output formatter rather than a new one:
|
||||
|
||||
```js
|
||||
/**
|
||||
* `ccam lanes` — one row per lane: what it is, where it is in its pipeline, and
|
||||
* `agenthub lanes` — one row per lane: what it is, where it is in its pipeline, and
|
||||
* whether the driving session is still breathing.
|
||||
*/
|
||||
async function cmdLanes() {
|
||||
const { lanes, counts } = await get("/api/lanes");
|
||||
if (!lanes.length) {
|
||||
console.log("no lanes yet — create one with: ccam lanes add --cwd <path> --title <text>");
|
||||
console.log("no lanes yet — create one with: agenthub lanes add --cwd <path> --title <text>");
|
||||
return;
|
||||
}
|
||||
for (const l of lanes) {
|
||||
@@ -1206,14 +1206,14 @@ async function cmdLanes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* `ccam stage <stage> [flags]` — the lane equivalent of Shipyard's
|
||||
* `agenthub stage <stage> [flags]` — the lane equivalent of Shipyard's
|
||||
* `state.sh N set stage=…`. A skill calls this at each phase boundary so the
|
||||
* dashboard shows a declared stage instead of an inferred one.
|
||||
*/
|
||||
async function cmdStage(args) {
|
||||
const stage = args[0];
|
||||
if (!stage || stage.startsWith("--")) {
|
||||
console.error("usage: ccam stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]");
|
||||
console.error("usage: agenthub stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -1230,7 +1230,7 @@ async function cmdStage(args) {
|
||||
.filter((l) => cwd === l.cwd || cwd.startsWith(`${l.cwd}/`))
|
||||
.sort((a, b) => b.cwd.length - a.cwd.length)[0];
|
||||
if (!match) {
|
||||
console.error(`no lane owns ${cwd} — create one with: ccam lanes add --cwd ${cwd}`);
|
||||
console.error(`no lane owns ${cwd} — create one with: agenthub lanes add --cwd ${cwd}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -1248,7 +1248,7 @@ async function cmdStage(args) {
|
||||
}
|
||||
```
|
||||
|
||||
Wire them into the command switch (`bin/ccam.js:2272`), matching the surrounding `case` style:
|
||||
Wire them into the command switch (`bin/agenthub.js:2272`), matching the surrounding `case` style:
|
||||
|
||||
```js
|
||||
case "lanes":
|
||||
@@ -1257,14 +1257,14 @@ Wire them into the command switch (`bin/ccam.js:2272`), matching the surrounding
|
||||
return cmdStage(rest);
|
||||
```
|
||||
|
||||
Add both to `cmdHelp()` (`bin/ccam.js:1589`) in the existing help layout:
|
||||
Add both to `cmdHelp()` (`bin/agenthub.js:1589`) in the existing help layout:
|
||||
|
||||
```
|
||||
lanes List lanes with stage, liveness and progress
|
||||
stage <stage> [flags] Report the current pipeline stage for a lane
|
||||
```
|
||||
|
||||
`get` and `post` are the module-level helpers already defined at `bin/ccam.js:191-192`; both are in scope for any command function in that file.
|
||||
`get` and `post` are the module-level helpers already defined at `bin/agenthub.js:191-192`; both are in scope for any command function in that file.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
@@ -1274,8 +1274,8 @@ Expected: PASS, 2 tests.
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js server/__tests__/lanes-cli.test.js
|
||||
git commit -m "feat(lanes): ccam stage and ccam lanes subcommands"
|
||||
git add bin/agenthub.js server/__tests__/lanes-cli.test.js
|
||||
git commit -m "feat(lanes): agenthub stage and agenthub lanes subcommands"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1801,7 +1801,7 @@ export default function Lanes() {
|
||||
|
||||
{!lanes.length && (
|
||||
<p className="text-sm text-neutral-500">
|
||||
No lanes yet. Create one from a working directory: <code>ccam lanes add --cwd $(pwd)</code>
|
||||
No lanes yet. Create one from a working directory: <code>agenthub lanes add --cwd $(pwd)</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1850,7 +1850,7 @@ The repo's own rules require docs to move with behavior. This is one task, not a
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/LANES.md`
|
||||
- Modify: `CLAUDE.md` (a `## Lanes` section: what a lane is, that CCAM does not orchestrate, and that stage reporting is the agent's job via `ccam stage`)
|
||||
- Modify: `CLAUDE.md` (a `## Lanes` section: what a lane is, that AgentHub does not orchestrate, and that stage reporting is the agent's job via `agenthub stage`)
|
||||
- Modify: `README.md` (one paragraph + the two new CLI commands in the command list)
|
||||
|
||||
**Interfaces:**
|
||||
@@ -1859,7 +1859,7 @@ The repo's own rules require docs to move with behavior. This is one task, not a
|
||||
|
||||
- [ ] **Step 1: Write `docs/LANES.md`**
|
||||
|
||||
It must cover, with exact paths and runnable commands: what a lane is and why it is keyed by `cwd` rather than `session_id`; the five node states and specifically why `passed-no-evidence` is amber rather than green; how a skill reports a stage (`ccam stage review --evidence "3 findings"`); how to add a custom pipeline template (`DASHBOARD_PIPELINES_DIR`, the JSON shape from `server/data/pipelines/default.json`); the liveness rule (`LANE_DEAD_SEC`, why a silent watcher is dead but a silent idle lane is not); and an explicit statement that CCAM does not chain, queue, retry, or gate — the driving Claude session does.
|
||||
It must cover, with exact paths and runnable commands: what a lane is and why it is keyed by `cwd` rather than `session_id`; the five node states and specifically why `passed-no-evidence` is amber rather than green; how a skill reports a stage (`agenthub stage review --evidence "3 findings"`); how to add a custom pipeline template (`DASHBOARD_PIPELINES_DIR`, the JSON shape from `server/data/pipelines/default.json`); the liveness rule (`LANE_DEAD_SEC`, why a silent watcher is dead but a silent idle lane is not); and an explicit statement that AgentHub does not chain, queue, retry, or gate — the driving Claude session does.
|
||||
|
||||
- [ ] **Step 2: Update `CLAUDE.md` and `README.md`**
|
||||
|
||||
@@ -1878,5 +1878,5 @@ Named here so a later reader knows these were decided, not forgotten:
|
||||
|
||||
- **Worktree / port / database provisioning per lane.** Shipyard's `bin/lane-up.sh` territory. A lane here is just a directory that already exists.
|
||||
- **Any orchestrator** — no queue, no step chaining, no retry, no gate evaluation. Approach A: adding one would create a second scheduler competing with the Claude session that is already driving.
|
||||
- **An MCP `report_stage` tool.** `ccam stage` covers it with no TypeScript build step. Add the MCP tool later only if a session turns out to reach for tools more reliably than Bash.
|
||||
- **An MCP `report_stage` tool.** `agenthub stage` covers it with no TypeScript build step. Add the MCP tool later only if a session turns out to reach for tools more reliably than Bash.
|
||||
- **Stage inference from `TodoWrite` / `workflows.phases` for un-instrumented sessions.** Worth doing, but it is a distinct feature (inferred vs declared stage, with the UI distinguishing them) and belongs in its own plan once declared stages are proven.
|
||||
|
||||
@@ -97,11 +97,11 @@ Pure logic, no DB, no HTTP. Everything else depends on its shape.
|
||||
|
||||
## Task 5 (B5): docs, CLI surface, and the honesty pass
|
||||
|
||||
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/openapi-extra/lanes.js`, `openapi.yaml` (regenerated), `server/README.md`, `bin/ccam.js`, `server/__tests__/lanes-cli.test.js`.
|
||||
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/openapi-extra/lanes.js`, `openapi.yaml` (regenerated), `server/README.md`, `bin/agenthub.js`, `server/__tests__/lanes-cli.test.js`.
|
||||
|
||||
**Produces:** `ccam lanes` gains a column or suffix showing the inferred stage when it leads the declared one. `docs/LANES.md` gains a Stage detection section stating: what signals are read; that rules live in the template and how to add one via `DASHBOARD_PIPELINES_DIR`; that detection is forward-only; that declared beats detected; and — prominently — **that an inferred stage never counts as evidence and never renders as done**, with the reason. `docs/API.md` and the OpenAPI fragment document the three new payload fields.
|
||||
**Produces:** `agenthub lanes` gains a column or suffix showing the inferred stage when it leads the declared one. `docs/LANES.md` gains a Stage detection section stating: what signals are read; that rules live in the template and how to add one via `DASHBOARD_PIPELINES_DIR`; that detection is forward-only; that declared beats detected; and — prominently — **that an inferred stage never counts as evidence and never renders as done**, with the reason. `docs/API.md` and the OpenAPI fragment document the three new payload fields.
|
||||
|
||||
- [ ] **Step 1: write the failing CLI test:** `ccam lanes` prints the inferred stage for a lane whose detection leads its declaration, and does not print one when the declaration leads.
|
||||
- [ ] **Step 1: write the failing CLI test:** `agenthub lanes` prints the inferred stage for a lane whose detection leads its declaration, and does not print one when the declaration leads.
|
||||
- [ ] **Step 2: run, confirm it fails.**
|
||||
- [ ] **Step 3: implement the CLI change and write the docs.** Every rule you document must match `server/data/pipelines/default.json` exactly — read the file, do not recall it.
|
||||
- [ ] **Step 4:** `npm run test:server`, `npm run test:client`, `node scripts/generate-openapi-yaml.js` then confirm `git diff openapi.yaml` is empty.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- Every `.js/.ts/.tsx` file created or modified MUST start with a file overview comment plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. Verify with `bash .claude/skills/file-headers/scripts/check-headers.sh` (must exit 0).
|
||||
- **Tasks A1-A3 are pure moves.** No behaviour may change, no logic may be "improved" in passing. The existing Run tests must pass with changes to import paths ONLY. If a test needs a real edit to keep passing, stop and report it — that means the move was not pure.
|
||||
- **Extraction and composition never share a commit.** A1, A2, A3 are refactors; A5 builds the page.
|
||||
- **The console never writes a lane's stage.** No code path from the console may call `POST /:id/stage`. Only `ccam stage` declares; only detection infers.
|
||||
- **The console never writes a lane's stage.** No code path from the console may call `POST /:id/stage`. Only `agenthub stage` declares; only detection infers.
|
||||
- Schema changes are additive with a per-column probe (`try { SELECT col } catch { ALTER }`).
|
||||
- Preserve existing behavior: `POST /api/run` and every existing WebSocket message type keep working exactly as they do — the CLI and other callers depend on them. `lane_update` stays the only lane WS type.
|
||||
- Server CommonJS; client React + TypeScript. No new npm dependencies. Exact-value assertions; bounded polling, no bare sleeps. i18n strings in all four locales (`en`, `zh`, `vi`, `ko`), genuinely translated.
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
**Files:** Modify `docs/LANES.md`, `docs/API.md`, `server/README.md`, `ARCHITECTURE.md`, `README.md`, `server/openapi-extra/lanes.js` (+ regenerate `openapi.yaml`), `CLAUDE.md`.
|
||||
|
||||
**Produces:** documentation of the merged page and the new seams: that `/lanes` redirects to `/run`; that the UI starts runs through the lane while `POST /api/run` remains for the CLI; the `ensure` endpoint and when the UI calls it; `dashboard_runs.lane_id`; that a finished run releases its lane. `CLAUDE.md` gains the rule: **the console never writes a lane's stage — declared comes from `ccam stage`, inferred from detection.** Every path and command you print must exist; verify each.
|
||||
**Produces:** documentation of the merged page and the new seams: that `/lanes` redirects to `/run`; that the UI starts runs through the lane while `POST /api/run` remains for the CLI; the `ensure` endpoint and when the UI calls it; `dashboard_runs.lane_id`; that a finished run releases its lane. `CLAUDE.md` gains the rule: **the console never writes a lane's stage — declared comes from `agenthub stage`, inferred from detection.** Every path and command you print must exist; verify each.
|
||||
|
||||
- [ ] **Step 1:** write the docs.
|
||||
- [ ] **Step 2:** verify every referenced file, route and command exists (`ls`, `grep`, or run it).
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Let a lane own a git worktree that CCAM creates, resets, removes and purges, with every destructive action gated on counted facts and on three independent safety checks.
|
||||
**Goal:** Let a lane own a git worktree that AgentHub creates, resets, removes and purges, with every destructive action gated on counted facts and on three independent safety checks.
|
||||
|
||||
**Architecture:** Design doc: `docs/superpowers/specs/2026-07-28-worktree-lanes-design.md` — read it once before Task 1. All git work goes through one module (`server/lib/worktree.js`) that shells out with `execFile` and an argv array, never a shell string, and re-verifies its own safety preconditions. Lanes gain a `kind` of `adopted` (pointer at a directory the user already had — never destroyable) or `managed` (a worktree CCAM created — destroyable). Destructive actions are serialised per lane and preceded by a preflight endpoint that returns counts, which the confirmation UI renders and the server re-checks before acting.
|
||||
**Architecture:** Design doc: `docs/superpowers/specs/2026-07-28-worktree-lanes-design.md` — read it once before Task 1. All git work goes through one module (`server/lib/worktree.js`) that shells out with `execFile` and an argv array, never a shell string, and re-verifies its own safety preconditions. Lanes gain a `kind` of `adopted` (pointer at a directory the user already had — never destroyable) or `managed` (a worktree AgentHub created — destroyable). Destructive actions are serialised per lane and preceded by a preflight endpoint that returns counts, which the confirmation UI renders and the server re-checks before acting.
|
||||
|
||||
**Tech Stack:** Node 18+, Express, better-sqlite3, `node:child_process.execFile`, real `git` against temp-directory fixtures, `node:test` (server), React 18 + TypeScript + Vitest (client).
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
- `server/db.js` — four additive columns.
|
||||
- `server/lib/lanes.js` — `kind`/`source_repo`/`base_branch`/`slug` in create/patch/payload; `purgeLaneSessions`.
|
||||
- `server/routes/lanes.js` — `POST /worktree`, `GET /:id/preflight`, `reset` + `purge` actions, lock usage.
|
||||
- `bin/ccam.js` — `ccam lanes add --repo`, `ccam lanes reset|remove|purge`.
|
||||
- `bin/agenthub.js` — `agenthub lanes add --repo`, `agenthub lanes reset|remove|purge`.
|
||||
- `client/src/lib/api.ts`, `client/src/lib/types.ts` — preflight + worktree types and calls.
|
||||
- `client/src/components/lanes/LaneCard.tsx` — kind badge; destructive buttons only for `managed`.
|
||||
- `docs/LANES.md`, `CLAUDE.md` — the new lifecycle.
|
||||
@@ -53,7 +53,7 @@
|
||||
**Interfaces:**
|
||||
- Consumes: nothing from earlier tasks.
|
||||
- Produces:
|
||||
- `LANES_ROOT` — `process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "ccam-lanes")`
|
||||
- `LANES_ROOT` — `process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "agenthub-lanes")`
|
||||
- `git(cwd, args): Promise<{stdout, stderr}>` — rejects with `err.git = {args, code, stderr}` on non-zero
|
||||
- `isGitRepo(dir): Promise<boolean>`
|
||||
- `resolveBase(sourceRepo, wanted): Promise<string>` — `origin/<wanted>` → `<wanted>` → current HEAD
|
||||
@@ -87,7 +87,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-wt-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-wt-"));
|
||||
process.env.LANES_ROOT = path.join(ROOT, "lanes");
|
||||
|
||||
const wt = require("../lib/worktree");
|
||||
@@ -327,7 +327,7 @@ Expected: FAIL — `Cannot find module '../lib/lane-lock'`.
|
||||
In `server/db.js`, after the `lanes` table and its index, following the probe pattern at `server/db.js:412-418`:
|
||||
|
||||
```js
|
||||
// Managed lanes own a git worktree CCAM created and may be destroyed; adopted
|
||||
// Managed lanes own a git worktree AgentHub created and may be destroyed; adopted
|
||||
// lanes merely point at a directory the user already had and never may be.
|
||||
// Existing rows default to 'adopted', so no lane gains a destructive path by
|
||||
// upgrading.
|
||||
@@ -536,18 +536,18 @@ git commit -m "feat(lanes): reset, remove and purge with preflight and stale-sta
|
||||
## Task 6: CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
- Test: `server/__tests__/lanes-cli.test.js` (append)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the routes from Tasks 3-5, via the existing `get` / `post` helpers (`bin/ccam.js:191-192`).
|
||||
- Produces: `ccam lanes add --repo <path> [--title <t>] [--base <branch>]` (worktree mode; the existing `--cwd` form still adopts); `ccam lanes reset|remove|purge <id> [--force]`, each printing the preflight table and refusing without `--yes`.
|
||||
- Consumes: the routes from Tasks 3-5, via the existing `get` / `post` helpers (`bin/agenthub.js:191-192`).
|
||||
- Produces: `agenthub lanes add --repo <path> [--title <t>] [--base <branch>]` (worktree mode; the existing `--cwd` form still adopts); `agenthub lanes reset|remove|purge <id> [--force]`, each printing the preflight table and refusing without `--yes`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests** (append) — worktree add via CLI lands a managed lane; `reset` without `--yes` exits non-zero and changes nothing; `--yes` performs it.
|
||||
- [ ] **Step 2: Run to verify they fail.**
|
||||
- [ ] **Step 3: Implement**, reusing the async `cli()` harness and the existing flag reader. Print the preflight counts as a small aligned table before asking for `--yes`, so the terminal path has the same "confirm against numbers" property as the UI.
|
||||
- [ ] **Step 4: Run to verify they pass.**
|
||||
- [ ] **Step 5: Commit** — `feat(lanes): ccam lanes add --repo, reset, remove, purge`
|
||||
- [ ] **Step 5: Commit** — `feat(lanes): agenthub lanes add --repo, reset, remove, purge`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ never posts a stage.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Tickets, preview-port links, per-lane credentials, and the `agents`/`creds` buttons from the reference screen — CCAM has no data behind any of them.
|
||||
- Tickets, preview-port links, per-lane credentials, and the `agents`/`creds` buttons from the reference screen — AgentHub has no data behind any of them.
|
||||
- Any change to `PipelineMap`'s node-state logic, the destroy guard, or the preflight contract.
|
||||
- Inferring `done` or a gate outcome. Still forbidden, TTL or not.
|
||||
- Re-styling any page other than `/run`.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `ccam lanes profile init <repo>` detects a Node.js project (single-service or backend+frontend monorepo) and scaffolds a working `.ccam/profile/`; `ccam lanes profile check [<path>]` validates one before it's trusted.
|
||||
**Goal:** `agenthub lanes profile init <repo>` detects a Node.js project (single-service or backend+frontend monorepo) and scaffolds a working `.agenthub/profile/`; `agenthub lanes profile check [<path>]` validates one before it's trusted.
|
||||
|
||||
**Architecture:** One new library module (`server/lib/lane-detect.js`) with three pure functions — `detectNode` (read-only filesystem inspection), `scaffoldProfile` (writes `.ccam/profile/**`), `checkProfile` (read-only validator) — plus two new CLI subcommands in `bin/ccam.js`. No new HTTP route, no database, no lane row required.
|
||||
**Architecture:** One new library module (`server/lib/lane-detect.js`) with three pure functions — `detectNode` (read-only filesystem inspection), `scaffoldProfile` (writes `.agenthub/profile/**`), `checkProfile` (read-only validator) — plus two new CLI subcommands in `bin/agenthub.js`. No new HTTP route, no database, no lane row required.
|
||||
|
||||
**Tech Stack:** Node.js, `js-yaml` (already a project dependency, used here for the first time to parse `docker-compose.yml`), `node:test` + `node:assert/strict` (matches every other `server/__tests__/*.test.js` in this repo).
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- **Shell-identifier validator:** any detected name (npm script name, docker-compose service name) written into generated **shell-script text** (not a `profile.env` `KEY=VALUE` line — those are already safe, parsed-never-sourced) must match `/^[\w.:-]+$/` first. A name that fails is treated as "not found" for that line.
|
||||
- **TODO hook bodies always `exit 0`.** A scaffold with an unresolved TODO must still be bootable; `checkProfile` is the hard gate, not the hook itself.
|
||||
- Node.js single preset only. No Python/Go/Ruby, no ORM/migration-tool detection, no pnpm/yarn/turborepo workspace layouts, no HTTP route, no `--fix` mode. If you find yourself building any of those, stop — it's out of scope, see the spec's Non-goals.
|
||||
- Run `npm run test:server` before considering any task done — this repo's whole `server/__tests__/*.test.js` suite must stay green (there are 3 pre-existing, unrelated `ccam-cli.test.js` color-detection failures in this sandbox — confirmed via `git stash` — don't chase those).
|
||||
- Run `npm run test:server` before considering any task done — this repo's whole `server/__tests__/*.test.js` suite must stay green (there are 3 pre-existing, unrelated `agenthub-cli.test.js` color-detection failures in this sandbox — confirmed via `git stash` — don't chase those).
|
||||
|
||||
---
|
||||
|
||||
@@ -63,13 +63,13 @@ const fs = require("node:fs");
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-detect-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-detect-"));
|
||||
// Must be set before the first require of anything that transitively loads
|
||||
// server/lib/secrets.js (lane-detect.js does, for checkProfile) — SECRETS_PATH
|
||||
// is bound once at that module's first load, and without this a test run on a
|
||||
// machine with a real ~/.ccam/secrets.env would read that file instead of a
|
||||
// machine with a real ~/.agenthub/secrets.env would read that file instead of a
|
||||
// deterministic, isolated one.
|
||||
process.env.CCAM_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.AGENTHUB_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
@@ -172,7 +172,7 @@ Create `server/lib/lane-detect.js`:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @file Node.js project detection and `.ccam/profile/` scaffolding (A3).
|
||||
* @file Node.js project detection and `.agenthub/profile/` scaffolding (A3).
|
||||
* `detectNode` reads a repository's filesystem (package.json layout,
|
||||
* docker-compose.yml) without executing anything; `scaffoldProfile` turns
|
||||
* those facts into a working profile; `checkProfile` validates one. Only a
|
||||
@@ -504,7 +504,7 @@ git commit -m "feat(lanes): detect docker-compose database/Redis and wire ENV_RE
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `scaffoldProfile` — write `.ccam/profile/`
|
||||
### Task 3: `scaffoldProfile` — write `.agenthub/profile/`
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/lib/lane-detect.js`
|
||||
@@ -513,7 +513,7 @@ git commit -m "feat(lanes): detect docker-compose database/Redis and wire ENV_RE
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `detectNode`'s facts object (Task 1+2 shape) and `IDENTIFIER_RE`.
|
||||
- Produces: `scaffoldProfile(repoPath, facts, options = {})` → `{ written: string[], todos: string[] }`. `options.force` (boolean) allows overwriting an existing `.ccam/profile/profile.env`. Throws `Object.assign(new Error(...), { code: "EPROFILEEXISTS" })` when one exists and `force` is not `true`.
|
||||
- Produces: `scaffoldProfile(repoPath, facts, options = {})` → `{ written: string[], todos: string[] }`. `options.force` (boolean) allows overwriting an existing `.agenthub/profile/profile.env`. Throws `Object.assign(new Error(...), { code: "EPROFILEEXISTS" })` when one exists and `force` is not `true`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
@@ -528,7 +528,7 @@ describe("scaffoldProfile", () => {
|
||||
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
|
||||
const profileDir = path.join(repo, ".ccam", "profile");
|
||||
const profileDir = path.join(repo, ".agenthub", "profile");
|
||||
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
|
||||
assert.match(env, /PORTS="app"/);
|
||||
assert.match(env, /PORT_BASE_app=3000/);
|
||||
@@ -557,7 +557,7 @@ describe("scaffoldProfile", () => {
|
||||
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
|
||||
const profileDir = path.join(repo, ".ccam", "profile");
|
||||
const profileDir = path.join(repo, ".agenthub", "profile");
|
||||
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
|
||||
assert.match(env, /DB_PREFIX=/);
|
||||
assert.match(env, /REDIS=1/);
|
||||
@@ -583,7 +583,7 @@ describe("scaffoldProfile", () => {
|
||||
writePkg(repo, { test: "jest" }); // no start, no dev
|
||||
const facts = { layout: "single-service", services: { app: { dir: ".", script: null } }, database: null, redis: false, env: null };
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
const boot = fs.readFileSync(path.join(repo, ".ccam", "profile", "hooks", "boot.sh"), "utf8");
|
||||
const boot = fs.readFileSync(path.join(repo, ".agenthub", "profile", "hooks", "boot.sh"), "utf8");
|
||||
assert.match(boot, /# TODO: no start\/dev script found/);
|
||||
assert.equal(result.todos.length, 1);
|
||||
});
|
||||
@@ -652,7 +652,7 @@ function bootLine(name, service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold `.ccam/profile/` under `repoPath` from `detectNode`'s facts.
|
||||
* Scaffold `.agenthub/profile/` under `repoPath` from `detectNode`'s facts.
|
||||
* Refuses to overwrite an existing `profile.env` unless `options.force`.
|
||||
*
|
||||
* @param {string} repoPath - Absolute repository path.
|
||||
@@ -661,7 +661,7 @@ function bootLine(name, service) {
|
||||
* @returns {{written: string[], todos: string[]}}
|
||||
*/
|
||||
function scaffoldProfile(repoPath, facts, options = {}) {
|
||||
const profileDir = path.join(repoPath, ".ccam", "profile");
|
||||
const profileDir = path.join(repoPath, ".agenthub", "profile");
|
||||
const envPath = path.join(profileDir, "profile.env");
|
||||
if (fs.existsSync(envPath) && !options.force) {
|
||||
throw Object.assign(new Error(`profile already exists at ${envPath}`), {
|
||||
@@ -768,7 +768,7 @@ Expected: pass (no new applicable files created this task — `db-create.sh`/`db
|
||||
|
||||
```bash
|
||||
git add server/lib/lane-detect.js server/__tests__/lane-detect.test.js
|
||||
git commit -m "feat(lanes): scaffold .ccam/profile/ from detected Node facts (A3)"
|
||||
git commit -m "feat(lanes): scaffold .agenthub/profile/ from detected Node facts (A3)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -781,7 +781,7 @@ git commit -m "feat(lanes): scaffold .ccam/profile/ from detected Node facts (A3
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `server/lib/lane-profile.js:parseEnvFile` (already exported), `server/lib/ports.js:isListening` (already exported, async), `server/lib/secrets.js:SECRETS_PATH` (already exported).
|
||||
- Produces: `async checkProfile(dir)` → `{ ok: boolean, errors: string[], warnings: string[] }`. `dir` is a directory that may contain `.ccam/profile/` directly, OR may already point at a `.ccam/profile/` directory itself — check `.ccam/profile/profile.env` first, then `profile.env` directly, so `checkProfile(repoPath)` and `checkProfile(profileDir)` both work.
|
||||
- Produces: `async checkProfile(dir)` → `{ ok: boolean, errors: string[], warnings: string[] }`. `dir` is a directory that may contain `.agenthub/profile/` directly, OR may already point at a `.agenthub/profile/` directory itself — check `.agenthub/profile/profile.env` first, then `profile.env` directly, so `checkProfile(repoPath)` and `checkProfile(profileDir)` both work.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
@@ -832,7 +832,7 @@ describe("checkProfile", () => {
|
||||
const repo = makeRepo();
|
||||
writePkg(repo, { start: "node index.js" });
|
||||
detect.scaffoldProfile(repo, detect.detectNode(repo));
|
||||
fs.rmSync(path.join(repo, ".ccam", "profile", "hooks", "health.sh"));
|
||||
fs.rmSync(path.join(repo, ".agenthub", "profile", "hooks", "health.sh"));
|
||||
const result = await detect.checkProfile(repo);
|
||||
assert.equal(result.ok, false);
|
||||
assert.ok(result.errors.some((e) => /health/.test(e)));
|
||||
@@ -844,7 +844,7 @@ describe("checkProfile", () => {
|
||||
writeCompose(repo, " postgres:\n image: postgres:16\n");
|
||||
detect.scaffoldProfile(repo, detect.detectNode(repo));
|
||||
// Remove the migrate/seed TODO lines so only the secrets warning is left to check.
|
||||
const hooksDir = path.join(repo, ".ccam", "profile", "hooks");
|
||||
const hooksDir = path.join(repo, ".agenthub", "profile", "hooks");
|
||||
fs.writeFileSync(path.join(hooksDir, "migrate.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
||||
fs.writeFileSync(path.join(hooksDir, "seed.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
||||
const result = await detect.checkProfile(repo);
|
||||
@@ -883,7 +883,7 @@ function containsTodo(text) {
|
||||
/**
|
||||
* Validate a scaffolded (or hand-written) profile. Read-only: never mutates
|
||||
* anything, never spawns a hook. `dir` may be a repo root (profile at
|
||||
* `.ccam/profile/`) or a `.ccam/profile/` directory itself.
|
||||
* `.agenthub/profile/`) or a `.agenthub/profile/` directory itself.
|
||||
*
|
||||
* @param {string} dir
|
||||
* @returns {Promise<{ok: boolean, errors: string[], warnings: string[]}>}
|
||||
@@ -892,7 +892,7 @@ async function checkProfile(dir) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const candidates = [path.join(dir, ".ccam", "profile"), dir];
|
||||
const candidates = [path.join(dir, ".agenthub", "profile"), dir];
|
||||
const profileDir = candidates.find((p) => fs.existsSync(path.join(p, "profile.env")));
|
||||
if (!profileDir) {
|
||||
return { ok: false, errors: [`no profile.env found under ${dir}`], warnings: [] };
|
||||
@@ -962,10 +962,10 @@ git commit -m "feat(lanes): validate a scaffolded profile with checkProfile (A3)
|
||||
|
||||
---
|
||||
|
||||
### Task 5: CLI — `ccam lanes profile init` / `ccam lanes profile check`
|
||||
### Task 5: CLI — `agenthub lanes profile init` / `agenthub lanes profile check`
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
- Test: manual (this repo's CLI tests spawn the real binary against a live/offline server; profile init/check are pure-filesystem and don't need the server — verified by hand per this task's steps, folded into Task 6's fixtures for automated coverage)
|
||||
|
||||
**Interfaces:**
|
||||
@@ -973,19 +973,19 @@ git commit -m "feat(lanes): validate a scaffolded profile with checkProfile (A3)
|
||||
|
||||
- [ ] **Step 1: Add the command implementations**
|
||||
|
||||
In `bin/ccam.js`, near `cmdLanesAdd` (search for that function), add:
|
||||
In `bin/agenthub.js`, near `cmdLanesAdd` (search for that function), add:
|
||||
|
||||
```js
|
||||
/**
|
||||
* `ccam lanes profile init <repo>` — detect a Node.js project and scaffold
|
||||
* `.ccam/profile/`. Pure filesystem action against the SOURCE repo; does not
|
||||
* `agenthub lanes profile init <repo>` — detect a Node.js project and scaffold
|
||||
* `.agenthub/profile/`. Pure filesystem action against the SOURCE repo; does not
|
||||
* talk to the dashboard server at all.
|
||||
*/
|
||||
function cmdLanesProfileInit(args) {
|
||||
const repo = args.find((arg) => !arg.startsWith("--"));
|
||||
const force = args.includes("--force");
|
||||
if (!repo) {
|
||||
console.error("usage: ccam lanes profile init <repo> [--force]");
|
||||
console.error("usage: agenthub lanes profile init <repo> [--force]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -1003,7 +1003,7 @@ function cmdLanesProfileInit(args) {
|
||||
`✖ No detectable Node.js project at ${resolved} (looked for backend/package.json +\n` +
|
||||
" frontend/package.json, or a root package.json).\n" +
|
||||
" Auto-scaffolding currently supports Node.js repos in that layout only.\n" +
|
||||
" Write .ccam/profile/ by hand — see docs/LANES.md."
|
||||
" Write .agenthub/profile/ by hand — see docs/LANES.md."
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -1021,17 +1021,17 @@ function cmdLanesProfileInit(args) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log(`${c.green("✔")} Scaffolded .ccam/profile/ at ${resolved} (${facts.layout})`);
|
||||
console.log(`${c.green("✔")} Scaffolded .agenthub/profile/ at ${resolved} (${facts.layout})`);
|
||||
for (const file of result.written) console.log(` wrote ${file}`);
|
||||
if (result.todos.length) {
|
||||
console.log(`\n${c.yellow(`⚠ ${result.todos.length} item(s) need manual attention:`)}`);
|
||||
for (const todo of result.todos) console.log(` ${todo}`);
|
||||
}
|
||||
console.log(`\nNext: ccam lanes profile check ${repo}`);
|
||||
console.log(`\nNext: agenthub lanes profile check ${repo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* `ccam lanes profile check [<path>]` — validate a profile without needing a
|
||||
* `agenthub lanes profile check [<path>]` — validate a profile without needing a
|
||||
* lane to exist for it yet. Defaults to the current directory, NOT lane-id
|
||||
* resolution (unlike every other `lanes` subcommand) — this is meant to run
|
||||
* against a bare repo right after `profile init`.
|
||||
@@ -1055,7 +1055,7 @@ async function cmdLanesProfileCheck(args) {
|
||||
|
||||
- [ ] **Step 2: Wire the subcommand dispatch**
|
||||
|
||||
In `bin/ccam.js`, the dispatcher (`const [cmd, ...rest] = argv;` around line 2695) has this exact `case "lanes":` block around line 2755 — find it with `grep -n 'case "lanes"' bin/ccam.js`:
|
||||
In `bin/agenthub.js`, the dispatcher (`const [cmd, ...rest] = argv;` around line 2695) has this exact `case "lanes":` block around line 2755 — find it with `grep -n 'case "lanes"' bin/agenthub.js`:
|
||||
|
||||
```js
|
||||
case "lanes":
|
||||
@@ -1082,7 +1082,7 @@ Replace it with (adds one `if` block for `profile`, everything else byte-identic
|
||||
if (rest[1] === "init") return cmdLanesProfileInit(rest.slice(2));
|
||||
if (rest[1] === "check") return cmdLanesProfileCheck(rest.slice(2));
|
||||
console.error(
|
||||
"usage: ccam lanes profile init <repo> [--force] | ccam lanes profile check [<path>]"
|
||||
"usage: agenthub lanes profile init <repo> [--force] | agenthub lanes profile check [<path>]"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -1098,13 +1098,13 @@ Replace it with (adds one `if` block for `profile`, everything else byte-identic
|
||||
|
||||
- [ ] **Step 3: Add the help-table entries**
|
||||
|
||||
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes runtime"`), add two rows right after the `lanes add --repo` row:
|
||||
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes runtime"`), add two rows right after the `lanes add --repo` row:
|
||||
|
||||
```js
|
||||
[
|
||||
"lanes profile init",
|
||||
"<repo> [--force]",
|
||||
"Detect a Node.js project and scaffold .ccam/profile/",
|
||||
"Detect a Node.js project and scaffold .agenthub/profile/",
|
||||
],
|
||||
[
|
||||
"lanes profile check",
|
||||
@@ -1116,30 +1116,30 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group (search for `["lanes
|
||||
- [ ] **Step 4: Manual smoke test**
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/ccam-smoke/backend /tmp/ccam-smoke/frontend
|
||||
echo '{"scripts":{"start":"node -e \"require(\\\"http\\\").createServer((_,r)=>r.end(\\\"ok\\\")).listen(process.env.PORT||8000)\"}}' > /tmp/ccam-smoke/backend/package.json
|
||||
echo '{"scripts":{"dev":"true"}}' > /tmp/ccam-smoke/frontend/package.json
|
||||
node bin/ccam.js lanes profile init /tmp/ccam-smoke
|
||||
node bin/ccam.js lanes profile check /tmp/ccam-smoke
|
||||
mkdir -p /tmp/agenthub-smoke/backend /tmp/agenthub-smoke/frontend
|
||||
echo '{"scripts":{"start":"node -e \"require(\\\"http\\\").createServer((_,r)=>r.end(\\\"ok\\\")).listen(process.env.PORT||8000)\"}}' > /tmp/agenthub-smoke/backend/package.json
|
||||
echo '{"scripts":{"dev":"true"}}' > /tmp/agenthub-smoke/frontend/package.json
|
||||
node bin/agenthub.js lanes profile init /tmp/agenthub-smoke
|
||||
node bin/agenthub.js lanes profile check /tmp/agenthub-smoke
|
||||
```
|
||||
|
||||
Expected: `init` prints `✔ Scaffolded ...`, no TODOs listed (both packages have runnable scripts, no database detected); `check` prints `✔ profile at ... looks good`, exit 0.
|
||||
|
||||
```bash
|
||||
node bin/ccam.js lanes profile init /tmp/ccam-smoke
|
||||
node bin/agenthub.js lanes profile init /tmp/agenthub-smoke
|
||||
```
|
||||
|
||||
Expected (run again without `--force`): `✖ profile already exists ... — pass --force to overwrite it.`, exit 1.
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/ccam-smoke
|
||||
rm -rf /tmp/agenthub-smoke
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes profile init/check CLI (A3)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes profile init/check CLI (A3)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1222,7 +1222,7 @@ Expected: PASS (29 tests total)
|
||||
- [ ] **Step 4: Run the full server suite**
|
||||
|
||||
Run: `npm run test:server`
|
||||
Expected: green, aside from the 3 pre-existing unrelated `ccam-cli.test.js` failures noted in Global Constraints.
|
||||
Expected: green, aside from the 3 pre-existing unrelated `agenthub-cli.test.js` failures noted in Global Constraints.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
@@ -1251,24 +1251,24 @@ Find the `### Declaring a profile` section (search for that heading). Immediatel
|
||||
### Scaffolding a profile automatically
|
||||
|
||||
```bash
|
||||
ccam lanes profile init <repo> [--force] # detect + write .ccam/profile/
|
||||
ccam lanes profile check [<path>] # validate one (path defaults to cwd)
|
||||
agenthub lanes profile init <repo> [--force] # detect + write .agenthub/profile/
|
||||
agenthub lanes profile check [<path>] # validate one (path defaults to cwd)
|
||||
```
|
||||
|
||||
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.ccam/profile/` by hand for those, following the reference above.
|
||||
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.agenthub/profile/` by hand for those, following the reference above.
|
||||
|
||||
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.ccam/secrets.env` doesn't exist yet for a database-declaring profile.
|
||||
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.agenthub/secrets.env` doesn't exist yet for a database-declaring profile.
|
||||
|
||||
`profile check` takes a **path**, not a lane id or `--lane`/`--cwd` flag like every other `lanes` subcommand — it is meant to run against a bare repository right after `init`, before any lane or worktree exists for it.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `docs/CLI.md`**
|
||||
|
||||
Find the `### Lanes` table (search for `ccam lanes reset`). Add two rows right after the `ccam lanes add --repo ...` row:
|
||||
Find the `### Lanes` table (search for `agenthub lanes reset`). Add two rows right after the `agenthub lanes add --repo ...` row:
|
||||
|
||||
```markdown
|
||||
| `ccam lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.ccam/profile/`. Refuses to overwrite an existing one without `--force` |
|
||||
| `ccam lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
|
||||
| `agenthub lanes profile init <repo> [--force]` | Detect a Node.js project (single-service or backend+frontend monorepo) and scaffold `.agenthub/profile/`. Refuses to overwrite an existing one without `--force` |
|
||||
| `agenthub lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `ARCHITECTURE.md`**
|
||||
@@ -1276,7 +1276,7 @@ Find the `### Lanes` table (search for `ccam lanes reset`). Add two rows right a
|
||||
Find the `lib/lane-slots.js` row in the module responsibility table (search for that exact string) and add a new row directly after the `lib/lane-runtime.js` row:
|
||||
|
||||
```markdown
|
||||
| `lib/lane-detect.js` | (A3) Node.js project detection and `.ccam/profile/` scaffolding. `detectNode` reads `package.json` layout (root, or `backend/`+`frontend/`) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`) — read-only, never executes anything. Any detected name written into GENERATED SHELL TEXT (an npm script name, a compose service name) must pass a strict identifier check first; a `profile.env` value is already safe regardless, since that file is parsed, never sourced. `scaffoldProfile` writes the profile, always leaving `migrate`/`seed` as an `exit 0` TODO stub when a database is found rather than guessing a migration tool. `checkProfile` is the read-only hard gate: unresolved `TODO:`, a missing/non-executable hook, or a port already in use all fail it; a missing `~/.ccam/secrets.env` only warns |
|
||||
| `lib/lane-detect.js` | (A3) Node.js project detection and `.agenthub/profile/` scaffolding. `detectNode` reads `package.json` layout (root, or `backend/`+`frontend/`) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`) — read-only, never executes anything. Any detected name written into GENERATED SHELL TEXT (an npm script name, a compose service name) must pass a strict identifier check first; a `profile.env` value is already safe regardless, since that file is parsed, never sourced. `scaffoldProfile` writes the profile, always leaving `migrate`/`seed` as an `exit 0` TODO stub when a database is found rather than guessing a migration tool. `checkProfile` is the read-only hard gate: unresolved `TODO:`, a missing/non-executable hook, or a port already in use all fail it; a missing `~/.agenthub/secrets.env` only warns |
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Mark A3 done in the parent plan**
|
||||
@@ -1290,11 +1290,11 @@ bash .claude/skills/file-headers/scripts/check-headers.sh
|
||||
npm run test:server
|
||||
```
|
||||
|
||||
Expected: header check passes; full suite green apart from the 3 known-unrelated `ccam-cli.test.js` failures.
|
||||
Expected: header check passes; full suite green apart from the 3 known-unrelated `agenthub-cli.test.js` failures.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/LANES.md docs/CLI.md ARCHITECTURE.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
|
||||
git commit -m "docs(lanes): document ccam lanes profile init/check (A3)"
|
||||
git commit -m "docs(lanes): document agenthub lanes profile init/check (A3)"
|
||||
```
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Shipyard parity for CCAM lanes — full plan
|
||||
# Shipyard parity for AgentHub lanes — full plan
|
||||
|
||||
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`
|
||||
(the "Parallel Feature Harness"). Every capability below traces to a real file
|
||||
there, so each port can be checked against the original rather than a memory of it.
|
||||
|
||||
**Goal:** bring the whole harness into CCAM — isolated per-lane runtime and data,
|
||||
**Goal:** bring the whole harness into AgentHub — isolated per-lane runtime and data,
|
||||
per-feature state with archive, proof gallery, cross-lane locks, the
|
||||
`ship-feature` pipeline with its QC agents, and the optional integrations.
|
||||
|
||||
@@ -45,11 +45,11 @@ spec → plan → implement cycle; this document is the map, not a substitute.
|
||||
|
||||
Carried from `CLAUDE.md` and proven in A1. Re-check each at review time.
|
||||
|
||||
1. **CCAM does not orchestrate.** It offers primitives; the session sequences them.
|
||||
1. **AgentHub does not orchestrate.** It offers primitives; the session sequences them.
|
||||
No chaining, no queue, no retry, no gate evaluation.
|
||||
2. **The runtime never writes `stage`, `status` or `notes`** — only allocation
|
||||
facts. `status=running` means an agent is working, not that a server listens.
|
||||
3. **Liveness is computed, never stored.** Anything CCAM does not control
|
||||
3. **Liveness is computed, never stored.** Anything AgentHub does not control
|
||||
(a process, a port, a deploy) is re-derived on read.
|
||||
4. **Never build a shell command as a string.** `execFile`/`spawn` with argv
|
||||
arrays. Hooks are the deliberate exception and are spawned with a fixed argv.
|
||||
@@ -74,22 +74,22 @@ sharing state. This is the half of Shipyard's isolation A1 did not cover.
|
||||
|
||||
| Question | Proposed answer | Why |
|
||||
|---|---|---|
|
||||
| Who creates the database? | A `db-create.sh` / `db-drop.sh` **hook** (allowlist already exists in A1) | CCAM must not assume Postgres. `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ; the template ships Shipyard's docker-compose Postgres commands so a Postgres user gets parity by copying. |
|
||||
| Who rewrites `.env`? | **CCAM**, not a hook | Mechanical and identical across stacks: parse `KEY=VALUE`, replace a declared set, keep the rest. Pushing it to every repo would duplicate ~30 lines and re-lose the `JWT_SECRET` lesson below in each. |
|
||||
| Where do database credentials live? | `~/.ccam/secrets.env`, mode `0600` | Machine-level, not repo-level (Shipyard's `config/secrets.env`). **Never** returned by any route and never logged; `GET /runtime` may report key *names* present, never values. |
|
||||
| Who creates the database? | A `db-create.sh` / `db-drop.sh` **hook** (allowlist already exists in A1) | AgentHub must not assume Postgres. `createdb` vs `mysqladmin create` vs `touch foo.db` genuinely differ; the template ships Shipyard's docker-compose Postgres commands so a Postgres user gets parity by copying. |
|
||||
| Who rewrites `.env`? | **AgentHub**, not a hook | Mechanical and identical across stacks: parse `KEY=VALUE`, replace a declared set, keep the rest. Pushing it to every repo would duplicate ~30 lines and re-lose the `JWT_SECRET` lesson below in each. |
|
||||
| Where do database credentials live? | `~/.agenthub/secrets.env`, mode `0600` | Machine-level, not repo-level (Shipyard's `config/secrets.env`). **Never** returned by any route and never logged; `GET /runtime` may report key *names* present, never values. |
|
||||
| Does a lane without a database break? | No | `DB_PREFIX` empty (the default) means "no per-lane database": no name allocated, no hook called, no dead code. Same for `REDIS=0`. |
|
||||
|
||||
### Profile additions
|
||||
|
||||
```bash
|
||||
# .ccam/profile/profile.env (all optional; empty = feature off)
|
||||
# .agenthub/profile/profile.env (all optional; empty = feature off)
|
||||
DB_PREFIX="myapp_l" # lane in slot 3 -> myapp_l3 ; empty = no per-lane DB
|
||||
DB_KIND="postgres" # informational for A3's scaffolder; A2 gates on DB_PREFIX
|
||||
DB_URL_SCHEME="postgresql" # DATABASE_URL scheme
|
||||
REDIS=1 # 1 = allocate logical index = slot
|
||||
ENV_FILES="backend/.env" # files to seed, relative to the lane
|
||||
ENV_SOURCE="backend/.env" # source path in the source repo (defaults to ENV_FILES)
|
||||
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys CCAM overwrites per lane
|
||||
ENV_REWRITE="DATABASE_URL REDIS_URL UPLOAD_DIR" # keys AgentHub overwrites per lane
|
||||
ENV_PRESERVE="JWT_SECRET" # keys kept from the lane's EXISTING file on a --force refresh
|
||||
UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
```
|
||||
@@ -99,7 +99,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
|
||||
### Steps
|
||||
|
||||
1. **`server/lib/secrets.js`** — read `~/.ccam/secrets.env` with the A1 parser
|
||||
1. **`server/lib/secrets.js`** — read `~/.agenthub/secrets.env` with the A1 parser
|
||||
(`lane-profile.js:parseEnvFile`, reuse it). Warn once and continue when the
|
||||
file is absent; refuse to load it when its mode is group/world-readable.
|
||||
2. **`server/lib/lane-slots.js`** — extend `slotFacts`: `dbName`,
|
||||
@@ -119,7 +119,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
calling the `db-create` / `db-drop` hooks.
|
||||
- `dropDatabase` runs `assertManaged` **and** asserts
|
||||
`DB_NAME === slotFacts(lane.slot).dbName` before spawning anything. Only a
|
||||
name CCAM derived can ever be dropped; nothing from a request reaches it.
|
||||
name AgentHub derived can ever be dropped; nothing from a request reaches it.
|
||||
5. **Wire into the lifecycle** (`lane-runtime.js`, `routes/lanes.js`):
|
||||
- *provision* → allocate slot → `seedEnv` → `bootstrap` → `db-create` →
|
||||
`migrate` → `seed`
|
||||
@@ -133,7 +133,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
`reset`/`remove` will drop, and the confirm dialog echoes it back like the
|
||||
existing counted facts. Dropping a database is the most destructive thing this
|
||||
whole roadmap adds; it gets the same echo-or-refuse contract as the rest.
|
||||
7. **CLI** — `ccam lanes reset --keep-db`; `ccam lanes runtime` gains the DB name
|
||||
7. **CLI** — `agenthub lanes reset --keep-db`; `agenthub lanes runtime` gains the DB name
|
||||
and Redis index rows.
|
||||
8. **Profile template** — `server/data/profile-templates/postgres-compose/` with
|
||||
`db-create.sh` / `db-drop.sh` holding Shipyard's exact
|
||||
@@ -148,7 +148,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
back to `.env.example` and warns.
|
||||
- `dropDatabase` with a tampered `DB_NAME` throws **before** spawning.
|
||||
- `seedEnv` and `dropDatabase` both throw `ENOTMANAGED` on an adopted lane.
|
||||
- Secrets: a `0644` `~/.ccam/secrets.env` is refused; `GET /runtime` response
|
||||
- Secrets: a `0644` `~/.agenthub/secrets.env` is refused; `GET /runtime` response
|
||||
contains no secret value (assert on the serialized JSON).
|
||||
- Two lanes up at once, each writing its own database, neither seeing the other's
|
||||
rows — the actual point of the phase.
|
||||
@@ -165,7 +165,7 @@ UPLOAD_SUBDIR="backend/data/uploads" # exported as UPLOAD_DIR
|
||||
|
||||
## A3 — Detection and scaffolding
|
||||
|
||||
**Goal:** `ccam lanes profile init <repo>` writes a working `.ccam/profile/`, so
|
||||
**Goal:** `agenthub lanes profile init <repo>` writes a working `.agenthub/profile/`, so
|
||||
adopting a repo is one command instead of nine hand-written hooks. This is the
|
||||
gap that left Shipyard's own `profiles/` empty.
|
||||
|
||||
@@ -179,18 +179,18 @@ reading them.
|
||||
`requirements.txt` (`django`/`fastapi`), `manage.py`, `alembic.ini`,
|
||||
`prisma/schema.prisma`, `go.mod`, `Gemfile`.
|
||||
- **Scaffold, don't interpret.** Detection writes a concrete, readable
|
||||
`.ccam/profile/` the user owns and edits. The runtime never re-detects at boot;
|
||||
a wrong guess is fixed by editing a file, not by changing CCAM.
|
||||
`.agenthub/profile/` the user owns and edits. The runtime never re-detects at boot;
|
||||
a wrong guess is fixed by editing a file, not by changing AgentHub.
|
||||
- Unknown values are written as explicit `TODO:` markers rather than plausible
|
||||
defaults — a wrong default that boots something is worse than a refusal.
|
||||
- **`ccam lanes profile check`** — the `harness-doctor` equivalent: profile
|
||||
- **`agenthub lanes profile check`** — the `harness-doctor` equivalent: profile
|
||||
parses, declared hooks exist and are executable, no `TODO:` left, declared
|
||||
ports are free, `secrets.env` has what the declared database needs. Must exit
|
||||
non-zero on any of these.
|
||||
- Templates in `server/data/profile-templates/<preset>/`.
|
||||
|
||||
**Verify:** a fixture repo per preset scaffolds, `profile check` passes, and
|
||||
`ccam lanes up` boots it — the same end-to-end proof A1 used, once per preset.
|
||||
`agenthub lanes up` boots it — the same end-to-end proof A1 used, once per preset.
|
||||
|
||||
---
|
||||
|
||||
@@ -216,7 +216,7 @@ keeps every past feature browsable.
|
||||
slug back** so callers store what the server stored. Reuse
|
||||
`worktree.js:slugify` only if its output is identical; otherwise a separate
|
||||
function with its own test. Do not let the two drift.
|
||||
- Routes `GET/POST /api/lanes/:id/features…`; CLI `ccam feature list|activate|show`.
|
||||
- Routes `GET/POST /api/lanes/:id/features…`; CLI `agenthub feature list|activate|show`.
|
||||
- UI: a feature picker that swaps the detail panel to an archived snapshot while
|
||||
the lane keeps running.
|
||||
|
||||
@@ -266,7 +266,7 @@ work *within one lane, within one process*. This is the other axis and must be a
|
||||
- **Port the etiquette text into `docs/LANES.md`.** Waiting is normal; never kill
|
||||
a holder, never delete the lock directory, never shrink `LOCK_MAX_HOLD`. Agents
|
||||
read docs, and this is the rule they break.
|
||||
- `GET /api/locks`, `ccam lock status|acquire|release`, and a lock indicator on
|
||||
- `GET /api/locks`, `agenthub lock status|acquire|release`, and a lock indicator on
|
||||
the lane card so a human can see *why* a lane is sitting still.
|
||||
|
||||
**Verify:** second acquire blocks then succeeds after release; a holder backdated
|
||||
@@ -281,32 +281,32 @@ with `LOCK_MAX_HOLD=1`; release from a non-holder is refused and leaves the lock
|
||||
|
||||
**Progress:** pipeline template + skill text (E1) done 2026-08-04 — see `docs/superpowers/specs/2026-08-04-ship-feature-skill-design.md`. `sync-base` (E2) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-sync-base-design.md`. `qc-local` + `senior-gate-reviewer` agents (E3) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-agents-port-design.md`. `ticketer`/`dev-qc`/`pr-reviewer` and F's integrations remain.
|
||||
|
||||
**This is where "CCAM does not orchestrate" is preserved by construction:** the
|
||||
skill runs in the session and calls `ccam` commands; the dashboard still only
|
||||
**This is where "AgentHub does not orchestrate" is preserved by construction:** the
|
||||
skill runs in the session and calls `agenthub` commands; the dashboard still only
|
||||
records.
|
||||
|
||||
- **Pipeline template** `server/data/pipelines/ship-feature.json` — the stages
|
||||
with aliases and `detect` rules in the existing node format. `default.json`
|
||||
untouched; a lane opts in via `pipeline`.
|
||||
- **Skill** `.claude/skills/ship-feature-lane/SKILL.md` — every `"$HARNESS/bin/…"`
|
||||
call rewritten to its `ccam` equivalent (`state.sh set` → `ccam stage`;
|
||||
`activate` → `ccam feature activate`; `lane-ci-gate.sh` → `ccam lanes hook
|
||||
ci-gate`; `lane-up.sh --qc` → `ccam lanes up --qc`; the `.harness-lane` marker
|
||||
disappears because CCAM already resolves a lane from `cwd`).
|
||||
call rewritten to its `agenthub` equivalent (`state.sh set` → `agenthub stage`;
|
||||
`activate` → `agenthub feature activate`; `lane-ci-gate.sh` → `agenthub lanes hook
|
||||
ci-gate`; `lane-up.sh --qc` → `agenthub lanes up --qc`; the `.harness-lane` marker
|
||||
disappears because AgentHub already resolves a lane from `cwd`).
|
||||
- **Keep, do not trim, the hard-won rules.** Each encodes a real failure: the
|
||||
turn-liveness rule (every turn between stage 1 and done leaves a re-invoker
|
||||
pending), backgrounded-`sleep` polling, the e2e active-poll (a hung suite never
|
||||
fires its completion re-invoke), no-retry-cap/phase-clock, one-driver-per-MCP,
|
||||
never-push-base, and the test-only / localized re-entry fast paths.
|
||||
- **Drop what CCAM makes unnecessary:** the `@@HARNESS_ROOT@@` install-time
|
||||
- **Drop what AgentHub makes unnecessary:** the `@@HARNESS_ROOT@@` install-time
|
||||
placeholder and the zsh `source _common.sh` warning. Keep an
|
||||
integration-mismatch check even though its original cause is gone — silently
|
||||
skipping ticket/dev-QC while reporting success is the failure that matters.
|
||||
- **Agents** → `.claude/agents/`: `qc-local`, `dev-qc`, `senior-gate-reviewer`,
|
||||
`ticketer`, `pr-reviewer`. Shipyard generates these per lane with embedded
|
||||
credentials; port that as `ccam lanes agents install`, writing to
|
||||
credentials; port that as `agenthub lanes agents install`, writing to
|
||||
`<lane>/.claude/agents/`, git-excluded, creds at mode `0600`.
|
||||
- **`ccam lanes sync-base`** — port `lane-sync-dev.sh`. The valuable part is the
|
||||
- **`agenthub lanes sync-base`** — port `lane-sync-dev.sh`. The valuable part is the
|
||||
**migration-number collision pre-check (exit 5)**: another lane's migration
|
||||
landed on the base with the number yours uses. Generic form driven by
|
||||
`MIGRATIONS_DIR` + a filename-number pattern. Also port the keep-ours merge
|
||||
@@ -314,7 +314,7 @@ records.
|
||||
the single most common cross-lane conflict.
|
||||
|
||||
**Verify:** the skill is prose, so verification is a real dry run on a scratch
|
||||
repo walking stages 0→8, confirming every `ccam` command it names exists and
|
||||
repo walking stages 0→8, confirming every `agenthub` command it names exists and
|
||||
behaves as documented. Each of those commands gets a CLI test.
|
||||
`sync-base --check` gets a unit test with a fixture collision.
|
||||
|
||||
@@ -323,10 +323,10 @@ behaves as documented. Each of those commands gets a CLI test.
|
||||
## F — Integrations
|
||||
|
||||
**Goal:** tracker, dev-site QC, CI deploy-wait. All off by default, read from
|
||||
`<repo>/.ccam/profile/integrations.env`, exposed as `ccam lanes integration <name>`
|
||||
`<repo>/.agenthub/profile/integrations.env`, exposed as `agenthub lanes integration <name>`
|
||||
(exit 0/1) and in `GET /runtime`.
|
||||
|
||||
**Progress:** `mcp sync` (F1) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-mcp-sync-design.md`. `integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`ccam ci`), and dev-QC remain.
|
||||
**Progress:** `mcp sync` (F1) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-mcp-sync-design.md`. `integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`agenthub ci`), and dev-QC remain.
|
||||
|
||||
- **Tracker** — file one ticket per feature, idempotent (update, never
|
||||
duplicate), writing `proof/<slug>/ticket/REPORT.html`.
|
||||
@@ -334,8 +334,8 @@ behaves as documented. Each of those commands gets a CLI test.
|
||||
per-lane account; owns the `qc_dev` field; checkpoints to
|
||||
`RESULTS.partial.md` so a died agent resumes instead of restarting.
|
||||
- **CI deploy-wait** — port `ci-job.sh` (`status`/`failures`/`rerun`/`cancel`) as
|
||||
`ccam ci`, which is what makes the skill's flake-triage rule executable.
|
||||
- **`ccam lanes mcp sync`** — port `lane-mcp-sync.sh`: writes the lane's
|
||||
`agenthub ci`, which is what makes the skill's flake-triage rule executable.
|
||||
- **`agenthub lanes mcp sync`** — port `lane-mcp-sync.sh`: writes the lane's
|
||||
`.mcp.json` with Playwright servers pinned to the lane's own `--output-dir`.
|
||||
This is what stops two lanes' browser QC from sharing a profile directory.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** serialize machine-thrashing steps (builds, e2e runs) across ALL lanes with a named lock — independent of `server/lib/lane-lock.js`'s per-lane serialization, which is a different axis (one lane, one process) and must not be touched.
|
||||
|
||||
**Architecture:** A new `server/lib/named-lock.js` (mkdir-based, filesystem-atomic, no `flock` dependency) holding lock directories under `LANES_ROOT/.locks/<name>/`; a thin `server/routes/locks.js` exposing single-shot acquire/release/status (the server never blocks — "CCAM does not orchestrate" — the CLI does the waiting loop); `ccam lock status|acquire|release` CLI subcommands; a lane-card UI badge polling the same endpoint `LaneCard.tsx`'s runtime hook already polls.
|
||||
**Architecture:** A new `server/lib/named-lock.js` (mkdir-based, filesystem-atomic, no `flock` dependency) holding lock directories under `LANES_ROOT/.locks/<name>/`; a thin `server/routes/locks.js` exposing single-shot acquire/release/status (the server never blocks — "AgentHub does not orchestrate" — the CLI does the waiting loop); `agenthub lock status|acquire|release` CLI subcommands; a lane-card UI badge polling the same endpoint `LaneCard.tsx`'s runtime hook already polls.
|
||||
|
||||
**Tech Stack:** Node.js `fs` (mkdir/rmdir for atomicity — no new dependency), existing Express/route conventions, existing React hook + WebSocket broadcast conventions.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- `named-lock.js` is a **separate module** from `server/lib/lane-lock.js`. Do not touch `lane-lock.js`, do not import it, do not conflate the two axes (in-process-per-lane serialization vs cross-lane named locks).
|
||||
- **Time-based staleness with a floor.** `LOCK_MAX_HOLD` env var, default `2700` (45 min) seconds. The *effective* break threshold is `Math.max(LOCK_MAX_HOLD, 300)` — a holder can never be force-broken by setting `LOCK_MAX_HOLD` below 300 seconds. This floor is the whole point: it stops an impatient env-var edit from breaking a genuinely live holder.
|
||||
- Atomicity comes from `fs.mkdirSync` throwing `EEXIST` when the directory already exists — never a check-then-create race (`existsSync` then `mkdirSync` is NOT atomic and must not be used for the acquire decision).
|
||||
- The server-side route layer is a single-shot primitive (try once, answer immediately) — it never loops or blocks waiting for a lock to free up. The CLI's `acquire` command owns the polling loop. This mirrors the project's standing rule: CCAM offers primitives, the caller sequences them.
|
||||
- The server-side route layer is a single-shot primitive (try once, answer immediately) — it never loops or blocks waiting for a lock to free up. The CLI's `acquire` command owns the polling loop. This mirrors the project's standing rule: AgentHub offers primitives, the caller sequences them.
|
||||
- Run `npm run test:server` (full suite) and `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit — both must stay green/passing. This repo's pre-commit hook enforces the full suite (backend + frontend) already; a clean run here avoids a blocked commit.
|
||||
- Never use `git add -A`. Stage exactly the files each task names.
|
||||
|
||||
@@ -48,7 +48,7 @@ const fs = require("node:fs");
|
||||
const { describe, it, after, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-named-lock-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-named-lock-"));
|
||||
process.env.LANES_ROOT = SUITE_ROOT;
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
|
||||
@@ -216,7 +216,7 @@ function readOwner(name) {
|
||||
|
||||
/**
|
||||
* Try to acquire a named lock once. Never blocks, never retries — the caller
|
||||
* (the CLI) owns the polling loop; CCAM's server-side primitives are always
|
||||
* (the CLI) owns the polling loop; AgentHub's server-side primitives are always
|
||||
* single-shot.
|
||||
*
|
||||
* A stale holder (older than `effectiveMaxHoldSec()`) is broken automatically:
|
||||
@@ -364,7 +364,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-locks-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-locks-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -494,7 +494,7 @@ Create `server/routes/locks.js`:
|
||||
/**
|
||||
* @file HTTP surface for cross-lane named locks (`server/lib/named-lock.js`).
|
||||
* Every route is single-shot — this layer never blocks waiting for a lock to
|
||||
* free up. The CLI (`ccam lock acquire`) owns the polling loop; CCAM's
|
||||
* free up. The CLI (`agenthub lock acquire`) owns the polling loop; AgentHub's
|
||||
* server-side primitives stay non-orchestrating, same as every other lane
|
||||
* route in this project.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -578,18 +578,18 @@ git commit -m "feat(locks): expose GET/POST /api/locks over named-lock.js (D)"
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `ccam lock status|acquire|release` CLI
|
||||
### Task 3: `agenthub lock status|acquire|release` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GET /api/locks`, `POST /api/locks/:name/acquire`, `POST /api/locks/:name/release` (Task 2, already committed) via the existing `get`/`post` helpers in `bin/ccam.js`.
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`, used by `cmdLanesRuntime`/`cmdStage`) to resolve which lane is acting, when no explicit `--holder` is given.
|
||||
- Consumes: `GET /api/locks`, `POST /api/locks/:name/acquire`, `POST /api/locks/:name/release` (Task 2, already committed) via the existing `get`/`post` helpers in `bin/agenthub.js`.
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`, used by `cmdLanesRuntime`/`cmdStage`) to resolve which lane is acting, when no explicit `--holder` is given.
|
||||
|
||||
- [ ] **Step 1: Add the command implementations**
|
||||
|
||||
In `bin/ccam.js`, near `cmdLanesRuntime` (search for that function), add:
|
||||
In `bin/agenthub.js`, near `cmdLanesRuntime` (search for that function), add:
|
||||
|
||||
```js
|
||||
/**
|
||||
@@ -622,7 +622,7 @@ function fmtLockRow(lock) {
|
||||
return `${lock.name.padEnd(20)} held by ${lock.holder.padEnd(10)} for ${mins}m`;
|
||||
}
|
||||
|
||||
/** `ccam lock status [<name>]` — one lock, or every held lock. */
|
||||
/** `agenthub lock status [<name>]` — one lock, or every held lock. */
|
||||
async function cmdLockStatus(args) {
|
||||
const name = args.find((arg) => !arg.startsWith("--"));
|
||||
if (name) {
|
||||
@@ -640,7 +640,7 @@ async function cmdLockStatus(args) {
|
||||
}
|
||||
|
||||
/**
|
||||
* `ccam lock acquire <name> [--holder X] [--timeout N]` — polls until the
|
||||
* `agenthub lock acquire <name> [--holder X] [--timeout N]` — polls until the
|
||||
* lock is free (or `--timeout` seconds elapse). Prints a status line every
|
||||
* ~60s of continued waiting so a long wait never reads as a hung command —
|
||||
* this is the CLI-side "heartbeat" the design calls for; it is terminal
|
||||
@@ -649,7 +649,7 @@ async function cmdLockStatus(args) {
|
||||
async function cmdLockAcquire(args) {
|
||||
const name = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!name) {
|
||||
console.error("usage: ccam lock acquire <name> [--holder X] [--timeout seconds]");
|
||||
console.error("usage: agenthub lock acquire <name> [--holder X] [--timeout seconds]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -689,11 +689,11 @@ async function cmdLockAcquire(args) {
|
||||
}
|
||||
}
|
||||
|
||||
/** `ccam lock release <name> [--holder X]`. */
|
||||
/** `agenthub lock release <name> [--holder X]`. */
|
||||
async function cmdLockRelease(args) {
|
||||
const name = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!name) {
|
||||
console.error("usage: ccam lock release <name> [--holder X]");
|
||||
console.error("usage: agenthub lock release <name> [--holder X]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -713,7 +713,7 @@ async function cmdLockRelease(args) {
|
||||
|
||||
- [ ] **Step 2: Wire the subcommand dispatch**
|
||||
|
||||
In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new case right after the `case "lanes":` block:
|
||||
In `bin/agenthub.js`'s `runCommand` switch (search for `case "stage":`), add a new case right after the `case "lanes":` block:
|
||||
|
||||
```js
|
||||
case "lock": {
|
||||
@@ -721,7 +721,7 @@ In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new c
|
||||
if (sub === "status") return cmdLockStatus(rest.slice(1));
|
||||
if (sub === "acquire") return cmdLockAcquire(rest.slice(1));
|
||||
if (sub === "release") return cmdLockRelease(rest.slice(1));
|
||||
console.error("usage: ccam lock status [<name>] | ccam lock acquire <name> [--holder X] [--timeout N] | ccam lock release <name> [--holder X]");
|
||||
console.error("usage: agenthub lock status [<name>] | agenthub lock acquire <name> [--holder X] [--timeout N] | agenthub lock release <name> [--holder X]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -729,7 +729,7 @@ In `bin/ccam.js`'s `runCommand` switch (search for `case "stage":`), add a new c
|
||||
|
||||
- [ ] **Step 3: Add the help-table entry**
|
||||
|
||||
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the `lanes hook` row:
|
||||
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the `lanes hook` row:
|
||||
|
||||
```js
|
||||
[
|
||||
@@ -742,12 +742,12 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add right after the
|
||||
- [ ] **Step 4: Manual smoke test**
|
||||
|
||||
```bash
|
||||
ccam lock status
|
||||
ccam lock acquire smoke-test --holder test-a
|
||||
ccam lock status smoke-test
|
||||
ccam lock acquire smoke-test --holder test-b --timeout 2 # should time out, exit 1
|
||||
ccam lock release smoke-test --holder test-a
|
||||
ccam lock status smoke-test
|
||||
agenthub lock status
|
||||
agenthub lock acquire smoke-test --holder test-a
|
||||
agenthub lock status smoke-test
|
||||
agenthub lock acquire smoke-test --holder test-b --timeout 2 # should time out, exit 1
|
||||
agenthub lock release smoke-test --holder test-a
|
||||
agenthub lock status smoke-test
|
||||
```
|
||||
|
||||
Expected: first acquire succeeds; status shows it held by `test-a`; second acquire (different holder, 2s timeout) prints a timeout error and exits 1; release succeeds; final status shows free.
|
||||
@@ -755,8 +755,8 @@ Expected: first acquire succeeds; status shows it held by `test-a`; second acqui
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(locks): add ccam lock status/acquire/release CLI (D)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(locks): add agenthub lock status/acquire/release CLI (D)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -914,10 +914,10 @@ git commit -m "feat(locks): show a lock indicator on the lane card (D)"
|
||||
|
||||
- [ ] **Step 1: `docs/LANES.md`**
|
||||
|
||||
Add a new top-level section (after the "Lane actions" section, before "Orchestration: what CCAM does NOT do" — search for both headings to place it precisely) titled `## Cross-lane named locks`, covering:
|
||||
Add a new top-level section (after the "Lane actions" section, before "Orchestration: what AgentHub does NOT do" — search for both headings to place it precisely) titled `## Cross-lane named locks`, covering:
|
||||
- What they're for (serializing a machine-thrashing step like a build or e2e run across every lane, not just within one lane — the other axis from the per-lane `withLaneLock` used internally by destructive actions).
|
||||
- The `LOCK_MAX_HOLD` env var (default 2700s / 45 min) and its 300s floor — a holder can never be force-broken by setting `LOCK_MAX_HOLD` below 300, even to `1`.
|
||||
- The CLI: `ccam lock status [<name>]`, `ccam lock acquire <name> [--holder X] [--timeout N]`, `ccam lock release <name> [--holder X]` — holder defaults to `lane<slot>` for the calling lane.
|
||||
- The CLI: `agenthub lock status [<name>]`, `agenthub lock acquire <name> [--holder X] [--timeout N]`, `agenthub lock release <name> [--holder X]` — holder defaults to `lane<slot>` for the calling lane.
|
||||
- **The etiquette text, verbatim in spirit from Shipyard**: waiting for a lock is normal, not a failure; never kill a holder to "fix" a stuck wait; never delete the lock directory (`LANES_ROOT/.locks/<name>/`) by hand; never shrink `LOCK_MAX_HOLD` to force through a wait — the 300s floor exists specifically to stop that.
|
||||
- The lane card's lock badge (client-side, polled every 30s like the git/runtime facts).
|
||||
|
||||
@@ -926,9 +926,9 @@ Add a new top-level section (after the "Lane actions" section, before "Orchestra
|
||||
Add to the `### Lanes` table, after the `lanes hook` row:
|
||||
|
||||
```markdown
|
||||
| `ccam lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||
| `ccam lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
||||
| `ccam lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||
| `agenthub lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||
| `agenthub lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
||||
| `agenthub lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `docs/API.md`**
|
||||
@@ -940,7 +940,7 @@ Add a new section documenting `GET /api/locks`, `POST /api/locks/:name/acquire`,
|
||||
Add a new row to the module responsibility table (search for the `lib/lane-lock.js` row if one exists, or place alphabetically near the other `lib/lane-*` rows):
|
||||
|
||||
```markdown
|
||||
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `ccam lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
|
||||
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `agenthub lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Mark D done in the parent plan**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** a lane's history survives switching features. Today `clearLane` erases the live row's bookkeeping; this makes `clearLane` archive it first into a new `lane_features` table, and adds `activate`/`list`/`show` so a lane can carry many features across its lifetime, each independently browsable after the lane has moved on.
|
||||
|
||||
**Architecture:** One new table (`lane_features`) plus one new nullable column on `lanes` (`active_feature_id`). One new library module, `server/lib/lane-features.js`, owning slug canonicalization, archive-on-switch, and read access — the `lanes` row itself stays the single live view (nothing downstream that already reads a lane's `stage`/`status`/etc. needs to change). Routes are added inline to the existing `server/routes/lanes.js` (matching how `/:id/git`, `/:id/preflight`, `/:id/runtime` are already sub-resources of that same file, not separate routers). CLI gains `ccam feature list|activate|show`. The Workspace UI gains a read-only feature picker that swaps the detail panel to an archived snapshot — it never mutates anything, matching the standing rule that the console never writes a lane's stage.
|
||||
**Architecture:** One new table (`lane_features`) plus one new nullable column on `lanes` (`active_feature_id`). One new library module, `server/lib/lane-features.js`, owning slug canonicalization, archive-on-switch, and read access — the `lanes` row itself stays the single live view (nothing downstream that already reads a lane's `stage`/`status`/etc. needs to change). Routes are added inline to the existing `server/routes/lanes.js` (matching how `/:id/git`, `/:id/preflight`, `/:id/runtime` are already sub-resources of that same file, not separate routers). CLI gains `agenthub feature list|activate|show`. The Workspace UI gains a read-only feature picker that swaps the detail panel to an archived snapshot — it never mutates anything, matching the standing rule that the console never writes a lane's stage.
|
||||
|
||||
**Tech Stack:** better-sqlite3 (existing), Express (existing), the existing `server/lib/pipelines.js` node-state renderer reused verbatim for archived snapshots.
|
||||
|
||||
@@ -66,7 +66,7 @@ Then, as a **separate, additive migration block** right after the existing lane-
|
||||
```js
|
||||
// Migrate: per-feature state (B). `active_feature_id` points at the
|
||||
// lane_features row currently "live" (unarchived) for this lane — null for a
|
||||
// lane that has never called `ccam feature activate`, which is why this
|
||||
// lane that has never called `agenthub feature activate`, which is why this
|
||||
// column is nullable and every downstream reader of a lane row is unaffected
|
||||
// by its addition. ON DELETE SET NULL, not CASCADE: deleting the ACTIVE
|
||||
// feature row (which normally only happens via cascade when the LANE itself
|
||||
@@ -96,7 +96,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-features-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-features-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -579,7 +579,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-features-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-features-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -800,18 +800,18 @@ git commit -m "feat(lanes): expose GET/POST /api/lanes/:id/features over lane-fe
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `ccam feature list|activate|show` CLI
|
||||
### Task 4: `agenthub feature list|activate|show` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GET /api/lanes/:id/features`, `GET /api/lanes/:id/features/:slug`, `POST /api/lanes/:id/features/activate` (Task 3, already committed) via the existing `get`/`post` helpers.
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`) to resolve which lane a command targets.
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`) to resolve which lane a command targets.
|
||||
|
||||
- [ ] **Step 1: Add the command implementations**
|
||||
|
||||
In `bin/ccam.js`, near `cmdStage` (search for that function), add:
|
||||
In `bin/agenthub.js`, near `cmdStage` (search for that function), add:
|
||||
|
||||
```js
|
||||
function fmtFeatureRow(f) {
|
||||
@@ -821,23 +821,23 @@ function fmtFeatureRow(f) {
|
||||
}`;
|
||||
}
|
||||
|
||||
/** `ccam feature list [<id>] [--cwd path]` — every feature this lane has activated. */
|
||||
/** `agenthub feature list [<id>] [--cwd path]` — every feature this lane has activated. */
|
||||
async function cmdFeatureList(args) {
|
||||
const resolved = await resolveLaneArg(args);
|
||||
if (!resolved) return;
|
||||
const { features } = await get(`/api/lanes/${resolved.laneId}/features`);
|
||||
if (!features.length) {
|
||||
console.log("no features activated yet — start one with: ccam feature activate <slug>");
|
||||
console.log("no features activated yet — start one with: agenthub feature activate <slug>");
|
||||
return;
|
||||
}
|
||||
for (const f of features) console.log(fmtFeatureRow(f));
|
||||
}
|
||||
|
||||
/** `ccam feature activate <slug> [--title X] [<id>] [--cwd path]`. */
|
||||
/** `agenthub feature activate <slug> [--title X] [<id>] [--cwd path]`. */
|
||||
async function cmdFeatureActivate(args) {
|
||||
const slug = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!slug) {
|
||||
console.error("usage: ccam feature activate <slug> [--title text]");
|
||||
console.error("usage: agenthub feature activate <slug> [--title text]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -856,11 +856,11 @@ async function cmdFeatureActivate(args) {
|
||||
);
|
||||
}
|
||||
|
||||
/** `ccam feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
|
||||
/** `agenthub feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
|
||||
async function cmdFeatureShow(args) {
|
||||
const slug = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!slug) {
|
||||
console.error("usage: ccam feature show <slug>");
|
||||
console.error("usage: agenthub feature show <slug>");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -887,7 +887,7 @@ Check whether `get()` in this file already supports a third `options` argument (
|
||||
|
||||
- [ ] **Step 2: Wire the subcommand dispatch**
|
||||
|
||||
In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere among the other top-level cases, e.g. right after the `case "lock":` block added in the previous plan):
|
||||
In `bin/agenthub.js`'s `runCommand` switch, add a new case (placement: anywhere among the other top-level cases, e.g. right after the `case "lock":` block added in the previous plan):
|
||||
|
||||
```js
|
||||
case "feature": {
|
||||
@@ -895,7 +895,7 @@ In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere amon
|
||||
if (sub === "list") return cmdFeatureList(rest.slice(1));
|
||||
if (sub === "activate") return cmdFeatureActivate(rest.slice(1));
|
||||
if (sub === "show") return cmdFeatureShow(rest.slice(1));
|
||||
console.error("usage: ccam feature list | ccam feature activate <slug> [--title text] | ccam feature show <slug>");
|
||||
console.error("usage: agenthub feature list | agenthub feature activate <slug> [--title text] | agenthub feature show <slug>");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -903,7 +903,7 @@ In `bin/ccam.js`'s `runCommand` switch, add a new case (placement: anywhere amon
|
||||
|
||||
- [ ] **Step 3: Add the help-table entries**
|
||||
|
||||
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stage <stage> [flags]` row:
|
||||
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stage <stage> [flags]` row:
|
||||
|
||||
```js
|
||||
["feature list", "[<id>]", "List every feature this lane has activated, archived or live"],
|
||||
@@ -918,12 +918,12 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `stag
|
||||
- [ ] **Step 4: Manual smoke test**
|
||||
|
||||
```bash
|
||||
ccam lanes add --cwd $(pwd) --title "smoke"
|
||||
ccam feature activate one --title "First thing"
|
||||
ccam stage review --evidence "looks fine"
|
||||
ccam feature activate two --title "Second thing"
|
||||
ccam feature list
|
||||
ccam feature show one
|
||||
agenthub lanes add --cwd $(pwd) --title "smoke"
|
||||
agenthub feature activate one --title "First thing"
|
||||
agenthub stage review --evidence "looks fine"
|
||||
agenthub feature activate two --title "Second thing"
|
||||
agenthub feature list
|
||||
agenthub feature show one
|
||||
```
|
||||
|
||||
Expected: `feature list` shows `two` marked active (`▶`) and `one` marked archived with a timestamp; `feature show one` prints `stage: review` and its saved pipeline nodes, not `two`'s.
|
||||
@@ -931,8 +931,8 @@ Expected: `feature list` shows `two` marked active (`▶`) and `one` marked arch
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam feature list/activate/show CLI (B)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub feature list/activate/show CLI (B)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1130,10 +1130,10 @@ git commit -m "feat(lanes): add a read-only feature picker to the Workspace page
|
||||
|
||||
Add a new top-level section (after "Pipeline stages and the five node states", before "Stage detection" — search for both headings) titled `## Per-feature state and archive`, covering:
|
||||
- Why: `clearLane` used to erase; now a lane can carry many features across its lifetime.
|
||||
- The opt-in model: nothing changes for a lane that never calls `ccam feature activate` — `clearLane` behaves exactly as before.
|
||||
- The opt-in model: nothing changes for a lane that never calls `agenthub feature activate` — `clearLane` behaves exactly as before.
|
||||
- Slug canonicalization rule, stated exactly: drops a leading `feat/`, turns `/` and whitespace into `-`, keeps `[A-Za-z0-9._-]`, does **not** lowercase — a deliberately different rule from `worktree.js:slugify`'s branch-name slugification, and every endpoint/CLI command echoes back the canonicalized form.
|
||||
- `activate` semantics: archives the current active feature (if any and if different), restores the target's saved stage onto the live lane row (so switching back to a past feature resumes where it left off), creates a fresh feature row for a never-seen slug.
|
||||
- The CLI: `ccam feature list|activate|show`.
|
||||
- The CLI: `agenthub feature list|activate|show`.
|
||||
- The Workspace picker is **read-only** — selecting an archived feature shows its saved pipeline; it never changes the live lane, matching the standing "console never writes a lane's stage" rule.
|
||||
|
||||
- [ ] **Step 2: `docs/CLI.md`**
|
||||
@@ -1141,9 +1141,9 @@ Add a new top-level section (after "Pipeline stages and the five node states", b
|
||||
Add to the `### Lanes` table, after the `stage <stage> [flags]` row:
|
||||
|
||||
```markdown
|
||||
| `ccam feature list [<id>]` | List every feature this lane has activated, archived or live |
|
||||
| `ccam feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
|
||||
| `ccam feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
|
||||
| `agenthub feature list [<id>]` | List every feature this lane has activated, archived or live |
|
||||
| `agenthub feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
|
||||
| `agenthub feature show <slug> [<id>]` | Show one feature's saved pipeline — works on an archived one too |
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `docs/API.md`**
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** the screenshots QC agents capture become visible in the dashboard, grouped by feature (B's `lane_features` slug) and phase. Ported from Shipyard's real implementation (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/dashboard/server/{routes,services}/proof.js`, `src/components/ProofGallery.jsx`, `src/hooks/useProof.js`), adapted from Shipyard's flat `state/laneN/*.json` file model to CCAM's `lane_features` DB rows (Task B, already shipped).
|
||||
**Goal:** the screenshots QC agents capture become visible in the dashboard, grouped by feature (B's `lane_features` slug) and phase. Ported from Shipyard's real implementation (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/dashboard/server/{routes,services}/proof.js`, `src/components/ProofGallery.jsx`, `src/hooks/useProof.js`), adapted from Shipyard's flat `state/laneN/*.json` file model to AgentHub's `lane_features` DB rows (Task B, already shipped).
|
||||
|
||||
**Architecture:** One new library module `server/lib/proof.js` (list/file/delete/link, all containment-checked against the lane's own proof root — never `LANES_ROOT`, since an adopted lane's `cwd` need not live under it). Routes added inline to `server/routes/lanes.js`, grouped with the other `/:id/*` sub-resources, same placement rule B's `/:id/features` used. CLI gains one primitive, `ccam lanes proof-link`, which is never auto-invoked — matching "CCAM does not orchestrate," a session or hook calls it explicitly. The Workspace UI adds a `ProofGallery` panel keyed off the **same `viewedFeatureSlug`** state Task B's picker already introduced — no second picker.
|
||||
**Architecture:** One new library module `server/lib/proof.js` (list/file/delete/link, all containment-checked against the lane's own proof root — never `LANES_ROOT`, since an adopted lane's `cwd` need not live under it). Routes added inline to `server/routes/lanes.js`, grouped with the other `/:id/*` sub-resources, same placement rule B's `/:id/features` used. CLI gains one primitive, `agenthub lanes proof-link`, which is never auto-invoked — matching "AgentHub does not orchestrate," a session or hook calls it explicitly. The Workspace UI adds a `ProofGallery` panel keyed off the **same `viewedFeatureSlug`** state Task B's picker already introduced — no second picker.
|
||||
|
||||
**Tech Stack:** Node `fs`/`path` (realpath-based containment, no new dependency), existing Express/route conventions, existing React state from Task B.
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
- **Path containment is the entire security surface.** Every one of `proofFile`/`deleteProof`/`listProof` resolves + `fs.realpathSync`s + asserts the result is inside the lane's proof root (`base === p || p.startsWith(base + path.sep)`) before any read/write/delete reaches `fs`. No request path — slug, group, file, image name — ever reaches `fs` unresolved. Port Shipyard's `proofFile`/`deleteProof`/`badName` check nearly verbatim; do not "clean up" or abstract this logic, it is the one part of C worth reviewing line-by-line.
|
||||
- **`deleteProof` never touches `ticket/`.** Whole-feature delete iterates every subdirectory except `ticket`; group delete refuses `group === "ticket"` outright.
|
||||
- **The proof root is `<lane.cwd>/.playwright-mcp/proof`, not anything under `LANES_ROOT`.** An adopted lane's `cwd` can be anywhere the user can read; proof storage follows the lane, same as Shipyard's `.playwright-mcp/proof` living inside the worktree.
|
||||
- **Grouping comes from `lane_features` (Task B), not from re-deriving "current" via a branch regex.** Shipyard's `useProof.js` parses `feat/<slug>` out of the git branch because it has no DB; CCAM already has `getFeature`/`active_feature_id` as the source of truth, so that derivation is deliberately dropped, not ported.
|
||||
- **`ccam lanes proof-link` is a primitive, not automatic.** It runs `ensureProofLink` once, on request. Nothing in this plan calls it from `feature activate`, `lanes up`, or any hook — a session or a project's own hook script decides when to call it, same reasoning as every other "CCAM does not orchestrate" primitive.
|
||||
- **Grouping comes from `lane_features` (Task B), not from re-deriving "current" via a branch regex.** Shipyard's `useProof.js` parses `feat/<slug>` out of the git branch because it has no DB; AgentHub already has `getFeature`/`active_feature_id` as the source of truth, so that derivation is deliberately dropped, not ported.
|
||||
- **`agenthub lanes proof-link` is a primitive, not automatic.** It runs `ensureProofLink` once, on request. Nothing in this plan calls it from `feature activate`, `lanes up`, or any hook — a session or a project's own hook script decides when to call it, same reasoning as every other "AgentHub does not orchestrate" primitive.
|
||||
- `DELETE /api/lanes/:id/proof/:slug` takes `{group?, images?}` in the **JSON body** (`express.json()` is already mounted globally in `server/index.js`), matching Shipyard's `deleteProof(n, req.body || {})` — not query params.
|
||||
- Run `npm run test:server` (full suite) and `npm run test:client` (when a task touches `client/`) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit.
|
||||
- Never use `git add -A`. Stage exactly the files each task names.
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `proofRoot(lane)` → `string` (`<lane.cwd>/.playwright-mcp/proof`, NOT realpath'd — the directory may not exist yet); `ensureProofLink(lane)` → `{linked: boolean}` (idempotent; `false` when there was nothing to link, e.g. clone-root `proof/` already the correct symlink or absent); `listProof(laneId)` → `Array<{slug, groups: {[group]: string[]}, ticket_report: string, mtime: number}>` (mtime = latest file mtime under that slug's dir, seconds; sorted most-recently-touched first); `proofFile(laneId, slug, group, file)` → `string | null` (absolute realpath, or null if missing/outside root/wrong extension); `deleteProof(laneId, {slug, group, images} = {})` → `{deleted: number}`, throws `Object.assign(new Error(...), {code: "EBADPATH"})` for a bad slug/group/image name, `{code: "ENOFEATURE"}` when the slug's directory doesn't exist.
|
||||
- `listProof` merges in every slug from `lane-features.js`'s `listFeatures(laneId)` that has no proof directory yet (so a just-activated feature with zero screenshots still appears, with empty `groups`) — the CCAM-native replacement for Shipyard's `_pending`/`_active` special-casing, since B's `lane_features` row already exists the moment `activate` is called.
|
||||
- `listProof` merges in every slug from `lane-features.js`'s `listFeatures(laneId)` that has no proof directory yet (so a just-activated feature with zero screenshots still appears, with empty `groups`) — the AgentHub-native replacement for Shipyard's `_pending`/`_active` special-casing, since B's `lane_features` row already exists the moment `activate` is called.
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
@@ -48,7 +48,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -278,7 +278,7 @@ Read `server/lib/lane-features.js` first (already committed) to match its hydrat
|
||||
* `lane_features` already tracks. Ported from Shipyard's
|
||||
* `dashboard/server/services/proof.js`; the "current feature" special-casing
|
||||
* that file needed (it has no DB, so it parsed a branch regex) is dropped —
|
||||
* CCAM's `lane_features` row already IS that source of truth.
|
||||
* AgentHub's `lane_features` row already IS that source of truth.
|
||||
*
|
||||
* Path containment is the entire security surface here: every path this
|
||||
* module touches is resolved, realpath'd, and checked to be inside the
|
||||
@@ -519,7 +519,7 @@ function deleteProof(laneId, { slug, group, images } = {}) {
|
||||
* already points at the right place.
|
||||
*
|
||||
* Never auto-invoked by this codebase; exposed only as the explicit
|
||||
* `ccam lanes proof-link` primitive.
|
||||
* `agenthub lanes proof-link` primitive.
|
||||
*
|
||||
* @param {object} lane - A hydrated lane row (must have `cwd`).
|
||||
* @returns {{linked: boolean}} `linked: false` when nothing changed.
|
||||
@@ -632,7 +632,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -861,21 +861,21 @@ git commit -m "feat(lanes): expose GET/DELETE /api/lanes/:id/proof + proof-link
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `ccam lanes proof-link` CLI
|
||||
### Task 3: `agenthub lanes proof-link` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `POST /api/lanes/:id/proof-link` (Task 2, already committed) via the existing `post` helper.
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/ccam.js`).
|
||||
- Consumes: `resolveLaneArg` (already defined in `bin/agenthub.js`).
|
||||
|
||||
**Step 1: Add the command implementation**
|
||||
|
||||
In `bin/ccam.js`, near `cmdFeatureShow` (search for that function, added in Task B), add:
|
||||
In `bin/agenthub.js`, near `cmdFeatureShow` (search for that function, added in Task B), add:
|
||||
|
||||
```js
|
||||
/** `ccam lanes proof-link [<id>] [--cwd path]` — converge the clone-root
|
||||
/** `agenthub lanes proof-link [<id>] [--cwd path]` — converge the clone-root
|
||||
* `proof/` onto `.playwright-mcp/proof` (idempotent). Never run automatically
|
||||
* by anything else in this codebase; a session or hook calls it explicitly. */
|
||||
async function cmdLanesProofLink(args) {
|
||||
@@ -892,7 +892,7 @@ async function cmdLanesProofLink(args) {
|
||||
|
||||
**Step 2: Wire the subcommand dispatch**
|
||||
|
||||
In `bin/ccam.js`'s `case "lanes":` block (search for other `lanes` subcommands like `up`/`down`/`hook`), add:
|
||||
In `bin/agenthub.js`'s `case "lanes":` block (search for other `lanes` subcommands like `up`/`down`/`hook`), add:
|
||||
|
||||
```js
|
||||
if (sub === "proof-link") return cmdLanesProofLink(rest.slice(1));
|
||||
@@ -900,7 +900,7 @@ In `bin/ccam.js`'s `case "lanes":` block (search for other `lanes` subcommands l
|
||||
|
||||
**Step 3: Add the help-table entry**
|
||||
|
||||
In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lanes hook` row:
|
||||
In `bin/agenthub.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lanes hook` row:
|
||||
|
||||
```js
|
||||
["lanes proof-link", "[<id>]", "Converge clone-root proof/ onto .playwright-mcp/proof (idempotent, never automatic)"],
|
||||
@@ -909,17 +909,17 @@ In `bin/ccam.js`'s `COMMAND_GROUPS`, in the `"Lanes"` group, add after the `lane
|
||||
**Step 4: Manual smoke test**
|
||||
|
||||
```bash
|
||||
ccam lanes add --cwd $(pwd) --title "smoke"
|
||||
ccam lanes proof-link
|
||||
agenthub lanes add --cwd $(pwd) --title "smoke"
|
||||
agenthub lanes proof-link
|
||||
ls -la proof # should be a symlink -> .playwright-mcp/proof
|
||||
ccam lanes proof-link # second run: "already linked, nothing to do"
|
||||
agenthub lanes proof-link # second run: "already linked, nothing to do"
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes proof-link CLI (C)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes proof-link CLI (C)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1101,7 +1101,7 @@ git commit -m "feat(lanes): show proof gallery panel in the Workspace page (C)"
|
||||
Add a new top-level section (after "Per-feature state and archive", before "Cross-lane named locks" — search for both headings) titled `## Proof gallery`, covering:
|
||||
- Why: QC screenshots land on disk during a lane's work; this makes them visible in the dashboard, grouped by feature (Task B's slug) and phase.
|
||||
- Storage layout: `<lane.cwd>/.playwright-mcp/proof/<slug>/{<phase-group>,ticket}/`.
|
||||
- `ensure_proof_link`'s purpose and that it is **never automatic** — `ccam lanes proof-link` is an explicit primitive, same "CCAM does not orchestrate" reasoning as every other primitive in this project.
|
||||
- `ensure_proof_link`'s purpose and that it is **never automatic** — `agenthub lanes proof-link` is an explicit primitive, same "AgentHub does not orchestrate" reasoning as every other primitive in this project.
|
||||
- The security invariant stated exactly: every proof route resolves + realpaths + checks containment before touching `fs`; `deleteProof` never removes `ticket/`.
|
||||
- The Workspace panel reuses Task B's feature picker — no second selector.
|
||||
|
||||
@@ -1110,7 +1110,7 @@ Add a new top-level section (after "Per-feature state and archive", before "Cros
|
||||
Add to the `### Lanes` table, after the `lanes hook` row:
|
||||
|
||||
```markdown
|
||||
| `ccam lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
| `agenthub lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
```
|
||||
|
||||
**Step 3: `docs/API.md`**
|
||||
@@ -1122,7 +1122,7 @@ Add a `### Lane proof gallery` section documenting `GET /api/lanes/:id/proof`, `
|
||||
Add a new row to the module responsibility table, near the `lib/lane-features.js` row:
|
||||
|
||||
```markdown
|
||||
| `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `<lane.cwd>/.playwright-mcp/proof/<slug>/<group>/`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `ccam lanes proof-link` |
|
||||
| `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `<lane.cwd>/.playwright-mcp/proof/<slug>/<group>/`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `agenthub lanes proof-link` |
|
||||
```
|
||||
|
||||
**Step 5: Mark C done in the parent plan**
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** a lane can opt into the `ship-feature` pipeline template and a session can drive it with `.claude/skills/ship-feature-lane/SKILL.md`, whose every command is a real, working `ccam` command today (stages 0–2 fully runnable; later stages reference the not-yet-built agents/`sync-base`/F integrations by name, same as the roadmap's own text does).
|
||||
**Goal:** a lane can opt into the `ship-feature` pipeline template and a session can drive it with `.claude/skills/ship-feature-lane/SKILL.md`, whose every command is a real, working `agenthub` command today (stages 0–2 fully runnable; later stages reference the not-yet-built agents/`sync-base`/F integrations by name, same as the roadmap's own text does).
|
||||
|
||||
**Architecture:** One backend addition (`--qc` boot flag + `QC_BOOT_ENV`, threaded through `server/lib/lane-profile.js`'s `hookEnv`/`runHook` and `server/lib/lane-runtime.js`'s `upLane`), one new pipeline template JSON, one new skill file. No new routes, no new CLI subcommands beyond one flag on the existing `ccam lanes up`.
|
||||
**Architecture:** One backend addition (`--qc` boot flag + `QC_BOOT_ENV`, threaded through `server/lib/lane-profile.js`'s `hookEnv`/`runHook` and `server/lib/lane-runtime.js`'s `upLane`), one new pipeline template JSON, one new skill file. No new routes, no new CLI subcommands beyond one flag on the existing `agenthub lanes up`.
|
||||
|
||||
**Tech Stack:** Existing profile/hook system (`server/lib/lane-profile.js`), existing runtime lifecycle (`server/lib/lane-runtime.js`), existing pipeline node-state renderer (`server/lib/pipelines.js`, reused verbatim).
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- Modify: `server/lib/lane-profile.js` (`DEFAULTS`, `hookEnv`, `runHook`)
|
||||
- Modify: `server/lib/lane-runtime.js` (`upLane`)
|
||||
- Modify: `server/routes/lanes.js` (`POST /:id/up`)
|
||||
- Modify: `bin/ccam.js` (`cmdLanesRuntime`'s `up` branch)
|
||||
- Modify: `bin/agenthub.js` (`cmdLanesRuntime`'s `up` branch)
|
||||
- Test: `server/__tests__/lane-profile.test.js` (or wherever `hookEnv`/`runHook` are already tested — find with `grep -rln "hookEnv\|runHook" server/__tests__/*.test.js` and add there), `server/__tests__/lane-runtime.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
@@ -242,7 +242,7 @@ In `server/routes/lanes.js`, find `router.post("/:id/up", ...)` (already read ab
|
||||
const facts = await upLane(lanesLib.getLane(lane.id), { build, qc, onLine });
|
||||
```
|
||||
|
||||
In `bin/ccam.js`, find the `if (sub === "up") {` block inside `cmdLanesRuntime` (confirmed at the line already read: `const body = laneArgs.includes("--no-build") ? { build: false } : {};`) and change:
|
||||
In `bin/agenthub.js`, find the `if (sub === "up") {` block inside `cmdLanesRuntime` (confirmed at the line already read: `const body = laneArgs.includes("--no-build") ? { build: false } : {};`) and change:
|
||||
|
||||
```js
|
||||
if (sub === "up") {
|
||||
@@ -262,7 +262,7 @@ Update the help-table row for `lanes up` (search `COMMAND_GROUPS` for the existi
|
||||
],
|
||||
```
|
||||
|
||||
(Read the exact current row text first with `grep -n "no-build" bin/ccam.js` — merge into the real current wording rather than overwriting unrelated parts of the description.)
|
||||
(Read the exact current row text first with `grep -n "no-build" bin/agenthub.js` — merge into the real current wording rather than overwriting unrelated parts of the description.)
|
||||
|
||||
- [ ] **Step 10: Run the full suite and header audit**
|
||||
|
||||
@@ -271,7 +271,7 @@ Run: `npm run test:server && bash .claude/skills/file-headers/scripts/check-head
|
||||
- [ ] **Step 11: Commit**
|
||||
|
||||
```bash
|
||||
git add server/lib/lane-profile.js server/lib/lane-runtime.js server/routes/lanes.js bin/ccam.js server/__tests__/lane-profile.test.js server/__tests__/lane-runtime.test.js
|
||||
git add server/lib/lane-profile.js server/lib/lane-runtime.js server/routes/lanes.js bin/agenthub.js server/__tests__/lane-profile.test.js server/__tests__/lane-runtime.test.js
|
||||
git commit -m "feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic QC stacks (E1)"
|
||||
```
|
||||
|
||||
@@ -291,7 +291,7 @@ git commit -m "feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic Q
|
||||
|
||||
- [ ] **Step 1: Read the existing template format**
|
||||
|
||||
`server/data/pipelines/default.json`'s real shape (read it yourself with `cat server/data/pipelines/default.json` to confirm this plan's transcription is still current before writing the new file): top-level `{id, name, nodes}`; each node is `{id, label, icon, gate, aliases, detect?}` — `id` is the canonical stage name, `aliases` are other declared-stage strings (from `ccam stage <name>`) that also map to this node, `gate: true` marks a node that needs `--evidence` to render `done` rather than the amber `passed-no-evidence` state (per this repo's five node states, documented in `docs/LANES.md`), and `detect` (an array of `{tool, match}` rules) is for INFERRING a stage from tool-call patterns when no session ever explicitly declares it — `ship-feature-lane` (Task 3) always explicitly declares every stage via `ccam stage`, so no `ship-feature.json` node needs a `detect` block.
|
||||
`server/data/pipelines/default.json`'s real shape (read it yourself with `cat server/data/pipelines/default.json` to confirm this plan's transcription is still current before writing the new file): top-level `{id, name, nodes}`; each node is `{id, label, icon, gate, aliases, detect?}` — `id` is the canonical stage name, `aliases` are other declared-stage strings (from `agenthub stage <name>`) that also map to this node, `gate: true` marks a node that needs `--evidence` to render `done` rather than the amber `passed-no-evidence` state (per this repo's five node states, documented in `docs/LANES.md`), and `detect` (an array of `{tool, match}` rules) is for INFERRING a stage from tool-call patterns when no session ever explicitly declares it — `ship-feature-lane` (Task 3) always explicitly declares every stage via `agenthub stage`, so no `ship-feature.json` node needs a `detect` block.
|
||||
|
||||
- [ ] **Step 2: Write the failing test**
|
||||
|
||||
@@ -364,7 +364,7 @@ Expected: FAIL — `getPipeline` never throws on an unknown id (it falls back to
|
||||
}
|
||||
```
|
||||
|
||||
(`gate: true` is a rendering hint only — `nodeStates` passes it straight through to the UI unchanged; it does NOT change whether a node needs `--evidence` to show `done` (every node does, regardless of `gate`). It's set here on the five real checkpoints a fix-loop can bounce off of — CI gates, e2e-passed, code review, QC, and the senior gate — purely so the dashboard can visually distinguish a checkpoint from a transit stage, matching `default.json`'s own use of `gate` on `tests`/`review`/`gate`. `pr-open`'s `"ship"` alias and `done`'s `"complete"/"completed"` aliases mirror `default.json`'s own aliasing for the same concepts, so a stray `ccam stage ship` or `ccam stage complete` from habit still resolves to the right node.)
|
||||
(`gate: true` is a rendering hint only — `nodeStates` passes it straight through to the UI unchanged; it does NOT change whether a node needs `--evidence` to show `done` (every node does, regardless of `gate`). It's set here on the five real checkpoints a fix-loop can bounce off of — CI gates, e2e-passed, code review, QC, and the senior gate — purely so the dashboard can visually distinguish a checkpoint from a transit stage, matching `default.json`'s own use of `gate` on `tests`/`review`/`gate`. `pr-open`'s `"ship"` alias and `done`'s `"complete"/"completed"` aliases mirror `default.json`'s own aliasing for the same concepts, so a stray `agenthub stage ship` or `agenthub stage complete` from habit still resolves to the right node.)
|
||||
|
||||
- [ ] **Step 5: Run test to verify it passes**
|
||||
|
||||
@@ -398,25 +398,25 @@ Create `.claude/skills/ship-feature-lane/SKILL.md` with this exact content:
|
||||
````markdown
|
||||
---
|
||||
name: ship-feature-lane
|
||||
description: "Autonomous end-to-end feature pipeline for ONE CCAM lane. Invoke inside a lane's working directory with a requirement: `/ship-feature-lane <requirement>`. Frontloads ALL clarifying questions once, then runs unattended: implement (TDD) → pre-push CI gates + dev preflight → e2e on the feature branch → code review → local QC → senior GO/NO-GO gate → push branch + open PR (base `development`) → CI watch → report, then watches the PR (new comments → gated fix-loop; base conflicts → sync development into the branch) until a HUMAN merges it. The PR is only published after all local gates pass and the senior gate says GO — it's finalized when reviewers see it. Declares lane stage at every step via `ccam stage` for the dashboard. Use when the user wants to build/ship/implement a feature in a CCAM lane. NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands."
|
||||
description: "Autonomous end-to-end feature pipeline for ONE AgentHub lane. Invoke inside a lane's working directory with a requirement: `/ship-feature-lane <requirement>`. Frontloads ALL clarifying questions once, then runs unattended: implement (TDD) → pre-push CI gates + dev preflight → e2e on the feature branch → code review → local QC → senior GO/NO-GO gate → push branch + open PR (base `development`) → CI watch → report, then watches the PR (new comments → gated fix-loop; base conflicts → sync development into the branch) until a HUMAN merges it. The PR is only published after all local gates pass and the senior gate says GO — it's finalized when reviewers see it. Declares lane stage at every step via `agenthub stage` for the dashboard. Use when the user wants to build/ship/implement a feature in a AgentHub lane. NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands."
|
||||
---
|
||||
|
||||
# Ship Feature Lane (CCAM lane pipeline)
|
||||
# Ship Feature Lane (AgentHub lane pipeline)
|
||||
|
||||
You are running the autonomous feature pipeline for **one CCAM lane**. The human's only interactive touchpoints are **Stage 0 (frontloaded Q&A)** and **merging the PR on GitHub**; everything else runs to completion or to a `blocked` escalation, reporting progress through `ccam stage` (which the dashboard renders).
|
||||
You are running the autonomous feature pipeline for **one AgentHub lane**. The human's only interactive touchpoints are **Stage 0 (frontloaded Q&A)** and **merging the PR on GitHub**; everything else runs to completion or to a `blocked` escalation, reporting progress through `agenthub stage` (which the dashboard renders).
|
||||
|
||||
## Setup — do this first, every run
|
||||
|
||||
```bash
|
||||
LANE_DIR="$(pwd)" # the lane clone IS your cwd — CCAM resolves the lane from this, never a hardcoded path
|
||||
LANE_DIR="$(pwd)" # the lane clone IS your cwd — AgentHub resolves the lane from this, never a hardcoded path
|
||||
```
|
||||
- CCAM resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `ccam stage` or `ccam feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
|
||||
- All stage updates go through `ccam stage <stage> [--status <s>] [--evidence "..."]` — **call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
|
||||
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in CCAM yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `ccam ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
|
||||
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 12–13) can run many minutes between stage transitions — bump the heartbeat with `ccam stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
|
||||
- Profile hooks (`bootstrap`/`boot`/`migrate`/`seed`/`ci-gate`/`e2e`/`health`/`regen`) run through `ccam lanes hook <name> [args…]` and `ccam lanes up`/`down`. Use them; don't reinvent their logic.
|
||||
- **NEVER merge or rebase branches manually.** The ONLY merge that ever happens in this flow is `origin/development` INTO the feature branch, and only through `ccam lanes sync-base` (fetches fresh, pre-checks migration collisions, auto-regenerates generated files — see the note on this command in Stage 2; it is a LATER task, referenced here by its intended contract). There is no other direction: never merge a feature branch into anything locally, never commit on `development`, and never touch `main`.
|
||||
- **NEVER push `origin/development` or `origin/main` (HARD).** `development` moves ONLY when a human merges a PR on GitHub. The only branch you ever push is your own `feat/<slug>` — and only after the senior gate's GO (Stage 8). If you ever find yourself typing `git push` with `development` or `main` on the line: STOP, `ccam stage <current> --status blocked`.
|
||||
- AgentHub resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `agenthub stage` or `agenthub feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
|
||||
- All stage updates go through `agenthub stage <stage> [--status <s>] [--evidence "..."]` — **call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
|
||||
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in AgentHub yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `agenthub ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
|
||||
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 12–13) can run many minutes between stage transitions — bump the heartbeat with `agenthub stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
|
||||
- Profile hooks (`bootstrap`/`boot`/`migrate`/`seed`/`ci-gate`/`e2e`/`health`/`regen`) run through `agenthub lanes hook <name> [args…]` and `agenthub lanes up`/`down`. Use them; don't reinvent their logic.
|
||||
- **NEVER merge or rebase branches manually.** The ONLY merge that ever happens in this flow is `origin/development` INTO the feature branch, and only through `agenthub lanes sync-base` (fetches fresh, pre-checks migration collisions, auto-regenerates generated files — see the note on this command in Stage 2; it is a LATER task, referenced here by its intended contract). There is no other direction: never merge a feature branch into anything locally, never commit on `development`, and never touch `main`.
|
||||
- **NEVER push `origin/development` or `origin/main` (HARD).** `development` moves ONLY when a human merges a PR on GitHub. The only branch you ever push is your own `feat/<slug>` — and only after the senior gate's GO (Stage 8). If you ever find yourself typing `git push` with `development` or `main` on the line: STOP, `agenthub stage <current> --status blocked`.
|
||||
|
||||
## Context recovery — after conversation compaction
|
||||
|
||||
@@ -427,12 +427,12 @@ LANE_DIR="$(pwd)" # lane = cwd, resolved fresh
|
||||
```
|
||||
|
||||
Then check your current position:
|
||||
- **Lane state**: `ccam feature show <slug>` (or `ccam lanes` for the lane's own row) — shows current stage, status, feature title, branch, gate decision, PR URL, notes.
|
||||
- **Lane state**: `agenthub feature show <slug>` (or `agenthub lanes` for the lane's own row) — shows current stage, status, feature title, branch, gate decision, PR URL, notes.
|
||||
- **Git branch**: `git rev-parse --abbrev-ref HEAD` — which branch you're on.
|
||||
- **Feature slug**: from the branch name (`feat/X` → `X`), or from `ccam feature list` (the lane's currently-active feature is marked `▶`).
|
||||
- **Feature slug**: from the branch name (`feat/X` → `X`), or from `agenthub feature list` (the lane's currently-active feature is marked `▶`).
|
||||
|
||||
Resume from the stage shown. If state says `stage=X status=running`, you were mid-stage X when context compacted — re-run that stage from the top (all `ccam` commands are idempotent).
|
||||
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `ccam lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
Resume from the stage shown. If state says `stage=X status=running`, you were mid-stage X when context compacted — re-run that stage from the top (all `agenthub` commands are idempotent).
|
||||
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `agenthub lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
|
||||
## Hard rules
|
||||
|
||||
@@ -443,17 +443,17 @@ Resume from the stage shown. If state says `stage=X status=running`, you were mi
|
||||
- **Run only if e2e spec files are among the changed tests:** Stage 3 (boot + e2e on the feature branch). No e2e specs changed → skip it.
|
||||
- **Always SKIP** (runtime UI unchanged): Stage 5 (QC plan), Stage 6 (qc-local). Record `--evidence "QC skipped: test-only change"`.
|
||||
- If the diff contains ANY non-test file → this fast-path does NOT apply; take the full path above. The first pass (not a re-entry) always runs full QC.
|
||||
- **EXCEPTION — localized re-entry (scoped-e2e fast-path).** On a re-entry whose diff since the last fully-validated commit is SMALL and LOCALIZED — only files inside the feature's own surface, NO migrations, NO contract/generated files, NO shared fixtures/utilities, NO dependency changes — you may shrink Stage 3's e2e to a SCOPED run of the specs covering the touched surface: `ccam lanes hook e2e -- <spec files>` (scoped runs still heartbeat + lock + time-bound like the full suite), and browser QC runs SCOPED to the affected QC-Plan scenarios (tell the qc agent exactly which scenario numbers). Know the trade-off: there is no dev-merged full suite anymore — post-merge dev CI + dev-QC (Stage 13, currently off) are normally the integration net; without them, a localized re-entry after this lands is a real gap until F ships. If in doubt whether the change is localized, it isn't — run the full path.
|
||||
- **Run long helpers so they can't be killed mid-flight or hang your turn.** `ccam lanes hook ci-gate`, `ccam lanes up`, `ccam lanes hook e2e` legitimately run 3–20+ minutes (builds, tests, Playwright, lock waits). NEVER invoke them with the default Bash timeout (2 min kills them mid-flight and strands the lane half-done): use `run_in_background: true` and poll the output file until done, or set `timeout: 600000` for the shorter gates. If a helper does die mid-run, don't panic: every hook is idempotent — re-run the step (e.g. re-run `ccam lanes up --no-build` to revive a stack).
|
||||
- **Waiting + polling NEVER use a foreground `sleep`** (the Bash tool blocks it) or `ScheduleWakeup` (that's a `/loop`-only primitive — this pipeline is not a `/loop` session, so it won't sustain your watch). To pace a poll loop or wait out a timer, **background the wait**: run `sleep <secs>` with `run_in_background: true` — you're re-invoked when it exits, and re-invoked the moment a backgrounded Agent or helper finishes, so you never busy-poll for background work. **If you ever can't sustain a wait/loop in this session, set `ccam stage <current> --note "<honest note>"` and STOP — never narrate a watch or loop you are not actually running.**
|
||||
- **e2e: actively poll — never wait on the completion re-invoke alone.** A hung suite never fires it, stranding the lane at `stage=e2e-feature`. When running `ccam lanes hook e2e` (Stage 3): start it `run_in_background: true` AND background a `sleep 90` beside it. Each wake — finished → parse PASS/FAIL; still running → read the e2e log tail (`ccam lanes logs <id> e2e`), bump the heartbeat, and re-background `sleep 90`, UNLESS it's erroring or has run past ~22 min, in which case kill the e2e task and treat it as FAIL → re-enter Stage 2.
|
||||
- **EXCEPTION — localized re-entry (scoped-e2e fast-path).** On a re-entry whose diff since the last fully-validated commit is SMALL and LOCALIZED — only files inside the feature's own surface, NO migrations, NO contract/generated files, NO shared fixtures/utilities, NO dependency changes — you may shrink Stage 3's e2e to a SCOPED run of the specs covering the touched surface: `agenthub lanes hook e2e -- <spec files>` (scoped runs still heartbeat + lock + time-bound like the full suite), and browser QC runs SCOPED to the affected QC-Plan scenarios (tell the qc agent exactly which scenario numbers). Know the trade-off: there is no dev-merged full suite anymore — post-merge dev CI + dev-QC (Stage 13, currently off) are normally the integration net; without them, a localized re-entry after this lands is a real gap until F ships. If in doubt whether the change is localized, it isn't — run the full path.
|
||||
- **Run long helpers so they can't be killed mid-flight or hang your turn.** `agenthub lanes hook ci-gate`, `agenthub lanes up`, `agenthub lanes hook e2e` legitimately run 3–20+ minutes (builds, tests, Playwright, lock waits). NEVER invoke them with the default Bash timeout (2 min kills them mid-flight and strands the lane half-done): use `run_in_background: true` and poll the output file until done, or set `timeout: 600000` for the shorter gates. If a helper does die mid-run, don't panic: every hook is idempotent — re-run the step (e.g. re-run `agenthub lanes up --no-build` to revive a stack).
|
||||
- **Waiting + polling NEVER use a foreground `sleep`** (the Bash tool blocks it) or `ScheduleWakeup` (that's a `/loop`-only primitive — this pipeline is not a `/loop` session, so it won't sustain your watch). To pace a poll loop or wait out a timer, **background the wait**: run `sleep <secs>` with `run_in_background: true` — you're re-invoked when it exits, and re-invoked the moment a backgrounded Agent or helper finishes, so you never busy-poll for background work. **If you ever can't sustain a wait/loop in this session, set `agenthub stage <current> --note "<honest note>"` and STOP — never narrate a watch or loop you are not actually running.**
|
||||
- **e2e: actively poll — never wait on the completion re-invoke alone.** A hung suite never fires it, stranding the lane at `stage=e2e-feature`. When running `agenthub lanes hook e2e` (Stage 3): start it `run_in_background: true` AND background a `sleep 90` beside it. Each wake — finished → parse PASS/FAIL; still running → read the e2e log tail (`agenthub lanes logs <id> e2e`), bump the heartbeat, and re-background `sleep 90`, UNLESS it's erroring or has run past ~22 min, in which case kill the e2e task and treat it as FAIL → re-enter Stage 2.
|
||||
- **Turn-liveness: never let the pipeline die silently.** A "stalled" lane usually died one of two ways: (a) a turn ended with NOTHING pending — no backgrounded wait, no running helper, no background agent — so nothing ever re-invoked the session; or (b) a transient API error (rate-limit, 529/overload, connection refused) killed the turn mid-stage. Rules: while the pipeline is anywhere between Stage 1 and Stage 14 (done), every turn you end MUST leave at least one re-invoker pending (a `run_in_background` helper/sleep or a background agent) — check before ending the turn. And on ANY resume after an error or a human nudge ("continue"), do not ask questions: re-derive position from lane state (Context recovery above) and continue the stage. If you truly cannot leave a re-invoker, set `--note "watch needs re-trigger: <what to do>"` so the dashboard shows it honestly.
|
||||
- **No retry cap — the phase clock is the signal.** A failing gate/QC/CI/e2e just re-enters the loop (fix on the feature branch, re-run from Stage 2); there is NO automatic block after N attempts. The dashboard shows how long the lane has sat in its current stage, so the human can spot a stuck or endlessly-looping lane and step in. Reserve `--status blocked` for GENUINE blockers you cannot resolve (an ambiguous merge conflict, a hard/unrecoverable error).
|
||||
- **Commit only on the feature branch.** Never commit on `development`/`main`. Stage only intended files (never `git add .` blindly — this repo collects stray build/QA artifacts).
|
||||
- Keep the lane's state truthful: on any stop, set an accurate `ccam stage <stage> --status <status> --note "<why>"`.
|
||||
- Keep the lane's state truthful: on any stop, set an accurate `agenthub stage <stage> --status <status> --note "<why>"`.
|
||||
- **Quality bar (applies to every code change, including fix-loop re-entries and follow-up PRs).** Tests are sharp and meaningful — each pins a real behavior/edge case (happy + negative + boundary), none trivial, redundant, or coverage-padding. Comments are minimal — only the non-obvious *why*, matching the surrounding density; never narrate the *what*. Investigate before fixing (root cause, not symptom — use **systematic-debugging**). Prefer reusing/extending existing code over duplicating it.
|
||||
- **One driver per MCP browser server.** Each MCP server owns ONE browser; two agents driving the SAME server interleave clicks in one tab. The local-QC MCP → the qc-local agent (Stage 6) only; the general `playwright` MCP → the main session for ad-hoc checks only (never while qc-local runs). Parallel agents on DIFFERENT servers are safe by design; a second concurrent driver on the SAME server is never OK.
|
||||
- **Cross-lane etiquette (locks + siblings).** Lanes share one machine and one dev site. Waiting on a cross-lane serializer (`ccam lock acquire <name>`, e.g. around a shared build/e2e step) is NORMAL — it heartbeats while it waits, so you won't look stalled. NEVER free a lock by killing another lane's session or processes, deleting the lock's directory by hand, or shrinking `LOCK_MAX_HOLD`; a dead holder's lock auto-expires on its own. If a lock wait times out: re-try with a longer `--timeout`, or set `--status blocked` with a note and report. Touch ONLY your own lane's clone, state, and locks you hold.
|
||||
- **Cross-lane etiquette (locks + siblings).** Lanes share one machine and one dev site. Waiting on a cross-lane serializer (`agenthub lock acquire <name>`, e.g. around a shared build/e2e step) is NORMAL — it heartbeats while it waits, so you won't look stalled. NEVER free a lock by killing another lane's session or processes, deleting the lock's directory by hand, or shrinking `LOCK_MAX_HOLD`; a dead holder's lock auto-expires on its own. If a lock wait times out: re-try with a longer `--timeout`, or set `--status blocked` with a note and report. Touch ONLY your own lane's clone, state, and locks you hold.
|
||||
|
||||
## Stages
|
||||
|
||||
@@ -461,10 +461,10 @@ Resume from the stage shown. If state says `stage=X status=running`, you were mi
|
||||
Do NOT jump to code. Understand the requirement first.
|
||||
|
||||
- **Restate + quick scan.** Restate the requirement. Do a fast targeted scan of the relevant code (use the `Explore` agent for breadth; the **brainstorming** skill if the requirement is fuzzy) so your questions are grounded in what actually exists.
|
||||
- **Frontloaded Q&A.** Ask the human **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. **Sibling-surface check (mandatory):** if your scan shows the app has N parallel surfaces of the pattern the requirement touches (e.g. several foldered areas, several list pages sharing a component) and the requirement names fewer than N, explicitly ask "this exists in [all N places] — apply to all, or only [the named ones]?" A missed sibling here costs a full second pipeline pass when a reviewer catches it on the PR. Then activate a clean feature slot for this run: `ccam feature activate <slug> --title "<short title>"` — Task B's `activate` archives whatever feature was previously active on this lane automatically, so this run's dashboard state starts clean without a separate "init" step. Then mark intake: `ccam stage intake --status running`. Announce "Questions answered — going autonomous now." After this, don't ask the human anything unless you hit a `blocked` escalation.
|
||||
- **Frontloaded Q&A.** Ask the human **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. **Sibling-surface check (mandatory):** if your scan shows the app has N parallel surfaces of the pattern the requirement touches (e.g. several foldered areas, several list pages sharing a component) and the requirement names fewer than N, explicitly ask "this exists in [all N places] — apply to all, or only [the named ones]?" A missed sibling here costs a full second pipeline pass when a reviewer catches it on the PR. Then activate a clean feature slot for this run: `agenthub feature activate <slug> --title "<short title>"` — Task B's `activate` archives whatever feature was previously active on this lane automatically, so this run's dashboard state starts clean without a separate "init" step. Then mark intake: `agenthub stage intake --status running`. Announce "Questions answered — going autonomous now." After this, don't ask the human anything unless you hit a `blocked` escalation.
|
||||
|
||||
### 0b — Investigate & plan *(autonomous)*
|
||||
- `ccam stage plan` — now design a real plan and have it independently challenged before you implement.
|
||||
- `agenthub stage plan` — now design a real plan and have it independently challenged before you implement.
|
||||
- **Investigate (autonomous, thorough).** Read the actual code paths, models, existing tests, and conventions the feature touches — `Explore`/`general-purpose` subagents for breadth, then read the key files yourself for depth. Pin down: integration points, data/migration needs, API/contract impact, reuse opportunities, and risks. Use **systematic-debugging** if the feature is a fix (root-cause first, no symptom patches).
|
||||
- **Plan.** Produce a concrete implementation plan (the **writing-plans** skill): approach, files to change, the test strategy (which behaviors/edge cases each test will pin), migration/contract impact, and how each acceptance criterion is met.
|
||||
- **Debate the plan (adversarial review).** Spawn a SEPARATE sub-agent (Agent tool — `Plan` or `general-purpose`) to critique the plan + investigation: missed requirements, wrong assumptions, a simpler approach, unhandled edge cases, acceptance-criteria gaps. Apply the worthwhile critiques (use **receiving-code-review** judgment — verify each point, don't blindly accept or reject). Iterate once or twice until the plan holds up.
|
||||
@@ -472,32 +472,32 @@ Do NOT jump to code. Understand the requirement first.
|
||||
|
||||
### 1 — Implement (TDD, to the plan)
|
||||
- Choose a **single-segment slug** for the feature — lowercase, hyphens, NO slashes. Cut the feature branch from **development** (the PR base): `git fetch origin && git checkout -b feat/<slug> origin/development`.
|
||||
- If Stage 0 activated a placeholder slug different from the final chosen one, reconcile: `ccam feature activate <slug>` — it echoes back the canonicalized slug it actually stored; use THAT for the branch and every later reference.
|
||||
- If Stage 0 activated a placeholder slug different from the final chosen one, reconcile: `agenthub feature activate <slug>` — it echoes back the canonicalized slug it actually stored; use THAT for the branch and every later reference.
|
||||
- Implement the agreed plan with the **test-driven-development** skill: failing test → minimal code → green → commit. Frequent small commits.
|
||||
- **Tests must be sharp and meaningful.** Each test pins a real behavior or edge case from the plan / acceptance criteria — cover the happy path, the negative/error path, and boundaries. NO trivial or redundant tests: don't assert constants or framework internals, don't re-test the same path twice, don't pad for coverage. A few precise tests that would actually catch a regression beat many shallow ones.
|
||||
- **Comment only when it earns its place.** Match the surrounding code's comment density. Comment the non-obvious *why* (intent, invariants, gotchas, links to context) — never narrate the *what* the code already says. Delete redundant/boilerplate/restating comments rather than adding them.
|
||||
- If your stack generates an API contract/client and the API changed, regenerate it (`ccam lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
|
||||
- `ccam stage implementing`
|
||||
- If your stack generates an API contract/client and the API changed, regenerate it (`agenthub lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
|
||||
- `agenthub stage implementing`
|
||||
|
||||
### 2 — Pre-push CI gates + dev preflight (on the feature branch)
|
||||
- `ccam lanes hook ci-gate` — runs the profile's CI gate (lint / test / contract checks) against an isolated per-lane test DB. On failure: read the output, fix on the feature branch, commit, re-run. Loop until green.
|
||||
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`ccam stage gates --evidence "sync-base preflight unavailable, skipped"`).
|
||||
- `agenthub lanes hook ci-gate` — runs the profile's CI gate (lint / test / contract checks) against an isolated per-lane test DB. On failure: read the output, fix on the feature branch, commit, re-run. Loop until green.
|
||||
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`agenthub stage gates --evidence "sync-base preflight unavailable, skipped"`).
|
||||
- Once built, its contract is: exit 5 on a migration-number collision (print the exact rename, do it on the feature branch, re-run Stage 2); informational `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise (you do NOT sync the branch for it — GitHub merges non-conflicting histories fine).
|
||||
|
||||
### 3 — E2E on the feature branch
|
||||
The e2e hook doesn't run migrations itself — it tests the already-running stack. To exercise the feature's code and any new schema, boot the lane stack with the feature branch first:
|
||||
|
||||
- `ccam stage e2e-feature --status running`
|
||||
- `ccam lanes up --qc` — boots with the profile's QC env (mock/stub flags so QC is deterministic, from `QC_BOOT_ENV`), applies the feature branch's own migrations, and reboots the stack. Idempotent; safe to re-run. (The branch was cut from `origin/development`, so this stack IS development + your feature as of the branch point.)
|
||||
- `ccam lanes hook e2e` — Playwright e2e under the e2e lock against the now-booted stack. **This is the only e2e gate in the flow** — there is no dev-merged suite behind it.
|
||||
- `agenthub stage e2e-feature --status running`
|
||||
- `agenthub lanes up --qc` — boots with the profile's QC env (mock/stub flags so QC is deterministic, from `QC_BOOT_ENV`), applies the feature branch's own migrations, and reboots the stack. Idempotent; safe to re-run. (The branch was cut from `origin/development`, so this stack IS development + your feature as of the branch point.)
|
||||
- `agenthub lanes hook e2e` — Playwright e2e under the e2e lock against the now-booted stack. **This is the only e2e gate in the flow** — there is no dev-merged suite behind it.
|
||||
- On failure: fix on the feature branch, commit, re-run from Stage 2.
|
||||
- **Iterating on a failing spec:** use scoped runs through the hook — `ccam lanes hook e2e -- <spec file/filter>` — never a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout, and the heartbeat, so the dashboard false-flags STALLED). A scoped green is never the gate; finish with the full suite (unless the localized fast-path applies — see Hard rules).
|
||||
- On success: `ccam stage e2e-feature-passed --status running`
|
||||
- **Iterating on a failing spec:** use scoped runs through the hook — `agenthub lanes hook e2e -- <spec file/filter>` — never a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout, and the heartbeat, so the dashboard false-flags STALLED). A scoped green is never the gate; finish with the full suite (unless the localized fast-path applies — see Hard rules).
|
||||
- On success: `agenthub stage e2e-feature-passed --status running`
|
||||
|
||||
### 4 — Code review *(no open PR yet — use local diff)*
|
||||
- Run the **`code-review` skill at effort `high`** on the feature diff vs `origin/development` — this is the deterministic code-review gate, not an ad-hoc read. The PR isn't open yet, so point it at the local diff: `git diff origin/development...feat/<slug>` (and `git log origin/development..feat/<slug>` for commits). ONLY if the `code-review` skill is unavailable, fall back to a manual review of that diff (correctness, security, tests, migration/contract safety). The Stage-6 `qc-local` report covers the user-flow review for the senior gate.
|
||||
- Apply the fixes worth making on the feature branch; if you change code, re-run **from Stage 2**.
|
||||
- `ccam stage review`
|
||||
- `agenthub stage review`
|
||||
|
||||
### 5 — QC plan *(bound the test scope before any browser QC)*
|
||||
- Author a **QC Plan** the browser-QC agents (Stage 6 now, and a future Stage 13 once dev-QC exists) will execute against — so QC covers everything that matters and nothing that doesn't (no missed scenarios, no wandering into unrelated areas). Derive it from the acceptance points (lane spec) + the real change surface (`git diff origin/development...feat/<slug>` and `--stat`). Three parts:
|
||||
@@ -505,11 +505,11 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
|
||||
- **Out-of-scope** (explicit): areas NOT to test because the change cannot affect them — this is what stops QC from over-testing.
|
||||
- **Smoke set**: login + main nav + ≥3 unaffected major areas.
|
||||
- Append it to the lane spec under a `## QC Plan` heading (`docs/superpowers/specs/lane-<slug>.md`) — the same file the senior gate reads. You are the **single writer** of this section; the QC agent only *proposes* additions in its report and you fold them in (Stage 6). This keeps the plan race-free yet living.
|
||||
- `ccam stage qc-plan --status running`
|
||||
- `agenthub stage qc-plan --status running`
|
||||
|
||||
### 6 — Browser QC via the qc-local agent
|
||||
- **Test-only fast-path:** on a fix-loop re-entry whose change is ENTIRELY test files (see the fix-loop rule), SKIP this stage — the app's runtime UI is unchanged — and record `--evidence "QC skipped: test-only change"`. Otherwise run it:
|
||||
- `ccam stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local` — FOREGROUND; it gates the pipeline. **This agent does not exist yet as of this skill's authoring — a separate, later task ports it.** Until then, this stage cannot run; treat a lane that reaches here as `--status blocked --note "qc-local agent not yet available"` and report to the human). When it exists, give it: the lane's working directory, the feature slug, the feature title, the acceptance points (lane spec), and the **QC Plan** (lane spec, Stage 5) as the authoritative scope to execute against. It owns the whole local browser QC and proof capture (`ccam lanes proof-link` first, then screenshots land under the proof gallery automatically). It runs against the lane's feature-branch stack from Stage 3. Do NOT drive the browser yourself at this stage.
|
||||
- `agenthub stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local` — FOREGROUND; it gates the pipeline. **This agent does not exist yet as of this skill's authoring — a separate, later task ports it.** Until then, this stage cannot run; treat a lane that reaches here as `--status blocked --note "qc-local agent not yet available"` and report to the human). When it exists, give it: the lane's working directory, the feature slug, the feature title, the acceptance points (lane spec), and the **QC Plan** (lane spec, Stage 5) as the authoritative scope to execute against. It owns the whole local browser QC and proof capture (`agenthub lanes proof-link` first, then screenshots land under the proof gallery automatically). It runs against the lane's feature-branch stack from Stage 3. Do NOT drive the browser yourself at this stage.
|
||||
- Parse its last line: `LOCAL-QC: PASS` → continue. `LOCAL-QC: FAIL — <reasons>` → fix on the feature branch → re-run from Stage 2. Keep its report — it is the feature user-flow review for the senior gate.
|
||||
- **Fold back discoveries:** if its report lists scenarios it found that weren't in the plan (its "Scenarios discovered during QC" section), add them to the `## QC Plan` in-scope list in the lane spec.
|
||||
|
||||
@@ -518,39 +518,39 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
|
||||
- Parse its final line:
|
||||
- `VERDICT: GO` → proceed to Stage 8.
|
||||
- `VERDICT: NO-GO — <fixes>` → fix on the feature branch, re-run **from Stage 2**. No attempt cap — the loop re-enters; the dashboard's time-on-stage surfaces a lane stuck cycling so the human can step in. Set `--status blocked` only for a genuine blocker you can't resolve.
|
||||
- `ccam stage gate --evidence "GO"` (or `NO-GO — <reason>`)
|
||||
- `agenthub stage gate --evidence "GO"` (or `NO-GO — <reason>`)
|
||||
|
||||
### 8 — Publish: push branch + open/update PR *(GATED — only on GO)*
|
||||
- `ccam stage publishing --status running`
|
||||
- Re-run the preflight if `ccam lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
|
||||
- `agenthub stage publishing --status running`
|
||||
- Re-run the preflight if `agenthub lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
|
||||
- `git push -u origin feat/<slug>` — first push of the feature branch to remote. All gates have passed before this point; the PR is finalized before reviewers see it.
|
||||
- Open or update the PR **based on and targeting `development`**: `gh pr create --base development --fill` (or `gh pr edit` / the push itself if a prior run already created it). Capture the URL.
|
||||
- `ccam stage pr-open --evidence "<pr-url>"` — the dashboard shows the PR link from here (via `--evidence` in `ccam feature show`/`ccam lanes`).
|
||||
- `agenthub stage pr-open --evidence "<pr-url>"` — the dashboard shows the PR link from here (via `--evidence` in `agenthub feature show`/`agenthub lanes`).
|
||||
|
||||
### 9 — Ticket *(currently SKIPPED — tracker integration is off)*
|
||||
- Tracker integration is hardcoded off (see Setup). Do nothing here; do not attempt to file a ticket. When F ships `ccam lanes integration tracker`, this stage gets a real implementation.
|
||||
- Tracker integration is hardcoded off (see Setup). Do nothing here; do not attempt to file a ticket. When F ships `agenthub lanes integration tracker`, this stage gets a real implementation.
|
||||
|
||||
### 10 — CI watch on the PR *(non-blocking)*
|
||||
- Check the PR's CI with `gh pr checks` / `gh`. Green → continue to the report + watch — never idle waiting for green.
|
||||
- **Red CI → triage before the fix-loop** (shared CI flakes under multi-lane load; treating every red as your defect wastes cycles):
|
||||
1. Read WHICH job/tests failed with `gh` (there is no `ccam ci` yet — F builds that; use `gh run view`/`gh pr checks --watch` directly).
|
||||
1. Read WHICH job/tests failed with `gh` (there is no `agenthub ci` yet — F builds that; use `gh run view`/`gh pr checks --watch` directly).
|
||||
2. **Your tests / your code implicated** → real failure: fix on the feature branch, re-enter from Stage 2.
|
||||
3. **Infra/flake signature** (a job with no test failures, OOM/contention on the shared runner, a hung job with no output, or a test that is green locally on the identical tree) → re-run the workflow via `gh run rerun <run-id> --failed` — ONCE. Still red after the rerun → treat it as real (or escalate with the evidence). Never rerun more than twice, and never push an empty commit to re-trigger CI.
|
||||
4. A **hung** workflow (running way past its normal duration with no output) → `gh run cancel <run-id>` then rerun once.
|
||||
|
||||
### 11 — Report
|
||||
- Post a concise report: PR URL, CI status, what shipped, and that the PR now **awaits a human merge** (this pipeline never merges).
|
||||
- `ccam stage reported --status running`
|
||||
- `agenthub stage reported --status running`
|
||||
- Do **NOT** clear or reset the lane's feature state. Cleanup is the human's call, from the dashboard — they may still be manually testing.
|
||||
|
||||
### 12 — Watch the PR *(until a human merges or closes it)*
|
||||
- `ccam stage watching-pr --note "watching PR for comments + base conflicts + the merge"`
|
||||
- `agenthub stage watching-pr --note "watching PR for comments + base conflicts + the merge"`
|
||||
- Loop every ~5 minutes, paced by a **backgrounded** wait so the turn isn't pinned (see the waiting-primitive rule above): run `sleep 300` with `run_in_background: true` — you're re-invoked when it elapses. Each iteration:
|
||||
- Check PR state: `gh pr view <pr_url> --json state,mergeable -q '.state + " " + (.mergeable|tostring)'`, and bump the heartbeat (`ccam stage watching-pr`).
|
||||
- Check PR state: `gh pr view <pr_url> --json state,mergeable -q '.state + " " + (.mergeable|tostring)'`, and bump the heartbeat (`agenthub stage watching-pr`).
|
||||
- `MERGED` → a human merged it: go to **Stage 13** (post-merge verification).
|
||||
- `CLOSED` (unmerged) → the human rejected/abandoned it: `ccam stage done --status passed --note "PR closed unmerged by human"` → STOP.
|
||||
- `CLOSED` (unmerged) → the human rejected/abandoned it: `agenthub stage done --status passed --note "PR closed unmerged by human"` → STOP.
|
||||
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
|
||||
- If `ccam lanes sync-base` exists by now: `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
|
||||
- If `agenthub lanes sync-base` exists by now: `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
|
||||
- If it doesn't exist yet: `git fetch origin && git merge origin/development` directly on the feature branch, resolve conflicts the same way, commit.
|
||||
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
|
||||
- For each new PR comment (list with `gh pr view <pr_url> --json comments`, tracking which you've already handled by comment id in your own notes), triage AND **always reply on its thread** (every comment gets a response — no silent handling, so reviewers see it was considered):
|
||||
@@ -561,15 +561,15 @@ The e2e hook doesn't run migrations itself — it tests the already-running stac
|
||||
- Nothing new → background another `sleep 300` (`run_in_background: true`) and end the turn; you'll be re-invoked for the next poll. A PR can sit for days — that's fine.
|
||||
|
||||
### 13 — Post-merge verification *(currently SKIPPED — CI-wait and dev-QC integrations are off)*
|
||||
- `ccam stage merged --status running --note "PR merged — post-merge verification unavailable (F not built)"`
|
||||
- `agenthub stage merged --status running --note "PR merged — post-merge verification unavailable (F not built)"`
|
||||
- Both dev-CI-wait and dev-QC are hardcoded off (see Setup). Go straight to Stage 14. When F ships these integrations, this stage gets its real implementation (mirroring Shipyard's: dev CI watch, then a `dev-qc` agent QCing the deployed site, looping to a Stage 15 follow-up-fix pattern on any issue found).
|
||||
|
||||
### 14 — Done
|
||||
- Post the final report: PR merged, that post-merge verification is unavailable pending F.
|
||||
- `ccam stage done --status passed --note "merged; post-merge verification unavailable (F not built)"` → STOP (leave the lane for the human to clear from the dashboard whenever).
|
||||
- `agenthub stage done --status passed --note "merged; post-merge verification unavailable (F not built)"` → STOP (leave the lane for the human to clear from the dashboard whenever).
|
||||
|
||||
## Escalation
|
||||
Whenever you STOP early (an ambiguous merge conflict, an unexpected/unrecoverable failure, a missing agent this skill depends on), set `ccam stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
|
||||
Whenever you STOP early (an ambiguous merge conflict, an unexpected/unrecoverable failure, a missing agent this skill depends on), set `agenthub stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Verify frontmatter parses**
|
||||
@@ -601,22 +601,22 @@ git commit -m "feat(lanes): port ship-feature-lane skill (Stages 0-14, E1)"
|
||||
|
||||
- [ ] **Step 1: Dry run on a scratch repo**
|
||||
|
||||
Create a throwaway repo with a minimal profile (reuse this repo's existing profile fixtures from `server/__tests__/` as a template — search `grep -rl "profile.env" server/__tests__/*.test.js` for one that builds a full working `bootstrap`/`boot`/`ci-gate` hook set) and a lane pointed at it via `ccam lanes add`. Walk Stage 0 through Stage 2 by hand, running the exact commands the skill text specifies:
|
||||
Create a throwaway repo with a minimal profile (reuse this repo's existing profile fixtures from `server/__tests__/` as a template — search `grep -rl "profile.env" server/__tests__/*.test.js` for one that builds a full working `bootstrap`/`boot`/`ci-gate` hook set) and a lane pointed at it via `agenthub lanes add`. Walk Stage 0 through Stage 2 by hand, running the exact commands the skill text specifies:
|
||||
|
||||
```bash
|
||||
ccam feature activate smoke-test --title "Dry run"
|
||||
ccam stage intake --status running
|
||||
ccam stage plan
|
||||
ccam stage implementing
|
||||
ccam lanes hook ci-gate
|
||||
ccam stage gates --evidence "sync-base preflight unavailable, skipped"
|
||||
agenthub feature activate smoke-test --title "Dry run"
|
||||
agenthub stage intake --status running
|
||||
agenthub stage plan
|
||||
agenthub stage implementing
|
||||
agenthub lanes hook ci-gate
|
||||
agenthub stage gates --evidence "sync-base preflight unavailable, skipped"
|
||||
```
|
||||
|
||||
Expected: every command exits 0 and the lane's stage visibly advances (`ccam feature show smoke-test` after each step). Also exercise Stage 3's new pieces in isolation:
|
||||
Expected: every command exits 0 and the lane's stage visibly advances (`agenthub feature show smoke-test` after each step). Also exercise Stage 3's new pieces in isolation:
|
||||
|
||||
```bash
|
||||
ccam lanes up --qc
|
||||
ccam lanes hook e2e
|
||||
agenthub lanes up --qc
|
||||
agenthub lanes hook e2e
|
||||
```
|
||||
|
||||
Expected: `up --qc` succeeds (or fails with a clear profile-level error if the scratch profile has no e2e hook — that's fine, the point is `--qc` itself doesn't error).
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `ccam lanes agents install` writes the `qc-local` and `senior-gate-reviewer` agent definitions into a lane's `.claude/agents/`, so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 6 and Stage 7 stop being hard-blocked on agents that don't exist.
|
||||
**Goal:** `agenthub lanes agents install` writes the `qc-local` and `senior-gate-reviewer` agent definitions into a lane's `.claude/agents/`, so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 6 and Stage 7 stop being hard-blocked on agents that don't exist.
|
||||
|
||||
**Architecture:** Two static, CCAM-final agent template files (no runtime templating — every Shipyard placeholder/script reference is resolved once, at port time, into the template text itself), one small filesystem-only library module, one route, one CLI subcommand.
|
||||
**Architecture:** Two static, AgentHub-final agent template files (no runtime templating — every Shipyard placeholder/script reference is resolved once, at port time, into the template text itself), one small filesystem-only library module, one route, one CLI subcommand.
|
||||
|
||||
**Tech Stack:** Existing `git(cwd, args)` helper (`server/lib/worktree.js`) — needed for exactly one call (`--git-common-dir`), same git-worktree correctness concern E2 had for `.git/info/attributes`. Plain `node:fs` for everything else.
|
||||
|
||||
@@ -39,7 +39,7 @@ Write `server/data/agent-templates/ship-feature-lane/qc-local.md` with this exac
|
||||
```markdown
|
||||
---
|
||||
name: "qc-local"
|
||||
description: "Local browser-QC agent for the ship-feature-lane pipeline in CCAM. Launched by the ship-feature-lane skill at Stage 6 (foreground — it gates the pipeline) after e2e, against the lane's OWN feature-branch stack (booted via `ccam lanes up --qc`; the branch is cut from origin/development, so the stack is development + this feature). Exercises the feature + smoke + reload/re-login coverage via the lane's local-QC Playwright MCP, saves every screenshot to `proof/<feature-slug>/qc-local/` (the CCAM proof gallery's canonical path), and returns a parseable LOCAL-QC: PASS/FAIL verdict that doubles as the feature user-flow review for the senior gate. <example>Context: ship-feature-lane in a lane finished Stage 3's e2e green. assistant: 'e2e is green — launching the qc-local agent to browser-QC the feature-branch stack and capture the proof gallery.' <commentary>Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser.</commentary></example>"
|
||||
description: "Local browser-QC agent for the ship-feature-lane pipeline in AgentHub. Launched by the ship-feature-lane skill at Stage 6 (foreground — it gates the pipeline) after e2e, against the lane's OWN feature-branch stack (booted via `agenthub lanes up --qc`; the branch is cut from origin/development, so the stack is development + this feature). Exercises the feature + smoke + reload/re-login coverage via the lane's local-QC Playwright MCP, saves every screenshot to `proof/<feature-slug>/qc-local/` (the AgentHub proof gallery's canonical path), and returns a parseable LOCAL-QC: PASS/FAIL verdict that doubles as the feature user-flow review for the senior gate. <example>Context: ship-feature-lane in a lane finished Stage 3's e2e green. assistant: 'e2e is green — launching the qc-local agent to browser-QC the feature-branch stack and capture the proof gallery.' <commentary>Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser.</commentary></example>"
|
||||
model: opus
|
||||
color: green
|
||||
memory: project
|
||||
@@ -49,15 +49,15 @@ You are an elite QA automation engineer validating ONE lane's feature on that la
|
||||
|
||||
## Context you will be given (by the ship-feature-lane skill)
|
||||
|
||||
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically; there is no marker file to check).
|
||||
- **The lane's working directory** (your cwd — AgentHub resolves the lane from it automatically; there is no marker file to check).
|
||||
- **Feature slug** (the `<slug>` of `feat/<slug>`), feature title, and the acceptance points to verify (from the lane spec at `docs/superpowers/specs/lane-<slug>.md` — read it yourself if the summary is thin; `git diff origin/development...HEAD` shows the change surface).
|
||||
|
||||
Setup: your cwd is the lane clone. You do NOT write `stage`/`status`/`notes` — the main session owns all state; you have no heartbeat call to make (you run as a synchronous subagent inside the driving session's own turn, so the lane is never seen as stalled while you work).
|
||||
|
||||
## Target environment
|
||||
|
||||
- **App**: the lane's own booted frontend — check `ccam lanes runtime` (or the `FE_PORT`/equivalent env the skill already resolved) for the actual port; it is isolated per lane. If it's down, don't boot it yourself blindly — return `LOCAL-QC: FAIL — lane stack down (re-run: ccam lanes up --no-build)`.
|
||||
- **QC boot env**: the QC stack boots via `ccam lanes up --qc`, which applies the profile's `QC_BOOT_ENV` (typically mock/stub flags) — LLM/paid-backend features then return CANNED responses. Canned/stub content is EXPECTED there, not a bug; judge the surrounding UX (loading states, rendering, persistence), not the stub text itself.
|
||||
- **App**: the lane's own booted frontend — check `agenthub lanes runtime` (or the `FE_PORT`/equivalent env the skill already resolved) for the actual port; it is isolated per lane. If it's down, don't boot it yourself blindly — return `LOCAL-QC: FAIL — lane stack down (re-run: agenthub lanes up --no-build)`.
|
||||
- **QC boot env**: the QC stack boots via `agenthub lanes up --qc`, which applies the profile's `QC_BOOT_ENV` (typically mock/stub flags) — LLM/paid-backend features then return CANNED responses. Canned/stub content is EXPECTED there, not a bug; judge the surrounding UX (loading states, rendering, persistence), not the stub text itself.
|
||||
- **Account / login.** Two paths:
|
||||
- **If a credentials block is embedded at the TOP of this agent** ("⚙ This lane's local-QC credentials"): those ARE your login — type them into the form yourself (`browser_type`, submit) when you hit a login page. You already have them; never read a file/script to fetch them.
|
||||
- **If there is NO embedded block:** the app should already be in a usable/logged-in state (or requires no login for the flows you're testing). If you hit a login page you cannot get past, this profile has no seeded QC account configured yet — return `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile`. Do not guess at credentials or invent a login flow.
|
||||
@@ -79,9 +79,9 @@ Setup: your cwd is the lane clone. You do NOT write `stage`/`status`/`notes` —
|
||||
2. **Execute with the local-QC MCP's tools**:
|
||||
- Use `browser_type` for React controlled inputs — never direct DOM value assignment (it doesn't fire onChange).
|
||||
- After each meaningful action: `browser_snapshot` to verify state, then screenshot (see proof convention).
|
||||
- Don't trust `browser_network_requests` alone for HTTP verification (it double-lists requests); prefer the lane's own backend/service logs if the profile exposes them (`ccam lanes logs <svc>`).
|
||||
- Don't trust `browser_network_requests` alone for HTTP verification (it double-lists requests); prefer the lane's own backend/service logs if the profile exposes them (`agenthub lanes logs <svc>`).
|
||||
- **Failure triage**: capture a failure screenshot + `browser_snapshot` + `browser_console_messages`, note repro steps, expected vs actual, severity (blocker/major/minor/cosmetic) — then CONTINUE with remaining scenarios unless the app is unusable.
|
||||
3. **Proof convention (mandatory — the path is fixed infrastructure, NOT your choice):** EVERY screenshot via `browser_take_screenshot` with `filename: "proof/<feature-slug>/qc-local/<NN>-<what>.png"`, numbered in flow order (e.g. `proof/edit-report/qc-local/03-dialog-open.png`). The MCP server's pinned `--output-dir` lands these in `<lane clone>/.playwright-mcp/proof/...` — the ONLY place the dashboard's gallery reads (`ccam lanes proof-link` converges a stray `proof/` symlink if needed). Absolute paths, other folders, or invented layouts = the proof is lost.
|
||||
3. **Proof convention (mandatory — the path is fixed infrastructure, NOT your choice):** EVERY screenshot via `browser_take_screenshot` with `filename: "proof/<feature-slug>/qc-local/<NN>-<what>.png"`, numbered in flow order (e.g. `proof/edit-report/qc-local/03-dialog-open.png`). The MCP server's pinned `--output-dir` lands these in `<lane clone>/.playwright-mcp/proof/...` — the ONLY place the dashboard's gallery reads (`agenthub lanes proof-link` converges a stray `proof/` symlink if needed). Absolute paths, other folders, or invented layouts = the proof is lost.
|
||||
|
||||
## Checkpoint & resume *(rate-limits and crashes must not zero your work)*
|
||||
|
||||
@@ -115,12 +115,12 @@ Record local-stack QA knowledge as you find it: flaky selectors/flows, features
|
||||
|
||||
- [ ] **Step 2: Write `senior-gate-reviewer.md`**
|
||||
|
||||
Write `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` with this exact content (ported from `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/senior-gate-reviewer.md` — `@@HARNESS_ROOT@@` dropped, `lane-sync-dev.sh --check` mapped to `ccam lanes sync-base --check`, `profile-cat.sh review-checks.md` mapped to a direct Read-tool reference; everything else carried over verbatim):
|
||||
Write `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` with this exact content (ported from `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/senior-gate-reviewer.md` — `@@HARNESS_ROOT@@` dropped, `lane-sync-dev.sh --check` mapped to `agenthub lanes sync-base --check`, `profile-cat.sh review-checks.md` mapped to a direct Read-tool reference; everything else carried over verbatim):
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "senior-gate-reviewer"
|
||||
description: "Final GO/NO-GO publish-readiness gate for the ship-feature-lane pipeline in CCAM. Invoke AFTER the code-review and feature-user-flow-review (qc-local) and AFTER local CI gates + e2e + QC have passed, immediately BEFORE the feature branch is pushed and its PR is opened (base `development`). Returns a single GO or NO-GO verdict with a required-fix list. A GO is the sole authorization to publish the PR. <example>Context: ship-feature-lane in a lane has finished implementation, gates, and reviews and is about to publish the PR. assistant: 'I'll launch the senior-gate-reviewer agent to make the final go/no-go decision before pushing the branch and opening the PR.' <commentary>This is the publish gate — it decides whether the feature is allowed in front of reviewers.</commentary></example>"
|
||||
description: "Final GO/NO-GO publish-readiness gate for the ship-feature-lane pipeline in AgentHub. Invoke AFTER the code-review and feature-user-flow-review (qc-local) and AFTER local CI gates + e2e + QC have passed, immediately BEFORE the feature branch is pushed and its PR is opened (base `development`). Returns a single GO or NO-GO verdict with a required-fix list. A GO is the sole authorization to publish the PR. <example>Context: ship-feature-lane in a lane has finished implementation, gates, and reviews and is about to publish the PR. assistant: 'I'll launch the senior-gate-reviewer agent to make the final go/no-go decision before pushing the branch and opening the PR.' <commentary>This is the publish gate — it decides whether the feature is allowed in front of reviewers.</commentary></example>"
|
||||
model: opus
|
||||
color: red
|
||||
memory: project
|
||||
@@ -130,11 +130,11 @@ You are the **Senior Gate Reviewer** — the final, independent publish-readines
|
||||
|
||||
## Context you will be given (by the ship-feature-lane skill)
|
||||
|
||||
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically).
|
||||
- **The lane's working directory** (your cwd — AgentHub resolves the lane from it automatically).
|
||||
- **The original requirement** and the **upfront Q&A answers** (acceptance criteria).
|
||||
- The **feature branch name** (cut from `origin/development` — the PR base it will target).
|
||||
- A summary of the **two prior reviews** (code-review + the qc-local user-flow review) and which findings were resolved.
|
||||
- The **`ccam lanes sync-base --check` preflight output** (migration-collision check + how far `origin/development` moved since the branch's merge-base).
|
||||
- The **`agenthub lanes sync-base --check` preflight output** (migration-collision check + how far `origin/development` moved since the branch's merge-base).
|
||||
- Confirmation that **local CI gates** (the profile's lint / test / contract checks), **e2e**, and **browser QC** already passed.
|
||||
|
||||
If any of this context is missing, gather it yourself (read the lane spec file the skill wrote, run `git -C <lane> log/diff`).
|
||||
@@ -147,8 +147,8 @@ If any of this context is missing, gather it yourself (read the lane spec file t
|
||||
|
||||
2. **Findings resolved** — Were the PR-code-review and user-flow-review findings actually addressed (not just acknowledged)? Spot-check **the PR diff** for each claimed fix. Unresolved material findings ⇒ NO-GO.
|
||||
|
||||
3. **Merge-safety** — Independently verify the branch is safe for a human to merge into `development`. **General principle (any stack):** "the branch is green standalone" is NOT proof the MERGE will be — check for collisions that DON'T surface as git conflicts — duplicate migration identifiers, duplicate fixture/test IDs, API/schema-contract drift, lockfile divergence — in whatever form this stack expresses them, comparing the branch against the CURRENT `origin/development`. Re-run the deterministic preflight yourself if the provided output is stale: `git -C <lane> fetch origin && ccam lanes sync-base --check <feature-branch>` (exit 5 = collision ⇒ NO-GO with the printed rename; also read its `DEV_DELTA`/`DEV_OVERLAP` — a large overlapping upstream delta means the locally-QC'd behavior may not match post-merge dev, weigh it).
|
||||
- **Stack-specific checks (from the active profile):** read `<lane>/.ccam/profile/review-checks.md` with the Read tool if it exists, and apply every check it lists (e.g. migration-identifier collisions, API/contract regeneration). Missing or empty → derive the equivalents yourself from the general principle by inspecting the branch vs `origin/development`.
|
||||
3. **Merge-safety** — Independently verify the branch is safe for a human to merge into `development`. **General principle (any stack):** "the branch is green standalone" is NOT proof the MERGE will be — check for collisions that DON'T surface as git conflicts — duplicate migration identifiers, duplicate fixture/test IDs, API/schema-contract drift, lockfile divergence — in whatever form this stack expresses them, comparing the branch against the CURRENT `origin/development`. Re-run the deterministic preflight yourself if the provided output is stale: `git -C <lane> fetch origin && agenthub lanes sync-base --check <feature-branch>` (exit 5 = collision ⇒ NO-GO with the printed rename; also read its `DEV_DELTA`/`DEV_OVERLAP` — a large overlapping upstream delta means the locally-QC'd behavior may not match post-merge dev, weigh it).
|
||||
- **Stack-specific checks (from the active profile):** read `<lane>/.agenthub/profile/review-checks.md` with the Read tool if it exists, and apply every check it lists (e.g. migration-identifier collisions, API/contract regeneration). Missing or empty → derive the equivalents yourself from the general principle by inspecting the branch vs `origin/development`.
|
||||
- **Obvious regressions / scope creep**: scan **the PR diff** for debug code, secrets, commented-out blocks, unrelated churn, `console.log`/`print` debugging, TODO/FIXME left in critical paths.
|
||||
|
||||
4. **UI/UX diligence (when the feature touches any form/screen)** — don't rubber-stamp the QC report; confirm it actually exercised layout rigor, because these defects slip through happy-path QC:
|
||||
@@ -242,7 +242,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-agents-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-agents-"));
|
||||
|
||||
const laneAgents = require("../lib/lane-agents");
|
||||
|
||||
@@ -505,21 +505,21 @@ git commit -m "feat(lanes): add POST /:id/agents/install route (E3)"
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `ccam lanes agents install` CLI
|
||||
### Task 4: `agenthub lanes agents install` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `POST /api/lanes/:id/agents/install` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
|
||||
- Produces: `ccam lanes agents install [<id>]`.
|
||||
- Consumes: `POST /api/lanes/:id/agents/install` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
|
||||
- Produces: `agenthub lanes agents install [<id>]`.
|
||||
|
||||
- [ ] **Step 1: Add the subcommand function**
|
||||
|
||||
In `bin/ccam.js`, add a new function near `cmdLanesProofLink` (`bin/ccam.js:2147`):
|
||||
In `bin/agenthub.js`, add a new function near `cmdLanesProofLink` (`bin/agenthub.js:2147`):
|
||||
|
||||
```js
|
||||
/** `ccam lanes agents install [<id>] [--cwd path]` — write the ship-feature-lane
|
||||
/** `agenthub lanes agents install [<id>] [--cwd path]` — write the ship-feature-lane
|
||||
* agent templates (qc-local, senior-gate-reviewer) into <lane>/.claude/agents/,
|
||||
* git-excluded. Never run automatically; a session calls it explicitly. */
|
||||
async function cmdLanesAgentsInstall(args) {
|
||||
@@ -541,7 +541,7 @@ async function cmdLanesAgentsInstall(args) {
|
||||
|
||||
- [ ] **Step 2: Wire the dispatcher**
|
||||
|
||||
In `bin/ccam.js`'s `lanes` case (`bin/ccam.js:3072` area, right after the `proof-link` line):
|
||||
In `bin/agenthub.js`'s `lanes` case (`bin/agenthub.js:3072` area, right after the `proof-link` line):
|
||||
|
||||
```js
|
||||
if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));
|
||||
@@ -557,7 +557,7 @@ Add directly below it:
|
||||
|
||||
- [ ] **Step 3: Add the help-catalog entry**
|
||||
|
||||
Right after the `lanes proof-link` catalog entry (`bin/ccam.js:2270-2273` area):
|
||||
Right after the `lanes proof-link` catalog entry (`bin/agenthub.js:2270-2273` area):
|
||||
|
||||
```js
|
||||
[
|
||||
@@ -572,7 +572,7 @@ Right after the `lanes proof-link` catalog entry (`bin/ccam.js:2270-2273` area):
|
||||
```bash
|
||||
npm run dev &
|
||||
sleep 3
|
||||
node bin/ccam.js lanes agents install 1
|
||||
node bin/agenthub.js lanes agents install 1
|
||||
echo "exit: $?"
|
||||
```
|
||||
|
||||
@@ -588,8 +588,8 @@ npm run test:server
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes agents install CLI (E3)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes agents install CLI (E3)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -612,10 +612,10 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)` (search `grep -n
|
||||
Stage 6 (browser QC) and Stage 7 (senior GO/NO-GO gate) run as subagents — `qc-local` and `senior-gate-reviewer` — that must exist in the lane's own `.claude/agents/` before the skill can launch them:
|
||||
|
||||
```bash
|
||||
ccam lanes agents install
|
||||
agenthub lanes agents install
|
||||
```
|
||||
|
||||
Writes both agent templates into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `ccam lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a CCAM upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
|
||||
Writes both agent templates into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `agenthub lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a AgentHub upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
|
||||
|
||||
**Credentials are not embedded.** Unlike Shipyard's original per-lane agent generation, these templates carry no QA account credentials — this repo has no seeded-QA-account system yet. `qc-local` degrades gracefully: if it hits a login page with no credentials block present, it reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile` rather than guessing.
|
||||
|
||||
@@ -637,7 +637,7 @@ Until these agents are ported, a lane reaching Stage 6 or 7 will be marked `bloc
|
||||
Replace with:
|
||||
|
||||
```
|
||||
**Stages 6 and 7 are now unblocked** — run `ccam lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
|
||||
**Stages 6 and 7 are now unblocked** — run `agenthub lanes agents install` once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the `qc-local`/`senior-gate-reviewer` subagent type; install and re-run.
|
||||
|
||||
See [the skill text](../.claude/skills/ship-feature-lane/SKILL.md) for the full pipeline definition.
|
||||
```
|
||||
@@ -665,5 +665,5 @@ npm run test:server
|
||||
|
||||
```bash
|
||||
git add docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
|
||||
git commit -m "docs(lanes): document ccam lanes agents install (E3)"
|
||||
git commit -m "docs(lanes): document agenthub lanes agents install (E3)"
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `ccam lanes integration <name>` reads a lane's `.ccam/profile/integrations.env` and reports whether `<NAME>_ENABLED=1` is set, exit 0/1 — so a driving skill can gate a stage on a real check instead of a hardcoded assumption.
|
||||
**Goal:** `agenthub lanes integration <name>` reads a lane's `.agenthub/profile/integrations.env` and reports whether `<NAME>_ENABLED=1` is set, exit 0/1 — so a driving skill can gate a stage on a real check instead of a hardcoded assumption.
|
||||
|
||||
**Architecture:** One function in the existing `lane-profile.js` (reusing its already-exported `parseEnvFile`), one lightweight route, one CLI subcommand. No new files beyond a test.
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("isIntegrationEnabled", () => {
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "PORTS=api\n");
|
||||
fs.writeFileSync(
|
||||
path.join(lane.cwd, ".ccam", "profile", "integrations.env"),
|
||||
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
|
||||
"TRACKER_ENABLED=1\nTRACKER_PROJECT=demo\n"
|
||||
);
|
||||
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), true);
|
||||
@@ -53,7 +53,7 @@ describe("isIntegrationEnabled", () => {
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "PORTS=api\n");
|
||||
fs.writeFileSync(
|
||||
path.join(lane.cwd, ".ccam", "profile", "integrations.env"),
|
||||
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
|
||||
"DEV_QC_ENABLED=0\n"
|
||||
);
|
||||
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "dev_qc"), false);
|
||||
@@ -74,7 +74,7 @@ In `server/lib/lane-profile.js`, add near `resolveProfile` (after it, since it r
|
||||
```js
|
||||
/**
|
||||
* Whether a named integration is turned on for this lane — reads
|
||||
* .ccam/profile/integrations.env (same two-location search as profile.env:
|
||||
* .agenthub/profile/integrations.env (same two-location search as profile.env:
|
||||
* the lane's own working copy first, the source repo second) and checks
|
||||
* <NAME>_ENABLED=1. A missing file or missing key is off, never an error —
|
||||
* same off-by-default shape every other optional declaration follows.
|
||||
@@ -144,11 +144,11 @@ git commit -m "feat(lanes): add isIntegrationEnabled toggle check (F3a)"
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/routes/lanes.js`
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `isIntegrationEnabled(lane, name) => boolean` from Task 1 (`require("../lib/lane-profile")` — already imported in `server/routes/lanes.js` for `HOOKS`/`runHook`/`resolveProfile`, just add `isIntegrationEnabled` to that existing destructure).
|
||||
- Produces: `GET /api/lanes/:id/integrations/:name` → `{enabled: boolean}`. `ccam lanes integration <name> [<id>]` → prints `on`/`off`, `process.exitCode` 0 when enabled, 1 when disabled.
|
||||
- Produces: `GET /api/lanes/:id/integrations/:name` → `{enabled: boolean}`. `agenthub lanes integration <name> [<id>]` → prints `on`/`off`, `process.exitCode` 0 when enabled, 1 when disabled.
|
||||
|
||||
- [ ] **Step 1: Add the route**
|
||||
|
||||
@@ -163,7 +163,7 @@ Add the route directly after the `/:id/mcp/sync` route (added in F1, search `rou
|
||||
```js
|
||||
/**
|
||||
* Whether a named integration (tracker, dev_qc, ci_wait, ...) is turned on
|
||||
* for this lane — reads .ccam/profile/integrations.env. Read-only, no
|
||||
* for this lane — reads .agenthub/profile/integrations.env. Read-only, no
|
||||
* sameOriginGuard needed (same reasoning GET /:id/git already documents:
|
||||
* that guard exists for destructive actions).
|
||||
*/
|
||||
@@ -176,16 +176,16 @@ router.get("/:id/integrations/:name", (req, res) => {
|
||||
|
||||
- [ ] **Step 2: Add the CLI subcommand**
|
||||
|
||||
In `bin/ccam.js`, add near `cmdLanesMcpSync`:
|
||||
In `bin/agenthub.js`, add near `cmdLanesMcpSync`:
|
||||
|
||||
```js
|
||||
/** `ccam lanes integration <name> [<id>]` — check whether a named
|
||||
/** `agenthub lanes integration <name> [<id>]` — check whether a named
|
||||
* integration (tracker, dev_qc, ci_wait) is turned on for this lane, per
|
||||
* its .ccam/profile/integrations.env. Exit 0 = on, 1 = off. */
|
||||
* its .agenthub/profile/integrations.env. Exit 0 = on, 1 = off. */
|
||||
async function cmdLanesIntegration(args) {
|
||||
const name = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!name) {
|
||||
console.error("usage: ccam lanes integration <name> [<id>]");
|
||||
console.error("usage: agenthub lanes integration <name> [<id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -230,7 +230,7 @@ npm run dev &
|
||||
sleep 3
|
||||
curl -s http://localhost:4820/api/lanes/1/integrations/tracker
|
||||
echo
|
||||
node bin/ccam.js lanes integration tracker 1
|
||||
node bin/agenthub.js lanes integration tracker 1
|
||||
echo "exit: $?"
|
||||
```
|
||||
|
||||
@@ -246,8 +246,8 @@ npm run test:server
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/routes/lanes.js bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes integration CLI + route (F3a)"
|
||||
git add server/routes/lanes.js bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes integration CLI + route (F3a)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -266,33 +266,33 @@ git commit -m "feat(lanes): add ccam lanes integration CLI + route (F3a)"
|
||||
Find:
|
||||
|
||||
```
|
||||
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in CCAM yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `ccam ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
|
||||
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in AgentHub yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `agenthub ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```
|
||||
- **Integration toggles: check, don't assume.** `ccam lanes integration tracker`, `ccam lanes integration dev_qc`, `ccam lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`ccam ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether CCAM can act on it yet.
|
||||
- **Integration toggles: check, don't assume.** `agenthub lanes integration tracker`, `agenthub lanes integration dev_qc`, `agenthub lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`agenthub ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether AgentHub can act on it yet.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Stage 9 (ticket) — record the toggle in evidence**
|
||||
|
||||
Find the Stage 9 section (search `grep -n "^### 9" .claude/skills/ship-feature-lane/SKILL.md`) and its skip line (likely something like `ccam stage reported --status ... ` or similar noting the skip — read the current text first with `sed -n '/^### 9/,/^### 10/p' .claude/skills/ship-feature-lane/SKILL.md` to get its exact current wording before editing, since this section's precise text was written in E1 and may have shifted slightly across later edits). Add one line noting `ccam lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`ccam lanes integration tracker\` — even when on, no ticketer agent exists yet to act on it)`.
|
||||
Find the Stage 9 section (search `grep -n "^### 9" .claude/skills/ship-feature-lane/SKILL.md`) and its skip line (likely something like `agenthub stage reported --status ... ` or similar noting the skip — read the current text first with `sed -n '/^### 9/,/^### 10/p' .claude/skills/ship-feature-lane/SKILL.md` to get its exact current wording before editing, since this section's precise text was written in E1 and may have shifted slightly across later edits). Add one line noting `agenthub lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`agenthub lanes integration tracker\` — even when on, no ticketer agent exists yet to act on it)`.
|
||||
|
||||
- [ ] **Step 3: `docs/LANES.md` — add a short subsection**
|
||||
|
||||
Under `## The ship-feature-lane skill (E1)`, after the `### Syncing MCP servers: mcp sync` subsection (added in F1), insert:
|
||||
|
||||
```markdown
|
||||
### Checking integration toggles: ccam lanes integration
|
||||
### Checking integration toggles: agenthub lanes integration
|
||||
|
||||
A profile can declare `.ccam/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
|
||||
A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
|
||||
|
||||
```bash
|
||||
ccam lanes integration tracker # exit 0 = on, 1 = off
|
||||
agenthub lanes integration tracker # exit 0 = on, 1 = off
|
||||
```
|
||||
|
||||
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`ccam ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.
|
||||
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`agenthub ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Roadmap progress line**
|
||||
@@ -300,7 +300,7 @@ This only reports what the PROFILE wants — it doesn't file a ticket, run dev-Q
|
||||
In `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, find the `## F` section's `**Progress:**` line (added in F1) and append:
|
||||
|
||||
```
|
||||
`integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`ccam ci`), and dev-QC remain.
|
||||
`integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`agenthub ci`), and dev-QC remain.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify and commit**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# F1 — `ccam lanes mcp sync` Implementation Plan
|
||||
# F1 — `agenthub lanes mcp sync` Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `ccam lanes mcp sync` writes a lane's `.mcp.json` from its source repo's already-configured MCP servers (relocating paths, pinning Playwright's proof output dir) and seeds Chromium browser profiles, so `SKILL.md`'s manual-fallback text stops being the only option.
|
||||
**Goal:** `agenthub lanes mcp sync` writes a lane's `.mcp.json` from its source repo's already-configured MCP servers (relocating paths, pinning Playwright's proof output dir) and seeds Chromium browser profiles, so `SKILL.md`'s manual-fallback text stops being the only option.
|
||||
|
||||
**Architecture:** One filesystem/JSON-only core module (`server/lib/lane-mcp.js` — no git calls, unlike E2/E3's cores), one route, one CLI subcommand, a `SKILL.md` edit.
|
||||
|
||||
@@ -48,7 +48,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-mcp-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-mcp-"));
|
||||
const FAKE_HOME = path.join(SUITE_ROOT, "home");
|
||||
fs.mkdirSync(FAKE_HOME, { recursive: true });
|
||||
process.env.HOME = FAKE_HOME;
|
||||
@@ -479,21 +479,21 @@ git commit -m "feat(lanes): add POST /:id/mcp/sync route (F1)"
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `ccam lanes mcp sync` CLI
|
||||
### Task 3: `agenthub lanes mcp sync` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `POST /api/lanes/:id/mcp/sync` (Task 2); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
|
||||
- Produces: `ccam lanes mcp sync [<id>]`.
|
||||
- Consumes: `POST /api/lanes/:id/mcp/sync` (Task 2); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
|
||||
- Produces: `agenthub lanes mcp sync [<id>]`.
|
||||
|
||||
- [ ] **Step 1: Add the subcommand function**
|
||||
|
||||
In `bin/ccam.js`, add near `cmdLanesAgentsInstall` (`bin/ccam.js:2161`):
|
||||
In `bin/agenthub.js`, add near `cmdLanesAgentsInstall` (`bin/agenthub.js:2161`):
|
||||
|
||||
```js
|
||||
/** `ccam lanes mcp sync [<id>] [--cwd path]` — relocate the source repo's
|
||||
/** `agenthub lanes mcp sync [<id>] [--cwd path]` — relocate the source repo's
|
||||
* already-configured MCP servers into <lane>/.mcp.json and seed Chromium
|
||||
* profiles. Never run automatically; a session calls it explicitly. */
|
||||
async function cmdLanesMcpSync(args) {
|
||||
@@ -518,7 +518,7 @@ async function cmdLanesMcpSync(args) {
|
||||
|
||||
- [ ] **Step 2: Wire the dispatcher**
|
||||
|
||||
In `bin/ccam.js`'s `lanes` case, right after the `agents install` check (`bin/ccam.js:3186-3188`):
|
||||
In `bin/agenthub.js`'s `lanes` case, right after the `agents install` check (`bin/agenthub.js:3186-3188`):
|
||||
|
||||
```js
|
||||
if (rest[0] === "agents" && rest[1] === "install") {
|
||||
@@ -536,7 +536,7 @@ Add directly below it:
|
||||
|
||||
- [ ] **Step 3: Add the help-catalog entry**
|
||||
|
||||
Right after the `lanes agents install` catalog entry (`bin/ccam.js:2364` area):
|
||||
Right after the `lanes agents install` catalog entry (`bin/agenthub.js:2364` area):
|
||||
|
||||
```js
|
||||
[
|
||||
@@ -551,7 +551,7 @@ Right after the `lanes agents install` catalog entry (`bin/ccam.js:2364` area):
|
||||
```bash
|
||||
npm run dev &
|
||||
sleep 3
|
||||
node bin/ccam.js lanes mcp sync 1
|
||||
node bin/agenthub.js lanes mcp sync 1
|
||||
echo "exit: $?"
|
||||
```
|
||||
|
||||
@@ -567,8 +567,8 @@ npm run test:server
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes mcp sync CLI (F1)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes mcp sync CLI (F1)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -587,13 +587,13 @@ git commit -m "feat(lanes): add ccam lanes mcp sync CLI (F1)"
|
||||
Find (Setup section, `.claude/skills/ship-feature-lane/SKILL.md:37`):
|
||||
|
||||
```
|
||||
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `ccam lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `agenthub lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```
|
||||
- **MCP preflight (fail fast):** if this is the lane's first run, or a required Playwright MCP's `browser_*` tools aren't available in this session, run `ccam lanes mcp sync` — it relocates the source repo's already-configured MCP servers into this lane's `.mcp.json`. If the source repo has none configured (`ENOMCPCONFIG`), tell the human to configure `.mcp.json` manually for the source repo first. Either way, restart the session after syncing so the new config loads — `playwright` and a local-QC MCP are always required for Stage 3/6. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
- **MCP preflight (fail fast):** if this is the lane's first run, or a required Playwright MCP's `browser_*` tools aren't available in this session, run `agenthub lanes mcp sync` — it relocates the source repo's already-configured MCP servers into this lane's `.mcp.json`. If the source repo has none configured (`ENOMCPCONFIG`), tell the human to configure `.mcp.json` manually for the source repo first. Either way, restart the session after syncing so the new config loads — `playwright` and a local-QC MCP are always required for Stage 3/6. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `docs/LANES.md` — add an mcp-sync subsection**
|
||||
@@ -603,10 +603,10 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)`, insert a new su
|
||||
```markdown
|
||||
### Syncing MCP servers: mcp sync
|
||||
|
||||
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `ccam lanes mcp sync` gives it those:
|
||||
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run Stage 3/6. `agenthub lanes mcp sync` gives it those:
|
||||
|
||||
```bash
|
||||
ccam lanes mcp sync
|
||||
agenthub lanes mcp sync
|
||||
```
|
||||
|
||||
Reads the source repo's already-configured `mcpServers` from `~/.claude.json` (normal Claude Code project-scope config — set this up for the source repo once, the same way you would for any project), relocates any absolute path under the source repo to the lane's own directory, pins a `@playwright/mcp` server's `--output-dir` to the lane's `.playwright-mcp` (so proof screenshots land where the proof gallery reads them), and writes `<lane>/.mcp.json`. Also seeds the lane's Chromium browser profiles from the source repo's own — preserves saved logins, and never overwrites a profile that already exists at the destination.
|
||||
@@ -635,5 +635,5 @@ npm run test:server
|
||||
|
||||
```bash
|
||||
git add .claude/skills/ship-feature-lane/SKILL.md docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
|
||||
git commit -m "docs(lanes): document ccam lanes mcp sync (F1)"
|
||||
git commit -m "docs(lanes): document agenthub lanes mcp sync (F1)"
|
||||
```
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# E2 — `ccam lanes sync-base` Implementation Plan
|
||||
# E2 — `agenthub lanes sync-base` Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `ccam lanes sync-base` exists as a real, working command — the migration-collision preflight (`--check`), the one sanctioned merge (`origin/development` into a feature branch), and its conflict-resolution follow-up (`--continue`) — so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 2/8/12 references to it stop being "later task" placeholders.
|
||||
**Goal:** `agenthub lanes sync-base` exists as a real, working command — the migration-collision preflight (`--check`), the one sanctioned merge (`origin/development` into a feature branch), and its conflict-resolution follow-up (`--continue`) — so `.claude/skills/ship-feature-lane/SKILL.md`'s Stage 2/8/12 references to it stop being "later task" placeholders.
|
||||
|
||||
**Architecture:** One new core module (`server/lib/lane-sync.js`, pure git operations via `worktree.js`'s existing `git()` helper), two new profile declarations (`MIGRATIONS_DIR`, `GENERATED_MERGE_PATHS`), one new synchronous route (`POST /:id/sync-base`), one new CLI subcommand, and a `SKILL.md` edit that turns three "if it exists yet" conditionals into real instructions.
|
||||
|
||||
@@ -145,7 +145,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-sync-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-sync-"));
|
||||
|
||||
const laneSync = require("../lib/lane-sync");
|
||||
|
||||
@@ -488,7 +488,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
|
||||
gc(GEN_PUSHER, "push", "-q", "origin", "development");
|
||||
|
||||
// A regen hook the fold-in step will run.
|
||||
const profileDir = path.join(GEN_LANE, ".ccam", "profile");
|
||||
const profileDir = path.join(GEN_LANE, ".agenthub", "profile");
|
||||
fs.mkdirSync(path.join(profileDir, "hooks"), { recursive: true });
|
||||
fs.writeFileSync(path.join(profileDir, "profile.env"), "PORTS=api\n");
|
||||
fs.writeFileSync(
|
||||
@@ -503,7 +503,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
|
||||
env: { MIGRATIONS_DIR: "", GENERATED_MERGE_PATHS: "api/openapi.json" },
|
||||
generatedMergePaths: ["api/openapi.json"],
|
||||
hooks: new Set(["regen"]),
|
||||
dir: path.join(GEN_LANE, ".ccam", "profile"),
|
||||
dir: path.join(GEN_LANE, ".agenthub", "profile"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -567,10 +567,10 @@ async function unmergedFiles(cwd) {
|
||||
* already established. */
|
||||
async function setupMergeDriver(cwd, generatedPaths) {
|
||||
if (!generatedPaths.length) return;
|
||||
await git(cwd, ["config", "merge.ccam-generated.driver", "true"]);
|
||||
await git(cwd, ["config", "merge.agenthub-generated.driver", "true"]);
|
||||
await git(cwd, [
|
||||
"config",
|
||||
"merge.ccam-generated.name",
|
||||
"merge.agenthub-generated.name",
|
||||
"keep ours; regenerated post-merge by the profile regen hook",
|
||||
]);
|
||||
|
||||
@@ -581,7 +581,7 @@ async function setupMergeDriver(cwd, generatedPaths) {
|
||||
const lines = new Set(existing.split("\n").filter(Boolean));
|
||||
let changed = false;
|
||||
for (const gp of generatedPaths) {
|
||||
const line = `${gp} merge=ccam-generated`;
|
||||
const line = `${gp} merge=agenthub-generated`;
|
||||
if (!lines.has(line)) {
|
||||
lines.add(line);
|
||||
changed = true;
|
||||
@@ -924,7 +924,7 @@ There's no HTTP-level test harness for lane routes in this repo (per the Global
|
||||
```bash
|
||||
npm run dev &
|
||||
sleep 3
|
||||
# Replace 1 with a real lane id that has a .ccam/profile and a feature branch.
|
||||
# Replace 1 with a real lane id that has a .agenthub/profile and a feature branch.
|
||||
curl -s -X POST http://localhost:4820/api/lanes/1/sync-base \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mode":"check","branch":"feat/some-branch"}' | node -e "process.stdin.pipe(require('node:fs').createWriteStream('/dev/stdout'))"
|
||||
@@ -948,18 +948,18 @@ git commit -m "feat(lanes): add POST /:id/sync-base route (E2)"
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `ccam lanes sync-base` CLI
|
||||
### Task 4: `agenthub lanes sync-base` CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js`
|
||||
- Modify: `bin/agenthub.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `POST /api/lanes/:id/sync-base` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/ccam.js`).
|
||||
- Produces: `ccam lanes sync-base [<id>] [--check|--continue] [branch]` — `process.exitCode` set to `0`/`4`/`5` matching the route's `code`, so `SKILL.md`'s documented exit-code contract holds when scripted.
|
||||
- Consumes: `POST /api/lanes/:id/sync-base` (Task 3); `resolveLaneArg(args)`, `post(path, body, options)` (both already defined in `bin/agenthub.js`).
|
||||
- Produces: `agenthub lanes sync-base [<id>] [--check|--continue] [branch]` — `process.exitCode` set to `0`/`4`/`5` matching the route's `code`, so `SKILL.md`'s documented exit-code contract holds when scripted.
|
||||
|
||||
- [ ] **Step 1: Add the subcommand to `cmdLanesRuntime`**
|
||||
|
||||
In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/ccam.js:1902`) with a new `sync-base` branch, inside the same `cmdLanesRuntime(sub, args)` function:
|
||||
In `bin/agenthub.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/agenthub.js:1902`) with a new `sync-base` branch, inside the same `cmdLanesRuntime(sub, args)` function:
|
||||
|
||||
```js
|
||||
if (sub === "sync-base") {
|
||||
@@ -994,7 +994,7 @@ In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/c
|
||||
console.error(` conflicted: ${result.conflictedFiles.join(", ")}`);
|
||||
console.error(" resolve, then: git add <resolved files> && git commit --no-edit");
|
||||
console.error(
|
||||
` then: ccam lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}`
|
||||
` then: agenthub lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}`
|
||||
);
|
||||
process.exitCode = 4;
|
||||
return;
|
||||
@@ -1028,7 +1028,7 @@ In `bin/ccam.js`, extend the `if (sub === "hook") { ... }` block (ends at `bin/c
|
||||
|
||||
- [ ] **Step 2: Wire the subcommand into the dispatcher**
|
||||
|
||||
`bin/ccam.js:3088` currently reads:
|
||||
`bin/agenthub.js:3088` currently reads:
|
||||
|
||||
```js
|
||||
if (["up", "down", "runtime", "logs", "hook"].includes(rest[0])) {
|
||||
@@ -1046,7 +1046,7 @@ Change to:
|
||||
|
||||
- [ ] **Step 3: Add the help-catalog entry**
|
||||
|
||||
In the command catalog array (`bin/ccam.js:2268`, right after the `lanes hook` entry), add:
|
||||
In the command catalog array (`bin/agenthub.js:2268`, right after the `lanes hook` entry), add:
|
||||
|
||||
```js
|
||||
[
|
||||
@@ -1061,7 +1061,7 @@ In the command catalog array (`bin/ccam.js:2268`, right after the `lanes hook` e
|
||||
```bash
|
||||
npm run dev &
|
||||
sleep 3
|
||||
node bin/ccam.js lanes sync-base --check feat/some-branch 1
|
||||
node bin/agenthub.js lanes sync-base --check feat/some-branch 1
|
||||
echo "exit: $?"
|
||||
```
|
||||
|
||||
@@ -1077,8 +1077,8 @@ npm run test:server
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(lanes): add ccam lanes sync-base CLI (E2)"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(lanes): add agenthub lanes sync-base CLI (E2)"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1097,14 +1097,14 @@ git commit -m "feat(lanes): add ccam lanes sync-base CLI (E2)"
|
||||
In `.claude/skills/ship-feature-lane/SKILL.md`, find (Stage 2, `### 2 — Pre-push CI gates + dev preflight`):
|
||||
|
||||
```
|
||||
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`ccam stage gates --evidence "sync-base preflight unavailable, skipped"`).
|
||||
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`agenthub stage gates --evidence "sync-base preflight unavailable, skipped"`).
|
||||
- Once built, its contract is: exit 5 on a migration-number collision (print the exact rename, do it on the feature branch, re-run Stage 2); informational `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise (you do NOT sync the branch for it — GitHub merges non-conflicting histories fine).
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```
|
||||
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. Exit 5 on a migration-number collision: rename the printed file to the suggested number on the feature branch (`git mv`, update any in-file references), then re-run Stage 2. Exit 0 with `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise — informational, you do NOT sync the branch for it (GitHub merges non-conflicting histories fine); a large overlapping delta is a heads-up that post-merge behavior may differ from what you test locally.
|
||||
- `agenthub lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. Exit 5 on a migration-number collision: rename the printed file to the suggested number on the feature branch (`git mv`, update any in-file references), then re-run Stage 2. Exit 0 with `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise — informational, you do NOT sync the branch for it (GitHub merges non-conflicting histories fine); a large overlapping delta is a heads-up that post-merge behavior may differ from what you test locally.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Stage 8 — make the re-run unconditional**
|
||||
@@ -1112,13 +1112,13 @@ Replace with:
|
||||
Find (Stage 8, around `SKILL.md:127`):
|
||||
|
||||
```
|
||||
- Re-run the preflight if `ccam lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
|
||||
- Re-run the preflight if `agenthub lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```
|
||||
- Re-run the preflight: `ccam lanes sync-base --check feat/<slug>` — development may have moved while you were in QC. A migration collision here (exit 5) sends you back to Stage 2 with the rename; a clean result (exit 0) proceeds to the push.
|
||||
- Re-run the preflight: `agenthub lanes sync-base --check feat/<slug>` — development may have moved while you were in QC. A migration collision here (exit 5) sends you back to Stage 2 with the rename; a clean result (exit 0) proceeds to the push.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Stage 12 — unconditional merge + the exit-4/`--continue` mechanics**
|
||||
@@ -1127,7 +1127,7 @@ Find (Stage 12, around `SKILL.md:155`):
|
||||
|
||||
```
|
||||
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
|
||||
- If `ccam lanes sync-base` exists by now: `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
|
||||
- If `agenthub lanes sync-base` exists by now: `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
|
||||
- If it doesn't exist yet: `git fetch origin && git merge origin/development` directly on the feature branch, resolve conflicts the same way, commit.
|
||||
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
|
||||
```
|
||||
@@ -1136,8 +1136,8 @@ Replace with:
|
||||
|
||||
```
|
||||
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
|
||||
- `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). A migration-number collision (exit 5) means nothing was merged — rename the printed file on the feature branch, re-run Stage 2, then retry this step.
|
||||
- **Exit 4 — merge conflict, left in place on purpose.** Resolve every conflict thoughtfully on the feature branch — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. Never hand-merge a generated contract/client file listed in the profile's `GENERATED_MERGE_PATHS` — the keep-ours driver + regen own them. `git add` ONLY the conflicted files, `git commit --no-edit`, then `ccam lanes sync-base --continue feat/<slug>` (folds any regenerated artifacts into a follow-up commit).
|
||||
- `agenthub lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). A migration-number collision (exit 5) means nothing was merged — rename the printed file on the feature branch, re-run Stage 2, then retry this step.
|
||||
- **Exit 4 — merge conflict, left in place on purpose.** Resolve every conflict thoughtfully on the feature branch — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. Never hand-merge a generated contract/client file listed in the profile's `GENERATED_MERGE_PATHS` — the keep-ours driver + regen own them. `git add` ONLY the conflicted files, `git commit --no-edit`, then `agenthub lanes sync-base --continue feat/<slug>` (folds any regenerated artifacts into a follow-up commit).
|
||||
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
|
||||
```
|
||||
|
||||
@@ -1148,12 +1148,12 @@ In `docs/LANES.md`, under `## The ship-feature-lane skill (E1)` (`docs/LANES.md:
|
||||
```markdown
|
||||
### Dev preflight and merge safety: sync-base
|
||||
|
||||
`ccam lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
|
||||
`agenthub lanes sync-base` is the ONE sanctioned merge in the pipeline — `origin/development` into a feature branch — used at Stages 2, 8, and 12. Three modes:
|
||||
|
||||
```bash
|
||||
ccam lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
|
||||
ccam lanes sync-base feat/<slug> # merge origin/development into the feature branch
|
||||
ccam lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
|
||||
agenthub lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
|
||||
agenthub lanes sync-base feat/<slug> # merge origin/development into the feature branch
|
||||
agenthub lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
|
||||
```
|
||||
|
||||
Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, then `--continue`), `5` migration-number collision (nothing merged — rename the printed file, re-run).
|
||||
@@ -1161,7 +1161,7 @@ Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, th
|
||||
Two profile declarations control it, both empty (off) by default:
|
||||
|
||||
```bash
|
||||
# .ccam/profile/profile.env
|
||||
# .agenthub/profile/profile.env
|
||||
MIGRATIONS_DIR="db/migrations"
|
||||
GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
|
||||
```
|
||||
@@ -1194,5 +1194,5 @@ npm run test:server
|
||||
|
||||
```bash
|
||||
git add .claude/skills/ship-feature-lane/SKILL.md docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
|
||||
git commit -m "docs(lanes): document ccam lanes sync-base (E2)"
|
||||
git commit -m "docs(lanes): document agenthub lanes sync-base (E2)"
|
||||
```
|
||||
|
||||
@@ -45,8 +45,8 @@ Insert directly after the `/:id/mcp/sync` route closes (`server/routes/lanes.js`
|
||||
```js
|
||||
/**
|
||||
* Detect a Node.js project at this lane's OWN directory and scaffold
|
||||
* .ccam/profile/ if one is found — the HTTP equivalent of
|
||||
* `ccam lanes profile init`, always targeting lane.cwd (never an arbitrary
|
||||
* .agenthub/profile/ if one is found — the HTTP equivalent of
|
||||
* `agenthub lanes profile init`, always targeting lane.cwd (never an arbitrary
|
||||
* path; the CLI's <repo> argument has no meaning here, this lane's own
|
||||
* directory is the only sensible target). "No Node.js project detected" is
|
||||
* a normal 200 outcome, not an error — most lanes won't be auto-detectable
|
||||
@@ -322,7 +322,7 @@ git commit -m "feat(lanes): auto-setup (profile/agents/mcp) after Add Lane (F5)"
|
||||
In `docs/LANES.md`, find the `## Creating a lane` section (search `grep -n "^## Creating a lane" docs/LANES.md`) and add a short paragraph after its existing content:
|
||||
|
||||
```markdown
|
||||
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `ccam lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `ccam lanes agents install`, and `ccam lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
|
||||
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `agenthub lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `agenthub lanes agents install`, and `agenthub lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify and commit**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Surface `ccam lanes agents install`, `ccam lanes mcp sync`, `ccam lanes integration`, `ccam lanes sync-base --check`, `ccam skills install`, and `ccam lanes gc` as clickable UI, so they aren't terminal-only.
|
||||
**Goal:** Surface `agenthub lanes agents install`, `agenthub lanes mcp sync`, `agenthub lanes integration`, `agenthub lanes sync-base --check`, `agenthub skills install`, and `agenthub lanes gc` as clickable UI, so they aren't terminal-only.
|
||||
|
||||
**Architecture:** Two new machine-wide backend routes (`POST /api/skills/install`, `POST /api/lanes/gc`) backed by a small extracted lib module (`server/lib/skills-install.js`) and the already-built `lane-gc.js`; six new client API methods; four additions to `LaneCard.tsx` (gated on `runtime?.available`); two additions to `Settings.tsx`; bilingual i18n keys; test coverage for both.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
- Every applicable `.js`/`.tsx` source file MUST start with the project's authorship header — verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`. `.json` i18n files do not need one (confirm against an existing locale file, which has none).
|
||||
- **No merge button anywhere.** The "Check dev sync" button in `LaneCard.tsx` calls `sync-base` in `mode: "check"` ONLY — read-only preflight. Merging (`mode: "merge"` / `--continue`) is never exposed in this UI; that stays a session/skill action.
|
||||
- `bin/ccam.js`'s `cmdSkillsInstall` must be refactored to CALL the extracted `server/lib/skills-install.js` function, not duplicate its logic — the CLI and the new route share one implementation.
|
||||
- `bin/agenthub.js`'s `cmdSkillsInstall` must be refactored to CALL the extracted `server/lib/skills-install.js` function, not duplicate its logic — the CLI and the new route share one implementation.
|
||||
- New routes follow this repo's existing error-shape convention: `200` with the resource on success, `4xx` with `{error: {code, message}}` on a documented failure, `500` with `{error: {code, message}}` on anything else.
|
||||
- Never use `git add -A`. Stage exactly the files each task names.
|
||||
- Run `npm run test:server` (full suite) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every backend-touching commit; run `npm run test:client` before every frontend-touching commit.
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
**Files:**
|
||||
- Create: `server/lib/skills-install.js`
|
||||
- Modify: `bin/ccam.js:1531-1542` (`cmdSkillsInstall`, refactor to call the new lib)
|
||||
- Modify: `bin/agenthub.js:1531-1542` (`cmdSkillsInstall`, refactor to call the new lib)
|
||||
- Modify: `server/routes/lanes.js` (new route — see note below on router mounting)
|
||||
- Modify: `server/index.js` or wherever routers are mounted (need to confirm — see Step 0)
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
* @file Installs .claude/skills/ship-feature-lane/ into ~/.claude/skills/, so
|
||||
* /ship-feature-lane is discoverable from a session running inside any
|
||||
* lane's own working directory — not just inside this repo, where it lives
|
||||
* until installed. Shared by bin/ccam.js's `ccam skills install` CLI and the
|
||||
* until installed. Shared by bin/agenthub.js's `agenthub skills install` CLI and the
|
||||
* POST /api/skills/install route, so there is exactly one copy of this
|
||||
* logic. Pure filesystem action.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -50,7 +50,7 @@ const path = require("node:path");
|
||||
|
||||
/**
|
||||
* @param {{repoRoot: string}} options - `repoRoot` is this checkout's own
|
||||
* root (bin/ccam.js already computes this as `REPO_ROOT`; the server
|
||||
* root (bin/agenthub.js already computes this as `REPO_ROOT`; the server
|
||||
* computes its own equivalent — see the route for how).
|
||||
* @returns {{installed: true, path: string}}
|
||||
*/
|
||||
@@ -68,9 +68,9 @@ function installShipFeatureLaneSkill({ repoRoot }) {
|
||||
module.exports = { installShipFeatureLaneSkill };
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Refactor `bin/ccam.js`'s `cmdSkillsInstall` to call it**
|
||||
- [ ] **Step 2: Refactor `bin/agenthub.js`'s `cmdSkillsInstall` to call it**
|
||||
|
||||
Replace the current body (`bin/ccam.js:1531-1542`):
|
||||
Replace the current body (`bin/agenthub.js:1531-1542`):
|
||||
|
||||
```js
|
||||
function cmdSkillsInstall() {
|
||||
@@ -121,7 +121,7 @@ const { sameOriginGuard } = require("./run");
|
||||
const router = Router();
|
||||
|
||||
/** This server's own checkout root — server/routes/skills.js is two levels
|
||||
* under it (server/routes/), same computation bin/ccam.js's REPO_ROOT
|
||||
* under it (server/routes/), same computation bin/agenthub.js's REPO_ROOT
|
||||
* already does from its own location. */
|
||||
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
@@ -158,7 +158,7 @@ app.use("/api/skills", skillsRouter);
|
||||
```bash
|
||||
npm run dev &
|
||||
sleep 3
|
||||
node bin/ccam.js skills install
|
||||
node bin/agenthub.js skills install
|
||||
echo "CLI exit: $?"
|
||||
curl -s -X POST http://localhost:4820/api/skills/install | head -c 200
|
||||
echo
|
||||
@@ -176,7 +176,7 @@ npm run test:server
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add server/lib/skills-install.js server/routes/skills.js server/index.js bin/ccam.js
|
||||
git add server/lib/skills-install.js server/routes/skills.js server/index.js bin/agenthub.js
|
||||
git commit -m "feat(lanes): extract skills-install lib, add POST /api/skills/install (F4)"
|
||||
```
|
||||
|
||||
@@ -551,7 +551,7 @@ npm run dev &
|
||||
sleep 3
|
||||
```
|
||||
|
||||
Open `http://localhost:5173`, navigate to Workspace, select a lane with a `.ccam/profile` (`runtime.available`). Confirm: three new buttons appear, three integration badges appear (gray = off, since no `integrations.env` exists yet on a fresh lane), clicking "Check dev sync" shows a result line. Stop the dev server afterward.
|
||||
Open `http://localhost:5173`, navigate to Workspace, select a lane with a `.agenthub/profile` (`runtime.available`). Confirm: three new buttons appear, three integration badges appear (gray = off, since no `integrations.env` exists yet on a fresh lane), clicking "Check dev sync" shows a result line. Stop the dev server afterward.
|
||||
|
||||
- [ ] **Step 6: Run the client suite**
|
||||
|
||||
@@ -767,7 +767,7 @@ describe("agents install / mcp sync / integration badges / sync check", () => {
|
||||
provisioned: true,
|
||||
up: false,
|
||||
slot: 1,
|
||||
profileDir: "/work/demo/.ccam/profile",
|
||||
profileDir: "/work/demo/.agenthub/profile",
|
||||
ports: {},
|
||||
});
|
||||
vi.mocked(api.lanes.integration).mockImplementation((_id, name) =>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Replace the dashboard's "Run Claude from the browser" feature (spawns `claude --output-format stream-json`, renders custom chat bubbles) with a real tmux-backed PTY streamed to the browser via `xterm.js`, giving full two-way interactivity (type, resume, kill) from both the dashboard and a real terminal attached to the same named tmux session.
|
||||
|
||||
**Architecture:** One tmux session per lane (`ccam-lane-<id>`), created/attached/killed via `tmux` CLI calls from a new `server/lib/tmux.js`. `server/lib/pty-run.js` owns the run lifecycle (Start/Resume/Kill/List — List is now a computed read of `tmux list-sessions`, not cached in-memory state) and a `node-pty`-backed attach used per WebSocket connection on a new `/ws-pty/:runId` path. The client's `TerminalView.tsx` (`@xterm/xterm`) replaces `RunConsole.tsx`. Hook-derived session/agent data is untouched — it already updates independent of the run transport.
|
||||
**Architecture:** One tmux session per lane (`agenthub-lane-<id>`), created/attached/killed via `tmux` CLI calls from a new `server/lib/tmux.js`. `server/lib/pty-run.js` owns the run lifecycle (Start/Resume/Kill/List — List is now a computed read of `tmux list-sessions`, not cached in-memory state) and a `node-pty`-backed attach used per WebSocket connection on a new `/ws-pty/:runId` path. The client's `TerminalView.tsx` (`@xterm/xterm`) replaces `RunConsole.tsx`. Hook-derived session/agent data is untouched — it already updates independent of the run transport.
|
||||
|
||||
**Tech Stack:** Node.js/Express, `ws`, `node-pty` (new), `better-sqlite3`, React, `@xterm/xterm` + `@xterm/addon-fit` (new), `node:test`, Vitest.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
- No fallback to the old stream-json mode — it is deleted, not flagged off (spec: "Approach").
|
||||
- The `/ws-pty/:runId` path must reuse the exact same `verifyClient` auth (Host allowlist + `DASHBOARD_TOKEN`) as the existing `/ws` path (spec: "WebSocket transport").
|
||||
- `runId` (tmux session name) must be validated against `^ccam-lane-\d+$` before any tmux command touches it — untrusted input must never reach a shell-adjacent command unvalidated (spec: "WebSocket transport" trust boundary).
|
||||
- `runId` (tmux session name) must be validated against `^agenthub-lane-\d+$` before any tmux command touches it — untrusted input must never reach a shell-adjacent command unvalidated (spec: "WebSocket transport" trust boundary).
|
||||
- All tmux/`node-pty` invocations use `execFile`/`node-pty.spawn` with an explicit argument array — never a concatenated shell string (matches this repo's existing rule for git in `server/lib/worktree.js`, `CLAUDE.md`).
|
||||
- Server tests must not exec real tmux (no CI has it installed) — mock the `tmux.js` module via an injectable exec seam, same style as `run-spawner.js`'s existing `__injectChildForTest`/`__reset` test seams.
|
||||
- Every new/modified `.js`/`.ts`/`.tsx` file keeps this repo's required file header (file overview + `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>` — `.claude/skills/file-headers/`).
|
||||
@@ -99,8 +99,8 @@ try {
|
||||
- [ ] **Step 5: Verify the migration runs clean on a fresh DB and an existing one**
|
||||
|
||||
```bash
|
||||
rm -f /tmp/ccam-migration-check.db
|
||||
DASHBOARD_DB_PATH=/tmp/ccam-migration-check.db node -e "require('./server/db')" && echo OK
|
||||
rm -f /tmp/agenthub-migration-check.db
|
||||
DASHBOARD_DB_PATH=/tmp/agenthub-migration-check.db node -e "require('./server/db')" && echo OK
|
||||
node --test server/__tests__/*.test.js 2>&1 | tail -20
|
||||
```
|
||||
|
||||
@@ -152,7 +152,7 @@ describe("tmux wrapper", () => {
|
||||
|
||||
it("hasSession returns true when execFileSync exits 0", () => {
|
||||
tmux.__setExecImpl(() => "");
|
||||
assert.equal(tmux.hasSession("ccam-lane-1"), true);
|
||||
assert.equal(tmux.hasSession("agenthub-lane-1"), true);
|
||||
});
|
||||
|
||||
it("hasSession returns false when execFileSync throws", () => {
|
||||
@@ -161,7 +161,7 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.equal(tmux.hasSession("ccam-lane-1"), false);
|
||||
assert.equal(tmux.hasSession("agenthub-lane-1"), false);
|
||||
});
|
||||
|
||||
it("newSession builds the correct argv", () => {
|
||||
@@ -170,12 +170,12 @@ describe("tmux wrapper", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
tmux.newSession({ name: "ccam-lane-1", cwd: "/tmp/repo", argv: ["claude", "--model", "opus"] });
|
||||
tmux.newSession({ name: "agenthub-lane-1", cwd: "/tmp/repo", argv: ["claude", "--model", "opus"] });
|
||||
assert.deepEqual(calls[0], [
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
"ccam-lane-1",
|
||||
"agenthub-lane-1",
|
||||
"-c",
|
||||
"/tmp/repo",
|
||||
"--",
|
||||
@@ -191,12 +191,12 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.doesNotThrow(() => tmux.killSession("ccam-lane-1"));
|
||||
assert.doesNotThrow(() => tmux.killSession("agenthub-lane-1"));
|
||||
});
|
||||
|
||||
it("listSessions filters by prefix and ignores unrelated sessions", () => {
|
||||
tmux.__setExecImpl(() => "ccam-lane-1\nccam-lane-2\nsome-other-session\n");
|
||||
assert.deepEqual(tmux.listSessions("ccam-lane-"), ["ccam-lane-1", "ccam-lane-2"]);
|
||||
tmux.__setExecImpl(() => "agenthub-lane-1\nagenthub-lane-2\nsome-other-session\n");
|
||||
assert.deepEqual(tmux.listSessions("agenthub-lane-"), ["agenthub-lane-1", "agenthub-lane-2"]);
|
||||
});
|
||||
|
||||
it("listSessions returns [] when tmux has no sessions at all (exit 1)", () => {
|
||||
@@ -205,7 +205,7 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.deepEqual(tmux.listSessions("ccam-lane-"), []);
|
||||
assert.deepEqual(tmux.listSessions("agenthub-lane-"), []);
|
||||
});
|
||||
|
||||
it("isTmuxAvailable reflects whether the binary resolves on PATH", () => {
|
||||
@@ -233,9 +233,9 @@ Expected: FAIL — `Cannot find module '../lib/tmux'`.
|
||||
/**
|
||||
* @file tmux.js
|
||||
* @description Thin wrapper around the `tmux` CLI for the terminal-run
|
||||
* feature. Every dashboard-managed session is named `ccam-lane-<id>` (see
|
||||
* feature. Every dashboard-managed session is named `agenthub-lane-<id>` (see
|
||||
* `pty-run.js`) so a real terminal can attach to the exact same session with
|
||||
* `tmux attach -t ccam-lane-<id>` (or `ccam lanes shell`). Never builds a
|
||||
* `tmux attach -t agenthub-lane-<id>` (or `agenthub lanes shell`). Never builds a
|
||||
* shell string — every call is `execFileSync("tmux", [...argv])` with an
|
||||
* explicit argument array (matches this repo's rule for git in worktree.js).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -342,9 +342,9 @@ git commit -m "feat(run): add tmux command wrapper with an injectable exec seam"
|
||||
- Produces (consumed by Task 4's route rewrite and Task 5's WS attach):
|
||||
- `spawnRun({laneId, cwd, model, permissionMode, effort, resumeSessionId, initialPrompt}): {id: string, ...}` — `id` is the tmux session name.
|
||||
- `killRun(id: string): boolean`
|
||||
- `listRuns(): Array<PublicRun>` — computed fresh from `tmux.listSessions("ccam-lane-")` each call, no cached Map.
|
||||
- `listRuns(): Array<PublicRun>` — computed fresh from `tmux.listSessions("agenthub-lane-")` each call, no cached Map.
|
||||
- `getRun(id: string): PublicRun | null`
|
||||
- `laneIdFromRunId(id: string): number | null` — parses `ccam-lane-<id>` back to the numeric lane id; used by Task 4's WS runId validation.
|
||||
- `laneIdFromRunId(id: string): number | null` — parses `agenthub-lane-<id>` back to the numeric lane id; used by Task 4's WS runId validation.
|
||||
|
||||
Where `PublicRun` is `{id, laneId, cwd, model, permissionMode, effort, resumeSessionId, status: "running"|"gone", startedAt, sessionId}` (`sessionId`/`startedAt` come from the `dashboard_runs` row via `dashboard-runs.getRun(id)`, since tmux itself doesn't know Claude's session id).
|
||||
|
||||
@@ -384,7 +384,7 @@ describe("pty-run", () => {
|
||||
tmux.__reset();
|
||||
});
|
||||
|
||||
it("spawnRun creates a new tmux session named ccam-lane-<id> when none exists", () => {
|
||||
it("spawnRun creates a new tmux session named agenthub-lane-<id> when none exists", () => {
|
||||
const calls = [];
|
||||
tmux.__setExecImpl((args) => {
|
||||
calls.push(args);
|
||||
@@ -396,10 +396,10 @@ describe("pty-run", () => {
|
||||
return "";
|
||||
});
|
||||
const handle = pty.spawnRun({ laneId: 42, cwd: "/tmp/repo", model: "opus" });
|
||||
assert.equal(handle.id, "ccam-lane-42");
|
||||
assert.equal(handle.id, "agenthub-lane-42");
|
||||
const newSessionCall = calls.find((c) => c[0] === "new-session");
|
||||
assert.ok(newSessionCall, "expected a new-session call");
|
||||
assert.deepEqual(newSessionCall.slice(0, 6), ["new-session", "-d", "-s", "ccam-lane-42", "-c", "/tmp/repo"]);
|
||||
assert.deepEqual(newSessionCall.slice(0, 6), ["new-session", "-d", "-s", "agenthub-lane-42", "-c", "/tmp/repo"]);
|
||||
assert.ok(newSessionCall.includes("claude"));
|
||||
assert.ok(newSessionCall.includes("--model"));
|
||||
assert.ok(newSessionCall.includes("opus"));
|
||||
@@ -412,7 +412,7 @@ describe("pty-run", () => {
|
||||
return ""; // has-session succeeds → already running
|
||||
});
|
||||
const handle = pty.spawnRun({ laneId: 7, cwd: "/tmp/repo" });
|
||||
assert.equal(handle.id, "ccam-lane-7");
|
||||
assert.equal(handle.id, "agenthub-lane-7");
|
||||
assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
|
||||
});
|
||||
|
||||
@@ -453,36 +453,36 @@ describe("pty-run", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
assert.equal(pty.killRun("ccam-lane-5"), true);
|
||||
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "ccam-lane-5"));
|
||||
assert.equal(pty.killRun("agenthub-lane-5"), true);
|
||||
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "agenthub-lane-5"));
|
||||
});
|
||||
|
||||
it("listRuns reflects live tmux-session state, not cached memory", () => {
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "list-sessions") return "ccam-lane-1\nccam-lane-2\n";
|
||||
if (args[0] === "list-sessions") return "agenthub-lane-1\nagenthub-lane-2\n";
|
||||
return "";
|
||||
});
|
||||
const first = pty.listRuns();
|
||||
assert.deepEqual(
|
||||
first.map((r) => r.id).sort(),
|
||||
["ccam-lane-1", "ccam-lane-2"]
|
||||
["agenthub-lane-1", "agenthub-lane-2"]
|
||||
);
|
||||
|
||||
// Session killed out-of-band (not through killRun) — next list() call
|
||||
// must self-correct, proving state is computed, not stored.
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "list-sessions") return "ccam-lane-1\n";
|
||||
if (args[0] === "list-sessions") return "agenthub-lane-1\n";
|
||||
return "";
|
||||
});
|
||||
const second = pty.listRuns();
|
||||
assert.deepEqual(
|
||||
second.map((r) => r.id),
|
||||
["ccam-lane-1"]
|
||||
["agenthub-lane-1"]
|
||||
);
|
||||
});
|
||||
|
||||
it("laneIdFromRunId parses the numeric lane id back out", () => {
|
||||
assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
|
||||
assert.equal(pty.laneIdFromRunId("agenthub-lane-42"), 42);
|
||||
assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
|
||||
});
|
||||
});
|
||||
@@ -510,9 +510,9 @@ Expected: FAIL — `Cannot find module '../lib/pty-run'`.
|
||||
* out-of-band (crash, manual `tmux kill-session`, host reboot) self-corrects
|
||||
* on the next read instead of leaving a ghost "running" row.
|
||||
*
|
||||
* Every session is named `ccam-lane-<laneId>` so a real terminal can attach
|
||||
* to the exact same session (`tmux attach -t ccam-lane-<id>`, or
|
||||
* `ccam lanes shell`) — that's the whole point: the dashboard both creates
|
||||
* Every session is named `agenthub-lane-<laneId>` so a real terminal can attach
|
||||
* to the exact same session (`tmux attach -t agenthub-lane-<id>`, or
|
||||
* `agenthub lanes shell`) — that's the whole point: the dashboard both creates
|
||||
* the session (one-click Start/Resume) and is just one of possibly several
|
||||
* attached clients tmux already keeps in sync.
|
||||
*
|
||||
@@ -528,12 +528,12 @@ try {
|
||||
/* db-less environment, skip persistence */
|
||||
}
|
||||
|
||||
const RUN_ID_RE = /^ccam-lane-(\d+)$/;
|
||||
const RUN_ID_RE = /^agenthub-lane-(\d+)$/;
|
||||
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
||||
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
|
||||
|
||||
function runIdForLane(laneId) {
|
||||
return `ccam-lane-${laneId}`;
|
||||
return `agenthub-lane-${laneId}`;
|
||||
}
|
||||
|
||||
function laneIdFromRunId(id) {
|
||||
@@ -657,7 +657,7 @@ function getRun(id) {
|
||||
|
||||
/** Computed fresh from tmux state every call — see file header. */
|
||||
function listRuns() {
|
||||
return tmux.listSessions("ccam-lane-").map(publicRun);
|
||||
return tmux.listSessions("agenthub-lane-").map(publicRun);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -955,7 +955,7 @@ describe("/api/run", () => {
|
||||
});
|
||||
const spawned = await fetchJson("/api/run", { method: "POST", body: { laneId: 9, cwd: TMP } });
|
||||
assert.equal(spawned.status, 201);
|
||||
assert.equal(spawned.body.id, "ccam-lane-9");
|
||||
assert.equal(spawned.body.id, "agenthub-lane-9");
|
||||
|
||||
const fetched = await fetchJson(`/api/run/${spawned.body.id}`);
|
||||
assert.equal(fetched.status, 200);
|
||||
@@ -964,7 +964,7 @@ describe("/api/run", () => {
|
||||
|
||||
it("DELETE /:id kills a live tmux session", async () => {
|
||||
tmux.__setExecImpl(() => ""); // has-session succeeds; kill-session succeeds
|
||||
const { status, body } = await fetchJson("/api/run/ccam-lane-9", { method: "DELETE" });
|
||||
const { status, body } = await fetchJson("/api/run/agenthub-lane-9", { method: "DELETE" });
|
||||
assert.equal(status, 200);
|
||||
assert.deepEqual(body, { ok: true });
|
||||
});
|
||||
@@ -978,7 +978,7 @@ describe("/api/run", () => {
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const { status } = await fetchJson("/api/run/ccam-lane-999", { method: "DELETE" });
|
||||
const { status } = await fetchJson("/api/run/agenthub-lane-999", { method: "DELETE" });
|
||||
assert.equal(status, 404);
|
||||
});
|
||||
|
||||
@@ -1073,18 +1073,18 @@ describe("pty-attach", () => {
|
||||
ptyAttach.__setSpawnImpl(() => fakePty);
|
||||
});
|
||||
|
||||
it("rejects a runId that doesn't match ccam-lane-<digits>", () => {
|
||||
it("rejects a runId that doesn't match agenthub-lane-<digits>", () => {
|
||||
assert.throws(() => ptyAttach.validateRunId("../../etc/passwd"), /EBADRUNID/);
|
||||
assert.throws(() => ptyAttach.validateRunId("some-other-session"), /EBADRUNID/);
|
||||
});
|
||||
|
||||
it("accepts a well-formed runId", () => {
|
||||
assert.doesNotThrow(() => ptyAttach.validateRunId("ccam-lane-42"));
|
||||
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42"));
|
||||
});
|
||||
|
||||
it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
|
||||
fakePty.__emitter.emit("data", "hello from claude");
|
||||
assert.equal(ws.__sent.length, 1);
|
||||
@@ -1097,7 +1097,7 @@ describe("pty-attach", () => {
|
||||
|
||||
it("routes a JSON text frame with type resize to pty.resize", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
ws.__emitter.emit("message", Buffer.from(JSON.stringify({ type: "resize", cols: 100, rows: 40 })), {
|
||||
binary: false,
|
||||
});
|
||||
@@ -1110,7 +1110,7 @@ describe("pty-attach", () => {
|
||||
ws.close = () => {
|
||||
closed = true;
|
||||
};
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
fakePty.__emitter.emit("exit", { exitCode: 0 });
|
||||
const last = ws.__sent[ws.__sent.length - 1];
|
||||
assert.equal(last.binary, false);
|
||||
@@ -1120,7 +1120,7 @@ describe("pty-attach", () => {
|
||||
|
||||
it("kills the PTY attach process when the WS connection closes", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
let killed = false;
|
||||
fakePty.kill = () => {
|
||||
killed = true;
|
||||
@@ -1154,10 +1154,10 @@ Expected: FAIL — `Cannot find module '../lib/pty-attach'`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const RUN_ID_RE = /^ccam-lane-\d+$/;
|
||||
const RUN_ID_RE = /^agenthub-lane-\d+$/;
|
||||
|
||||
/**
|
||||
* Reject anything that isn't exactly `ccam-lane-<digits>` before it can ever
|
||||
* Reject anything that isn't exactly `agenthub-lane-<digits>` before it can ever
|
||||
* reach a tmux/PTY command — the trust boundary for this WS path, since a
|
||||
* validated runId is the only thing standing between an authenticated WS
|
||||
* client and naming an arbitrary session on the host.
|
||||
@@ -1262,7 +1262,7 @@ Add below the existing `initWebSocket` function (after its closing brace, before
|
||||
|
||||
```javascript
|
||||
let ptyWss = null;
|
||||
const PTY_PATH_RE = /^\/ws-pty\/(ccam-lane-\d+)$/;
|
||||
const PTY_PATH_RE = /^\/ws-pty\/(agenthub-lane-\d+)$/;
|
||||
|
||||
/**
|
||||
* Second WebSocket server, dedicated to the terminal-run PTY transport
|
||||
@@ -1372,16 +1372,16 @@ Find `initWebSocket(server);` (line 152) and add directly below it:
|
||||
```bash
|
||||
npm start &
|
||||
sleep 1
|
||||
tmux new-session -d -s ccam-lane-999 -c /tmp -- sleep 60
|
||||
tmux new-session -d -s agenthub-lane-999 -c /tmp -- sleep 60
|
||||
node -e "
|
||||
const WebSocket = require('ws');
|
||||
const ws = new WebSocket('ws://127.0.0.1:4820/ws-pty/ccam-lane-999');
|
||||
const ws = new WebSocket('ws://127.0.0.1:4820/ws-pty/agenthub-lane-999');
|
||||
ws.on('open', () => console.log('connected'));
|
||||
ws.on('message', (d) => console.log('got', d.length, 'bytes'));
|
||||
ws.on('close', (code) => console.log('closed', code));
|
||||
setTimeout(() => process.exit(0), 3000);
|
||||
"
|
||||
tmux kill-session -t ccam-lane-999
|
||||
tmux kill-session -t agenthub-lane-999
|
||||
kill %1
|
||||
```
|
||||
|
||||
@@ -1471,13 +1471,13 @@ describe("TerminalView", () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it("opens a WS connection to the run's ws-pty path", () => {
|
||||
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
expect(MockWebSocket.instances).toHaveLength(1);
|
||||
expect(MockWebSocket.instances[0].url).toBe("ws://localhost:4820/ws-pty/ccam-lane-1");
|
||||
expect(MockWebSocket.instances[0].url).toBe("ws://localhost:4820/ws-pty/agenthub-lane-1");
|
||||
});
|
||||
|
||||
it("writes incoming WS data to the terminal", () => {
|
||||
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0];
|
||||
ws.onopen?.();
|
||||
ws.onmessage?.({ data: "hello" });
|
||||
@@ -1485,7 +1485,7 @@ describe("TerminalView", () => {
|
||||
});
|
||||
|
||||
it("forwards terminal keystrokes as outgoing WS sends", () => {
|
||||
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0];
|
||||
onDataHandlers[0]("ls -la\r");
|
||||
expect(ws.sent).toEqual(["ls -la\r"]);
|
||||
@@ -1638,7 +1638,7 @@ export interface RunStartArgs {
|
||||
}
|
||||
|
||||
/** A lane's tmux-backed terminal run — one per lane, id is the tmux session
|
||||
* name (`ccam-lane-<laneId>`). */
|
||||
* name (`agenthub-lane-<laneId>`). */
|
||||
export interface RunHandle {
|
||||
id: string;
|
||||
laneId: number | null;
|
||||
@@ -1729,7 +1729,7 @@ Replace the block from `// ───── Interactive run streaming ───
|
||||
|
||||
/** Payload for the `run_status` WebSocket message. */
|
||||
export interface RunStatusPayload {
|
||||
/** The run id (tmux session name, `ccam-lane-<laneId>`). */
|
||||
/** The run id (tmux session name, `agenthub-lane-<laneId>`). */
|
||||
id: string;
|
||||
status: "running" | "gone";
|
||||
/** Epoch-ms timestamp of this transition. */
|
||||
@@ -1989,10 +1989,10 @@ git commit -m "feat(run): wire Workspace to TerminalView, drop stream-json envel
|
||||
|
||||
---
|
||||
|
||||
### Task 11: `ccam lanes shell` CLI command
|
||||
### Task 11: `agenthub lanes shell` CLI command
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js` (dispatch table + new `cmdLanesShell` function)
|
||||
- Modify: `bin/agenthub.js` (dispatch table + new `cmdLanesShell` function)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `resolveLaneArg` (existing helper, same one `cmdLanesPipeline`/`cmdStage` use), `get`/`patch` HTTP helpers (existing), `node:child_process`'s `spawn` (already imported at the top of this file per the earlier research).
|
||||
@@ -2021,7 +2021,7 @@ async function cmdLanesShell(args) {
|
||||
const resolved = await resolveLaneArg(args);
|
||||
if (!resolved) return;
|
||||
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
|
||||
const sessionName = `ccam-lane-${lane.id}`;
|
||||
const sessionName = `agenthub-lane-${lane.id}`;
|
||||
const child = spawn("tmux", ["new-session", "-A", "-s", sessionName, "-c", lane.cwd], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -2040,24 +2040,24 @@ async function cmdLanesShell(args) {
|
||||
|
||||
```bash
|
||||
cd /path/to/some/adopted/lane
|
||||
node /path/to/ccam-lanes/bin/ccam.js lanes shell
|
||||
node /path/to/agenthub-lanes/bin/agenthub.js lanes shell
|
||||
```
|
||||
|
||||
Expected: drops into a tmux session named `ccam-lane-<id>`; typing `claude` starts a real interactive session; `Ctrl-B D` detaches without killing it; running the same command again re-attaches to the same session instead of creating a new one.
|
||||
Expected: drops into a tmux session named `agenthub-lane-<id>`; typing `claude` starts a real interactive session; `Ctrl-B D` detaches without killing it; running the same command again re-attaches to the same session instead of creating a new one.
|
||||
|
||||
- [ ] **Step 4: Update `ccam --help` and `docs/LANES.md`'s command reference**
|
||||
- [ ] **Step 4: Update `agenthub --help` and `docs/LANES.md`'s command reference**
|
||||
|
||||
```bash
|
||||
grep -n '"help"' bin/ccam.js | head -5
|
||||
grep -n '"help"' bin/agenthub.js | head -5
|
||||
```
|
||||
|
||||
Add a one-line entry for `ccam lanes shell` next to the existing `ccam lanes pipeline` entry in whatever help-text block that grep surfaces (match the existing entries' format exactly).
|
||||
Add a one-line entry for `agenthub lanes shell` next to the existing `agenthub lanes pipeline` entry in whatever help-text block that grep surfaces (match the existing entries' format exactly).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js
|
||||
git commit -m "feat(cli): add 'ccam lanes shell' to attach a real terminal to a lane's tmux session"
|
||||
git add bin/agenthub.js
|
||||
git commit -m "feat(cli): add 'agenthub lanes shell' to attach a real terminal to a lane's tmux session"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -2069,7 +2069,7 @@ git commit -m "feat(cli): add 'ccam lanes shell' to attach a real terminal to a
|
||||
- Modify: `README.vi.md` (mirror the same edit — this repo keeps a Vietnamese translation in sync)
|
||||
- Modify: `ARCHITECTURE.md` (find and update whatever section documents the old stream-json Run feature — search first)
|
||||
- Modify: `docs/API.md` (the `/api/run/*` endpoint reference)
|
||||
- Modify: `docs/LANES.md` (mention `ccam lanes shell` next to the existing `ccam lanes pipeline` documentation added in the prior session's work)
|
||||
- Modify: `docs/LANES.md` (mention `agenthub lanes shell` next to the existing `agenthub lanes pipeline` documentation added in the prior session's work)
|
||||
|
||||
**Interfaces:** none — documentation only.
|
||||
|
||||
@@ -2094,7 +2094,7 @@ with:
|
||||
- **Run Claude from the browser.** A real terminal (tmux + a real PTY,
|
||||
rendered with xterm.js) attached to a lane's directory — the exact TUI you'd
|
||||
see locally, fully interactive, resumable, and attachable from a real
|
||||
terminal too via `ccam lanes shell`.
|
||||
terminal too via `agenthub lanes shell`.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Mirror the same edit in `README.vi.md`**
|
||||
@@ -2105,13 +2105,13 @@ Find the corresponding Vietnamese bullet (search for "Chạy Claude từ trình
|
||||
|
||||
Rewrite the endpoint table/prose to match Task 4's actual routes: `GET /api/run`, `GET /api/run/history`, `GET /api/run/binary`, `GET /api/run/tmux`, `GET /api/run/cwds`, `GET /api/run/files`, `POST /api/run` (now requires `laneId`, body shape per `RunStartArgs`), `GET /api/run/:id`, `DELETE /api/run/:id`. Remove the `POST /api/run/:id/message` entry entirely (deleted in Task 4) and any `run_stream`/`run_input_ack` WebSocket message documentation (deleted in Task 7) — add a `/ws-pty/:runId` entry describing the binary-frame PTY transport instead, matching this doc's existing WebSocket-message table format.
|
||||
|
||||
- [ ] **Step 5: Add `ccam lanes shell` to `docs/LANES.md`**
|
||||
- [ ] **Step 5: Add `agenthub lanes shell` to `docs/LANES.md`**
|
||||
|
||||
Find the existing `ccam lanes pipeline` documentation (added in the prior session, per this repo's `docs/LANES.md` "Reporting a stage"/pipeline sections) and add a short paragraph for `ccam lanes shell` directly after it, matching that section's tone:
|
||||
Find the existing `agenthub lanes pipeline` documentation (added in the prior session, per this repo's `docs/LANES.md` "Reporting a stage"/pipeline sections) and add a short paragraph for `agenthub lanes shell` directly after it, matching that section's tone:
|
||||
|
||||
```markdown
|
||||
`ccam lanes shell` attaches a real terminal to the exact tmux session the
|
||||
dashboard's Start/Resume buttons use for this lane (`ccam-lane-<id>`),
|
||||
`agenthub lanes shell` attaches a real terminal to the exact tmux session the
|
||||
dashboard's Start/Resume buttons use for this lane (`agenthub-lane-<id>`),
|
||||
creating it if it doesn't exist yet. Type `claude` inside it like any normal
|
||||
terminal session — the dashboard's Workspace terminal view is just another
|
||||
client attached to the same tmux session, so both stay in sync live.
|
||||
@@ -2168,7 +2168,7 @@ Expected: no type errors, build succeeds.
|
||||
bash .claude/skills/file-headers/scripts/check-headers.sh
|
||||
```
|
||||
|
||||
Expected: exit 0 (aside from the pre-existing, unrelated `.ccam/profile/hooks/*.sh` gaps noted in this repo's own history — do not fix those as part of this plan).
|
||||
Expected: exit 0 (aside from the pre-existing, unrelated `.agenthub/profile/hooks/*.sh` gaps noted in this repo's own history — do not fix those as part of this plan).
|
||||
|
||||
- [ ] **Step 5: Manual end-to-end click-through (per the spec's "Verify" section)**
|
||||
|
||||
@@ -2176,7 +2176,7 @@ Install `tmux` locally if not already present, then:
|
||||
1. `npm run dev`, open Workspace for an adopted lane.
|
||||
2. Click Start — confirm the real Claude Code TUI renders in the browser via `TerminalView`.
|
||||
3. Type in the browser terminal — confirm input reaches the pane.
|
||||
4. In a real terminal, run `ccam lanes shell` for the same lane — confirm it drops into the exact same live session (typed output appears in both places).
|
||||
4. In a real terminal, run `agenthub lanes shell` for the same lane — confirm it drops into the exact same live session (typed output appears in both places).
|
||||
5. Click Kill on the dashboard — confirm the tmux session ends and the real terminal's attach also exits.
|
||||
6. Confirm `GET /api/run` no longer lists that run once killed.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- No server/API changes — this is a client-only feature (per the approved spec, docs/superpowers/specs/2026-08-14-split-terminal-view-design.md).
|
||||
- No synchronized input across panes — each `TerminalView` keeps its own independent WebSocket connection.
|
||||
- Layout 1 must remain behaviorally and structurally identical to today's Workspace (existing `Workspace.test.tsx` assertions about `console-body`, `lane-detail` nesting, and the no-`/stage`-call invariant must still pass unmodified where they test layout-1 behavior).
|
||||
- Layout + pane lane selections persist to `localStorage` under key `ccam.workspace.splitView`; a persisted lane id that no longer exists in the loaded lane list falls back to unselected for that pane.
|
||||
- Layout + pane lane selections persist to `localStorage` under key `agenthub.workspace.splitView`; a persisted lane id that no longer exists in the loaded lane list falls back to unselected for that pane.
|
||||
- Every new/modified `.ts`/`.tsx` file must carry the project's file header (see `.claude/skills/file-headers/`).
|
||||
- Run `npm run test:client` before finishing; this is a client-only change so `npm run test:server` is not required, but do not skip `test:client`.
|
||||
|
||||
@@ -61,12 +61,12 @@ describe("splitViewStorage", () => {
|
||||
});
|
||||
|
||||
it("falls back to the default when stored JSON is malformed", () => {
|
||||
localStorage.setItem("ccam.workspace.splitView", "{not json");
|
||||
localStorage.setItem("agenthub.workspace.splitView", "{not json");
|
||||
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||
});
|
||||
|
||||
it("falls back to the default when the stored layout is not 1, 2, or 4", () => {
|
||||
localStorage.setItem("ccam.workspace.splitView", JSON.stringify({ layout: 3, paneLaneIds: [] }));
|
||||
localStorage.setItem("agenthub.workspace.splitView", JSON.stringify({ layout: 3, paneLaneIds: [] }));
|
||||
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||
});
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export interface SplitViewState {
|
||||
paneLaneIds: (number | null)[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "ccam.workspace.splitView";
|
||||
const STORAGE_KEY = "agenthub.workspace.splitView";
|
||||
|
||||
export function defaultSplitViewState(): SplitViewState {
|
||||
return { layout: 1, paneLaneIds: [null] };
|
||||
@@ -888,7 +888,7 @@ describe("split terminal view", () => {
|
||||
|
||||
it("falls back to unselected when a persisted lane id no longer exists", async () => {
|
||||
localStorage.setItem(
|
||||
"ccam.workspace.splitView",
|
||||
"agenthub.workspace.splitView",
|
||||
JSON.stringify({ layout: 2, paneLaneIds: [9999, null] })
|
||||
);
|
||||
await renderWorkspace();
|
||||
@@ -1048,7 +1048,7 @@ git commit -m "feat(workspace): add 1/2/4-pane split terminal view toggle"
|
||||
In `docs/LANES.md`, in the "## The Workspace page (`/run`)" section, after the existing "**Terminal**" bullet (line 312), add:
|
||||
|
||||
```markdown
|
||||
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2x2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`ccam.workspace.splitView`) across reloads.
|
||||
- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2x2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`agenthub.workspace.splitView`) across reloads.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify doc accuracy against the implementation**
|
||||
|
||||
+36
-36
@@ -1,8 +1,8 @@
|
||||
# `ccam ci status` Implementation Plan
|
||||
# `agenthub ci status` Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add `ccam lanes ci status <branch-or-sha>` — a one-shot GitHub Actions status snapshot backed by a new server route — and wire it into `ship-feature-lane` Stage 13 so the CI-wait half of post-merge verification stops being hardcoded-skipped.
|
||||
**Goal:** Add `agenthub lanes ci status <branch-or-sha>` — a one-shot GitHub Actions status snapshot backed by a new server route — and wire it into `ship-feature-lane` Stage 13 so the CI-wait half of post-merge verification stops being hardcoded-skipped.
|
||||
|
||||
**Architecture:** A `server/lib/gh.js` execFile wrapper (mirrors `worktree.js`'s `git()`), a `server/lib/ci-status.js` pure-logic module that turns `gh run list`/`gh run view` output into `{state, ref, checks}`, a thin `GET /:id/ci/status` route (mirrors `GET /:id/git`'s try/available shape), and a thin CLI command (mirrors `cmdLanesIntegration`) that maps the response onto exit codes 0/1/3. No polling, no retry, no flake classification anywhere in this stack — the skill keeps that judgment, per the project's no-orchestration rule.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-08-25-ccam-ci-status-design.md` — read it before starting; every task below implements one section of it.
|
||||
- Spec: `docs/superpowers/specs/2026-08-25-agenthub-ci-status-design.md` — read it before starting; every task below implements one section of it.
|
||||
- Every new/edited source file needs the repo's copyright header (file overview + `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`) — see `skills/file-headers/`.
|
||||
- No flake auto-classification or auto-rerun logic anywhere in this stack — `state` is a derived fact, nothing more.
|
||||
- Route never writes lane `stage`/`status`/`notes` (same boundary every other read-only lane route keeps).
|
||||
@@ -45,7 +45,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gh-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gh-"));
|
||||
const { gh } = require("../lib/gh");
|
||||
|
||||
/** Puts a fake `gh` script on PATH for the duration of one test. */
|
||||
@@ -188,7 +188,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-"));
|
||||
const { ciStatus } = require("../lib/ci-status");
|
||||
|
||||
/**
|
||||
@@ -322,7 +322,7 @@ Expected: FAIL — `Cannot find module '../lib/ci-status'`
|
||||
* @file Turns a GitHub Actions run for one ref into a single derived fact —
|
||||
* {state, checks} — for ship-feature-lane's Stage 13 CI-wait. Deliberately a
|
||||
* snapshot, not a poll loop: the driving skill owns the wait/retry decision
|
||||
* (docs/LANES.md "Orchestration: what CCAM does NOT do"), this module only
|
||||
* (docs/LANES.md "Orchestration: what AgentHub does NOT do"), this module only
|
||||
* reports what gh already knows.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -421,11 +421,11 @@ describe("GET /:id/ci/status", () => {
|
||||
let restorePath;
|
||||
|
||||
before(async () => {
|
||||
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-route-"));
|
||||
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-route-"));
|
||||
const r = await request("POST", "/api/lanes", { title: "CI status lane", cwd: laneCwd });
|
||||
laneId = r.body.lane.id;
|
||||
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-bin-"));
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
@@ -492,7 +492,7 @@ Add the route directly after the existing `GET /:id/git` route (after its closin
|
||||
/**
|
||||
* A single GitHub Actions status snapshot for one ref (branch or commit sha)
|
||||
* in the lane's repo — NOT a poll loop. ship-feature-lane's Stage 13 owns
|
||||
* the wait/retry decision (docs/LANES.md "Orchestration: what CCAM does NOT
|
||||
* the wait/retry decision (docs/LANES.md "Orchestration: what AgentHub does NOT
|
||||
* do"); this route only reports what `gh` currently knows. Read-only, no
|
||||
* same-origin guard, same reasoning as GET /:id/git.
|
||||
*/
|
||||
@@ -525,26 +525,26 @@ git commit -m "feat(api): add GET /:id/ci/status"
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `ccam lanes ci status` CLI command
|
||||
### Task 4: `agenthub lanes ci status` CLI command
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js` — add `cmdLanesCi`, wire the dispatch, add the help table row.
|
||||
- Modify: `bin/agenthub.js` — add `cmdLanesCi`, wire the dispatch, add the help table row.
|
||||
- Modify: `server/__tests__/lanes-cli.test.js` — add a describe block.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GET /api/lanes/:id/ci/status?ref=...` from Task 3; `resolveLaneArg`, `get`, `c` (chalk-like color helper already used elsewhere in `bin/ccam.js`).
|
||||
- Produces: `ccam lanes ci status <branch-or-sha> [<lane-id>]` — prints the JSON response to stdout, sets `process.exitCode` to `0` (success), `1` (failure or any error), or `3` (pending).
|
||||
- Consumes: `GET /api/lanes/:id/ci/status?ref=...` from Task 3; `resolveLaneArg`, `get`, `c` (chalk-like color helper already used elsewhere in `bin/agenthub.js`).
|
||||
- Produces: `agenthub lanes ci status <branch-or-sha> [<lane-id>]` — prints the JSON response to stdout, sets `process.exitCode` to `0` (success), `1` (failure or any error), or `3` (pending).
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to `server/__tests__/lanes-cli.test.js`, in a new `describe` block (reusing that file's existing `cli()`, `git()`, `BASE` helpers and `SOURCE_REPO`/`LANE_DIR` fixtures already set up in its `before()`):
|
||||
|
||||
```js
|
||||
describe("ccam lanes ci status", () => {
|
||||
describe("agenthub lanes ci status", () => {
|
||||
let restorePath;
|
||||
|
||||
before(() => {
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-cli-bin-"));
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-cli-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
@@ -625,26 +625,26 @@ function cli(args, cwd, extraEnv = {}) {
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `node --test server/__tests__/lanes-cli.test.js`
|
||||
Expected: FAIL — `ccam lanes ci` prints a "no such subcommand" usage error, exit code 1 (not the expected 0/1/3 from real data), and no `"state"` in stdout
|
||||
Expected: FAIL — `agenthub lanes ci` prints a "no such subcommand" usage error, exit code 1 (not the expected 0/1/3 from real data), and no `"state"` in stdout
|
||||
|
||||
- [ ] **Step 3: Add the CLI command**
|
||||
|
||||
In `bin/ccam.js`, add near `cmdLanesIntegration` (after its closing `}`):
|
||||
In `bin/agenthub.js`, add near `cmdLanesIntegration` (after its closing `}`):
|
||||
|
||||
```js
|
||||
/** `ccam lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
|
||||
/** `agenthub lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
|
||||
* Actions status snapshot for a ref, via GET /:id/ci/status. Never polls;
|
||||
* the caller (ship-feature-lane Stage 13) owns the wait loop. Exit code
|
||||
* mirrors the derived state: 0 success, 1 failure or any error, 3 pending. */
|
||||
async function cmdLanesCi(args) {
|
||||
if (args[0] !== "status") {
|
||||
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
|
||||
console.error("usage: agenthub lanes ci status <branch-or-sha> [<lane-id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const ref = args[1];
|
||||
if (!ref) {
|
||||
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
|
||||
console.error("usage: agenthub lanes ci status <branch-or-sha> [<lane-id>]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -691,8 +691,8 @@ Expected: PASS (all tests in the file, including the 3 new ones)
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js server/__tests__/lanes-cli.test.js
|
||||
git commit -m "feat(cli): add ccam lanes ci status"
|
||||
git add bin/agenthub.js server/__tests__/lanes-cli.test.js
|
||||
git commit -m "feat(cli): add agenthub lanes ci status"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -701,7 +701,7 @@ git commit -m "feat(cli): add ccam lanes ci status"
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/ship-feature-lane/SKILL.md` (Stage 13 section, and the "Current status" summary near the top)
|
||||
- Modify: `docs/LANES.md` ("Stages currently hardcoded skipped", the `ccam lanes integration` section's claim about `ci_wait`'s consumer, and the command reference)
|
||||
- Modify: `docs/LANES.md` ("Stages currently hardcoded skipped", the `agenthub lanes integration` section's claim about `ci_wait`'s consumer, and the command reference)
|
||||
|
||||
No test for this task — it's documentation/skill-text only, verified by reading it back and by the `update-project-docs` checklist. This task has no code, so it isn't gated behind `npm run test:server`; run that suite once at the very end of the plan instead (see Task 5 Step 4).
|
||||
|
||||
@@ -711,13 +711,13 @@ Replace the existing Stage 13 section (currently: `### 13 — Post-merge verific
|
||||
|
||||
```markdown
|
||||
### 13 — Post-merge verification *(CI-wait works; dev-QC still unavailable)*
|
||||
- `ccam stage merged --status running --note "watching development CI post-merge"`
|
||||
- `agenthub stage merged --status running --note "watching development CI post-merge"`
|
||||
- Get the merge commit: `gh pr view <pr_url> --json mergeCommit -q .mergeCommit.oid`.
|
||||
- Loop every ~5 minutes, paced by a **backgrounded** wait (same shape as Stage 12): `sleep 300` with `run_in_background: true`. Each iteration, bump the heartbeat and check:
|
||||
- `ccam lanes ci status <mergeSha>` — exit `3` (pending) → keep waiting, background another `sleep 300`, end the turn.
|
||||
- Exit `1` (failure) → triage exactly like Stage 10 (read the failing job via `gh run view <run-id>` — the run id is embedded in each failing check's `url` in the command's JSON output; distinguish real failure from shared-runner flake the same way). A real failure on `development` post-merge is a regression the fix-loop doesn't reach (the PR is already merged) — `ccam stage merged --status blocked --note "<job> red on development after merge — human decision"` and STOP; this is not something the pipeline auto-fixes on a shared branch.
|
||||
- Exit `0` (success) → CI-wait passed. Dev-QC (QCing the deployed site) is a separate, not-yet-built integration — `ccam stage merged --status passed --note "CI green on development; dev-QC unavailable (separate task)"` and proceed to Stage 14.
|
||||
- If `ccam lanes integration ci_wait` reads off for this profile, skip the loop entirely and go straight to Stage 14 with `--note "ci_wait integration off for this profile"` — the toggle means the profile doesn't want this check, not that the command is unavailable.
|
||||
- `agenthub lanes ci status <mergeSha>` — exit `3` (pending) → keep waiting, background another `sleep 300`, end the turn.
|
||||
- Exit `1` (failure) → triage exactly like Stage 10 (read the failing job via `gh run view <run-id>` — the run id is embedded in each failing check's `url` in the command's JSON output; distinguish real failure from shared-runner flake the same way). A real failure on `development` post-merge is a regression the fix-loop doesn't reach (the PR is already merged) — `agenthub stage merged --status blocked --note "<job> red on development after merge — human decision"` and STOP; this is not something the pipeline auto-fixes on a shared branch.
|
||||
- Exit `0` (success) → CI-wait passed. Dev-QC (QCing the deployed site) is a separate, not-yet-built integration — `agenthub stage merged --status passed --note "CI green on development; dev-QC unavailable (separate task)"` and proceed to Stage 14.
|
||||
- If `agenthub lanes integration ci_wait` reads off for this profile, skip the loop entirely and go straight to Stage 14 with `--note "ci_wait integration off for this profile"` — the toggle means the profile doesn't want this check, not that the command is unavailable.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the skill's front-matter description and "Current status" summary**
|
||||
@@ -726,7 +726,7 @@ In `skills/ship-feature-lane/SKILL.md`, the YAML front-matter `description` fiel
|
||||
> `NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands.`
|
||||
|
||||
Change to:
|
||||
> `NOTE: ticket-filing (Stage 9) and post-merge dev-site QC (Stage 13's second half) are currently OFF (F's integrations aren't built yet) — Stage 13's CI-wait half now runs via ccam lanes ci status.`
|
||||
> `NOTE: ticket-filing (Stage 9) and post-merge dev-site QC (Stage 13's second half) are currently OFF (F's integrations aren't built yet) — Stage 13's CI-wait half now runs via agenthub lanes ci status.`
|
||||
|
||||
Find the "Current status" bullet list (`**Stages currently hardcoded skipped**`) and change the Stage 13 line from:
|
||||
> `**13 (Post-merge verification)**: Dev-CI-wait and dev-QC integrations are off — the stage transitions immediately to `done` with a note explaining the gap.`
|
||||
@@ -738,19 +738,19 @@ to a line under a renamed heading that separates the two:
|
||||
- **9 (Ticket)**: Ticket-filing integration not yet built — this stage is skipped entirely.
|
||||
|
||||
**Partially wired:**
|
||||
- **13 (Post-merge verification)**: CI-wait half now runs for real via `ccam lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
|
||||
- **13 (Post-merge verification)**: CI-wait half now runs for real via `agenthub lanes ci status`. The dev-QC half (QCing the deployed site) is still unavailable — Stage 13 always ends noting that gap even when CI-wait passes clean.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `docs/LANES.md`**
|
||||
|
||||
1. In the "Stages currently hardcoded skipped" list (around line 1137-1139), change the Stage 13 bullet to match the SKILL.md wording from Step 2 above (CI-wait works, dev-QC doesn't).
|
||||
2. In the `ccam lanes integration` section (around line 1224), change:
|
||||
> `The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`ccam ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.`
|
||||
2. In the `agenthub lanes integration` section (around line 1224), change:
|
||||
> `The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`agenthub ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.`
|
||||
|
||||
to:
|
||||
> `The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `ccam lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.`
|
||||
3. Add `ccam lanes ci status` to wherever `docs/LANES.md` lists `ccam lanes` subcommands (same table/section `ccam lanes sync-base`, `ccam lanes integration` etc. already appear in) — one row: `ccam lanes ci status <branch-or-sha> [<id>]` — one-shot GitHub Actions status for a ref (exit 0/1/3), used by Stage 13's CI-wait.
|
||||
4. Update the `**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet" line (around line 1198) to drop `ccam ci`/`ci_wait` framing if it's still described there as unbuilt — cross-check by re-reading that paragraph once Step 1-3 are done, since it may already only be about the two agents.
|
||||
> `The agent that would act on an enabled `dev_qc` toggle isn't built yet, and neither is a `tracker` ticketer — `ship-feature-lane`'s Stage 9 stays skipped and Stage 13's dev-QC half stays unavailable regardless of what those two toggles read. `ci_wait`'s consumer is `agenthub lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real.`
|
||||
3. Add `agenthub lanes ci status` to wherever `docs/LANES.md` lists `agenthub lanes` subcommands (same table/section `agenthub lanes sync-base`, `agenthub lanes integration` etc. already appear in) — one row: `agenthub lanes ci status <branch-or-sha> [<id>]` — one-shot GitHub Actions status for a ref (exit 0/1/3), used by Stage 13's CI-wait.
|
||||
4. Update the `**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet" line (around line 1198) to drop `agenthub ci`/`ci_wait` framing if it's still described there as unbuilt — cross-check by re-reading that paragraph once Step 1-3 are done, since it may already only be about the two agents.
|
||||
|
||||
- [ ] **Step 4: Full verification pass**
|
||||
|
||||
@@ -765,5 +765,5 @@ Both must pass clean — this is the first point every file this plan touched (c
|
||||
|
||||
```bash
|
||||
git add skills/ship-feature-lane/SKILL.md docs/LANES.md
|
||||
git commit -m "docs(lanes): wire Stage 13 CI-wait to ccam lanes ci status"
|
||||
git commit -m "docs(lanes): wire Stage 13 CI-wait to agenthub lanes ci status"
|
||||
```
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** A lane can be driven by the `codex` CLI instead of `claude`, started and watched from the Workspace page with the same lane strip, pipeline map, terminal console and run history.
|
||||
|
||||
**Architecture:** The tool is a lane-level, immutable column (`lanes.tool`), chosen in the Add-lane modal. `pty-run.js` branches its argv builder on that tool; everything else (tmux session naming `ccam-lane-<id>`, create-or-adopt, console attach) is untouched. Runs record their tool in `dashboard_runs.tool` so history and the active-runs list can badge them.
|
||||
**Architecture:** The tool is a lane-level, immutable column (`lanes.tool`), chosen in the Add-lane modal. `pty-run.js` branches its argv builder on that tool; everything else (tmux session naming `agenthub-lane-<id>`, create-or-adopt, console attach) is untouched. Runs record their tool in `dashboard_runs.tool` so history and the active-runs list can badge them.
|
||||
|
||||
**Tech Stack:** Node 20 + Express + better-sqlite3 (server, `node:test`), React 18 + Vite + TypeScript + Vitest/Testing Library (client), tmux for panes.
|
||||
|
||||
@@ -851,7 +851,7 @@ Claude lanes render nothing new, so existing lane cards are pixel-identical.
|
||||
|
||||
- [ ] **Step 4: Update the docs**
|
||||
|
||||
In `docs/LANES.md`, add a "Lane tool" section stating: a lane is driven by `claude` (default) or `codex`, chosen in Add lane and immutable afterwards; the Codex pane maps model/sandbox/approval/reasoning-effort onto `codex -m/-s/-a/-c model_reasoning_effort=`; resume for a Codex lane is `codex resume --last`; and the known limitations — Codex emits no Claude Code hooks, so Sessions, Analytics, cost and `stage-detect` stay empty for a Codex lane, `ccam stage` still works because the agent calls it itself, and liveness comes from the tmux pane plus `dashboard_runs`.
|
||||
In `docs/LANES.md`, add a "Lane tool" section stating: a lane is driven by `claude` (default) or `codex`, chosen in Add lane and immutable afterwards; the Codex pane maps model/sandbox/approval/reasoning-effort onto `codex -m/-s/-a/-c model_reasoning_effort=`; resume for a Codex lane is `codex resume --last`; and the known limitations — Codex emits no Claude Code hooks, so Sessions, Analytics, cost and `stage-detect` stay empty for a Codex lane, `agenthub stage` still works because the agent calls it itself, and liveness comes from the tmux pane plus `dashboard_runs`.
|
||||
|
||||
In `docs/API.md`, document `tool` on the lane payload and on run history, the new `tool` body field for `POST /api/lanes/ensure` and `POST /api/lanes/worktree` (400 `EBADTOOL`), the `sandbox`/`approval`/`resumeLast` body fields on `POST /api/lanes/:id/start`, and the `?tool=` query on `GET /api/run/binary`.
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** A lane on the 16-node `ship-feature` template shows real progress again — a `codex` lane gets auto-detected stages and a live status, a lane whose declared stage is the pipeline's last node can be detected again, and `ccam stage` from a cwd that carries both a `claude` and a `codex` lane writes to the right one.
|
||||
**Goal:** A lane on the 16-node `ship-feature` template shows real progress again — a `codex` lane gets auto-detected stages and a live status, a lane whose declared stage is the pipeline's last node can be detected again, and `agenthub stage` from a cwd that carries both a `claude` and a `codex` lane writes to the right one.
|
||||
|
||||
**Architecture:** Four independent fixes at their shared choke points. (1) `bin/ccam.js`'s single `resolveLaneArg` learns a tool, sourced from `CCAM_LANE_ID` (injected by `pty-run` into the tmux pane), an explicit `--tool`, or the harness's own env markers — all 15 cwd-resolving commands inherit it. (2) `recordDetection` drops the declared-wins veto only when the declaration sits on the pipeline's LAST node. (3) A new `server/lib/codex-ingest.js` tails Codex's own on-disk rollout JSONL — Codex emits no hooks, so this is the codex analogue of `server/lib/workflow-ingest.js` — maps its tool calls onto hook-shaped `{tool_name, tool_input}` events, and runs the existing `detect()` + `recordDetection()` + idle/running mirror. (4) The run console stops pre-filling a Claude slash command into a codex pane.
|
||||
**Architecture:** Four independent fixes at their shared choke points. (1) `bin/agenthub.js`'s single `resolveLaneArg` learns a tool, sourced from `AGENTHUB_LANE_ID` (injected by `pty-run` into the tmux pane), an explicit `--tool`, or the harness's own env markers — all 15 cwd-resolving commands inherit it. (2) `recordDetection` drops the declared-wins veto only when the declaration sits on the pipeline's LAST node. (3) A new `server/lib/codex-ingest.js` tails Codex's own on-disk rollout JSONL — Codex emits no hooks, so this is the codex analogue of `server/lib/workflow-ingest.js` — maps its tool calls onto hook-shaped `{tool_name, tool_input}` events, and runs the existing `detect()` + `recordDetection()` + idle/running mirror. (4) The run console stops pre-filling a Claude slash command into a codex pane.
|
||||
|
||||
**Tech Stack:** Node 20 CommonJS (server, `bin/ccam.js`), `node:test` + `node --test` for server tests, React 19 + Vite + Vitest for the client, tmux 3.4 CLI, SQLite via better-sqlite3.
|
||||
**Tech Stack:** Node 20 CommonJS (server, `bin/agenthub.js`), `node:test` + `node --test` for server tests, React 19 + Vite + Vitest for the client, tmux 3.4 CLI, SQLite via better-sqlite3.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
|
||||
### Task 1: Tool-aware lane resolution for the CLI
|
||||
|
||||
A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_tool`). `resolveLaneArg` matches on cwd only and takes the first of the tie, so every cwd-resolved command — `ccam stage`, `ccam lanes pipeline`, `ccam feature activate`, 15 call sites in total — can silently drive the sibling lane's card. Fix it once in the shared function, and make the dashboard-launched pane unambiguous by construction.
|
||||
A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_tool`). `resolveLaneArg` matches on cwd only and takes the first of the tie, so every cwd-resolved command — `agenthub stage`, `agenthub lanes pipeline`, `agenthub feature activate`, 15 call sites in total — can silently drive the sibling lane's card. Fix it once in the shared function, and make the dashboard-launched pane unambiguous by construction.
|
||||
|
||||
**Files:**
|
||||
- Modify: `bin/ccam.js:1788-1809` (`resolveLaneArg`)
|
||||
- Modify: `bin/agenthub.js:1788-1809` (`resolveLaneArg`)
|
||||
- Modify: `server/lib/tmux.js:41-44` (`newSession` gains `env`)
|
||||
- Modify: `server/lib/pty-run.js:184-236` (`spawnRun` passes `CCAM_LANE_ID`)
|
||||
- Modify: `server/lib/pty-run.js:184-236` (`spawnRun` passes `AGENTHUB_LANE_ID`)
|
||||
- Test: `server/__tests__/lanes-cli.test.js` (resolution precedence)
|
||||
- Test: `server/__tests__/pty-run.test.js` (env injection)
|
||||
|
||||
@@ -37,17 +37,17 @@ A cwd can back one `claude` lane AND one `codex` lane (composite `idx_lanes_cwd_
|
||||
- Produces:
|
||||
- `resolveLaneArg(args) -> Promise<{laneId: string|number, rest: string[]}|null>` — unchanged signature, new precedence.
|
||||
- `tmux.newSession({name, cwd, argv, env})` — `env` is an optional plain object of string→string; omitted/empty behaves exactly as before.
|
||||
- Env contract: `CCAM_LANE_ID=<lane id>` is present in every pane the dashboard starts.
|
||||
- Env contract: `AGENTHUB_LANE_ID=<lane id>` is present in every pane the dashboard starts.
|
||||
|
||||
- [ ] **Step 1: Write the failing CLI-resolution tests**
|
||||
|
||||
In `server/__tests__/lanes-cli.test.js`, following that file's existing harness (it already boots the API and shells out to `bin/ccam.js`): create two lanes at the SAME cwd, one `claude` and one `codex`, then assert
|
||||
1. with `CCAM_LANE_ID` set to the codex lane's id, `ccam stage plan` moves the codex lane and leaves the claude lane untouched;
|
||||
In `server/__tests__/lanes-cli.test.js`, following that file's existing harness (it already boots the API and shells out to `bin/agenthub.js`): create two lanes at the SAME cwd, one `claude` and one `codex`, then assert
|
||||
1. with `AGENTHUB_LANE_ID` set to the codex lane's id, `agenthub stage plan` moves the codex lane and leaves the claude lane untouched;
|
||||
2. with `--tool codex` and no env, the codex lane moves;
|
||||
3. with `CLAUDECODE=1` in the environment and no flag, the claude lane moves;
|
||||
4. with neither a flag nor any env marker (delete `CLAUDECODE` and `CLAUDE_CODE_ENTRYPOINT` from the child env — the test itself runs under Claude Code), the command FAILS (non-zero exit) and its stderr names both candidate lanes — an ambiguous write is worse than no write.
|
||||
|
||||
Note for case 2/3: there is deliberately no codex env marker (see the comment in Step 3), so `--tool codex` and `CCAM_LANE_ID` are the only two ways a codex session outside a dashboard pane can identify itself.
|
||||
Note for case 2/3: there is deliberately no codex env marker (see the comment in Step 3), so `--tool codex` and `AGENTHUB_LANE_ID` are the only two ways a codex session outside a dashboard pane can identify itself.
|
||||
|
||||
- [ ] **Step 2: Run them to verify they fail**
|
||||
|
||||
@@ -58,7 +58,7 @@ Expected: FAIL — cases 1, 2 and 4 write to the claude lane (the first tie-brea
|
||||
|
||||
```js
|
||||
/**
|
||||
* Resolution order: `--lane`/positional id → `CCAM_LANE_ID` (the dashboard
|
||||
* Resolution order: `--lane`/positional id → `AGENTHUB_LANE_ID` (the dashboard
|
||||
* injects it into every pane it starts) → `--tool` → the harness's own env
|
||||
* markers → the lane owning the cwd. The tool matters because a cwd can back
|
||||
* one `claude` lane AND one `codex` lane (composite idx_lanes_cwd_tool): with
|
||||
@@ -72,7 +72,7 @@ Expected: FAIL — cases 1, 2 and 4 write to the claude lane (the first tie-brea
|
||||
// READS) and CODEX_SANDBOX* (set only under a sandbox policy), so sniffing for
|
||||
// a CODEX_ prefix would be a guess. A codex session identifies itself with
|
||||
// `--tool codex`, or needs nothing at all inside a dashboard-started pane
|
||||
// (CCAM_LANE_ID above). Absent any signal with two candidates we error.
|
||||
// (AGENTHUB_LANE_ID above). Absent any signal with two candidates we error.
|
||||
function toolFromEnv() {
|
||||
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return "claude";
|
||||
return null;
|
||||
@@ -87,8 +87,8 @@ async function resolveLaneArg(args) {
|
||||
const explicit = flagValue("lane");
|
||||
if (explicit) return { laneId: explicit, rest: args };
|
||||
if (args.length && /^\d+$/.test(args[0])) return { laneId: args[0], rest: args.slice(1) };
|
||||
if (process.env.CCAM_LANE_ID && /^\d+$/.test(process.env.CCAM_LANE_ID)) {
|
||||
return { laneId: process.env.CCAM_LANE_ID, rest: args };
|
||||
if (process.env.AGENTHUB_LANE_ID && /^\d+$/.test(process.env.AGENTHUB_LANE_ID)) {
|
||||
return { laneId: process.env.AGENTHUB_LANE_ID, rest: args };
|
||||
}
|
||||
|
||||
const tool = flagValue("tool") || toolFromEnv();
|
||||
@@ -98,7 +98,7 @@ async function resolveLaneArg(args) {
|
||||
.filter((l) => cwd === l.cwd || cwd.startsWith(`${l.cwd}/`))
|
||||
.sort((a, b) => b.cwd.length - a.cwd.length);
|
||||
if (!owning.length) {
|
||||
console.error(`no lane owns ${cwd} — create one with: ccam lanes add --cwd ${cwd}`);
|
||||
console.error(`no lane owns ${cwd} — create one with: agenthub lanes add --cwd ${cwd}`);
|
||||
process.exitCode = 1;
|
||||
return null;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ async function resolveLaneArg(args) {
|
||||
}
|
||||
```
|
||||
|
||||
Also add `--tool <claude|codex>` to the `usage:` line of `cmdStage` (`bin/ccam.js:2470-2473`) and to the `stage` entry in `COMMAND_GROUPS`.
|
||||
Also add `--tool <claude|codex>` to the `usage:` line of `cmdStage` (`bin/agenthub.js:2470-2473`) and to the `stage` entry in `COMMAND_GROUPS`.
|
||||
|
||||
- [ ] **Step 4: Run the CLI tests to verify they pass**
|
||||
|
||||
@@ -132,7 +132,7 @@ Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Write the failing env-injection test**
|
||||
|
||||
In `server/__tests__/pty-run.test.js`, using that file's existing `tmux.__setExecImpl` capture: assert `spawnRun({laneId: 7, cwd: "/tmp/x", tool: "codex"})` produces a `new-session` argv containing `-e` immediately followed by `CCAM_LANE_ID=7`, and that the shell-pane branch (pane command `bash`) first sends `export CCAM_LANE_ID=7` and then the tool argv.
|
||||
In `server/__tests__/pty-run.test.js`, using that file's existing `tmux.__setExecImpl` capture: assert `spawnRun({laneId: 7, cwd: "/tmp/x", tool: "codex"})` produces a `new-session` argv containing `-e` immediately followed by `AGENTHUB_LANE_ID=7`, and that the shell-pane branch (pane command `bash`) first sends `export AGENTHUB_LANE_ID=7` and then the tool argv.
|
||||
|
||||
- [ ] **Step 6: Run it to verify it fails**
|
||||
|
||||
@@ -147,8 +147,8 @@ Expected: FAIL — no `-e` in the argv, single `send-keys` sequence.
|
||||
/**
|
||||
* `env` entries become tmux `-e NAME=value` pairs, which tmux puts in the
|
||||
* pane's own environment (tmux >= 3.2). The dashboard uses it for
|
||||
* CCAM_LANE_ID so a CLI call from inside the pane resolves its lane exactly
|
||||
* instead of guessing from cwd — see bin/ccam.js's resolveLaneArg.
|
||||
* AGENTHUB_LANE_ID so a CLI call from inside the pane resolves its lane exactly
|
||||
* instead of guessing from cwd — see bin/agenthub.js's resolveLaneArg.
|
||||
*/
|
||||
function newSession({ name, cwd, argv, env }) {
|
||||
const envFlags = [];
|
||||
@@ -162,13 +162,13 @@ function newSession({ name, cwd, argv, env }) {
|
||||
|
||||
```js
|
||||
if (!tmux.hasSession(id)) {
|
||||
tmux.newSession({ name: id, cwd, argv, env: { CCAM_LANE_ID: String(laneId) } });
|
||||
tmux.newSession({ name: id, cwd, argv, env: { AGENTHUB_LANE_ID: String(laneId) } });
|
||||
record();
|
||||
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
|
||||
// tmux's -e only applies at session creation, so an existing shell pane
|
||||
// gets the variable the only way a live shell can: an export line typed
|
||||
// ahead of the tool's own argv.
|
||||
tmux.sendCommand(id, ["export", `CCAM_LANE_ID=${laneId}`]);
|
||||
tmux.sendCommand(id, ["export", `AGENTHUB_LANE_ID=${laneId}`]);
|
||||
tmux.sendCommand(id, argv);
|
||||
record();
|
||||
}
|
||||
@@ -182,7 +182,7 @@ Expected: PASS.
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add bin/ccam.js server/lib/tmux.js server/lib/pty-run.js server/__tests__/pty-run.test.js server/__tests__/lanes-cli.test.js
|
||||
git add bin/agenthub.js server/lib/tmux.js server/lib/pty-run.js server/__tests__/pty-run.test.js server/__tests__/lanes-cli.test.js
|
||||
git commit -m "fix(cli): resolve a lane by tool, not by cwd alone"
|
||||
```
|
||||
|
||||
@@ -720,7 +720,7 @@ git commit -m "feat(lanes): detect codex lane stages from codex rollout journals
|
||||
|
||||
### Task 4: The run console stops pre-filling a Claude slash command for codex lanes
|
||||
|
||||
`pipeline_skill` is a Claude Code slash command (`/ccam:ship-feature-lane`). Pre-filled into a codex pane it is plain text Codex cannot run, which is why the codex lane in issue 11 ran for 34 minutes and never declared a stage. A codex lane opens with an empty prompt box instead.
|
||||
`pipeline_skill` is a Claude Code slash command (`/agenthub:ship-feature-lane`). Pre-filled into a codex pane it is plain text Codex cannot run, which is why the codex lane in issue 11 ran for 34 minutes and never declared a stage. A codex lane opens with an empty prompt box instead.
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/components/run/LaneConsolePane.tsx:141-156`
|
||||
@@ -732,7 +732,7 @@ git commit -m "feat(lanes): detect codex lane stages from codex rollout journals
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
In `client/src/components/run/__tests__/LaneConsolePane.test.tsx`, following the existing tool-aware tests there: render the pane for a lane with `tool: "codex"` and `pipeline_skill: "/ccam:ship-feature-lane"`, assert the prompt textarea is empty; render for the same lane with `tool: "claude"`, assert it holds `/ccam:ship-feature-lane `.
|
||||
In `client/src/components/run/__tests__/LaneConsolePane.test.tsx`, following the existing tool-aware tests there: render the pane for a lane with `tool: "codex"` and `pipeline_skill: "/agenthub:ship-feature-lane"`, assert the prompt textarea is empty; render for the same lane with `tool: "claude"`, assert it holds `/agenthub:ship-feature-lane `.
|
||||
|
||||
- [ ] **Step 2: Run to verify it fails**
|
||||
|
||||
@@ -769,26 +769,26 @@ git commit -m "fix(run): don't prefill a claude slash command into a codex pane"
|
||||
|
||||
### Task 5: Docs, and the instruction file Codex actually reads
|
||||
|
||||
`CLAUDE.md` tells a Claude session to declare `ccam stage` even in plain chat. Codex never sees that file, and this repo has no `AGENTS.md` at all — so a codex lane has no instruction to declare anything. That file is also the generic answer for any repo a codex lane runs in, so it is documented as such.
|
||||
`CLAUDE.md` tells a Claude session to declare `agenthub stage` even in plain chat. Codex never sees that file, and this repo has no `AGENTS.md` at all — so a codex lane has no instruction to declare anything. That file is also the generic answer for any repo a codex lane runs in, so it is documented as such.
|
||||
|
||||
**Files:**
|
||||
- Create: `AGENTS.md`
|
||||
- Modify: `CLAUDE.md` (the `_Last synced:_` marker; the detection invariant bullet; the lanes bullet list)
|
||||
- Modify: `docs/LANES.md` (codex detection, tool-aware CLI resolution, terminal-declaration exception)
|
||||
- Modify: `docs/CLI.md` (`--tool` on cwd-resolved commands, `CCAM_LANE_ID`)
|
||||
- Modify: `docs/CLI.md` (`--tool` on cwd-resolved commands, `AGENTHUB_LANE_ID`)
|
||||
- Modify: `docs/DEPLOYMENT.md` (the new `DASHBOARD_CODEX_POLL_MS` / `CODEX_HOME` env vars, beside `DASHBOARD_WORKFLOW_POLL_MS`)
|
||||
- Modify: `ARCHITECTURE.md` (the ingestion paths list, beside workflow-ingest)
|
||||
|
||||
**What this task does NOT fix, stated plainly:** after Tasks 1-4 a codex lane's
|
||||
pipeline map moves on its own (detection) and its card shows running/idle, but
|
||||
its *declared* stage still depends on the codex session actually calling
|
||||
`ccam stage` — CCAM has no way to inject an instruction into a CLI it merely
|
||||
`agenthub stage` — AgentHub has no way to inject an instruction into a CLI it merely
|
||||
spawns. `AGENTS.md` is the only lever, and it is per-repository. A codex lane in
|
||||
a repo without one will show detected stages and no declarations.
|
||||
|
||||
- [ ] **Step 1: Write `AGENTS.md`**
|
||||
|
||||
Short and behavioral, not a second copy of `CLAUDE.md`: point at `CLAUDE.md` for the engineering rules, then state the two things Codex specifically has to be told — declare `ccam stage <stage>` at every real transition (with `--tool codex`, or rely on `CCAM_LANE_ID` inside a dashboard-started pane), and the file-header rule from `.claude/rules/file-headers.md`.
|
||||
Short and behavioral, not a second copy of `CLAUDE.md`: point at `CLAUDE.md` for the engineering rules, then state the two things Codex specifically has to be told — declare `agenthub stage <stage>` at every real transition (with `--tool codex`, or rely on `AGENTHUB_LANE_ID` inside a dashboard-started pane), and the file-header rule from `.claude/rules/file-headers.md`.
|
||||
|
||||
- [ ] **Step 2: Update the docs above**
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Problem
|
||||
|
||||
A lane's stage only moves when the driving agent calls `ccam stage <name>`. Every un-instrumented session — which is most of them — sits at `idle` forever while its agent works, so the pipeline map shows nothing. The dashboard already receives every tool call the agent makes; it just never reads them.
|
||||
A lane's stage only moves when the driving agent calls `agenthub stage <name>`. Every un-instrumented session — which is most of them — sits at `idle` forever while its agent works, so the pipeline map shows nothing. The dashboard already receives every tool call the agent makes; it just never reads them.
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -12,7 +12,7 @@ Infer a lane's stage from the hook stream it already ingests, and show it **with
|
||||
|
||||
## The rule that shapes everything
|
||||
|
||||
**Inference never renders green.** A node the dashboard inferred reaches `passed-no-evidence` (amber) at most; `done` requires a declared stage carrying evidence via `ccam stage --evidence`. If inference could paint a node green, the amber/green distinction — the reason this feature exists — would be worthless.
|
||||
**Inference never renders green.** A node the dashboard inferred reaches `passed-no-evidence` (amber) at most; `done` requires a declared stage carrying evidence via `agenthub stage --evidence`. If inference could paint a node green, the amber/green distinction — the reason this feature exists — would be worthless.
|
||||
|
||||
Declared always outranks detected. A lane that has declared `review` ignores a detection for `implement`.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Four small pieces, each independently useful:
|
||||
|
||||
## What the console must not do
|
||||
|
||||
**It never touches the lane's stage.** Typing `/code-review` in the UI does not move the lane to `review`; only `ccam stage` declares, and only detection (sub-project B) infers. The console is a window onto a process, not a driver of the pipeline. Keeping that boundary is what stops the pipeline from becoming a lie.
|
||||
**It never touches the lane's stage.** Typing `/code-review` in the UI does not move the lane to `review`; only `agenthub stage` declares, and only detection (sub-project B) infers. The console is a window onto a process, not a driver of the pipeline. Keeping that boundary is what stops the pipeline from becoming a lie.
|
||||
|
||||
## Risks and how they are contained
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
## Problem
|
||||
|
||||
A lane today is a pointer at a directory that already exists. Two agents working in parallel therefore work in the *same* checkout and collide — the exact failure Shipyard solves by giving every lane its own clone. CCAM has no provisioning at all: no way to create a lane's working copy, no way to reset it between features, no way to remove it, and no way to reclaim the database a finished lane leaves behind (a real install reached 121 MB).
|
||||
A lane today is a pointer at a directory that already exists. Two agents working in parallel therefore work in the *same* checkout and collide — the exact failure Shipyard solves by giving every lane its own clone. AgentHub has no provisioning at all: no way to create a lane's working copy, no way to reset it between features, no way to remove it, and no way to reclaim the database a finished lane leaves behind (a real install reached 121 MB).
|
||||
|
||||
## Goals
|
||||
|
||||
- A lane can own a **git worktree** that CCAM creates, resets and removes.
|
||||
- The lifecycle verbs mirror Shipyard's, because that vocabulary is proven: `add`, `clear`, `reset`, `remove`, plus `purge` as CCAM's analogue of Shipyard's per-lane `dropdb`.
|
||||
- A lane can own a **git worktree** that AgentHub creates, resets and removes.
|
||||
- The lifecycle verbs mirror Shipyard's, because that vocabulary is proven: `add`, `clear`, `reset`, `remove`, plus `purge` as AgentHub's analogue of Shipyard's per-lane `dropdb`.
|
||||
- Every destructive action is confirmed **against counted facts**, not adjectives.
|
||||
- Directories the user already had must be impossible for CCAM to destroy.
|
||||
- Directories the user already had must be impossible for AgentHub to destroy.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -33,9 +33,9 @@ Additive columns on `lanes`, each guarded by the repo's `try { SELECT col } catc
|
||||
|
||||
`branch`, `cwd`, `stage`, `stages` and the rest keep their current meaning. `cwd` stays `UNIQUE`.
|
||||
|
||||
Environment: `LANES_ROOT` (default `~/.claude/ccam-lanes`), `LANE_BASE_BRANCH`, `LANE_BRANCH_PREFIX` (default `feat/`).
|
||||
Environment: `LANES_ROOT` (default `~/.claude/agenthub-lanes`), `LANE_BASE_BRANCH`, `LANE_BRANCH_PREFIX` (default `feat/`).
|
||||
|
||||
Layout: worktree at `$LANES_ROOT/<repo-basename>__<slug>`, branch `<prefix><slug>`. Numbered `lane1..lane9` slots were considered and rejected — CCAM is multi-repo, and slot numbers carry no meaning without Shipyard's per-lane ports.
|
||||
Layout: worktree at `$LANES_ROOT/<repo-basename>__<slug>`, branch `<prefix><slug>`. Numbered `lane1..lane9` slots were considered and rejected — AgentHub is multi-repo, and slot numbers carry no meaning without Shipyard's per-lane ports.
|
||||
|
||||
## Safety model
|
||||
|
||||
@@ -49,7 +49,7 @@ A `managed` lane may be destroyed only when **all three** independent checks pas
|
||||
|
||||
Shipyard gets away with one check (`case "$DIR" in */lane$N`) because its directory names are fixed. Dropping numbered slots costs that guarantee, so three cheaper checks replace it. Every destructive function in `server/lib/worktree.js` re-runs the three checks itself rather than trusting its caller.
|
||||
|
||||
CCAM never runs `rm -rf` on a lane. Removal goes through `git worktree remove`; if git refuses, the error surfaces unchanged.
|
||||
AgentHub never runs `rm -rf` on a lane. Removal goes through `git worktree remove`; if git refuses, the error surfaces unchanged.
|
||||
|
||||
## Verbs
|
||||
|
||||
@@ -110,7 +110,7 @@ SQLite does not shrink on `DELETE`. `purge` runs `DELETE` plus `PRAGMA optimize`
|
||||
|
||||
**API:** `POST /api/lanes/worktree` (add), `GET /api/lanes/:id/preflight`, and `reset` / `purge` joining the existing `POST /api/lanes/:id/:action` set, all behind the existing same-origin guard.
|
||||
|
||||
**CLI:** `ccam lanes add --repo <path> [--title <t>] [--base <branch>]`, `ccam lanes reset|remove|purge <id> [--force]`.
|
||||
**CLI:** `agenthub lanes add --repo <path> [--title <t>] [--base <branch>]`, `agenthub lanes reset|remove|purge <id> [--force]`.
|
||||
|
||||
**UI:** a `managed` / `adopted` badge on the lane card; destructive buttons rendered only for `managed`; the existing `ConfirmModal` showing the preflight table.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Two facts also make lanes look emptier than they are:
|
||||
|
||||
## Goal
|
||||
|
||||
The reference screen's legibility, on CCAM's real data: a lane's state readable
|
||||
The reference screen's legibility, on AgentHub's real data: a lane's state readable
|
||||
from across the room, the pipeline large enough to trace, and the console present
|
||||
but out of the way until wanted.
|
||||
|
||||
@@ -37,7 +37,7 @@ but out of the way until wanted.
|
||||
- **The console collapses.** It keeps every capability from A; it starts
|
||||
collapsed and opens for the selected lane. Watching lanes is the default
|
||||
posture, driving one is the exception.
|
||||
- **Only real data.** No placeholder tiles for facts CCAM does not have
|
||||
- **Only real data.** No placeholder tiles for facts AgentHub does not have
|
||||
(tickets, preview ports, per-lane credentials). Branch/commit/CI are added
|
||||
because they can be read for real — see below.
|
||||
- **Detection expires.** A detection older than a TTL stops holding the floor.
|
||||
@@ -59,7 +59,7 @@ A wired it. The console is unchanged behind its new disclosure — no prop of
|
||||
|
||||
## The card
|
||||
|
||||
Reference layout, CCAM's fields, nothing invented:
|
||||
Reference layout, AgentHub's fields, nothing invented:
|
||||
|
||||
| Row | Content | Source |
|
||||
|---|---|---|
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Lane profile scaffolding (A3, v1: Node preset) — design
|
||||
|
||||
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`. This is the third phase of the Shipyard-parity roadmap (`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, phase A3), scoped down to exactly the piece that phase's own text asked for: **detect a Node.js project and scaffold a working `.ccam/profile/`**, so adopting a repo becomes one command instead of hand-writing nine hook scripts. Python/Go/Ruby detection, named in the long-range plan, is explicitly **out of scope** for this design — see Non-goals.
|
||||
**Source of truth for "Shipyard":** `~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/`. This is the third phase of the Shipyard-parity roadmap (`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, phase A3), scoped down to exactly the piece that phase's own text asked for: **detect a Node.js project and scaffold a working `.agenthub/profile/`**, so adopting a repo becomes one command instead of hand-writing nine hook scripts. Python/Go/Ruby detection, named in the long-range plan, is explicitly **out of scope** for this design — see Non-goals.
|
||||
|
||||
## Problem
|
||||
|
||||
Today, using CCAM's runtime/data-isolation features (A1: slots+ports, A2: database/Redis/`.env`) on a new repo means a human reads `docs/LANES.md` and hand-writes `.ccam/profile/profile.env` plus every hook script from scratch. That's the adoption barrier this removes.
|
||||
Today, using AgentHub's runtime/data-isolation features (A1: slots+ports, A2: database/Redis/`.env`) on a new repo means a human reads `docs/LANES.md` and hand-writes `.agenthub/profile/profile.env` plus every hook script from scratch. That's the adoption barrier this removes.
|
||||
|
||||
## Scope (v1)
|
||||
|
||||
@@ -19,7 +19,7 @@ Anything else (no package.json anywhere, or a `backend/` without a matching `fro
|
||||
|
||||
- Detecting Python (Django/FastAPI), Go, Ruby, or any non-Node stack. That is real, planned work (see the parent plan's A3 section) but a separate preset, added later, informed by whatever this Node preset's implementation actually looks like once it exists.
|
||||
- Detecting ORMs/migration tools (Prisma, Knex, TypeORM, Sequelize, alembic, …). `migrate`/`seed` are always scaffolded as an explicit TODO stub when a database is detected — never a guessed command.
|
||||
- pnpm/yarn/npm **workspaces** or turborepo-style (`apps/`, `packages/`) monorepo layouts. Only the flat `backend/` + `frontend/` layout already documented as CCAM's convention (`docs/LANES.md`'s `BACKEND_DIR`/`FRONTEND_DIR` example) is detected.
|
||||
- pnpm/yarn/npm **workspaces** or turborepo-style (`apps/`, `packages/`) monorepo layouts. Only the flat `backend/` + `frontend/` layout already documented as AgentHub's convention (`docs/LANES.md`'s `BACKEND_DIR`/`FRONTEND_DIR` example) is detected.
|
||||
- Any HTTP API route. `profile init`/`profile check` are CLI-only, local filesystem actions against the *source* repo — no dashboard surface needs them.
|
||||
- A `--fix` mode for `profile check`. It is read-only/diagnostic only.
|
||||
|
||||
@@ -30,17 +30,17 @@ One new library module, **`server/lib/lane-detect.js`**, plus two new CLI subcom
|
||||
```
|
||||
server/lib/lane-detect.js
|
||||
detectNode(repoPath) -> { layout: "monorepo"|"single-service", ... facts } | null
|
||||
scaffoldProfile(repoPath, facts, { force }) -> writes .ccam/profile/**, returns { written: [...], todos: [...] }
|
||||
scaffoldProfile(repoPath, facts, { force }) -> writes .agenthub/profile/**, returns { written: [...], todos: [...] }
|
||||
checkProfile(dir) -> { ok: boolean, errors: [...], warnings: [...] }
|
||||
```
|
||||
|
||||
`bin/ccam.js` gains:
|
||||
`bin/agenthub.js` gains:
|
||||
```
|
||||
ccam lanes profile init <repo> [--force]
|
||||
ccam lanes profile check [<path>] # defaults to cwd
|
||||
agenthub lanes profile init <repo> [--force]
|
||||
agenthub lanes profile check [<path>] # defaults to cwd
|
||||
```
|
||||
|
||||
Both are pure local actions on whatever machine runs the CLI — no dashboard server round trip needed for the write itself (unlike `lanes add --repo`, this never creates a worktree or a lane row; it just prepares a repo so that a *later* `lanes add --repo` on it gets a working profile for free, because `resolveProfile` already reads `.ccam/profile/` from the lane's own working copy or its source repo — nothing downstream needs to change).
|
||||
Both are pure local actions on whatever machine runs the CLI — no dashboard server round trip needed for the write itself (unlike `lanes add --repo`, this never creates a worktree or a lane row; it just prepares a repo so that a *later* `lanes add --repo` on it gets a working profile for free, because `resolveProfile` already reads `.agenthub/profile/` from the lane's own working copy or its source repo — nothing downstream needs to change).
|
||||
|
||||
## Detection rules (`detectNode`)
|
||||
|
||||
@@ -77,7 +77,7 @@ Reads only; never executes `npm`, never shells out.
|
||||
|
||||
### `scaffoldProfile`
|
||||
|
||||
Writes `<repo>/.ccam/profile/profile.env` and `<repo>/.ccam/profile/hooks/*.sh` (mode `0755`) from the facts `detectNode` returned. Refuses if `.ccam/profile/profile.env` already exists, unless `--force`. Returns which files it wrote and which lines are TODO placeholders, so the CLI can print a clear "N things need your attention" summary instead of silently declaring success.
|
||||
Writes `<repo>/.agenthub/profile/profile.env` and `<repo>/.agenthub/profile/hooks/*.sh` (mode `0755`) from the facts `detectNode` returned. Refuses if `.agenthub/profile/profile.env` already exists, unless `--force`. Returns which files it wrote and which lines are TODO placeholders, so the CLI can print a clear "N things need your attention" summary instead of silently declaring success.
|
||||
|
||||
### `checkProfile` (validator, read-only)
|
||||
|
||||
@@ -87,7 +87,7 @@ Given a directory (a repo root, or a lane's cwd — the same two places `resolve
|
||||
- Every hook referenced (`bootstrap`, `boot`, `health`, plus `db-create`/`db-drop` when `DB_PREFIX` is set) exists on disk and is executable (`fs.statSync(...).mode & 0o111`).
|
||||
- No literal string `TODO:` remains anywhere in `profile.env` or any hook file's contents. **This is expected to fail immediately after `profile init` scaffolds a repo whose detection left any TODO (no start/dev script, or a database with unconfigured migrate/seed) — that is the intended signal to finish the manual step, not a bug.**
|
||||
- If `DB_PREFIX` is set: `db-create.sh`/`db-drop.sh` both exist (redundant with the hook check above, but reported as its own line so the message reads as "database configuration" rather than a generic missing-hook error).
|
||||
- If `DB_PREFIX` is set and `~/.ccam/secrets.env` (`server/lib/secrets.js:SECRETS_PATH`) does not exist: a **warning** (not a failure) — "no secrets.env; database will use built-in defaults until you create one."
|
||||
- If `DB_PREFIX` is set and `~/.agenthub/secrets.env` (`server/lib/secrets.js:SECRETS_PATH`) does not exist: a **warning** (not a failure) — "no secrets.env; database will use built-in defaults until you create one."
|
||||
- Every declared `PORT_BASE_<name>` port is currently free (`server/lib/ports.js:isListening`, reused as-is).
|
||||
|
||||
Exits non-zero and prints every failure found (not just the first) when any of the hard checks fail; warnings never affect the exit code.
|
||||
@@ -95,20 +95,20 @@ Exits non-zero and prints every failure found (not just the first) when any of t
|
||||
## CLI
|
||||
|
||||
```
|
||||
ccam lanes profile init <repo> [--force]
|
||||
agenthub lanes profile init <repo> [--force]
|
||||
```
|
||||
- `<repo>` must be an absolute, existing, git-repository path (same validation `POST /api/lanes/worktree` already applies to `sourceRepo`).
|
||||
- On success: prints what was scaffolded, and — if anything was left as a TODO — a clear "N item(s) need manual attention before this profile is usable: <list>" block, plus the suggested next command (`ccam lanes profile check <repo>`).
|
||||
- On success: prints what was scaffolded, and — if anything was left as a TODO — a clear "N item(s) need manual attention before this profile is usable: <list>" block, plus the suggested next command (`agenthub lanes profile check <repo>`).
|
||||
- On "nothing detected": a clear, actionable refusal — *not* a bare error:
|
||||
```
|
||||
✖ No detectable Node.js project at <repo> (looked for backend/package.json +
|
||||
frontend/package.json, or a root package.json).
|
||||
Auto-scaffolding currently supports Node.js repos in that layout only.
|
||||
Write .ccam/profile/ by hand — see docs/LANES.md.
|
||||
Write .agenthub/profile/ by hand — see docs/LANES.md.
|
||||
```
|
||||
|
||||
```
|
||||
ccam lanes profile check [<path>]
|
||||
agenthub lanes profile check [<path>]
|
||||
```
|
||||
- `<path>` defaults to `process.cwd()`. Deliberately **not** lane-id/cwd-resolved the way `lanes up`/`lanes logs`/etc. are — `profile check` is meant to run against a bare repo right after `profile init`, before any lane/worktree exists for it.
|
||||
- Prints every failure and warning found; exits 0 only when there are zero failures (warnings are fine).
|
||||
|
||||
@@ -22,7 +22,7 @@ isolation — goes first because it blocks the rest, and splits into three:
|
||||
- **A1 (this spec)** — a lane can boot its stack on isolated ports and directories,
|
||||
and the stack survives a dashboard restart. Profiles are hand-written.
|
||||
- **A2** — data isolation: `.env` seeding, database create/drop/migrate/seed, Redis index.
|
||||
- **A3** — stack detection + `ccam lane profile init` scaffolding, designed **from**
|
||||
- **A3** — stack detection + `agenthub lane profile init` scaffolding, designed **from**
|
||||
the real profiles written during A1/A2.
|
||||
|
||||
The split point matters. Shipyard was genericized but its `profiles/` is still
|
||||
@@ -30,14 +30,14 @@ empty (`PROFILE=_template`, `SOURCE_REPO=""`, `state/` empty) — a clean seam t
|
||||
never ran against a real app. Designing presets before a real profile exists
|
||||
repeats exactly that.
|
||||
|
||||
**A1 done means:** `ccam lane up` boots a lane's stack on ports no other lane uses,
|
||||
and `ccam update` (or any dashboard restart) leaves running stacks alone.
|
||||
**A1 done means:** `agenthub lane up` boots a lane's stack on ports no other lane uses,
|
||||
and `agenthub update` (or any dashboard restart) leaves running stacks alone.
|
||||
|
||||
## Decisions
|
||||
|
||||
| | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Profile location | `<repo>/.ccam/profile/` | CCAM is multi-repo (lanes keyed by `cwd`); one global `PROFILE` is wrong the moment two repos have lanes. A profile that lives in the repo is already present in a fresh worktree. |
|
||||
| Profile location | `<repo>/.agenthub/profile/` | AgentHub is multi-repo (lanes keyed by `cwd`); one global `PROFILE` is wrong the moment two repos have lanes. A profile that lives in the repo is already present in a fresh worktree. |
|
||||
| Engine | Node, `server/lib/` | `execFile` with argument arrays (repo rule: never build a git/shell command as a string). Shares the SQLite DB and the websocket. Hooks stay shell scripts; the engine spawns them. |
|
||||
| Resource model | Declared per repo | `PORTS="api fe worker"`, `LANE_DIRS="…"`. An app with no database declares none, so A2 generates no database hooks — no dead code. |
|
||||
| Process ownership | Fully detached | pid files outside the worktree; a dashboard restart does not touch a running stack. |
|
||||
@@ -48,11 +48,11 @@ and `ccam update` (or any dashboard restart) leaves running stacks alone.
|
||||
## Invariants
|
||||
|
||||
1. **The runtime never writes `stage`, `status`, or `notes`** — only `slot` and
|
||||
`ports`. In CCAM `status=running` means *an agent is working*, not *a stack is
|
||||
`ports`. In AgentHub `status=running` means *an agent is working*, not *a stack is
|
||||
up*; conflating the two breaks `classifyLiveness`. Boot failures surface through
|
||||
`GET /runtime` and the hook log. Same reasoning as "the console never writes a
|
||||
lane's stage".
|
||||
2. **CCAM still does not orchestrate.** A1 adds *primitives* (`up`/`down`/`hook`)
|
||||
2. **AgentHub still does not orchestrate.** A1 adds *primitives* (`up`/`down`/`hook`)
|
||||
that a session calls. The dashboard chains nothing, evaluates no gate, retries nothing.
|
||||
3. **No shell command built as a string.** `execFile` + argv arrays everywhere.
|
||||
Hooks are the deliberate exception — they *are* scripts, spawned as
|
||||
@@ -89,7 +89,7 @@ No column records "is up". Deliberate.
|
||||
|---|---|
|
||||
| `server/lib/ports.js` | `isListening(port)` via a short-timeout `net.connect`; `listenerPids(port)` via `lsof`, falling back to `ss`, returning `[]` with a one-time warning when neither exists. |
|
||||
| `server/lib/lane-slots.js` | `allocateSlot()` — lowest free in `[1, LANE_MAX_SLOTS]` (default 9), called inside the existing `withLaneLock`, with the unique index as the backstop. `releaseSlot()` on `remove` only — **not** on `reset`, or a lane's ports and database move under a running session. `resolvePorts(slot, profile)` — prefers `PORT_BASE_<name> + slot`, then `+100`, `+200`… (the last digit stays the slot, so the number still reads as "lane 3"), up to 10 tries before `EPORTBUSY` naming the occupying pid. |
|
||||
| `server/lib/lane-profile.js` | `resolveProfile(lane)` — looks in `lane.cwd/.ccam/profile/` **first**, `lane.source_repo/.ccam/profile/` second, because the profile lives in the repo and a branch that edits its hooks must run its own. `runHook(lane, name, args, {onLine})` — fixed hook-name allowlist, `bash <hook>` with an argv array, Shipyard's env contract verbatim so its profiles port unchanged (`LANE` = the **slot**, `LANE_DIR`, `RUN_DIR`, `LOG_DIR`, `PROFILE_DIR`, `SOURCE_REPO`, `<NAME>_PORT` per declared port), `harness_spawn` exported as a bash function writing the same `$RUN_DIR/<name>.pid` + `$LOG_DIR/<name>.log`, the `GIT_*` scrub from `worktree.js:git()`, and output streamed both to the log file and to a new `lane_hook_output` websocket message. |
|
||||
| `server/lib/lane-profile.js` | `resolveProfile(lane)` — looks in `lane.cwd/.agenthub/profile/` **first**, `lane.source_repo/.agenthub/profile/` second, because the profile lives in the repo and a branch that edits its hooks must run its own. `runHook(lane, name, args, {onLine})` — fixed hook-name allowlist, `bash <hook>` with an argv array, Shipyard's env contract verbatim so its profiles port unchanged (`LANE` = the **slot**, `LANE_DIR`, `RUN_DIR`, `LOG_DIR`, `PROFILE_DIR`, `SOURCE_REPO`, `<NAME>_PORT` per declared port), `harness_spawn` exported as a bash function writing the same `$RUN_DIR/<name>.pid` + `$LOG_DIR/<name>.log`, the `GIT_*` scrub from `worktree.js:git()`, and output streamed both to the log file and to a new `lane_hook_output` websocket message. |
|
||||
| `server/lib/lane-runtime.js` | `upLane` (defensive down → mkdir run/log/`LANE_DIRS` → `boot` → `health`; no `bootstrap`, which belongs to provision/reset as in `lane-up.sh`), `downLane` (recursive pid-tree kill from the pid files — ported from `lane-down.sh:kill_tree`, which exists because detached uvicorn/celery prefork children were being missed — then a port-listener backstop; idempotent, sleep-free), `runtimeFacts` (**computed**: `kill -0` per pid file, `isListening` per port, plus `last-error.json`). |
|
||||
|
||||
State lives at `LANES_ROOT/.state/lane<slot>/{run,logs}/`, **outside** the
|
||||
@@ -123,7 +123,7 @@ argv, never joined.
|
||||
|
||||
| Condition | Code | Behaviour |
|
||||
|---|---|---|
|
||||
| No `.ccam/profile/` | `ENOPROFILE` | 400, naming both paths searched |
|
||||
| No `.agenthub/profile/` | `ENOPROFILE` | 400, naming both paths searched |
|
||||
| Unknown hook | `ENOHOOK` | 400 |
|
||||
| Hook exits non-zero | — | `lane_hook_output` + `last-error.json`; reflected by `/runtime` |
|
||||
| Every candidate port busy | `EPORTBUSY` | names the occupying pid |
|
||||
@@ -154,8 +154,8 @@ Covered:
|
||||
the allowlist returns 400 and spawns nothing.
|
||||
- **Env** — with `GIT_DIR` set on the parent, the hook does not see it.
|
||||
|
||||
Manual: create a lane on ccam-lanes itself, write a three-line profile,
|
||||
`ccam lane up`, open the port, restart the server, confirm the stack survived.
|
||||
Manual: create a lane on agenthub-lanes itself, write a three-line profile,
|
||||
`agenthub lane up`, open the port, restart the server, confirm the stack survived.
|
||||
|
||||
## Out of scope
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
|
||||
## Problem
|
||||
|
||||
Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/skills/ship-feature/SKILL.md`, 208 lines, 16 stages) is the driving skill that runs a lane end-to-end: intake → TDD implement → gates → e2e → review → QC → senior gate → publish PR → watch → post-merge verify → done. CCAM has no equivalent yet. E (per the roadmap) is four independent pieces — this spec covers only the first: the pipeline template + the skill text itself. Agents (qc-local, senior-gate-reviewer, ticketer, dev-qc, pr-reviewer) and `ccam lanes sync-base` are separate, later tasks; F's integrations (tracker, dev-QC, CI-wait) are later still.
|
||||
Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/skills/ship-feature/SKILL.md`, 208 lines, 16 stages) is the driving skill that runs a lane end-to-end: intake → TDD implement → gates → e2e → review → QC → senior gate → publish PR → watch → post-merge verify → done. AgentHub has no equivalent yet. E (per the roadmap) is four independent pieces — this spec covers only the first: the pipeline template + the skill text itself. Agents (qc-local, senior-gate-reviewer, ticketer, dev-qc, pr-reviewer) and `agenthub lanes sync-base` are separate, later tasks; F's integrations (tracker, dev-QC, CI-wait) are later still.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
1. A backend gap the skill text requires to be truthful: `ccam lanes up --qc`, backed by a new `QC_BOOT_ENV` profile declaration.
|
||||
1. A backend gap the skill text requires to be truthful: `agenthub lanes up --qc`, backed by a new `QC_BOOT_ENV` profile declaration.
|
||||
2. `server/data/pipelines/ship-feature.json` — the node/stage definitions the dashboard renders against.
|
||||
3. `.claude/skills/ship-feature-lane/SKILL.md` — the ported skill, every Shipyard `bin/…` call rewritten to its CCAM equivalent.
|
||||
3. `.claude/skills/ship-feature-lane/SKILL.md` — the ported skill, every Shipyard `bin/…` call rewritten to its AgentHub equivalent.
|
||||
|
||||
**Out of scope (later tasks, referenced by name in the ported skill but not built here):**
|
||||
- `ccam lanes sync-base` (the migration-collision + merge-driver command Stage 2/8/12 call).
|
||||
- `ccam lanes mcp sync` (F) — Stage 0's MCP preflight becomes a manual check + a message to the human, not an auto-sync call.
|
||||
- `ccam lanes integration <name>` / `.ccam/profile/integrations.env` (F) — tracker/dev_qc/ci_wait are **hardcoded off** in this port; Stage 9 (ticket) and the dev-QC half of Stage 13 unconditionally skip; Stage 10's CI watch unconditionally uses the `gh pr checks` fallback the original skill already documents for "helper not configured" (no conditional branch needed — there is nothing to branch on yet).
|
||||
- `ccam ci` (F, `ci-job.sh` port) — same reasoning; Stage 10/13's CI-triage logic that depends on it is dropped to the fallback path.
|
||||
- `agenthub lanes sync-base` (the migration-collision + merge-driver command Stage 2/8/12 call).
|
||||
- `agenthub lanes mcp sync` (F) — Stage 0's MCP preflight becomes a manual check + a message to the human, not an auto-sync call.
|
||||
- `agenthub lanes integration <name>` / `.agenthub/profile/integrations.env` (F) — tracker/dev_qc/ci_wait are **hardcoded off** in this port; Stage 9 (ticket) and the dev-QC half of Stage 13 unconditionally skip; Stage 10's CI watch unconditionally uses the `gh pr checks` fallback the original skill already documents for "helper not configured" (no conditional branch needed — there is nothing to branch on yet).
|
||||
- `agenthub ci` (F, `ci-job.sh` port) — same reasoning; Stage 10/13's CI-triage logic that depends on it is dropped to the fallback path.
|
||||
- The five agents — the skill references them by `subagent_type` at Stages 6/7/9/13/12(pr-reviewer callout), but none exist yet. This spec's own verification stops where an agent invocation would occur.
|
||||
|
||||
## Decisions
|
||||
@@ -26,26 +26,26 @@ Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/A
|
||||
| | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| `--qc` boot flag | New `QC_BOOT_ENV` profile declaration (space-separated `KEY=value` pairs, same shape as Shipyard's), injected into the boot hook's environment only when `up` is called with `qc: true` | The skill calls `lane-up.sh "$N" --qc` in Stage 3 to get a deterministic (mocked/stubbed) stack for QC. Without a real flag this line in the ported skill would be false documentation — violates this repo's docs rule (executable, aligned with actual scripts). A profile that never declares `QC_BOOT_ENV` gets no extra env (dead code doesn't run), same pattern every other A2 declaration in `DEFAULTS` already follows. |
|
||||
| `state.sh "$N" init` (open a clean feature slot before Stage 0's Q&A) | Dropped — replaced by a plain `ccam feature activate <slug>` at Stage 1 | Task B's `activateFeature` already archives the outgoing active feature automatically on switch; Shipyard needed a separate `init` step because it had no DB-backed archive. Porting `init` as a distinct call would be redundant with what `activate` already does. |
|
||||
| Integration toggles (tracker/dev_qc/ci_wait) | Hardcoded off, not a runtime check | F doesn't exist yet. The alternative (a stub `ccam lanes integration` that always returns off) adds surface area for no behavioral difference today; the skill's own text already documents the "toggle off" behavior for each stage, so hardcoding is truthful, not a shortcut. When F ships, this becomes a follow-up edit to swap hardcoded skips for real checks — an explicit, later, reviewable diff. |
|
||||
| Hook names for `lane-ci-gate.sh` / `lane-e2e.sh` | `ccam lanes hook ci-gate` / `ccam lanes hook e2e` | Both names are **already** in `server/lib/lane-profile.js`'s `HOOKS` allowlist, added during A1 specifically "because... the driving skill needs that surface stable" — this spec is that driving skill. No backend change needed for these two. |
|
||||
| `.harness-lane` marker / `lane-assign.sh` | Dropped entirely | CCAM already resolves a lane from `cwd` (path-boundary prefix match, documented in this repo's CLAUDE.md) — the marker file and the assign step exist in Shipyard only because it has no such resolution. |
|
||||
| `@@HARNESS_ROOT@@` / `@@DEV_QC_MCP@@` / `@@TRACKER_MCP@@` placeholders | Dropped | Install-time templating that doesn't exist in CCAM; every reference becomes the literal CCAM command or, for the two MCP placeholders, is deleted along with the (out-of-scope) integrations that used them. |
|
||||
| `state.sh "$N" init` (open a clean feature slot before Stage 0's Q&A) | Dropped — replaced by a plain `agenthub feature activate <slug>` at Stage 1 | Task B's `activateFeature` already archives the outgoing active feature automatically on switch; Shipyard needed a separate `init` step because it had no DB-backed archive. Porting `init` as a distinct call would be redundant with what `activate` already does. |
|
||||
| Integration toggles (tracker/dev_qc/ci_wait) | Hardcoded off, not a runtime check | F doesn't exist yet. The alternative (a stub `agenthub lanes integration` that always returns off) adds surface area for no behavioral difference today; the skill's own text already documents the "toggle off" behavior for each stage, so hardcoding is truthful, not a shortcut. When F ships, this becomes a follow-up edit to swap hardcoded skips for real checks — an explicit, later, reviewable diff. |
|
||||
| Hook names for `lane-ci-gate.sh` / `lane-e2e.sh` | `agenthub lanes hook ci-gate` / `agenthub lanes hook e2e` | Both names are **already** in `server/lib/lane-profile.js`'s `HOOKS` allowlist, added during A1 specifically "because... the driving skill needs that surface stable" — this spec is that driving skill. No backend change needed for these two. |
|
||||
| `.harness-lane` marker / `lane-assign.sh` | Dropped entirely | AgentHub already resolves a lane from `cwd` (path-boundary prefix match, documented in this repo's CLAUDE.md) — the marker file and the assign step exist in Shipyard only because it has no such resolution. |
|
||||
| `@@HARNESS_ROOT@@` / `@@DEV_QC_MCP@@` / `@@TRACKER_MCP@@` placeholders | Dropped | Install-time templating that doesn't exist in AgentHub; every reference becomes the literal AgentHub command or, for the two MCP placeholders, is deleted along with the (out-of-scope) integrations that used them. |
|
||||
|
||||
## Command mapping
|
||||
|
||||
| Shipyard | CCAM | Status |
|
||||
| Shipyard | AgentHub | Status |
|
||||
|---|---|---|
|
||||
| `state.sh "$N" set stage=X status=Y` | `ccam stage X --status Y` | exists |
|
||||
| `state.sh "$N" activate feat/<slug>` | `ccam feature activate <slug>` | exists (Task B) |
|
||||
| `state.sh "$N" set stage=X status=Y` | `agenthub stage X --status Y` | exists |
|
||||
| `state.sh "$N" activate feat/<slug>` | `agenthub feature activate <slug>` | exists (Task B) |
|
||||
| `state.sh "$N" init` | (dropped — see Decisions) | n/a |
|
||||
| `lane-ci-gate.sh "$N"` | `ccam lanes hook ci-gate` | exists (hook allowlisted) |
|
||||
| `lane-up.sh "$N" --qc` | `ccam lanes up --qc` | **new in this task** |
|
||||
| `lane-e2e.sh "$N" [-- specs]` | `ccam lanes hook e2e [-- specs]` | exists (hook allowlisted) |
|
||||
| `lane-sync-dev.sh …` | `ccam lanes sync-base …` | later task (referenced, not built) |
|
||||
| `lane-mcp-sync.sh "$N"` | `ccam lanes mcp sync` | F (referenced as manual fallback) |
|
||||
| `lane-ci-gate.sh "$N"` | `agenthub lanes hook ci-gate` | exists (hook allowlisted) |
|
||||
| `lane-up.sh "$N" --qc` | `agenthub lanes up --qc` | **new in this task** |
|
||||
| `lane-e2e.sh "$N" [-- specs]` | `agenthub lanes hook e2e [-- specs]` | exists (hook allowlisted) |
|
||||
| `lane-sync-dev.sh …` | `agenthub lanes sync-base …` | later task (referenced, not built) |
|
||||
| `lane-mcp-sync.sh "$N"` | `agenthub lanes mcp sync` | F (referenced as manual fallback) |
|
||||
| `lane-env.sh "$N" --check X` | (hardcoded off) | F (deferred) |
|
||||
| `ci-job.sh status\|failures\|rerun\|cancel` | `ccam ci …` | F (deferred; skill uses its own `gh`-only fallback) |
|
||||
| `ci-job.sh status\|failures\|rerun\|cancel` | `agenthub ci …` | F (deferred; skill uses its own `gh`-only fallback) |
|
||||
|
||||
## Design
|
||||
|
||||
@@ -53,23 +53,23 @@ Shipyard's `ship-feature/SKILL.md` (`~/MyDrive/Projects/ResearchAndDevelopment/A
|
||||
|
||||
- `QC_BOOT_ENV` added to `server/lib/lane-profile.js`'s `DEFAULTS` (empty string default — off).
|
||||
- `POST /:id/up` accepts `{build, qc}` in its body (currently only reads `build`); `qc: true` is threaded through to whatever assembles the boot hook's environment (read the actual `upLane`/hook-invocation code path in `server/lib/lane-runtime.js` before implementing — this spec names the profile declaration and the route contract, not the exact internal plumbing function).
|
||||
- `ccam lanes up [--no-build] [--qc]` in `bin/ccam.js` — `--qc` sets `{qc: true}` in the POST body alongside the existing `--no-build` → `{build: false}` handling.
|
||||
- `agenthub lanes up [--no-build] [--qc]` in `bin/agenthub.js` — `--qc` sets `{qc: true}` in the POST body alongside the existing `--no-build` → `{build: false}` handling.
|
||||
- `QC_BOOT_ENV` format: space-separated `KEY=value` pairs, parsed the same way this file already parses `PORTS`/`LANE_DIRS` (space-separated lists) — not a new parsing convention.
|
||||
|
||||
### Pipeline template
|
||||
|
||||
`server/data/pipelines/ship-feature.json` — one node per skill stage (0 through 15, using the skill's own stage names: `intake`, `plan`, `implementing`, `gates`, `e2e-feature`, `e2e-feature-passed`, `review`, `qc-plan`, `qc`, `gate`, `publishing`, `pr-open`, `reported`, `watching-pr`, `merged`, `done`), same node-definition shape `default.json` already uses (label, `detect` rules matching the `stage=` values `ccam stage` sets). `default.json` untouched. A lane opts into this template via its `pipeline` field, same mechanism every other custom template already uses.
|
||||
`server/data/pipelines/ship-feature.json` — one node per skill stage (0 through 15, using the skill's own stage names: `intake`, `plan`, `implementing`, `gates`, `e2e-feature`, `e2e-feature-passed`, `review`, `qc-plan`, `qc`, `gate`, `publishing`, `pr-open`, `reported`, `watching-pr`, `merged`, `done`), same node-definition shape `default.json` already uses (label, `detect` rules matching the `stage=` values `agenthub stage` sets). `default.json` untouched. A lane opts into this template via its `pipeline` field, same mechanism every other custom template already uses.
|
||||
|
||||
### Skill
|
||||
|
||||
`.claude/skills/ship-feature-lane/SKILL.md` — the full port. Frontmatter, hard rules, and all 16 stages carried over with the Command mapping table's substitutions applied line-by-line. Every hard-won rule (turn-liveness, no-retry-cap/phase-clock, one-driver-per-MCP, never-push-base, the fix-loop's two fast-path exceptions, cross-lane etiquette) is kept verbatim in spirit, with `lane-lock.sh` etiquette re-pointed at D's `ccam lock` CLI and named locks.
|
||||
`.claude/skills/ship-feature-lane/SKILL.md` — the full port. Frontmatter, hard rules, and all 16 stages carried over with the Command mapping table's substitutions applied line-by-line. Every hard-won rule (turn-liveness, no-retry-cap/phase-clock, one-driver-per-MCP, never-push-base, the fix-loop's two fast-path exceptions, cross-lane etiquette) is kept verbatim in spirit, with `lane-lock.sh` etiquette re-pointed at D's `agenthub lock` CLI and named locks.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. **CCAM still does not orchestrate.** The skill runs in the session and calls `ccam` commands one at a time; nothing in this task adds chaining, gate evaluation, or retry logic to the dashboard itself.
|
||||
1. **AgentHub still does not orchestrate.** The skill runs in the session and calls `agenthub` commands one at a time; nothing in this task adds chaining, gate evaluation, or retry logic to the dashboard itself.
|
||||
2. **Every command the skill text names must actually exist and behave as documented** — the docs-markdown rule (executable examples, aligned with actual scripts) applies to a skill file exactly as it applies to `docs/*.md`.
|
||||
3. **A profile that never declares `QC_BOOT_ENV` sees no behavior change** — `--qc` degrades to a plain boot when the declaration is absent, same as every other optional A2 declaration.
|
||||
|
||||
## Verify
|
||||
|
||||
A real dry run on a scratch repo with a profile, walking Stage 0 through Stage 2 (intake → plan → `ccam lanes hook ci-gate`) — the furthest the pipeline can go without the not-yet-built agents. Confirms every `ccam` command named in stages 0–2 exists and behaves as documented. `ccam lanes up --qc` gets its own unit/integration test verifying `QC_BOOT_ENV` reaches the boot hook's environment when declared, and is a no-op when absent. Full Stage 0→14 verification is the agents-port task's responsibility, once qc-local/senior-gate-reviewer exist.
|
||||
A real dry run on a scratch repo with a profile, walking Stage 0 through Stage 2 (intake → plan → `agenthub lanes hook ci-gate`) — the furthest the pipeline can go without the not-yet-built agents. Confirms every `agenthub` command named in stages 0–2 exists and behaves as documented. `agenthub lanes up --qc` gets its own unit/integration test verifying `QC_BOOT_ENV` reaches the boot hook's environment when declared, and is a no-op when absent. Full Stage 0→14 verification is the agents-port task's responsibility, once qc-local/senior-gate-reviewer exist.
|
||||
|
||||
@@ -12,39 +12,39 @@
|
||||
**In scope:**
|
||||
1. `server/data/agent-templates/ship-feature-lane/qc-local.md` — the ported browser-QC agent template.
|
||||
2. `server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md` — the ported publish-gate agent template.
|
||||
3. `ccam lanes agents install [<id>]` — copies both templates into `<lane>/.claude/agents/`, git-excluded.
|
||||
3. `agenthub lanes agents install [<id>]` — copies both templates into `<lane>/.claude/agents/`, git-excluded.
|
||||
4. `docs/LANES.md` + roadmap progress line.
|
||||
|
||||
**Out of scope (later, when their dependencies exist):**
|
||||
- **`ticketer.md`, `dev-qc.md`** — both are invoked only by Stages 9 and 13, which are hardcoded-skipped pending F's tracker/dev-QC integrations. Porting them now would ship agent files nothing calls.
|
||||
- **`pr-reviewer.md`** — not referenced anywhere in this repo's `SKILL.md` (the CCAM port dropped the reference during E1; confirmed by grep). Nothing to unblock.
|
||||
- **Per-lane embedded credentials** — Shipyard's installer injects a real QA account's email/password directly into `qc-local.md`'s prompt text, sourced from a per-lane seeded account (`.harness-qa.env`) that has no CCAM equivalent (no seed-account system, no matching `secrets.js` category). This spec ports the agent's *fallback* path (no embedded creds → report `FAIL` with a clear reason) rather than inventing a seed-account system to unblock a credentials feature nobody asked for yet.
|
||||
- **MCP auto-provisioning** (`ccam lanes mcp sync`) — F, not built. `qc-local.md` keeps referencing the `playwright-qa-local` MCP server name by convention; `SKILL.md`'s Stage 0 already documents the manual-`.mcp.json` fallback this depends on.
|
||||
- **`review-checks.md` reader command** — `senior-gate-reviewer.md` reads the profile's optional `review-checks.md` directly with the Read tool (a plain file, no CLI needed), not through a new `ccam lanes profile cat` command.
|
||||
- **`pr-reviewer.md`** — not referenced anywhere in this repo's `SKILL.md` (the AgentHub port dropped the reference during E1; confirmed by grep). Nothing to unblock.
|
||||
- **Per-lane embedded credentials** — Shipyard's installer injects a real QA account's email/password directly into `qc-local.md`'s prompt text, sourced from a per-lane seeded account (`.harness-qa.env`) that has no AgentHub equivalent (no seed-account system, no matching `secrets.js` category). This spec ports the agent's *fallback* path (no embedded creds → report `FAIL` with a clear reason) rather than inventing a seed-account system to unblock a credentials feature nobody asked for yet.
|
||||
- **MCP auto-provisioning** (`agenthub lanes mcp sync`) — F, not built. `qc-local.md` keeps referencing the `playwright-qa-local` MCP server name by convention; `SKILL.md`'s Stage 0 already documents the manual-`.mcp.json` fallback this depends on.
|
||||
- **`review-checks.md` reader command** — `senior-gate-reviewer.md` reads the profile's optional `review-checks.md` directly with the Read tool (a plain file, no CLI needed), not through a new `agenthub lanes profile cat` command.
|
||||
|
||||
## Decisions
|
||||
|
||||
| | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Template location | `server/data/agent-templates/ship-feature-lane/*.md` | Parallel to `server/data/pipelines/*.json` (E1's pattern) — data files, not source. **Not** `ccam-lanes/.claude/agents/`, which already holds this repo's own dev-facing agents (`backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`); mixing a lane's runtime-installed agents into that directory would be a name-collision risk and conflate two different agent populations. |
|
||||
| Heartbeat calls (`state.sh set` between QC scenarios) | Dropped entirely | Shipyard dispatches these agents as a separate background-capable process, so a stalled multi-minute run needs an explicit touch to avoid a false "stalled" flag. CCAM's `qc-local`/`senior-gate-reviewer` run as a synchronous Agent-tool call inside the driving session's own turn — the turn is blocked on the subagent's return, so the session is already "alive" by definition for the whole run. No CCAM heartbeat primitive exists for a subagent to call anyway (this repo's own rule: only `ccam stage` writes progress, and these agents must never call it). |
|
||||
| Auto pre-seed login (`lane-qa-login.sh`, `lane-reset.sh`) | Dropped; `qc-local.md`'s login fallback becomes an honest failure, not a script call | CCAM has no seeded-QA-account system these scripts assumed. Calling a command that doesn't exist would be the same "docs must be executable" violation E1's Decisions table already ruled against for hardcoded-off integrations — this spec's version reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured` instead. |
|
||||
| `lane-sync-dev.sh --check` reference (senior-gate-reviewer's merge-safety check) | → `ccam lanes sync-base --check <branch>` | Direct 1:1 mapping — E2 already built and shipped this command with the exact contract (exit 5 collision, `DEV_DELTA`/`DEV_OVERLAP` output) the agent's text already describes. |
|
||||
| `@@HARNESS_ROOT@@` / `.harness-lane` | Dropped | Same reasoning as E1's skill port — CCAM resolves a lane from `cwd` directly. |
|
||||
| Install command semantics | `ccam lanes agents install [<id>]`, idempotent (overwrite-on-reinstall, not merge) | Matches the "explicit, never automatic" shape this repo already uses for `proof-link` and the E2 merge-driver setup — a session calls it when it wants updated templates, the dashboard never calls it for them. |
|
||||
| Template location | `server/data/agent-templates/ship-feature-lane/*.md` | Parallel to `server/data/pipelines/*.json` (E1's pattern) — data files, not source. **Not** `agenthub-lanes/.claude/agents/`, which already holds this repo's own dev-facing agents (`backend-reviewer`, `frontend-reviewer`, `mcp-reviewer`); mixing a lane's runtime-installed agents into that directory would be a name-collision risk and conflate two different agent populations. |
|
||||
| Heartbeat calls (`state.sh set` between QC scenarios) | Dropped entirely | Shipyard dispatches these agents as a separate background-capable process, so a stalled multi-minute run needs an explicit touch to avoid a false "stalled" flag. AgentHub's `qc-local`/`senior-gate-reviewer` run as a synchronous Agent-tool call inside the driving session's own turn — the turn is blocked on the subagent's return, so the session is already "alive" by definition for the whole run. No AgentHub heartbeat primitive exists for a subagent to call anyway (this repo's own rule: only `agenthub stage` writes progress, and these agents must never call it). |
|
||||
| Auto pre-seed login (`lane-qa-login.sh`, `lane-reset.sh`) | Dropped; `qc-local.md`'s login fallback becomes an honest failure, not a script call | AgentHub has no seeded-QA-account system these scripts assumed. Calling a command that doesn't exist would be the same "docs must be executable" violation E1's Decisions table already ruled against for hardcoded-off integrations — this spec's version reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured` instead. |
|
||||
| `lane-sync-dev.sh --check` reference (senior-gate-reviewer's merge-safety check) | → `agenthub lanes sync-base --check <branch>` | Direct 1:1 mapping — E2 already built and shipped this command with the exact contract (exit 5 collision, `DEV_DELTA`/`DEV_OVERLAP` output) the agent's text already describes. |
|
||||
| `@@HARNESS_ROOT@@` / `.harness-lane` | Dropped | Same reasoning as E1's skill port — AgentHub resolves a lane from `cwd` directly. |
|
||||
| Install command semantics | `agenthub lanes agents install [<id>]`, idempotent (overwrite-on-reinstall, not merge) | Matches the "explicit, never automatic" shape this repo already uses for `proof-link` and the E2 merge-driver setup — a session calls it when it wants updated templates, the dashboard never calls it for them. |
|
||||
| `.claude/agents/` git-exclusion | Appended to `<lane>/.git/info/exclude` (clone-local, not the tracked `.gitignore`) | Same mechanism `write_lane_markers` used in Shipyard and the same file this repo's proof-link machinery already touches — a lane-runtime concern never becomes a change to the app repo's own tracked ignore rules. |
|
||||
|
||||
## Command mapping
|
||||
|
||||
| Shipyard | CCAM | Status |
|
||||
| Shipyard | AgentHub | Status |
|
||||
|---|---|---|
|
||||
| `lane-agents-install.sh <N>` | `ccam lanes agents install [<id>]` | **new in this task** |
|
||||
| `lane-agents-install.sh <N>` | `agenthub lanes agents install [<id>]` | **new in this task** |
|
||||
| `state.sh "$N" set` (mid-run heartbeat) | dropped — see Decisions | n/a |
|
||||
| `lane-qa-login.sh "$N" local` | dropped — see Decisions | n/a |
|
||||
| `lane-reset.sh "$N"` (reseed on login failure) | dropped — see Decisions | n/a |
|
||||
| `lane-sync-dev.sh "$N" --check <branch>` (senior-gate-reviewer's merge-safety check) | `ccam lanes sync-base --check <branch>` | exists (E2) |
|
||||
| `lane-up.sh "$N" --no-build` (revive a down stack) | `ccam lanes up --no-build` | exists (E1 runtime, pre-existing flag) |
|
||||
| `profile-cat.sh review-checks.md` | Read tool on `<lane>/.ccam/profile/review-checks.md` directly | no command needed |
|
||||
| `lane-sync-dev.sh "$N" --check <branch>` (senior-gate-reviewer's merge-safety check) | `agenthub lanes sync-base --check <branch>` | exists (E2) |
|
||||
| `lane-up.sh "$N" --no-build` (revive a down stack) | `agenthub lanes up --no-build` | exists (E1 runtime, pre-existing flag) |
|
||||
| `profile-cat.sh review-checks.md` | Read tool on `<lane>/.agenthub/profile/review-checks.md` directly | no command needed |
|
||||
| `"$HARNESS/bin/lane-mcp-sync.sh"` (implied MCP setup) | manual `.mcp.json` — `SKILL.md` Stage 0 already documents this fallback | F (deferred) |
|
||||
|
||||
## Design
|
||||
@@ -54,17 +54,17 @@
|
||||
Both files are near-verbatim ports of the Shipyard source with the Command-mapping substitutions applied and the out-of-scope credential/heartbeat/login sections rewritten per the Decisions table:
|
||||
|
||||
- **`qc-local.md`** — frontmatter (`name`, `description`, `model: opus`, `color: green`) unchanged in shape. Body: drops the heartbeat loop and the `@@HARNESS_ROOT@@`/`.harness-lane` setup; the "Account / login" section keeps the "if a credentials block is embedded at the top of this agent, use it" path (future-proofed for when a seed-account system exists) but replaces the auto-login-script fallback with an honest `FAIL`. Proof convention (`proof/<feature-slug>/qc-local/<NN>-<what>.png`) is unchanged — it already matches this repo's proof gallery (C) verbatim.
|
||||
- **`senior-gate-reviewer.md`** — frontmatter unchanged (`model: opus`, `color: red`). Body: the merge-safety check's command becomes `ccam lanes sync-base --check <branch>`; the stack-specific-checks step reads `review-checks.md` with the Read tool if present, otherwise falls back to "derive the equivalents yourself" exactly as the original already instructs when that file is empty. Nothing else changes — this agent never touched credentials or heartbeats to begin with.
|
||||
- **`senior-gate-reviewer.md`** — frontmatter unchanged (`model: opus`, `color: red`). Body: the merge-safety check's command becomes `agenthub lanes sync-base --check <branch>`; the stack-specific-checks step reads `review-checks.md` with the Read tool if present, otherwise falls back to "derive the equivalents yourself" exactly as the original already instructs when that file is empty. Nothing else changes — this agent never touched credentials or heartbeats to begin with.
|
||||
|
||||
### `ccam lanes agents install`
|
||||
### `agenthub lanes agents install`
|
||||
|
||||
- `POST /api/lanes/:id/agents/install` — reads both templates from `server/data/agent-templates/ship-feature-lane/`, writes them to `<lane>/.claude/agents/{qc-local.md,senior-gate-reviewer.md}` (creating the directory if needed), appends `.claude/agents/` to `<lane>/.git/info/exclude` if not already present (same idempotent read-existing-then-append pattern E2's `setupMergeDriver` already established for `.git/info/attributes`). Returns `{installed: ["qc-local.md", "senior-gate-reviewer.md"]}`.
|
||||
- No templating/placeholder substitution needed at install time — both files are already CCAM-final text (all substitutions happened once, at port time, into the template files themselves). This is a deliberate simplification from Shipyard's Python templating step, which existed only to inject per-lane credentials and MCP names this scope doesn't have.
|
||||
- CLI: `ccam lanes agents install [<id>]`, same `resolveLaneArg` pattern as `lanes proof-link`.
|
||||
- No templating/placeholder substitution needed at install time — both files are already AgentHub-final text (all substitutions happened once, at port time, into the template files themselves). This is a deliberate simplification from Shipyard's Python templating step, which existed only to inject per-lane credentials and MCP names this scope doesn't have.
|
||||
- CLI: `agenthub lanes agents install [<id>]`, same `resolveLaneArg` pattern as `lanes proof-link`.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. **CCAM still does not orchestrate.** `agents install` copies static files and returns; nothing about it triggers, gates, or sequences pipeline stages.
|
||||
1. **AgentHub still does not orchestrate.** `agents install` copies static files and returns; nothing about it triggers, gates, or sequences pipeline stages.
|
||||
2. **Neither agent ever writes `stage`/`status`/`notes`.** Both already say so explicitly in their own prompt text (ported verbatim); this spec doesn't change that boundary.
|
||||
3. **A lane that never runs `agents install` sees no behavior change** — Stage 6/7 remain `blocked` exactly as today until a session explicitly installs the agents, same "explicit, never automatic" shape as `proof-link`.
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# F3a — `ccam lanes integration <name>` toggle reader
|
||||
# F3a — `agenthub lanes integration <name>` toggle reader
|
||||
|
||||
**Status:** approved 2026-08-05. Roadmap it belongs to:
|
||||
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**).
|
||||
|
||||
## Problem
|
||||
|
||||
`SKILL.md`'s Setup section hardcodes tracker/dev-QC/CI-wait as permanently off ("treat all three as permanently off for this run") because F's `integrations.env` toggle system doesn't exist in CCAM yet. Shipyard's source, `lane-env.sh --check NAME`, is the reference primitive: read one `<NAME>_ENABLED` flag from a profile's `integrations.env`, exit 0/1.
|
||||
`SKILL.md`'s Setup section hardcodes tracker/dev-QC/CI-wait as permanently off ("treat all three as permanently off for this run") because F's `integrations.env` toggle system doesn't exist in AgentHub yet. Shipyard's source, `lane-env.sh --check NAME`, is the reference primitive: read one `<NAME>_ENABLED` flag from a profile's `integrations.env`, exit 0/1.
|
||||
|
||||
This is a narrow slice of F, not all of F3. `ticketer` (the agent that would actually consume `TRACKER_ENABLED`) is explicitly deferred — no tracker MCP is configured for any lane yet, and CCAM has no per-lane credential source (same gap E3 already documented for `qc-local`). This task only builds the toggle-reading primitive itself, so `SKILL.md`'s hardcoded-off text can become a real check instead of a permanent assumption, once something exists to check.
|
||||
This is a narrow slice of F, not all of F3. `ticketer` (the agent that would actually consume `TRACKER_ENABLED`) is explicitly deferred — no tracker MCP is configured for any lane yet, and AgentHub has no per-lane credential source (same gap E3 already documented for `qc-local`). This task only builds the toggle-reading primitive itself, so `SKILL.md`'s hardcoded-off text can become a real check instead of a permanent assumption, once something exists to check.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
1. `<repo>/.ccam/profile/integrations.env` — a new, optional profile file (same directory as `profile.env`), parsed with the EXISTING `parseEnvFile` helper in `server/lib/lane-profile.js` (comments/quotes/`export` already handled — no new parser).
|
||||
1. `<repo>/.agenthub/profile/integrations.env` — a new, optional profile file (same directory as `profile.env`), parsed with the EXISTING `parseEnvFile` helper in `server/lib/lane-profile.js` (comments/quotes/`export` already handled — no new parser).
|
||||
2. `isIntegrationEnabled(lane, name)` in `server/lib/lane-profile.js` — reads `integrations.env` (falling back `lane.cwd` → `lane.source_repo`, same two-location search `resolveProfile` already does for `profile.env`), returns `true` iff `<NAME_UPPER>_ENABLED === "1"`. A missing file or missing key is `false` — off by default, consistent with A2's declarations.
|
||||
3. `GET /api/lanes/:id/integrations/:name` route — `{enabled: boolean}`.
|
||||
4. `ccam lanes integration <name> [<id>]` CLI — exit `0` when enabled, `1` when disabled (the exit-code contract Shipyard's `--check` already established, and what a skill's `&&`-gated stage-skip logic needs).
|
||||
4. `agenthub lanes integration <name> [<id>]` CLI — exit `0` when enabled, `1` when disabled (the exit-code contract Shipyard's `--check` already established, and what a skill's `&&`-gated stage-skip logic needs).
|
||||
|
||||
**Explicitly out of scope:**
|
||||
- `ticketer.md` / `dev-qc.md` agents — deferred pending a tracker/dev-QC MCP and credential source.
|
||||
- Every other field `integrations.env` can declare (`TRACKER_URL`, `TRACKER_PROJECT`, `DEV_SITE_URL`, `CI_REPO`, etc.) — only the `<NAME>_ENABLED` flags matter for this primitive; the rest is read by whatever later task actually consumes a specific integration (F3/F4's real agent work), not by this generic toggle check.
|
||||
- Surfacing toggle state in `GET /api/lanes/:id/runtime` — that endpoint is polled and broadcast on every hook; adding fields nothing consumes yet is premature. `GET /:id/integrations/:name` is its own lightweight endpoint instead.
|
||||
- `lane-env.sh`'s default (no `--check`) mode — dumping the full per-lane runtime env as `export` lines exists in Shipyard because bash/zsh sourcing has a HARNESS_ROOT resolution footgun CCAM's cwd-based lane resolution doesn't have. Nothing in CCAM needs this.
|
||||
- `lane-env.sh`'s default (no `--check`) mode — dumping the full per-lane runtime env as `export` lines exists in Shipyard because bash/zsh sourcing has a HARNESS_ROOT resolution footgun AgentHub's cwd-based lane resolution doesn't have. Nothing in AgentHub needs this.
|
||||
|
||||
## Decisions
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# F3c — `ccam lanes gc`: orphan MCP reap + log capping
|
||||
# F3c — `agenthub lanes gc`: orphan MCP reap + log capping
|
||||
|
||||
**Status:** approved 2026-08-05. Roadmap it belongs to: E (found auditing E1-F3b for Shipyard parity).
|
||||
|
||||
## Problem
|
||||
|
||||
Shipyard's `lane-gc.sh` does 5 things. Two match CCAM's actual architecture; three don't (see Decisions). No CCAM equivalent exists for either of the two that do.
|
||||
Shipyard's `lane-gc.sh` does 5 things. Two match AgentHub's actual architecture; three don't (see Decisions). No AgentHub equivalent exists for either of the two that do.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -13,15 +13,15 @@ Shipyard's `lane-gc.sh` does 5 things. Two match CCAM's actual architecture; thr
|
||||
2. **Cap oversized hook logs.** `LANES_ROOT/.state/lane<slot>/logs/*.log` (written by `runHook`) grow unbounded across many boots/hooks; cap anything over 10MB to its last 2MB, in place (write-then-replace, not a truncate-while-open, so a concurrent writer's fd stays valid).
|
||||
|
||||
**Explicitly out of scope (architecture mismatch, not an oversight):**
|
||||
- **Auto-removing stale worktrees by age.** This is the piece that actually conflicts with a hard CCAM rule: "Never `rm -rf` a lane... destructive lane operations go through `worktree.js`/`lanes.js`, never a raw filesystem delete." An age-based automatic worktree removal is exactly the kind of automatic destructive action this repo's own CLAUDE.md forbids. `ccam lanes reset|remove|purge` already exist for a human to call explicitly.
|
||||
- **Feature-state archiving.** Shipyard's version moves flat JSON state files to an archive dir. CCAM's lane/feature state lives in the SQLite DB (`lane_features` table, from subsystem B) — there's no flat-file equivalent to archive.
|
||||
- **Scratch-debris sweep** (`scratch-lane*.log`, `manual-*.png`, `state.bak.*`). These are artifacts of Shipyard's own scripts (`lane-add.sh` writing `scratch-lane*.log`, manual QA screenshots named that way, `state.sh`'s own backup rotation). CCAM doesn't generate any of these — nothing to sweep.
|
||||
- **Auto-removing stale worktrees by age.** This is the piece that actually conflicts with a hard AgentHub rule: "Never `rm -rf` a lane... destructive lane operations go through `worktree.js`/`lanes.js`, never a raw filesystem delete." An age-based automatic worktree removal is exactly the kind of automatic destructive action this repo's own CLAUDE.md forbids. `agenthub lanes reset|remove|purge` already exist for a human to call explicitly.
|
||||
- **Feature-state archiving.** Shipyard's version moves flat JSON state files to an archive dir. AgentHub's lane/feature state lives in the SQLite DB (`lane_features` table, from subsystem B) — there's no flat-file equivalent to archive.
|
||||
- **Scratch-debris sweep** (`scratch-lane*.log`, `manual-*.png`, `state.bak.*`). These are artifacts of Shipyard's own scripts (`lane-add.sh` writing `scratch-lane*.log`, manual QA screenshots named that way, `state.sh`'s own backup rotation). AgentHub doesn't generate any of these — nothing to sweep.
|
||||
|
||||
## Design
|
||||
|
||||
`ccam lanes gc [--dry-run]` — no lane argument; this scans the whole machine's CCAM footprint, not one lane. Pure local operation (process listing + file I/O), no HTTP route — same shape as `ccam skills install`.
|
||||
`agenthub lanes gc [--dry-run]` — no lane argument; this scans the whole machine's AgentHub footprint, not one lane. Pure local operation (process listing + file I/O), no HTTP route — same shape as `agenthub skills install`.
|
||||
|
||||
- **Orphan reap:** `pgrep -f "<LANES_ROOT>.*\.playwright-mcp"` to find candidate pids (scoped to CCAM's own `LANES_ROOT`, same safety scoping Shipyard's own pattern used — an adopted lane's `cwd` living outside `LANES_ROOT` is a known gap, same one Shipyard's own numbered-lane-dir pattern had). For each match, `ps -o ppid= -p <pid>`; `ppid === 1` → orphaned, kill its process tree (`pgrep -P` recursively, then `SIGKILL` each, leaves not children first — same shape as Shipyard's `kill_tree`).
|
||||
- **Orphan reap:** `pgrep -f "<LANES_ROOT>.*\.playwright-mcp"` to find candidate pids (scoped to AgentHub's own `LANES_ROOT`, same safety scoping Shipyard's own pattern used — an adopted lane's `cwd` living outside `LANES_ROOT` is a known gap, same one Shipyard's own numbered-lane-dir pattern had). For each match, `ps -o ppid= -p <pid>`; `ppid === 1` → orphaned, kill its process tree (`pgrep -P` recursively, then `SIGKILL` each, leaves not children first — same shape as Shipyard's `kill_tree`).
|
||||
- **Log cap:** walk `LANES_ROOT/.state/lane*/logs/*.log`; for any file over 10MB, read its last 2MB and rewrite the file in place.
|
||||
- `--dry-run` prints what would happen without doing it (same flag Shipyard's version has).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# F1 — `ccam lanes mcp sync`
|
||||
# F1 — `agenthub lanes mcp sync`
|
||||
|
||||
**Status:** approved 2026-08-05. Roadmap it belongs to:
|
||||
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**).
|
||||
@@ -13,12 +13,12 @@
|
||||
1. `server/lib/lane-mcp.js` — reads the lane's `source_repo`'s already-configured MCP servers from `~/.claude.json`, relocates any absolute path under `source_repo` to the lane's own `cwd`, pins a `playwright`-family server's `--output-dir` to the lane's `.playwright-mcp`, writes `<lane.cwd>/.mcp.json`.
|
||||
2. Seeding the lane's `.playwright-mcp/profiles/*` Chromium profile directories from the source repo's own (preserves saved MCP browser logins — a real QA-account login only needs to happen once per machine, not once per lane).
|
||||
3. `.mcp.json` added to the lane's local `.git/info/exclude`.
|
||||
4. `ccam lanes mcp sync [<id>]` CLI + `POST /api/lanes/:id/mcp/sync` route.
|
||||
4. `agenthub lanes mcp sync [<id>]` CLI + `POST /api/lanes/:id/mcp/sync` route.
|
||||
5. `SKILL.md` Setup section edit: replace the manual-fallback text with the real command.
|
||||
6. `docs/LANES.md` + roadmap progress line.
|
||||
|
||||
**Deliberately out of scope (deviates from Shipyard, by explicit decision — not an oversight):**
|
||||
- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `<lane>/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__<server>` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. CCAM has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated.
|
||||
- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `<lane>/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__<server>` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. AgentHub has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated.
|
||||
- **The `mcp-node-exec.sh` node-version wrapper.** Shipyard's script routes `npx`/`node`-based MCP servers through a wrapper that resolves the newest Node ≥ 18 at spawn time, working around an nvm/PATH resolution bug specific to that environment. Not a problem this repo has evidence of; add it later if it actually bites someone (YAGNI).
|
||||
- **Merging with an existing `.mcp.json`.** This is a full overwrite from the source repo's current MCP config, same "reinstall, not merge" shape E3's `agents install` already established — not a per-server incremental sync.
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
| Missing config | Throws `ENOMCPCONFIG` with a clear message pointing at `SKILL.md`'s Setup section, not a silent no-op | A lane with zero MCP servers synced would silently break Stage 3/6 later, at a much less debuggable point. Fail loud, fail early, matching `requireProfile`'s existing shape for a missing profile. |
|
||||
| Overwrite semantics | Full overwrite of `.mcp.json` on every call, profiles seeded only if the destination directory doesn't already exist | Matches E3's "reinstall, not merge" for `.mcp.json` (config should always reflect the source repo's current state) but "never clobber a working login" for profiles (an existing profile dir means a session already logged in there — copying over it would lose that). |
|
||||
| `--output-dir` pinning | Only added to a server whose args reference `@playwright/mcp` and don't already declare `--output-dir` | Direct port of Shipyard's own conditional — this is what makes proof screenshots land in the lane's own canonical `.playwright-mcp/proof` (the same path the proof gallery, C, already reads) instead of wherever the server's cwd happens to be. |
|
||||
| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing CCAM pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). |
|
||||
| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing AgentHub pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). |
|
||||
|
||||
## Command mapping
|
||||
|
||||
| Shipyard | CCAM | Status |
|
||||
| Shipyard | AgentHub | Status |
|
||||
|---|---|---|
|
||||
| `lane-mcp-sync.sh <N>` | `ccam lanes mcp sync [<id>]` | **new in this task** |
|
||||
| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `ccam lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) |
|
||||
| `lane-mcp-sync.sh <N>` | `agenthub lanes mcp sync [<id>]` | **new in this task** |
|
||||
| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `agenthub lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) |
|
||||
| Chromium profile seeding | ported directly, same logic | **new in this task** |
|
||||
| `.mcp.json` write + relocate + `--output-dir` pin | ported directly, same logic | **new in this task** |
|
||||
| `settings.local.json` permission/autoMode writes | dropped | n/a (see Scope) |
|
||||
@@ -55,11 +55,11 @@
|
||||
|
||||
### Route + CLI
|
||||
|
||||
`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `ccam lanes mcp sync [<id>]` — same `resolveLaneArg` pattern as `lanes agents install`.
|
||||
`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `agenthub lanes mcp sync [<id>]` — same `resolveLaneArg` pattern as `lanes agents install`.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. **CCAM still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything.
|
||||
1. **AgentHub still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything.
|
||||
2. **Never a silent partial state.** A lane with no source-repo MCP config gets a clear error, not an empty-but-present `.mcp.json`.
|
||||
3. **A lane's existing browser-profile login is never overwritten**, only ever seeded once.
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# F3b — `ccam skills install`: making `/ship-feature-lane` actually invokable
|
||||
# F3b — `agenthub skills install`: making `/ship-feature-lane` actually invokable
|
||||
|
||||
**Status:** approved 2026-08-05. Roadmap it belongs to:
|
||||
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **E**, a gap found auditing E1-F3a for Shipyard parity).
|
||||
|
||||
## Problem
|
||||
|
||||
`.claude/skills/ship-feature-lane/SKILL.md`'s own description says "Invoke inside a lane's working directory" — but Claude Code only auto-discovers a `.claude/skills/` directory when the session's cwd is inside the repo that OWNS that directory. The skill lives only at `ccam-lanes/.claude/skills/ship-feature-lane/SKILL.md`; confirmed empty: `~/.claude/skills/` has no `ship-feature-lane` entry. Every piece built across E1/E2/E3/F1/F3a is unreachable from its actual intended invocation context — a session inside some OTHER lane's repo, not inside ccam-lanes itself.
|
||||
`.claude/skills/ship-feature-lane/SKILL.md`'s own description says "Invoke inside a lane's working directory" — but Claude Code only auto-discovers a `.claude/skills/` directory when the session's cwd is inside the repo that OWNS that directory. The skill lives only at `agenthub-lanes/.claude/skills/ship-feature-lane/SKILL.md`; confirmed empty: `~/.claude/skills/` has no `ship-feature-lane` entry. Every piece built across E1/E2/E3/F1/F3a is unreachable from its actual intended invocation context — a session inside some OTHER lane's repo, not inside agenthub-lanes itself.
|
||||
|
||||
Shipyard's own `install-claude-assets.sh` solves exactly this: it copies bundled skills into `~/.claude/skills/` (user-level, global) so every project's session sees them.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
- `ccam skills install` — copies `<ccam-lanes repo>/.claude/skills/ship-feature-lane/` to `~/.claude/skills/ship-feature-lane/`, overwriting on reinstall (same "reinstall, not merge" shape as `ccam lanes agents install`).
|
||||
- `agenthub skills install` — copies `<agenthub-lanes repo>/.claude/skills/ship-feature-lane/` to `~/.claude/skills/ship-feature-lane/`, overwriting on reinstall (same "reinstall, not merge" shape as `agenthub lanes agents install`).
|
||||
- `docs/LANES.md` note.
|
||||
|
||||
**Out of scope:**
|
||||
- Changing `ccam lanes agents install`'s existing per-lane `.claude/agents/` mechanism. That one's already reachable correctly (Claude Code discovers `.claude/agents/` from the lane's own cwd, which IS where a driving session runs) — this gap is specific to the SKILL, not the agents. No architecture change needed there.
|
||||
- Changing `agenthub lanes agents install`'s existing per-lane `.claude/agents/` mechanism. That one's already reachable correctly (Claude Code discovers `.claude/agents/` from the lane's own cwd, which IS where a driving session runs) — this gap is specific to the SKILL, not the agents. No architecture change needed there.
|
||||
- Auto-running this from `npm run setup` or any other command. Explicit, like every other install primitive in this codebase (`lanes agents install`, `lanes mcp sync`, `lanes proof-link`) — a human runs it once per machine.
|
||||
- Any other bundled skill Shipyard ships (`/review-prs`, `/harness-adapt`) — CCAM has no equivalent of either; out of scope by the same reasoning `pr-reviewer` was dropped (no driving skill to attach to).
|
||||
- Any other bundled skill Shipyard ships (`/review-prs`, `/harness-adapt`) — AgentHub has no equivalent of either; out of scope by the same reasoning `pr-reviewer` was dropped (no driving skill to attach to).
|
||||
|
||||
## Design
|
||||
|
||||
`bin/ccam.js` already has `REPO_ROOT` (`path.resolve(path.dirname(fs.realpathSync(__filename)), "..")`) — the exact self-location primitive Shipyard needed `HARNESS_ROOT` for, already present because `ccam` is npm-linked. No new resolution logic needed.
|
||||
`bin/agenthub.js` already has `REPO_ROOT` (`path.resolve(path.dirname(fs.realpathSync(__filename)), "..")`) — the exact self-location primitive Shipyard needed `HARNESS_ROOT` for, already present because `agenthub` is npm-linked. No new resolution logic needed.
|
||||
|
||||
- `cmdSkillsInstall()` in `bin/ccam.js` (no HTTP round-trip — this has nothing to do with any lane or the dashboard's DB, purely a local file copy from the CLI's own checkout to the user's home directory; matches the existing local-only pattern `cmdLanesProfileInit` already uses via `REPO_ROOT`).
|
||||
- `cmdSkillsInstall()` in `bin/agenthub.js` (no HTTP round-trip — this has nothing to do with any lane or the dashboard's DB, purely a local file copy from the CLI's own checkout to the user's home directory; matches the existing local-only pattern `cmdLanesProfileInit` already uses via `REPO_ROOT`).
|
||||
- Copies `path.join(REPO_ROOT, ".claude", "skills", "ship-feature-lane")` → `path.join(os.homedir(), ".claude", "skills", "ship-feature-lane")` via `fs.cpSync(..., {recursive: true, force: true})` (force: true = overwrite existing files, matching reinstall semantics).
|
||||
- Command: `ccam skills install`, no lane argument (nothing lane-specific here).
|
||||
- Command: `agenthub skills install`, no lane argument (nothing lane-specific here).
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# E2 — `ccam lanes sync-base`
|
||||
# E2 — `agenthub lanes sync-base`
|
||||
|
||||
**Status:** approved 2026-08-05. Roadmap it belongs to:
|
||||
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **E**).
|
||||
|
||||
## Problem
|
||||
|
||||
`.claude/skills/ship-feature-lane/SKILL.md` (ported in E1) names `ccam lanes sync-base` at Stages 2, 8 and 12 by its intended contract, but the command doesn't exist — Stage 2 hardcodes "preflight unavailable, skipped" and Stage 12 falls back to a bare `git merge`. Shipyard's source, `lane-sync-dev.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-sync-dev.sh`, 173 lines, plus its test `tests/test_sync_dev.sh`), is the reference: the ONE sanctioned merge in the pipeline (`origin/development` INTO the feature branch), with a migration-number collision preflight and a keep-ours merge driver for generated files. E (per the roadmap) has three pieces left after E1: agents, sync-base, F's integrations. This spec covers sync-base only.
|
||||
`.claude/skills/ship-feature-lane/SKILL.md` (ported in E1) names `agenthub lanes sync-base` at Stages 2, 8 and 12 by its intended contract, but the command doesn't exist — Stage 2 hardcodes "preflight unavailable, skipped" and Stage 12 falls back to a bare `git merge`. Shipyard's source, `lane-sync-dev.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-sync-dev.sh`, 173 lines, plus its test `tests/test_sync_dev.sh`), is the reference: the ONE sanctioned merge in the pipeline (`origin/development` INTO the feature branch), with a migration-number collision preflight and a keep-ours merge driver for generated files. E (per the roadmap) has three pieces left after E1: agents, sync-base, F's integrations. This spec covers sync-base only.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
1. Two new profile declarations: `MIGRATIONS_DIR`, `GENERATED_MERGE_PATHS` (empty = off, same `DEFAULTS` pattern as every other optional A2 declaration).
|
||||
2. `server/lib/lane-sync.js` — the git-only core: collision check, dev-delta report, merge, continue-after-conflict.
|
||||
3. `POST /api/lanes/:id/sync-base` route.
|
||||
4. `ccam lanes sync-base [--check|--continue] [branch] [<id>]` CLI subcommand.
|
||||
5. `.claude/skills/ship-feature-lane/SKILL.md` — Stage 2/8/12 edits: drop the "if it exists yet" conditionals, add the exit-4/`--continue` mechanics (currently absent from the CCAM port; Shipyard's original `ship-feature/SKILL.md` Stage 12 has the text to carry over).
|
||||
4. `agenthub lanes sync-base [--check|--continue] [branch] [<id>]` CLI subcommand.
|
||||
5. `.claude/skills/ship-feature-lane/SKILL.md` — Stage 2/8/12 edits: drop the "if it exists yet" conditionals, add the exit-4/`--continue` mechanics (currently absent from the AgentHub port; Shipyard's original `ship-feature/SKILL.md` Stage 12 has the text to carry over).
|
||||
6. `docs/LANES.md` + roadmap progress line.
|
||||
|
||||
**Out of scope:**
|
||||
@@ -27,19 +27,19 @@
|
||||
| | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Route timing model | Synchronous — the route runs the git operation and returns JSON directly, no `202` + websocket broadcast | Unlike `ci-gate`/`e2e` (test suites, minutes), a fetch + collision-check + merge is a few seconds of git work. Matches the existing `GET /:id/git` pattern (working-copy facts, synchronous), not the `hook` pattern. |
|
||||
| Merge-driver setup timing | Lazy, inside `mergeSync` itself, idempotent every call | Matches this repo's own established precedent: proof-link is "idempotent, never automatic" rather than a provision-time step. No other CCAM lane-creation path (`addWorktree`/`provisionLane`) currently touches git config or `.git/info/*`; adding one there for this alone would be a new concern in an unrelated file for a need only sync-base has. |
|
||||
| Stage/status writes | None. `sync-base` returns a structured result (`code`, `collisions`, `devDelta`, `overlap`, `conflictedFiles`); it never calls anything that sets a lane's `stage`, `status`, or `notes` | This repo's own rule: "declared stages come from `ccam stage`... The console never writes a lane's stage" and "the runtime never writes stage, status or notes." Shipyard's `state.sh set` heartbeat/notes calls inside `lane-sync-dev.sh` are dropped for the same reason `--qc`'s design dropped `state.sh init` in E1 — CCAM has a narrower, already-established boundary for who writes stage, and it's not this command. The skill is responsible for calling `ccam stage ... --status blocked` itself on a collision or conflict, same as any other gate failure. |
|
||||
| Merge-driver setup timing | Lazy, inside `mergeSync` itself, idempotent every call | Matches this repo's own established precedent: proof-link is "idempotent, never automatic" rather than a provision-time step. No other AgentHub lane-creation path (`addWorktree`/`provisionLane`) currently touches git config or `.git/info/*`; adding one there for this alone would be a new concern in an unrelated file for a need only sync-base has. |
|
||||
| Stage/status writes | None. `sync-base` returns a structured result (`code`, `collisions`, `devDelta`, `overlap`, `conflictedFiles`); it never calls anything that sets a lane's `stage`, `status`, or `notes` | This repo's own rule: "declared stages come from `agenthub stage`... The console never writes a lane's stage" and "the runtime never writes stage, status or notes." Shipyard's `state.sh set` heartbeat/notes calls inside `lane-sync-dev.sh` are dropped for the same reason `--qc`'s design dropped `state.sh init` in E1 — AgentHub has a narrower, already-established boundary for who writes stage, and it's not this command. The skill is responsible for calling `agenthub stage ... --status blocked` itself on a collision or conflict, same as any other gate failure. |
|
||||
| Exit codes | Preserved verbatim: `0` clean, `4` conflict (left in place), `5` migration collision | `SKILL.md` already documents these two non-zero codes by name (written during E1, before the command existed) — the contract predates this implementation and this spec fulfills it rather than inventing a new one. |
|
||||
| `git` execution | Reuse `worktree.js`'s `git(cwd, args)` helper (export it — not currently exported) | This repo's binding rule: never build a git command as a shell string; `worktree.js` already has the one approved execFile wrapper. A second implementation in `lane-sync.js` would duplicate it. |
|
||||
| State for `--continue` | None — read straight from the lane's own git state (`git ls-files -u`, presence of `.git/MERGE_HEAD`), exactly like the source script | Stateless by construction: the working copy IS the source of truth for "is this conflict resolved yet," so a separate DB flag would just be a second place to get out of sync with the first. |
|
||||
|
||||
## Command mapping
|
||||
|
||||
| Shipyard | CCAM | Status |
|
||||
| Shipyard | AgentHub | Status |
|
||||
|---|---|---|
|
||||
| `lane-sync-dev.sh <N> --check [branch]` | `ccam lanes sync-base --check [branch] [<id>]` | **new in this task** |
|
||||
| `lane-sync-dev.sh <N> [branch]` | `ccam lanes sync-base [branch] [<id>]` | **new in this task** |
|
||||
| `lane-sync-dev.sh <N> --continue [branch]` | `ccam lanes sync-base --continue [branch] [<id>]` | **new in this task** |
|
||||
| `lane-sync-dev.sh <N> --check [branch]` | `agenthub lanes sync-base --check [branch] [<id>]` | **new in this task** |
|
||||
| `lane-sync-dev.sh <N> [branch]` | `agenthub lanes sync-base [branch] [<id>]` | **new in this task** |
|
||||
| `lane-sync-dev.sh <N> --continue [branch]` | `agenthub lanes sync-base --continue [branch] [<id>]` | **new in this task** |
|
||||
| `state.sh "$N" set stage=migration-collision ...` (on exit 5) | dropped — see Decisions | n/a |
|
||||
| `state.sh "$N" set stage=sync-conflict ...` (on exit 4) | dropped — see Decisions | n/a |
|
||||
| `write_lane_markers` (push guards, `.harness-lane` exclude, proof-link) | dropped — out of scope, unrelated to sync | n/a |
|
||||
@@ -59,7 +59,7 @@
|
||||
Three exported async functions, each taking `(lane, profile, branch)`:
|
||||
|
||||
- **`checkSync(lane, profile, branch)`** — validates `branch` isn't `development`/`main` and exists; `git fetch origin --prune`; runs the collision check (below); if a collision is found, returns `{code: 5, collisions: [{file, num, clash, suggestion}]}` without going further. Otherwise computes `devDelta` (files changed on `origin/development` since the merge-base with `branch`, filtered against `GENERATED_MERGE_PATHS`) and `overlap` (the subset of that delta touching `branch`'s own changed files), returns `{code: 0, devDelta, overlap}`.
|
||||
- **`mergeSync(lane, profile, branch)`** — same validation + fetch; installs the keep-ours merge driver for every `GENERATED_MERGE_PATHS` entry via `git config merge.ccam-generated.driver true` + a line per path in `.git/info/attributes` (both local-only, idempotent — `grep -qxF` before appending, exactly like the source's guard); runs the collision check (exit 5 short-circuits, nothing merged); checks out `branch`; runs `git merge --no-edit origin/development`. A clean merge or a fully rerere-auto-resolved one falls through to the regen fold-in (below) and returns `{code: 0}`. A real conflict is left in place — returns `{code: 4, conflictedFiles: [...]}` from `git diff --name-only --diff-filter=U`.
|
||||
- **`mergeSync(lane, profile, branch)`** — same validation + fetch; installs the keep-ours merge driver for every `GENERATED_MERGE_PATHS` entry via `git config merge.agenthub-generated.driver true` + a line per path in `.git/info/attributes` (both local-only, idempotent — `grep -qxF` before appending, exactly like the source's guard); runs the collision check (exit 5 short-circuits, nothing merged); checks out `branch`; runs `git merge --no-edit origin/development`. A clean merge or a fully rerere-auto-resolved one falls through to the regen fold-in (below) and returns `{code: 0}`. A real conflict is left in place — returns `{code: 4, conflictedFiles: [...]}` from `git diff --name-only --diff-filter=U`.
|
||||
- **`continueSync(lane, profile, branch)`** — asserts current branch equals `branch`, no unmerged paths (`git ls-files -u` empty), and no `.git/MERGE_HEAD` (already committed) — any violation is a thrown error with a clear message, not a silent no-op. Then runs the regen fold-in and returns `{code: 0}`.
|
||||
|
||||
**Collision check** (shared helper, not exported): if `MIGRATIONS_DIR` is unset, no-op. Otherwise diff `origin/development...branch` for added files under `MIGRATIONS_DIR`, list `origin/development`'s existing `NNN_*` files in that dir, and for each added file whose number already exists upstream, compute the next free number and report it — same arithmetic as the source script (`10#${maxnum:-0} + 1`, incrementing per additional clash in the same batch).
|
||||
@@ -72,17 +72,17 @@ Three exported async functions, each taking `(lane, profile, branch)`:
|
||||
|
||||
### CLI
|
||||
|
||||
`ccam lanes sync-base [--check|--continue] [branch] [<id>]` in `bin/ccam.js`, same flag-parsing style as `lanes up`. Maps the response `code` to `process.exitCode` (0/4/5) so a driving skill's `$?`/exit-code check works exactly as `SKILL.md` already documents. On `code: 5`, prints each collision's suggested rename; on `code: 4`, prints the conflicted file list plus the exact `--continue` follow-up command; on `code: 0`, prints `DEV_DELTA:`/`DEV_OVERLAP:` (check mode) or a plain success line (merge/continue mode) — same output shape as the source script, since the skill's stage text already expects to read `DEV_DELTA:`/`DEV_OVERLAP:` lines.
|
||||
`agenthub lanes sync-base [--check|--continue] [branch] [<id>]` in `bin/agenthub.js`, same flag-parsing style as `lanes up`. Maps the response `code` to `process.exitCode` (0/4/5) so a driving skill's `$?`/exit-code check works exactly as `SKILL.md` already documents. On `code: 5`, prints each collision's suggested rename; on `code: 4`, prints the conflicted file list plus the exact `--continue` follow-up command; on `code: 0`, prints `DEV_DELTA:`/`DEV_OVERLAP:` (check mode) or a plain success line (merge/continue mode) — same output shape as the source script, since the skill's stage text already expects to read `DEV_DELTA:`/`DEV_OVERLAP:` lines.
|
||||
|
||||
### `SKILL.md` edits
|
||||
|
||||
- Stage 2: replace the "later task, not yet built" paragraph with the real command + its exit-5 handling (rename, re-run gates) — text already half-written in E1's port, just needs the conditional removed.
|
||||
- Stage 8: same — "re-run the preflight if it exists by now" becomes unconditional.
|
||||
- Stage 12: replace the "if it exists / if it doesn't" branch with the unconditional `ccam lanes sync-base feat/<slug>` call, and add the exit-4 handling this repo's port currently lacks: resolve conflicts, `git add` only the resolved files, `git commit --no-edit`, then `ccam lanes sync-base --continue feat/<slug>` (folds regenerated artifacts in) — carried over from Shipyard's original `ship-feature/SKILL.md` Stage 12, which already has this exact text.
|
||||
- Stage 12: replace the "if it exists / if it doesn't" branch with the unconditional `agenthub lanes sync-base feat/<slug>` call, and add the exit-4 handling this repo's port currently lacks: resolve conflicts, `git add` only the resolved files, `git commit --no-edit`, then `agenthub lanes sync-base --continue feat/<slug>` (folds regenerated artifacts in) — carried over from Shipyard's original `ship-feature/SKILL.md` Stage 12, which already has this exact text.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. **CCAM still does not orchestrate.** `sync-base` performs one git operation per call and returns; it never chains into gate re-runs or stage transitions itself — the skill decides what happens next.
|
||||
1. **AgentHub still does not orchestrate.** `sync-base` performs one git operation per call and returns; it never chains into gate re-runs or stage transitions itself — the skill decides what happens next.
|
||||
2. **A profile that never declares `MIGRATIONS_DIR` or `GENERATED_MERGE_PATHS` sees no behavior change** — collision checking and the merge driver both degrade to no-ops, same as every other optional declaration.
|
||||
3. **Never a shell-string git command.** Every git invocation in `lane-sync.js` goes through the shared `git(cwd, args)` execFile helper.
|
||||
4. **`sync-base` never writes `stage`/`status`/`notes`.** Same boundary as the runtime and the hook routes.
|
||||
|
||||
@@ -13,7 +13,7 @@ F4 surfaced `agents install`/`mcp sync`/profile-init as per-lane manual buttons,
|
||||
2. `AddLaneModal.tsx`: after `api.lanes.worktree()` succeeds, fire `profileInit`, `agentsInstall`, and `mcpSync` in parallel (`Promise.allSettled` — all three are independent of each other: `agentsInstall`/`mcpSync` only need `.git`, not a profile). Each outcome (success/failure/skipped) is collected and shown as a short per-step result list before the modal closes, instead of closing silently the instant the lane record exists.
|
||||
|
||||
**Explicitly out of scope:**
|
||||
- `ccam skills install` — machine-wide, one-time-per-machine, not lane-specific. Running it every time someone adds a lane would be redundant (idempotent, but pointless) and conceptually wrong (it has nothing to do with THIS lane). Stays a manual Settings-page action.
|
||||
- `agenthub skills install` — machine-wide, one-time-per-machine, not lane-specific. Running it every time someone adds a lane would be redundant (idempotent, but pointless) and conceptually wrong (it has nothing to do with THIS lane). Stays a manual Settings-page action.
|
||||
- Blocking lane creation on any of the three setup steps failing. A lane with a non-Node.js repo, no MCP servers configured, or a git quirk should still exist and be usable — setup is best-effort enrichment, not a precondition.
|
||||
- Adopting an EXISTING folder (a second "add lane" mode) — `AddLaneModal` only does managed-worktree creation today; that's unchanged. This auto-setup applies to whatever lane the existing flow just created, regardless of how.
|
||||
|
||||
|
||||
@@ -4,30 +4,30 @@
|
||||
|
||||
## Problem
|
||||
|
||||
E1 through F3c (this session) built six CLI-only primitives — `ccam lanes sync-base`, `ccam lanes agents install`, `ccam lanes mcp sync`, `ccam lanes integration <name>`, `ccam lanes gc`, `ccam skills install` — with no UI surface. The user wants the lane-scoped, day-to-day-useful ones clickable from the dashboard instead of terminal-only.
|
||||
E1 through F3c (this session) built six CLI-only primitives — `agenthub lanes sync-base`, `agenthub lanes agents install`, `agenthub lanes mcp sync`, `agenthub lanes integration <name>`, `agenthub lanes gc`, `agenthub skills install` — with no UI surface. The user wants the lane-scoped, day-to-day-useful ones clickable from the dashboard instead of terminal-only.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope — LaneCard.tsx (per-lane, gated on `runtime?.available`, same condition the existing up/down button already uses since all four need a `.ccam/profile`):**
|
||||
**In scope — LaneCard.tsx (per-lane, gated on `runtime?.available`, same condition the existing up/down button already uses since all four need a `.agenthub/profile`):**
|
||||
1. **"Install agents" button** → `POST /lanes/:id/agents/install` (exists). Shows the installed file list on success.
|
||||
2. **"Sync MCP" button** → `POST /lanes/:id/mcp/sync` (exists). Shows synced servers + seeded profiles.
|
||||
3. **Integration status badges** (tracker / dev_qc / ci_wait), read-only, on/off — reads `GET /lanes/:id/integrations/:name` (exists) for each of the three fixed names.
|
||||
4. **"Check dev sync" button** → `POST /lanes/:id/sync-base` with `{mode: "check"}` (exists — the route already supports `mode`). Shows `DEV_DELTA`/`DEV_OVERLAP` or a collision list. **Read-only preflight only — no merge button.** Merging is a deliberate, evidence-recording action the driving skill/session takes (`ccam lanes sync-base` bare, or `--continue`); a UI click is the wrong place for a git merge with fix-loop implications. This matches the project's own boundary: "the console never writes a lane's stage" — extending that spirit, the console doesn't perform git merges either.
|
||||
4. **"Check dev sync" button** → `POST /lanes/:id/sync-base` with `{mode: "check"}` (exists — the route already supports `mode`). Shows `DEV_DELTA`/`DEV_OVERLAP` or a collision list. **Read-only preflight only — no merge button.** Merging is a deliberate, evidence-recording action the driving skill/session takes (`agenthub lanes sync-base` bare, or `--continue`); a UI click is the wrong place for a git merge with fix-loop implications. This matches the project's own boundary: "the console never writes a lane's stage" — extending that spirit, the console doesn't perform git merges either.
|
||||
|
||||
**In scope — Settings.tsx (machine-wide, new "Lanes" section near the existing Hooks section — modeled on "Reinstall Hooks"/"Cleanup"):**
|
||||
5. **"Install ship-feature-lane skill" button** → new route `POST /api/skills/install` (doesn't exist yet — needs building; wraps the same `fs.cpSync` logic `ccam skills install`'s CLI already has, factored into `server/lib/lane-agents.js` or a new tiny `server/lib/skills-install.js` so the CLI and route share one implementation instead of duplicating the copy logic).
|
||||
5. **"Install ship-feature-lane skill" button** → new route `POST /api/skills/install` (doesn't exist yet — needs building; wraps the same `fs.cpSync` logic `agenthub skills install`'s CLI already has, factored into `server/lib/lane-agents.js` or a new tiny `server/lib/skills-install.js` so the CLI and route share one implementation instead of duplicating the copy logic).
|
||||
6. **"Run housekeeping" button, with a "dry run" checkbox** → new route `POST /api/lanes/gc` (doesn't exist yet — wraps `server/lib/lane-gc.js`'s `reapOrphanMcp`/`capOversizedLogs`, already built and tested in F3c). Shows reaped pids + capped log paths.
|
||||
|
||||
**Explicitly out of scope:**
|
||||
- A merge button for `sync-base` (see #4's reasoning).
|
||||
- Any UI for `ccam lanes sync-base --continue` (conflict resolution is a session/skill action with real judgment calls about which side to keep — not a button).
|
||||
- Refactoring the CLI (`bin/ccam.js`) to call the new HTTP routes instead of its current direct `require()` of the lib modules — the CLI's local-only shape (no server round-trip needed when you're already on the machine) stays as-is; only the browser needs an HTTP path, so the routes are additive, not a replacement for the CLI's existing implementation.
|
||||
- Any UI for `agenthub lanes sync-base --continue` (conflict resolution is a session/skill action with real judgment calls about which side to keep — not a button).
|
||||
- Refactoring the CLI (`bin/agenthub.js`) to call the new HTTP routes instead of its current direct `require()` of the lib modules — the CLI's local-only shape (no server round-trip needed when you're already on the machine) stays as-is; only the browser needs an HTTP path, so the routes are additive, not a replacement for the CLI's existing implementation.
|
||||
|
||||
## Design
|
||||
|
||||
### Two new backend routes
|
||||
|
||||
- `POST /api/skills/install` — same `sameOriginGuard` as every other mutating route. Extracts the copy logic (`REPO_ROOT/.claude/skills/ship-feature-lane` → `~/.claude/skills/ship-feature-lane`, `fs.cpSync` with `force: true`) out of `bin/ccam.js`'s `cmdSkillsInstall` into a small exported function in `server/lib/skills-install.js`, so `bin/ccam.js` and the route both call the same code (no duplication). Returns `{installed: true, path: string}`.
|
||||
- `POST /api/skills/install` — same `sameOriginGuard` as every other mutating route. Extracts the copy logic (`REPO_ROOT/.claude/skills/ship-feature-lane` → `~/.claude/skills/ship-feature-lane`, `fs.cpSync` with `force: true`) out of `bin/agenthub.js`'s `cmdSkillsInstall` into a small exported function in `server/lib/skills-install.js`, so `bin/agenthub.js` and the route both call the same code (no duplication). Returns `{installed: true, path: string}`.
|
||||
- `POST /api/lanes/gc` — same guard. Body `{dryRun?: boolean}`. Calls `reapOrphanMcp`/`capOversizedLogs` from the already-built `server/lib/lane-gc.js` (no changes needed there — Task 2 of F3c already exports both). Returns `{reaped: number[], capped: {path: string, sizeBefore: number}[]}`.
|
||||
|
||||
Both routes are machine-wide (no `:id`), registered at the router's top level (not under `/lanes/:id/...`) — `/api/skills/install` sits alongside other non-lane-scoped routes; `/api/lanes/gc` sits in the lanes router but before any `:id` param route (order matters: Express would otherwise try to resolve `gc` as a lane id on a route like `/lanes/:id`).
|
||||
@@ -60,7 +60,7 @@ gc: (body: { dryRun?: boolean } = {}) =>
|
||||
|
||||
### LaneCard.tsx
|
||||
|
||||
Four additions, all gated on `runtime?.available` (same condition the up/down button already uses — every one of these four needs a `.ccam/profile`):
|
||||
Four additions, all gated on `runtime?.available` (same condition the up/down button already uses — every one of these four needs a `.agenthub/profile`):
|
||||
|
||||
- **Install agents / Sync MCP buttons**: same visual pattern as the up/down toggle (`rounded px-2 py-1 text-fg-secondary transition-colors hover:bg-surface-2 disabled:opacity-50`), a `useState<"agents" | "mcp" | "sync-check" | null>` busy flag (one shared state covering all three new button actions plus check, mirroring `runtimeBusy`'s shape), result shown as a transient inline line below the button (reuse the `laneActionError`-style banner pattern, but for success text too — a new `actionResult: {label: string; message: string} | null` state scoped to LaneCard).
|
||||
- **Integration badges**: a small row of three pill-shaped badges (`tracker` / `dev_qc` / `ci_wait`), fetched once when the card mounts and `runtime.available` is true (parallel `Promise.all` of the three `api.lanes.integration` calls, same effect-on-mount shape the existing `git`/`runtime` fetches already use), green/gray styling for on/off, no click handler — read-only.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Plugin-first install — CCAM usable straight from a Claude Code plugin install
|
||||
# Plugin-first install — AgentHub usable straight from a Claude Code plugin install
|
||||
|
||||
Date: 2026-08-10
|
||||
Status: approved (design)
|
||||
@@ -6,12 +6,12 @@ Status: approved (design)
|
||||
## Goal
|
||||
|
||||
On a machine with nothing but Claude Code installed, these two commands must
|
||||
leave the user with a working CCAM — hooks flowing, server up, dashboard
|
||||
reachable, `ccam` CLI on PATH, MCP tools connected:
|
||||
leave the user with a working AgentHub — hooks flowing, server up, dashboard
|
||||
reachable, `agenthub` CLI on PATH, MCP tools connected:
|
||||
|
||||
```
|
||||
/plugin marketplace add <ccam repo>
|
||||
/plugin install ccam@claude-code-agent-monitor-plugins
|
||||
/plugin marketplace add <agenthub repo>
|
||||
/plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
```
|
||||
|
||||
No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
|
||||
@@ -19,8 +19,8 @@ No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
|
||||
|
||||
## Why the current state does not do this
|
||||
|
||||
- The only plugin with a server connection, `ccam-dashboard`, declares its MCP
|
||||
server as `../../mcp/build/index.js` (`plugins/ccam-dashboard/.mcp.json`).
|
||||
- The only plugin with a server connection, `agenthub-dashboard`, declares its MCP
|
||||
server as `../../mcp/build/index.js` (`plugins/agenthub-dashboard/.mcp.json`).
|
||||
Plugins installed from a marketplace subdir are cached as **that subdir
|
||||
only** — verified against `~/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/`,
|
||||
which contains the plugin directory's contents and nothing above it. The
|
||||
@@ -32,14 +32,14 @@ No repo clone, no `npm run setup`, no `npm run install-hooks`, no manual
|
||||
|
||||
## Approach: declare the whole repo as one plugin
|
||||
|
||||
Add a **root-level** plugin named `ccam` with `"source": "."` in
|
||||
Add a **root-level** plugin named `agenthub` with `"source": "."` in
|
||||
`.claude-plugin/marketplace.json`. Claude Code then caches the *entire repo*
|
||||
into `~/.claude/plugins/cache/<marketplace>/ccam/<version>/` — verified against
|
||||
into `~/.claude/plugins/cache/<marketplace>/agenthub/<version>/` — verified against
|
||||
`~/.claude/plugins/cache/caveman/`, whose cached tree contains the full source
|
||||
repo (`src/`, `tests/`, `benchmarks/`), because that marketplace declares its
|
||||
plugin with `"source": "./"`.
|
||||
|
||||
So `server/`, `client/`, `mcp/`, `scripts/` and `bin/ccam.js` are all present
|
||||
So `server/`, `client/`, `mcp/`, `scripts/` and `bin/agenthub.js` are all present
|
||||
under `${CLAUDE_PLUGIN_ROOT}` the moment the plugin is installed. No second
|
||||
clone, no credentials beyond the marketplace add itself, no committed copy of
|
||||
the server, no dependency on undocumented cache internals beyond the documented
|
||||
@@ -61,7 +61,7 @@ gains no new top-level directories.
|
||||
`.claude-plugin/marketplace.json` gains one entry:
|
||||
|
||||
```json
|
||||
{ "name": "ccam", "source": ".", "description": "...", "tags": [...] }
|
||||
{ "name": "agenthub", "source": ".", "description": "...", "tags": [...] }
|
||||
```
|
||||
|
||||
### 2. Hooks — inline in `plugin.json`
|
||||
@@ -95,7 +95,7 @@ Runs from the `SessionStart` hook. Never blocks a session.
|
||||
|
||||
Runtime state lives under `~/.claude/agent-dashboard/runtime/`, reusing the
|
||||
existing data-dir convention (`server/lib/claude-home.js:37`, where the SQLite
|
||||
DB already lives) rather than inventing a new directory. `/.ccam/` at the repo
|
||||
DB already lives) rather than inventing a new directory. `/.agenthub/` at the repo
|
||||
root is an unrelated lane profile and must not be confused with it.
|
||||
|
||||
Sequence:
|
||||
@@ -118,13 +118,13 @@ Sequence:
|
||||
fresh version directory, discarding anything installed into the old one.
|
||||
`--ignore-scripts` is required because `scripts/postinstall.js` otherwise
|
||||
runs a full `client/` install, pulling the whole Vite dev toolchain.
|
||||
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries CCAM hook
|
||||
5. **Legacy hook cleanup.** If `~/.claude/settings.json` still carries AgentHub hook
|
||||
entries from a previous `npm run install-hooks`, remove them and print one
|
||||
line. Without this, every event is POSTed twice and token/cost figures
|
||||
double. Detection reuses `isOurEntry()` from `scripts/install-hooks.js`.
|
||||
6. **CLI on PATH.** Symlink `bin/ccam.js` to `~/.local/bin/ccam`. Skills and
|
||||
commands throughout the repo (`ccam stage`, `ship-feature-lane`) shell out
|
||||
to `ccam` and fail without it.
|
||||
6. **CLI on PATH.** Symlink `bin/agenthub.js` to `~/.local/bin/agenthub`. Skills and
|
||||
commands throughout the repo (`agenthub stage`, `ship-feature-lane`) shell out
|
||||
to `agenthub` and fail without it.
|
||||
7. **Start server.** Spawn `server/index.js` detached, `stdio: "ignore"`,
|
||||
`unref()`, with `NODE_PATH` pointed at `runtime/node_modules`. The hook
|
||||
returns immediately; the first install (1–3 minutes) proceeds in the
|
||||
@@ -132,7 +132,7 @@ Sequence:
|
||||
8. **Record state.** Write `runtime/state.json`.
|
||||
|
||||
The client UI bundle is *not* built here. Hooks, the API and MCP do not need
|
||||
it; it is built lazily by `/ccam-open`.
|
||||
it; it is built lazily by `/agenthub-open`.
|
||||
|
||||
### 4. Client dist relocation — `server/index.js`
|
||||
|
||||
@@ -143,7 +143,7 @@ checkout behavior is unchanged.
|
||||
|
||||
The bootstrap always starts the server with `DASHBOARD_CLIENT_DIST` set to
|
||||
`runtime/client-dist/`, whether or not a bundle is there yet — a missing
|
||||
directory serves the API fine and only the UI route 404s. `/ccam-open` builds
|
||||
directory serves the API fine and only the UI route 404s. `/agenthub-open` builds
|
||||
into that same directory, so no server restart is needed to pick the bundle up.
|
||||
|
||||
### 5. MCP — commit `mcp/build/`
|
||||
@@ -152,27 +152,27 @@ Claude Code starts a plugin's MCP servers as the session opens and offers no
|
||||
"not ready yet, retry" state, so an async bootstrap cannot win that race.
|
||||
`mcp/build/` is therefore committed (removed from `.gitignore` and `mcp/.gitignore`)
|
||||
and the root `.mcp.json` points at `${CLAUDE_PLUGIN_ROOT}/mcp/build/index.js`
|
||||
with `CCAM_DASHBOARD_URL` defaulted to `http://localhost:4820`.
|
||||
with `AGENTHUB_DASHBOARD_URL` defaulted to `http://localhost:4820`.
|
||||
|
||||
Cost: the build artifact must stay in sync with `mcp/src`. A pre-commit check
|
||||
fails when `mcp/src` is newer than `mcp/build`.
|
||||
|
||||
The existing `plugins/ccam-dashboard/.mcp.json` keeps its broken relative path
|
||||
The existing `plugins/agenthub-dashboard/.mcp.json` keeps its broken relative path
|
||||
fixed to `${CLAUDE_PLUGIN_ROOT}` form as part of this work, so the subdir plugin
|
||||
is not left in a knowingly broken state.
|
||||
|
||||
### 6. Commands — `plugins/ccam/commands/`
|
||||
### 6. Commands — `plugins/agenthub/commands/`
|
||||
|
||||
The plugin root is the repo root, so `plugin.json` declares an explicit
|
||||
`"commands": "./plugins/ccam/commands"` path. This keeps the new command files
|
||||
`"commands": "./plugins/agenthub/commands"` path. This keeps the new command files
|
||||
inside `plugins/`, next to the ten existing plugins, instead of adding a
|
||||
top-level `commands/` directory to the repo root.
|
||||
|
||||
- `/ccam-doctor` — bootstrap state, Node version, server liveness, DB path,
|
||||
- `/agenthub-doctor` — bootstrap state, Node version, server liveness, DB path,
|
||||
duplicate-hook detection, CLI symlink, MCP build freshness.
|
||||
- `/ccam-update` — refresh dependencies and restart the server after a plugin
|
||||
- `/agenthub-update` — refresh dependencies and restart the server after a plugin
|
||||
update.
|
||||
- `/ccam-open` — lazily build the client bundle if missing, then open the
|
||||
- `/agenthub-open` — lazily build the client bundle if missing, then open the
|
||||
dashboard.
|
||||
|
||||
## Data and lifecycle
|
||||
@@ -196,7 +196,7 @@ deduplicates hook ingest to a single port.
|
||||
by full detachment (`detached`, `stdio: "ignore"`, `unref()`) and an
|
||||
immediate hook return. Verified by timing the hook on a cold `HOME`.
|
||||
- *Duplicate hooks double-count cost.* Events carry no id, so ingest cannot
|
||||
deduplicate. Mitigated by step 5 of the bootstrap plus a `/ccam-doctor` check.
|
||||
deduplicate. Mitigated by step 5 of the bootstrap plus a `/agenthub-doctor` check.
|
||||
- *MCP unavailable at session start.* Resolved by committing `mcp/build/`.
|
||||
|
||||
**Medium severity**
|
||||
@@ -204,8 +204,8 @@ deduplicates hook ingest to a single port.
|
||||
- *Two sessions spawn two servers* → `EADDRINUSE`. Mitigated by the lock, with
|
||||
a stale-lock timeout so a crashed bootstrap does not wedge every later
|
||||
session.
|
||||
- *`ccam` not on PATH* when `~/.local/bin` is absent from the user's PATH.
|
||||
`/ccam-doctor` reports it and prints the line to add.
|
||||
- *`agenthub` not on PATH* when `~/.local/bin` is absent from the user's PATH.
|
||||
`/agenthub-doctor` reports it and prints the line to add.
|
||||
- *Windows.* `npm.cmd` resolution (already handled in `scripts/postinstall.js`
|
||||
via `shell: true`), detached spawn semantics, and `mkdir` locking are the
|
||||
least-tested paths.
|
||||
@@ -222,7 +222,7 @@ deduplicates hook ingest to a single port.
|
||||
## Verification
|
||||
|
||||
1. Install the local marketplace and confirm
|
||||
`~/.claude/plugins/cache/<mp>/ccam/*/server/` exists — this gates everything
|
||||
`~/.claude/plugins/cache/<mp>/agenthub/*/server/` exists — this gates everything
|
||||
else.
|
||||
2. `npm run test:server` for the `DASHBOARD_CLIENT_DIST` override.
|
||||
3. Unit tests for bootstrap: fast path, stale-lock reclaim, Node version gate,
|
||||
|
||||
@@ -54,14 +54,14 @@ sync — they were never coupled to the run mechanism in the first place.
|
||||
|
||||
**In scope:**
|
||||
- tmux session lifecycle (create/attach/resume/kill) per lane, named
|
||||
`ccam-lane-<id>`.
|
||||
`agenthub-lane-<id>`.
|
||||
- A dedicated WebSocket path streaming a real PTY (`node-pty` running
|
||||
`tmux attach-session`) to the browser, rendered with `@xterm/xterm`.
|
||||
- Replacing `RunConsole.tsx` with a `TerminalView.tsx` component.
|
||||
- Reusing (not rebuilding) `RunSetup.tsx`'s cwd/model/permission-mode/effort
|
||||
pickers and `RunHistory.tsx`/`ActiveRunsSwitcher`'s multi-run list, both
|
||||
adjusted to the new data source.
|
||||
- `ccam lanes shell` — CLI convenience to attach a real terminal to the same
|
||||
- `agenthub lanes shell` — CLI convenience to attach a real terminal to the same
|
||||
named tmux session.
|
||||
- `dashboard_runs` migration: drop `mode` (headless/conversation no longer
|
||||
applies — a live pane is always interactive), add `tmux_session`.
|
||||
@@ -81,11 +81,11 @@ sync — they were never coupled to the run mechanism in the first place.
|
||||
|
||||
### Session naming and lifecycle
|
||||
|
||||
One tmux session per lane, name `ccam-lane-<lane.id>` — stable, collision-free
|
||||
One tmux session per lane, name `agenthub-lane-<lane.id>` — stable, collision-free
|
||||
(numeric lane id, not a user-editable slug).
|
||||
|
||||
- **Start:** `tmux has-session -t ccam-lane-<id>` (exit code only, no
|
||||
output). If absent: `tmux new-session -d -s ccam-lane-<id> -c <lane.cwd> --
|
||||
- **Start:** `tmux has-session -t agenthub-lane-<id>` (exit code only, no
|
||||
output). If absent: `tmux new-session -d -s agenthub-lane-<id> -c <lane.cwd> --
|
||||
claude <argv...> [initialPrompt]` — `claude` runs directly as the pane's
|
||||
command (nothing is "typed"). `argv` carries `--model`, `--permission-mode`,
|
||||
`--effort` as today; an optional initial prompt is passed as a trailing
|
||||
@@ -97,14 +97,14 @@ One tmux session per lane, name `ccam-lane-<lane.id>` — stable, collision-free
|
||||
already-live server instead of double-binding; see `server/index.js`'s
|
||||
port-adoption logic) applied to tmux sessions.
|
||||
- **Resume:** identical, `argv` includes `--resume <session_id>`.
|
||||
- **Kill:** `tmux kill-session -t ccam-lane-<id>`. This sends SIGHUP to the
|
||||
- **Kill:** `tmux kill-session -t agenthub-lane-<id>`. This sends SIGHUP to the
|
||||
pane's process group. Whether `claude` treats that as a clean shutdown
|
||||
(firing `SessionEnd`) is unverified — flag for implementation to check; if
|
||||
not, this repo's existing dead-session liveness reap (`server/lib/
|
||||
session-liveness.js`, referenced in `CLAUDE.md`) is the safety net that
|
||||
already exists for exactly this kind of gap, no new code needed.
|
||||
- **List (`GET /api/run`):** no longer a Map read. Runs `tmux list-sessions
|
||||
-F '#{session_name}'`, filters the `ccam-lane-` prefix, and joins against
|
||||
-F '#{session_name}'`, filters the `agenthub-lane-` prefix, and joins against
|
||||
`dashboard_runs` rows (started_at, model, lane_id, …) for display. This
|
||||
makes "which runs are active" a **computed fact from tmux state**, not
|
||||
cached server memory — the same principle this repo already applies to
|
||||
@@ -136,7 +136,7 @@ A second server is added for the PTY stream, same `verifyClient` auth guard
|
||||
(Host allowlist + `DASHBOARD_TOKEN`) reused as-is:
|
||||
|
||||
- Path: `/ws-pty/:runId` (runId = tmux session name, validated against the
|
||||
`ccam-lane-<numeric id>` pattern before any tmux command touches it — this
|
||||
`agenthub-lane-<numeric id>` pattern before any tmux command touches it — this
|
||||
is the trust boundary: without validation, a WS client could name an
|
||||
arbitrary tmux session on the host and attach to something unrelated to
|
||||
this dashboard).
|
||||
@@ -179,13 +179,13 @@ A second server is added for the PTY stream, same `verifyClient` auth guard
|
||||
separate code path — expected to be unaffected, but verify before
|
||||
deleting).
|
||||
|
||||
### `ccam lanes shell`
|
||||
### `agenthub lanes shell`
|
||||
|
||||
`bin/ccam.js`, same dispatch pattern as the other `lanes` subcommands
|
||||
`bin/agenthub.js`, same dispatch pattern as the other `lanes` subcommands
|
||||
(`rest[0] === "shell"` → `cmdLanesShell`). Resolves the lane from `cwd` the
|
||||
same way `ccam stage` does, computes `ccam-lane-<id>`, and `execve`s (replace
|
||||
same way `agenthub stage` does, computes `agenthub-lane-<id>`, and `execve`s (replace
|
||||
the current process image, not a child — so Ctrl-C/signals behave like a
|
||||
normal terminal command) `tmux new-session -A -s ccam-lane-<id> -c
|
||||
normal terminal command) `tmux new-session -A -s agenthub-lane-<id> -c
|
||||
<lane.cwd>`. `-A` creates-or-attaches, so this is the exact same idempotent
|
||||
behavior as clicking Start on the dashboard. The user types `claude`
|
||||
themselves inside — this command's only job is getting them into the right
|
||||
@@ -236,7 +236,7 @@ invocation regardless of transport.
|
||||
`npm run test:server`, `npm run test:client` (including the regenerated
|
||||
snapshot), and a manual click-through: install `tmux` locally, Start a run
|
||||
from Workspace, confirm the real Claude Code TUI renders in the browser,
|
||||
type in the browser and confirm it reaches the pane, run `ccam lanes shell`
|
||||
type in the browser and confirm it reaches the pane, run `agenthub lanes shell`
|
||||
from a real terminal for the same lane and confirm it drops into the exact
|
||||
same live session, kill from the dashboard and confirm the tmux session and
|
||||
row both clear.
|
||||
|
||||
@@ -51,7 +51,7 @@ today.
|
||||
## Persistence
|
||||
|
||||
The chosen layout mode and each pane's selected `laneId` are saved to
|
||||
`localStorage` (e.g. key `ccam.workspace.splitView`) and restored on next
|
||||
`localStorage` (e.g. key `agenthub.workspace.splitView`) and restored on next
|
||||
visit to Workspace. If a persisted lane no longer exists, that pane falls
|
||||
back to unselected (dropdown placeholder).
|
||||
|
||||
|
||||
+18
-18
@@ -1,4 +1,4 @@
|
||||
# `ccam ci status` — unblock Stage 13's CI-wait half
|
||||
# `agenthub ci status` — unblock Stage 13's CI-wait half
|
||||
|
||||
**Status:** approved 2026-08-25.
|
||||
|
||||
@@ -9,10 +9,10 @@ skipped. Two integrations gate it: dev-CI-wait (watch CI on `development`
|
||||
after a PR merges, before QCing the deployed site) and dev-QC (QC the
|
||||
deployed site). Neither exists yet.
|
||||
|
||||
This spec covers only the CI-wait half. `ccam lanes integration ci_wait`
|
||||
This spec covers only the CI-wait half. `agenthub lanes integration ci_wait`
|
||||
already exists (`server/lib/lane-profile.js:isIntegrationEnabled`,
|
||||
`bin/ccam.js:cmdLanesIntegration`) and reports whether a profile *wants* the
|
||||
integration on — but nothing acts on it. `ccam ci` doesn't exist
|
||||
`bin/agenthub.js:cmdLanesIntegration`) and reports whether a profile *wants* the
|
||||
integration on — but nothing acts on it. `agenthub ci` doesn't exist
|
||||
(`skills/ship-feature-lane/SKILL.md:167` notes this explicitly at Stage 10,
|
||||
where the skill falls back to raw `gh pr checks`/`gh run view`).
|
||||
|
||||
@@ -22,21 +22,21 @@ here. Stage 13's dev-QC half stays noted as unavailable until that lands.
|
||||
## Approach
|
||||
|
||||
**A read-only status snapshot, not a wait loop.** Per the project's
|
||||
orchestration doctrine (`docs/LANES.md` § "Orchestration: what CCAM does NOT
|
||||
do" — no chaining, no queue, no retry logic), `ccam ci status` reports a
|
||||
orchestration doctrine (`docs/LANES.md` § "Orchestration: what AgentHub does NOT
|
||||
do" — no chaining, no queue, no retry logic), `agenthub ci status` reports a
|
||||
fact once; it does not poll, block, or retry. The driving skill owns the
|
||||
poll loop and the fix/escalate decision, the same shape Stage 12 already
|
||||
uses for watching a PR (`sleep 300` backgrounded, re-invoked per iteration).
|
||||
|
||||
This also means `ccam ci status` needs no flake classification. Stage 10
|
||||
This also means `agenthub ci status` needs no flake classification. Stage 10
|
||||
already does that triage (infra/flake signature vs. real failure) reading
|
||||
`gh` output directly — Stage 13 reuses the same triage, now reading this
|
||||
command's structured JSON instead of raw `gh`.
|
||||
|
||||
**Server-backed, following the `sync-base` pattern**, not a CLI-local `gh`
|
||||
spawn. Every existing `ccam` command is a thin HTTP client — no command in
|
||||
`bin/ccam.js` spawns a child process directly (`worktree.js`'s `git()`
|
||||
helper is the only child-process boundary CCAM has today, always
|
||||
spawn. Every existing `agenthub` command is a thin HTTP client — no command in
|
||||
`bin/agenthub.js` spawns a child process directly (`worktree.js`'s `git()`
|
||||
helper is the only child-process boundary AgentHub has today, always
|
||||
server-side). A new `server/lib/gh.js` mirrors that helper:
|
||||
|
||||
```js
|
||||
@@ -56,12 +56,12 @@ New route: `GET /api/lanes/:id/ci/status?ref=<branch-or-sha>`
|
||||
yet, not an error).
|
||||
- `gh` missing or not authenticated → `{ error: { message } }`, HTTP 500.
|
||||
|
||||
New CLI: `ccam lanes ci status <branch-or-sha> [<lane-id>]`
|
||||
New CLI: `agenthub lanes ci status <branch-or-sha> [<lane-id>]`
|
||||
|
||||
- GETs the route above, prints the JSON, sets `process.exitCode`.
|
||||
- Resolves the lane the same way every other `ccam lanes <verb>` command
|
||||
- Resolves the lane the same way every other `agenthub lanes <verb>` command
|
||||
does (`resolveLaneArg`), so it works with an implicit lane-from-cwd or an
|
||||
explicit id, matching `ccam lanes integration`.
|
||||
explicit id, matching `agenthub lanes integration`.
|
||||
|
||||
## Response shape
|
||||
|
||||
@@ -97,7 +97,7 @@ note with a poll loop mirroring Stage 12's:
|
||||
|
||||
- Get the merge SHA: `gh pr view <pr_url> --json mergeCommit -q .mergeCommit.oid`.
|
||||
- Loop (`sleep 300`, `run_in_background: true`, bump heartbeat each
|
||||
iteration): `ccam lanes ci status <sha>`.
|
||||
iteration): `agenthub lanes ci status <sha>`.
|
||||
- `pending` → keep waiting.
|
||||
- `failure` → triage like Stage 10 (`gh run view` for job logs on the
|
||||
same run id returned in `checks[].url`), fix + re-enter from Stage 2,
|
||||
@@ -107,14 +107,14 @@ note with a poll loop mirroring Stage 12's:
|
||||
that CI-wait specifically passed.
|
||||
- Update `docs/LANES.md`: the "Stages currently hardcoded skipped" list
|
||||
(Stage 13 becomes "CI-wait works; dev-QC still off"), the
|
||||
`ccam lanes integration` section's claim that `ci_wait`'s consumer isn't
|
||||
built, and add `ccam ci status` to the command reference.
|
||||
`agenthub lanes integration` section's claim that `ci_wait`'s consumer isn't
|
||||
built, and add `agenthub ci status` to the command reference.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No flake auto-classification or auto-rerun inside `ccam ci status` —
|
||||
- No flake auto-classification or auto-rerun inside `agenthub ci status` —
|
||||
stays a dumb fact-reporter; the skill keeps owning that judgment, per
|
||||
CCAM's no-retry-logic rule.
|
||||
AgentHub's no-retry-logic rule.
|
||||
- No change to Stage 10 (PR CI watch) — it keeps using raw `gh` directly.
|
||||
Reusing this command there is a plausible future cleanup, not required
|
||||
now.
|
||||
@@ -14,7 +14,7 @@ same tmux-backed terminal console, same run history.
|
||||
1. Scope: run Codex in the lane's tmux pane. No ingestion of `~/.codex` history.
|
||||
2. The tool belongs to the **lane**, not to the run: one lane = one tool, picked
|
||||
in the "Add lane" modal, not changeable afterwards. One tmux session per lane
|
||||
stays `ccam-lane-<id>`.
|
||||
stays `agenthub-lane-<id>`.
|
||||
3. Codex run controls: model, sandbox, ask-for-approval, reasoning effort.
|
||||
4. Codex runs persist in `dashboard_runs` (new `tool` column) so Active runs and
|
||||
Run history show them next to Claude runs.
|
||||
@@ -97,7 +97,7 @@ Codex does not emit this dashboard's Claude Code hooks, so for a Codex lane:
|
||||
|
||||
- Sessions, Analytics, and cost stay empty — nothing ingests Codex usage.
|
||||
- `server/lib/stage-detect.js` never paints the amber detected badge.
|
||||
- Pipeline stages still work when the agent calls `ccam stage` itself (the repo
|
||||
- Pipeline stages still work when the agent calls `agenthub stage` itself (the repo
|
||||
already ships `AGENTS.md` and `.codex/`).
|
||||
- Lane liveness for Codex lanes comes from the tmux pane plus `dashboard_runs`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user