refactor(lanes): drop the workflow skills and Superpowers link; pipelines go dormant
The plugin stops shipping its workflow layer: - skills/ship-feature and skills/ship-feature-lane, the 18 stage/ship slash commands (plugin.json keeps agenthub-doctor/update/open), and all four agents/ (qc-local, senior-gate-reviewer, backend-reviewer, frontend-reviewer). - CLI `agenthub skills install`, `agenthub lanes agents install`, `agenthub ship-full|ship-quick`; routes POST /api/skills/install and POST /api/lanes/:id/agents/install; the Settings button, the lane Tools menu item and the Add-lane setup step that called them. - The Superpowers auto-install: server/lib/skill-plugins.js, the bootstrap's autoInstallSkillPlugins / skillPluginsSettled gate and the doctor row. Pipelines are kept but dormant: server/data/pipelines/ is deleted, so no template ships. `default` resolves to EMPTY_PIPELINE (no nodes, progress 0), createLane still accepts it, POST /api/lanes/:id/stage answers 400 ENOPIPELINE, and the Workspace hides the stage timer, pickers, PipelineMap and strip stage chip for a lane with no nodes. A template dropped into DASHBOARD_PIPELINES_DIR turns it all back on. The server suite loads its templates from server/__tests__/fixtures/pipelines/. Docs: CLAUDE.md (marker, retired rules), README(.vi), INSTALL, ARCHITECTURE, docs/LANES|PLUGINS|CLI|API, agenthub-introduction.html.
This commit is contained in:
@@ -12,25 +12,7 @@
|
||||
"commands": [
|
||||
"./plugins/agenthub/commands/agenthub-doctor.md",
|
||||
"./plugins/agenthub/commands/agenthub-update.md",
|
||||
"./plugins/agenthub/commands/agenthub-open.md",
|
||||
"./plugins/agenthub/commands/intake.md",
|
||||
"./plugins/agenthub/commands/plan.md",
|
||||
"./plugins/agenthub/commands/implement.md",
|
||||
"./plugins/agenthub/commands/tests.md",
|
||||
"./plugins/agenthub/commands/e2e.md",
|
||||
"./plugins/agenthub/commands/e2e-passed.md",
|
||||
"./plugins/agenthub/commands/review.md",
|
||||
"./plugins/agenthub/commands/qc-plan.md",
|
||||
"./plugins/agenthub/commands/qc.md",
|
||||
"./plugins/agenthub/commands/gate.md",
|
||||
"./plugins/agenthub/commands/publishing.md",
|
||||
"./plugins/agenthub/commands/pr-open.md",
|
||||
"./plugins/agenthub/commands/reported.md",
|
||||
"./plugins/agenthub/commands/watching-pr.md",
|
||||
"./plugins/agenthub/commands/merged.md",
|
||||
"./plugins/agenthub/commands/done.md",
|
||||
"./plugins/agenthub/commands/ship-full.md",
|
||||
"./plugins/agenthub/commands/ship-quick.md"
|
||||
"./plugins/agenthub/commands/agenthub-open.md"
|
||||
],
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
|
||||
+3
-14
@@ -606,7 +606,7 @@ graph LR
|
||||
| `/activity` | ActivityFeed | `GET /api/events?limit=100` — click row to expand inline payload; "Session →" button navigates to `/sessions/:id` |
|
||||
| `/workflows` | Workflows | `GET /api/workflows?status=active\|completed`, `GET /api/workflows/session/:id` + WebSocket auto-refresh (3s debounce) |
|
||||
| `/cc-config` | *(redirect)* | Redirects to `/settings?tool=claude-code` — the config explorer moved under the Settings hub as one of its per-tool panels (see `/settings` below) |
|
||||
| `/run` | Workspace | Merged workspace page combining lanes and runs. Attaches to a tmux-backed pseudoterminal tied to a lane: the UI selects a lane, calls `POST /api/run` with that lane's `id`, and receives a `runId` + tmux session name. The Workspace displays a horizontal lane strip at the top, the selected lane's pipeline map, and a real interactive terminal (xterm.js) fed by `/ws-pty/:runId` binary frames below. `TerminalView` takes the keyboard on mount (`term.focus()`), so typing in a lane reaches tmux without clicking the pane first, and it reports the server's `exit` frame to its owner through `onExit` — `LaneConsolePane` runs `newRun()` there, so a pane that ends (a double Ctrl+C out of `claude`, `exit`, a crash) swaps straight back to the run setup form instead of leaving a dead terminal behind a "New run" button. It re-attaches 1.5 s after any unexpected socket close (server restart included) and stops only on that `exit` frame — a dead socket used to leave the pane frozen on its last frame, looking live while swallowing every keystroke and mouse report. It also calls `preventDefault()` on Shift+Tab (keeping xterm's own handling): xterm emits `ESC[Z` for that combo but, unlike plain Tab, never marks the event cancelled, so the browser's default moved focus out of the terminal and the pane saw only the first press — `shift+tab to cycle` (claude's permission mode) looked dead in the browser while working in a real terminal. It also installs a custom wheel handler that returns `false`: without it, xterm falls back on an alt-screen buffer to translating each wheel notch into a cursor-key press (`ESC[A`/`ESC[B`), which a `claude` pane reads as arrow up/down and uses to walk the prompt history instead of scrolling. That handler suppresses only the emulation branch — real mouse reports are emitted by a separate listener xterm registers when the pane's program enables mouse tracking. Its xterm viewport scrollbar is hidden by design (`.xterm-viewport` rule in `client/src/index.css`): tmux repaints the whole pane, so xterm's scrollback is always empty and scrolling happens via mouse reports forwarded to the pane's program, not the DOM. Pre-flight: `GET /api/run/{tmux,binary,cwds,files}` for tmux availability + `claude` binary check + `@`-file autocomplete. Start/resume: `POST /api/run` (requires `laneId`; optionally accepts `prompt` to send immediately); `GET /api/run/:id` (returns handle); `DELETE /api/run/:id` (stops). History: `GET /api/run/history?laneId=<n>` lists only that lane's runs. PTY streaming: `/ws-pty/:runId` delivers raw PTY frames as binary WebSocket frames — no JSON envelope overhead, direct to xterm.js for live rendering; the same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub lanes shell` CLI, other tools), all synced live. Lane self-heal: `GET /api/lanes/:id` auto-corrects `run_id`/`status` if the tmux session has been killed externally. Tier 1 TUI parity: tmux session is a real shell, not headless — supports editors, pagers, interactive subcommands. **The console never writes a lane's stage** — stage moves only through `agenthub stage` commands. Live / Offline indicator next to the title |
|
||||
| `/run` | Workspace | Merged workspace page combining lanes and runs. Attaches to a tmux-backed pseudoterminal tied to a lane: the UI selects a lane, calls `POST /api/run` with that lane's `id`, and receives a `runId` + tmux session name. The Workspace displays a horizontal lane strip at the top, the selected lane's pipeline map (only while a pipeline template is installed — none ships), and a real interactive terminal (xterm.js) fed by `/ws-pty/:runId` binary frames below. `TerminalView` takes the keyboard on mount (`term.focus()`), so typing in a lane reaches tmux without clicking the pane first, and it reports the server's `exit` frame to its owner through `onExit` — `LaneConsolePane` runs `newRun()` there, so a pane that ends (a double Ctrl+C out of `claude`, `exit`, a crash) swaps straight back to the run setup form instead of leaving a dead terminal behind a "New run" button. It re-attaches 1.5 s after any unexpected socket close (server restart included) and stops only on that `exit` frame — a dead socket used to leave the pane frozen on its last frame, looking live while swallowing every keystroke and mouse report. It also calls `preventDefault()` on Shift+Tab (keeping xterm's own handling): xterm emits `ESC[Z` for that combo but, unlike plain Tab, never marks the event cancelled, so the browser's default moved focus out of the terminal and the pane saw only the first press — `shift+tab to cycle` (claude's permission mode) looked dead in the browser while working in a real terminal. It also installs a custom wheel handler that returns `false`: without it, xterm falls back on an alt-screen buffer to translating each wheel notch into a cursor-key press (`ESC[A`/`ESC[B`), which a `claude` pane reads as arrow up/down and uses to walk the prompt history instead of scrolling. That handler suppresses only the emulation branch — real mouse reports are emitted by a separate listener xterm registers when the pane's program enables mouse tracking. Its xterm viewport scrollbar is hidden by design (`.xterm-viewport` rule in `client/src/index.css`): tmux repaints the whole pane, so xterm's scrollback is always empty and scrolling happens via mouse reports forwarded to the pane's program, not the DOM. Pre-flight: `GET /api/run/{tmux,binary,cwds,files}` for tmux availability + `claude` binary check + `@`-file autocomplete. Start/resume: `POST /api/run` (requires `laneId`; optionally accepts `prompt` to send immediately); `GET /api/run/:id` (returns handle); `DELETE /api/run/:id` (stops). History: `GET /api/run/history?laneId=<n>` lists only that lane's runs. PTY streaming: `/ws-pty/:runId` delivers raw PTY frames as binary WebSocket frames — no JSON envelope overhead, direct to xterm.js for live rendering; the same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub lanes shell` CLI, other tools), all synced live. Lane self-heal: `GET /api/lanes/:id` auto-corrects `run_id`/`status` if the tmux session has been killed externally. Tier 1 TUI parity: tmux session is a real shell, not headless — supports editors, pagers, interactive subcommands. **The console never writes a lane's stage** — stage moves only through `agenthub stage` commands. Live / Offline indicator next to the title |
|
||||
| `/settings` | SettingsHub | Tabbed shell (`?tool=` query param picks the active tab, `PageTabs`) hosting the dashboard's own settings plus one configuration panel per AI tool. **dashboard** tab (default, no `?tool=`) → `Settings`: an **Appearance** section first (mode + named theme, `localStorage` only), `GET /api/settings/info`, `GET /api/pricing`, `GET /api/pricing/cost` + `localStorage` for notification prefs; hosts the **Remote Data Sources** panel (`components/RemoteSources.tsx`) — CRUD + test + sync over `/api/remote-sources`, live status from `remote_source.status` WS messages. **claude-code** tab (`?tool=claude-code`) → `CcConfig`: 12-tab Claude Code configuration explorer. Reads via `GET /api/cc-config/{overview,skills,agents,commands,output-styles,plugins,marketplaces,mcp,hooks,hook-scripts,keybindings,statusline,settings,memory}`. Mutations for skills/agents/commands/output-styles/memory — including the per-project file-based auto-memory store (`*.md` under `~/.claude/projects/<slug>/memory/`, grouped by project and searchable in the Memory tab, with clickable `MEMORY.md` index links that scroll to + highlight the matching fact file) — via `PUT /api/cc-config/file` + `DELETE /api/cc-config/file` (timestamped backups, atomic writes). The Keybindings tab additionally offers a structured inline editor that persists via `PUT /api/cc-config/keybindings` (same backup-first, atomic-write guarantees). `GET /api/cc-config/file?path=…` for single-file viewer. `GET /api/cc-config/backups` for the recovery modal. A **Switch profile** button left of Backups opens the auth-profile modal (`/api/cc-config/profiles*`): save the logged-in Anthropic account, rename it by clicking its name, then switch between accounts — only the login is swapped, so skills / subagents / memory / plugins / marketplaces / settings stay put. There is no name field (the server derives the identifier) and the save button shows only while the live account is unsaved, since one account is one profile. It cannot log in for you (`claude auth login` does that) and a switch applies to new sessions only. Subscribes to `cc_config_changed` WS messages for live refresh on both dashboard mutations and external file edits picked up by `cc-watcher`. The Settings tab (within CcConfig) leads with a client-side **Current configuration** summary that resolves the `/config` options (model, verbose, theme, output style, effort, auto-compact, notifications, …) across user / project / project-local scopes, showing defaults when unset. **codex** tab (`?tool=codex`) → `CodexConfig`: the Codex CLI counterpart — 8 tabs (overview, skills, agents, rules, memory, config, mcp, plugins; marketplaces are folded into the plugins tab rather than given their own, since the CLI treats them as one small add/remove surface). Codex's scope union is `"user"` \| `"project"` only — no `"project-local"`, no per-project file-based memory. Reads/mutates via `GET/PUT/DELETE /api/codex-config/*` (skills/agents/rules/memory/config are file-backed with the same backup-first write path as `cc-config`; mcp/plugins/marketplaces are CLI-owned add/remove only, no artifact editor). Subscribes to `codex_config_changed` WS messages. Both config panels share a tool-agnostic component set under `client/src/components/config/` (`EditorModal`, `ConfirmDeleteModal`, `FileViewer`, `ToastNotice`, `MdItemList`, `ScopeBadge`, `configPrimitives`) generic over an artifact-type parameter, so the two tools' editors stay visually and behaviorally consistent without duplicating the modal code. Every tab shows the Live / Offline indicator next to the page title |
|
||||
| `/*` | NotFound | None (static 404 page) |
|
||||
|
||||
@@ -1107,14 +1107,6 @@ launcher, and the detached server spawn with `NODE_PATH` and
|
||||
the plugin cache, which Claude Code garbage-collects and replaces on every
|
||||
update.
|
||||
|
||||
Last in that worker — deliberately, because it clones from the network and
|
||||
nothing above it may wait — it installs the plugins the pipeline templates drive
|
||||
(`superpowers`) via `server/lib/skill-plugins.js`, the same module
|
||||
`/agenthub-doctor`'s `Skill plugins` row reports from. The report is derived from
|
||||
the templates; the install consults a fixed allowlist in that file instead,
|
||||
because a template is user-writable. Full behavior:
|
||||
[`docs/PLUGINS.md`](docs/PLUGINS.md).
|
||||
|
||||
---
|
||||
|
||||
## Import Pipeline
|
||||
@@ -1587,12 +1579,9 @@ graph TD
|
||||
- `.claude/rules/` adds path-scoped guidance by file area.
|
||||
- `skills/` provides reusable workflows, shipped as the `agenthub` plugin's own skills (`/agenthub:<name>`, bare `/<name>` alias):
|
||||
- onboarding
|
||||
- feature shipping
|
||||
- live issue debugging
|
||||
- `agents/` provides specialized review workers (shipped as the `agenthub` plugin's agents):
|
||||
- backend reviewer
|
||||
- frontend reviewer
|
||||
- qc-local, senior-gate-reviewer (the ship-feature-lane pipeline's QC and publish gate)
|
||||
- project-doc updates
|
||||
- file headers
|
||||
- `plugins/` provides distributable plugin marketplace (see [Plugin Marketplace](#plugin-marketplace)):
|
||||
- agenthub-analytics (session reports, cost analysis, usage trends, productivity scoring)
|
||||
- agenthub-productivity (standups, weekly reports, sprint summaries, workflow optimization)
|
||||
|
||||
+1
-3
@@ -33,9 +33,8 @@ dashboard, run it from source, or stay on Node 20.
|
||||
| Git | Any | Cloning the repository, and every lane operation (worktrees, branch/status reads) |
|
||||
| **tmux** | 3.x | **Required for the run console** — the browser terminal, `agenthub lanes shell`, and typing `/reload-plugins` into a live pane. See [Step 0](#step-0--system-packages) |
|
||||
| Codex CLI | Any | Optional — only for lanes whose tool is `codex` |
|
||||
| GitHub CLI (`gh`) | Any | Optional — only for `agenthub lanes ci status` and the PR stages of `/agenthub:ship-full` |
|
||||
| GitHub CLI (`gh`) | Any | Optional — only for `agenthub lanes ci status` |
|
||||
| Python | 3.6+ | Optional — only for `scripts/expand-ts-module-docs.py` (doc generation) |
|
||||
| `superpowers` plugin | Any | Optional — required only by `/agenthub:ship-feature` and `/agenthub:ship-feature-lane`, which drive its skills step by step (and which the built-in pipeline templates name as their per-node commands). Nothing else in AgentHub needs it; `/agenthub-doctor` reports it as `WARN` when absent. **Installed automatically** by the plugin's `SessionStart` bootstrap (opt out with `AGENTHUB_AUTO_INSTALL_PLUGINS=0`); by hand: `claude plugin install superpowers@claude-plugins-official` |
|
||||
|
||||
---
|
||||
|
||||
@@ -319,7 +318,6 @@ This repository includes extension packs for both Claude Code and Codex.
|
||||
- `CLAUDE.md`
|
||||
- `.claude/rules/`
|
||||
- `skills/` (shipped as the `agenthub` plugin's skills)
|
||||
- `agents/` (shipped as the `agenthub` plugin's agents)
|
||||
- Codex project packs live under `.codex/`:
|
||||
- `AGENTS.md`
|
||||
- `.codex/rules/`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Internal SmartGift build. Local-first **control surface** for Claude Code agents —
|
||||
monitoring is one part of it, not the whole. Hooks POST every tool call to an
|
||||
Express + SQLite server and a React UI updates over WebSocket; **lanes** track
|
||||
parallel agent work through a pipeline; and the `agenthub` CLI drives real terminal
|
||||
parallel agent work, one working directory each; and the `agenthub` CLI drives real terminal
|
||||
sessions, lane runtimes, and Claude Code's own configuration.
|
||||
|
||||
AgentHub records and shows — it does not orchestrate. The session in control makes
|
||||
@@ -31,11 +31,13 @@ Internal build — all rights reserved.
|
||||
|
||||
**Drive**
|
||||
|
||||
- **Lanes.** One lane per working directory, surviving session restarts. A lane
|
||||
moves through pipeline stages and the dashboard shows where it is.
|
||||
- **Stage detection.** The stage is inferred from the tool stream, so a session
|
||||
that never calls `agenthub stage` still shows progress — rendered dashed amber and
|
||||
never as done, because an inference is not evidence.
|
||||
- **Lanes.** One lane per working directory, surviving session restarts. Once you
|
||||
install a pipeline template (none ships), a lane moves through its stages and
|
||||
the dashboard shows where it is.
|
||||
- **Stage detection.** With a template installed, the stage is inferred from the
|
||||
tool stream, so a session that never calls `agenthub stage` still shows
|
||||
progress — rendered dashed amber and never as done, because an inference is
|
||||
not evidence.
|
||||
- **Run Claude Code or Codex 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
|
||||
@@ -51,8 +53,6 @@ Internal build — all rights reserved.
|
||||
merge/rebase/reset button by design: recovery from those needs a terminal.
|
||||
- **Per-lane app stacks.** A repo that declares `.agenthub/profile/` gets its own
|
||||
slot, ports and services per lane (`agenthub lanes up|down|logs`).
|
||||
- **Ship pipelines.** `/agenthub:ship-full` and `/agenthub:ship-quick` drive a
|
||||
feature from intake to PR, declaring every stage to the dashboard as it goes.
|
||||
|
||||
**Configure**
|
||||
|
||||
@@ -81,7 +81,7 @@ Internal build — all rights reserved.
|
||||
| **tmux** | 3.x | The browser run console, `agenthub lanes shell`, typing `/reload-plugins` into a live pane |
|
||||
| **Claude Code** | 2.x | Hook ingestion — the whole monitoring side |
|
||||
| **Codex CLI** | any | Optional — only for `codex` lanes |
|
||||
| **GitHub CLI (`gh`)** | any | Optional — only for `agenthub lanes ci status` and the PR stages of the ship pipelines |
|
||||
| **GitHub CLI (`gh`)** | any | Optional — only for `agenthub lanes ci status` |
|
||||
| **systemd (user)** | — | Optional — only for `agenthub service install` (Linux auto-start on boot) |
|
||||
|
||||
Node **25** is the one version to avoid: it breaks 6 server tests through a
|
||||
@@ -122,34 +122,6 @@ terminal and a local `tmux attach -t agenthub-lane-<id>` (or `agenthub lanes
|
||||
shell`) are two clients on the same pane — they stay in sync, and killing the
|
||||
dashboard never kills the run.
|
||||
|
||||
### The superpowers plugin
|
||||
|
||||
The **`superpowers` plugin is required by the ship-feature pipelines only**, not
|
||||
by the dashboard. `/agenthub:ship-feature` and `/agenthub:ship-feature-lane` drive
|
||||
`superpowers:brainstorming`, `writing-plans`, `test-driven-development`,
|
||||
`requesting-code-review` and `verification-before-completion` phase by phase,
|
||||
and both built-in pipeline templates name those same slash commands as their
|
||||
per-node drivers. Without it the dashboard, hooks and CLI all work
|
||||
normally — the two pipeline skills are what stop partway. `/agenthub-doctor` reports
|
||||
it as a `WARN` row.
|
||||
|
||||
**You do not normally have to install it.** The plugin's `SessionStart`
|
||||
bootstrap installs it for you, once, in its detached worker — so a session never
|
||||
waits on the clone, and the current session needs `/reload-plugins` before the
|
||||
skills resolve (any later session picks them up on its own). The outcome is
|
||||
logged to `~/.claude/agenthub/runtime/bootstrap.log`, and `/agenthub-update`
|
||||
retries a failed one. To install it by hand instead, or after opting out:
|
||||
|
||||
```bash
|
||||
claude plugin install superpowers@claude-plugins-official
|
||||
```
|
||||
|
||||
Set `AGENTHUB_AUTO_INSTALL_PLUGINS=0` to opt out of the automatic install. Only the
|
||||
plugins on a fixed allowlist in `server/lib/skill-plugins.js` are ever installed
|
||||
this way — `superpowers` today. A custom pipeline template naming some other
|
||||
plugin's command is *reported* by `/agenthub-doctor` and never installed, because a
|
||||
template is a file anyone can drop into `DASHBOARD_PIPELINES_DIR`.
|
||||
|
||||
## Install as a Claude Code plugin
|
||||
|
||||
Two commands on a machine that has nothing but Claude Code, no clone and no
|
||||
@@ -224,7 +196,6 @@ agenthub sessions # recent sessions
|
||||
agenthub lanes # lanes with stage and progress
|
||||
agenthub lanes pipeline # this lane's pipeline template, or switch it
|
||||
agenthub lanes node <id> # run ONE pipeline node's own command (no id: list them)
|
||||
# (in a session: /intake, /qc, /ship-full, /ship-quick …)
|
||||
agenthub stage <name> # declare the current lane's stage
|
||||
agenthub tail # live event feed
|
||||
```
|
||||
@@ -259,6 +230,12 @@ running/idle status from Codex's own rollout journals on disk
|
||||
(`$CODEX_HOME/sessions/**/rollout-*.jsonl`, polled every
|
||||
`DASHBOARD_CODEX_POLL_MS` ms, default 15000).
|
||||
|
||||
**Pipelines are dormant.** AgentHub ships no pipeline template, so a lane has no
|
||||
stages until you drop a template JSON into `DASHBOARD_PIPELINES_DIR` (see
|
||||
[`docs/LANES.md`](docs/LANES.md#custom-pipeline-templates)); until then
|
||||
`agenthub stage` answers `ENOPIPELINE` and the Workspace hides the pipeline map.
|
||||
Further development is planned.
|
||||
|
||||
The declared stage comes from `agenthub stage`. The inferred stage comes from tool
|
||||
events and expires after `DETECTION_TTL_MS` (default 5 minutes), so a lane can
|
||||
move backwards between work sessions. Detection never writes the declared stage,
|
||||
|
||||
+15
-26
@@ -3,7 +3,7 @@
|
||||
Bản build nội bộ SmartGift. **Bàn điều khiển** local-first cho agent Claude Code —
|
||||
monitoring chỉ là một phần, không phải tất cả. Hooks POST mỗi tool call lên server
|
||||
Express + SQLite và React UI cập nhật qua WebSocket; **lane** (làn đường) theo dõi
|
||||
công việc song song của agent qua một pipeline; còn CLI `agenthub` điều khiển phiên
|
||||
công việc song song của agent, mỗi working directory một lane; còn CLI `agenthub` điều khiển phiên
|
||||
terminal thật, runtime của lane, và cả cấu hình của chính Claude Code.
|
||||
|
||||
AgentHub ghi nhận và trình bày — nó không điều phối. Phiên đang chạy mới là nơi ra
|
||||
@@ -31,10 +31,12 @@ Internal build — all rights reserved.
|
||||
|
||||
**Điều khiển**
|
||||
|
||||
- **Lane.** Mỗi working directory một lane, sống sót qua session restart. Lane
|
||||
đi qua các pipeline stage và dashboard hiển thị nó đang ở đâu.
|
||||
- **Stage detection.** Stage được suy luận (infer) từ tool stream, nên một
|
||||
session không bao giờ gọi `agenthub stage` vẫn hiển thị progress — render dạng
|
||||
- **Lane.** Mỗi working directory một lane, sống sót qua session restart. Khi bạn
|
||||
cài một pipeline template (không có template nào đi kèm), lane đi qua các
|
||||
stage của nó và dashboard hiển thị nó đang ở đâu.
|
||||
- **Stage detection.** Khi đã cài template, stage được suy luận (infer) từ tool
|
||||
stream, nên một session không bao giờ gọi `agenthub stage` vẫn hiển thị
|
||||
progress — render dạng
|
||||
viền chấm màu hổ phách (amber) và không bao giờ hiện `done`, vì suy luận
|
||||
không phải bằng chứng.
|
||||
- **Chạy Claude Code hoặc Codex từ trình duyệt.** Một terminal thật (tmux + PTY
|
||||
@@ -44,8 +46,6 @@ Internal build — all rights reserved.
|
||||
chính là phần cần cài `tmux`** — xem [Yêu cầu hệ thống](#yêu-cầu-hệ-thống).
|
||||
- **App stack riêng cho từng lane.** Repo khai báo `.agenthub/profile/` thì mỗi
|
||||
lane được cấp slot, port và service riêng (`agenthub lanes up|down|logs`).
|
||||
- **Ship pipeline.** `/agenthub:ship-full` và `/agenthub:ship-quick` đưa một
|
||||
feature từ intake tới PR, khai báo từng stage về dashboard trên đường đi.
|
||||
|
||||
**Cấu hình**
|
||||
|
||||
@@ -70,7 +70,7 @@ Internal build — all rights reserved.
|
||||
| **tmux** | 3.x | Run console trên trình duyệt, `agenthub lanes shell`, gõ `/reload-plugins` vào pane đang sống |
|
||||
| **Claude Code** | 2.x | Hook ingestion — toàn bộ phần monitoring |
|
||||
| **Codex CLI** | bất kỳ | Tùy chọn — chỉ cho lane dùng `codex` |
|
||||
| **GitHub CLI (`gh`)** | bất kỳ | Tùy chọn — chỉ cho `agenthub lanes ci status` và các stage PR của ship pipeline |
|
||||
| **GitHub CLI (`gh`)** | bất kỳ | Tùy chọn — chỉ cho `agenthub lanes ci status` |
|
||||
| **systemd (user)** | — | Tùy chọn — chỉ cho `agenthub service install` (tự khởi động khi boot, Linux) |
|
||||
|
||||
Node **25** là bản duy nhất nên tránh: nó làm gãy 6 test server do lệch ABI
|
||||
@@ -110,23 +110,6 @@ trên trình duyệt và `tmux attach -t agenthub-lane-<id>` (hoặc `agenthub l
|
||||
shell`) ở máy là hai client trên cùng một pane — chúng đồng bộ với nhau, và tắt
|
||||
dashboard không bao giờ giết run đang chạy. Detach bằng `Ctrl-b d`.
|
||||
|
||||
### Plugin superpowers
|
||||
|
||||
Plugin **`superpowers` chỉ cần cho ship-feature pipeline**, không cần cho
|
||||
dashboard. `/agenthub:ship-feature` và `/agenthub:ship-feature-lane` lái các skill
|
||||
của nó theo từng phase. Thiếu nó thì dashboard, hook và CLI vẫn chạy bình thường
|
||||
— chỉ hai skill pipeline là dừng giữa chừng; `/agenthub-doctor` báo dòng `WARN`.
|
||||
|
||||
**Bình thường bạn không phải tự cài.** Bootstrap `SessionStart` của plugin cài
|
||||
giúp một lần trong worker chạy nền, và session hiện tại cần `/reload-plugins`
|
||||
thì skill mới resolve được (session sau tự nhận). Cài tay:
|
||||
|
||||
```bash
|
||||
claude plugin install superpowers@claude-plugins-official
|
||||
```
|
||||
|
||||
Đặt `AGENTHUB_AUTO_INSTALL_PLUGINS=0` để tắt cài tự động.
|
||||
|
||||
## Cài như Claude Code plugin
|
||||
|
||||
Hai lệnh, trên máy chỉ có Claude Code, không cần clone repo, không cần
|
||||
@@ -200,7 +183,6 @@ agenthub sessions # session gần đây
|
||||
agenthub lanes # danh sách lane kèm stage và progress
|
||||
agenthub lanes pipeline # pipeline template của lane này, hoặc đổi nó
|
||||
agenthub lanes node <id> # chạy riêng command của MỘT node (không có id: liệt kê)
|
||||
# (trong session: /intake, /qc, /ship-full, /ship-quick …)
|
||||
agenthub stage <name> # khai báo stage hiện tại của lane
|
||||
agenthub tail # xem live event feed
|
||||
```
|
||||
@@ -233,6 +215,13 @@ suy luận hay tự nhảy stage; dashboard chỉ phản ánh trạng thái runn
|
||||
rollout journal của chính Codex trên đĩa (`$CODEX_HOME/sessions/**/rollout-*.jsonl`,
|
||||
poll mỗi `DASHBOARD_CODEX_POLL_MS` ms, mặc định 15000).
|
||||
|
||||
**Pipeline đang tạm ngưng (dormant).** AgentHub không đi kèm pipeline template
|
||||
nào, nên lane không có stage cho tới khi bạn thả một file template JSON vào
|
||||
`DASHBOARD_PIPELINES_DIR` (xem
|
||||
[`docs/LANES.md`](docs/LANES.md#custom-pipeline-templates)); trước đó
|
||||
`agenthub stage` trả về `ENOPIPELINE` và trang Workspace ẩn pipeline map. Phần
|
||||
này sẽ được phát triển tiếp.
|
||||
|
||||
Stage khai báo (declared) đến từ `agenthub stage`. Stage suy luận (inferred) đến
|
||||
từ tool event và hết hạn sau `DETECTION_TTL_MS` (mặc định 5 phút), nên lane có
|
||||
thể lùi lại giữa các work session. Detection không bao giờ ghi đè stage khai
|
||||
|
||||
+11
-76
@@ -888,8 +888,10 @@
|
||||
<div class="d">Skill, plugin, MCP, hook, memory cho cả Claude Code lẫn Codex</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="t">+ Quy trình ship</div>
|
||||
<div class="d">Gõ một lệnh, chạy từ lúc lên plan tới lúc mở Pull Request</div>
|
||||
<div class="t">+ Pipeline (tạm ngưng)</div>
|
||||
<div class="d">
|
||||
Chưa có template đi kèm - cài template vào DASHBOARD_PIPELINES_DIR để bật lại
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -923,9 +925,7 @@
|
||||
alt="Tạo lane"
|
||||
/>
|
||||
</div>
|
||||
<div class="cap">
|
||||
Tạo lane: Repo hay Worktree · Claude hay Codex · mẫu pipeline · AI Router
|
||||
</div>
|
||||
<div class="cap">Tạo lane: Repo hay Worktree · Claude hay Codex · AI Router</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -941,10 +941,15 @@
|
||||
<section
|
||||
class="slide"
|
||||
data-t="1 · Lane - pipeline 5 màu"
|
||||
data-n="Tiếp theo là pipeline - nó giúp chúng ta biết được giai đoạn hiện tại mà AI đang làm trên dự án là gì. Có 5 màu: xám là chưa làm đến, xanh dương là đang làm, xanh lá là đã xong giai đoạn và có evidence lưu lại, vàng cam nét đứt là AgentHub tự động detect, và đỏ là chạy thất bại hoặc có lỗi gì đó gây đứng. Đây là slide quan trọng nhất cả bài nên em nói chậm. Có ĐÚNG HAI cách một stage đổi màu. Một là chính session AI tự khai báo bằng lệnh agenthub stage, kèm evidence - đây là cách duy nhất ra được màu xanh lá. Hai là AgentHub tự suy đoán bằng cách đọc luồng lệnh mà AI gọi: thấy npm test thì đoán đang ở stage Tests, thấy gh pr create thì đoán đang ở stage Ship. Nhưng suy đoán chỉ cho ra màu vàng cam, vì AgentHub thấy AI GỌI lệnh chứ không thấy KẾT QUẢ của lệnh đó - thấy nó chạy test không có nghĩa test đã pass. Ba luật kèm theo: suy đoán không bao giờ đè lên khai báo; node suy đoán không bao giờ thành xanh lá; và suy đoán hết hạn sau 5 phút nên lane lùi lại được giữa hai buổi làm. Nói với QC: màu vàng cam chính là chỗ cần soi."
|
||||
data-n="Tiếp theo là pipeline - nó giúp chúng ta biết được giai đoạn hiện tại mà AI đang làm trên dự án là gì. Có 5 màu: xám là chưa làm đến, xanh dương là đang làm, xanh lá là đã xong giai đoạn và có evidence lưu lại, vàng cam nét đứt là AgentHub tự động detect, và đỏ là chạy thất bại hoặc có lỗi gì đó gây đứng. Đây là slide quan trọng nhất cả bài nên em nói chậm. Có ĐÚNG HAI cách một stage đổi màu. Một là chính session AI tự khai báo bằng lệnh agenthub stage, kèm evidence - đây là cách duy nhất ra được màu xanh lá. Hai là AgentHub tự suy đoán bằng cách đọc luồng lệnh mà AI gọi: thấy npm test thì đoán đang ở stage Tests, thấy gh pr create thì đoán đang ở stage Ship. Nhưng suy đoán chỉ cho ra màu vàng cam, vì AgentHub thấy AI GỌI lệnh chứ không thấy KẾT QUẢ của lệnh đó - thấy nó chạy test không có nghĩa test đã pass. Ba luật kèm theo: suy đoán không bao giờ đè lên khai báo; node suy đoán không bao giờ thành xanh lá; và suy đoán hết hạn sau 5 phút nên lane lùi lại được giữa hai buổi làm. Nói với QC: màu vàng cam chính là chỗ cần soi. Lưu ý: hiện tại pipeline đang tạm ngưng - AgentHub không đi kèm template nào, phải cài template vào DASHBOARD_PIPELINES_DIR thì stage mới hiện lại."
|
||||
>
|
||||
<div class="kicker">Chức năng 1 · Lane</div>
|
||||
<h2>Pipeline - thấy tiến độ bằng màu</h2>
|
||||
<div class="note w" style="margin: 0 0 14px; padding: 10px 16px">
|
||||
Hiện tại pipeline đang <b>tạm ngưng</b>: AgentHub không đi kèm pipeline template nào. Thả
|
||||
một file template vào <span class="mono">DASHBOARD_PIPELINES_DIR</span> thì stage và các
|
||||
màu dưới đây hoạt động lại. Phần này sẽ được phát triển tiếp.
|
||||
</div>
|
||||
<div class="pipe full">
|
||||
<span class="node d">Lấy thông tin</span><span class="arr">→</span>
|
||||
<span class="node d">Lên plan</span><span class="arr">→</span>
|
||||
@@ -1330,76 +1335,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 13 -->
|
||||
<section
|
||||
class="slide"
|
||||
data-t="Quy trình ship tính năng"
|
||||
data-n="Ngoài các chức năng chính trên, AgentHub còn đi kèm nhiều tiện ích và skill có sẵn, trong đó nổi bật nhất là các workflow lập trình. Ví dụ quy trình hiện tại: đầu tiên là trao đổi, brainstorming và lên plan trước khi bắt đầu; sau đó viết ra các testcase để lát nữa có cái mà đánh giá kết quả output; code xong thì có code review, bằng nhiều agent chuyên biệt như frontend, backend và một reviewer có context trống. Điểm cần nói: đây không phải AgentHub tự nghĩ ra quy trình - nó dùng lại bộ skill chuẩn của Superpowers, chỉ thêm việc khai báo tiến độ về dashboard ở từng stage. Nói kỹ hai card giữa. Card 3: agent soát code được dispatch với CONTEXT TRỐNG - nó chỉ nhận phần code thay đổi và yêu cầu ban đầu, nên nó không thể bị thuyết phục bằng câu chữ, chỉ chấm trên code thật. Card 4: đây không phải skill review, nó là cổng kiểm soát lời tuyên bố - chưa chạy lại lệnh kiểm tra trong chính lượt đó thì không được nói là pass, và cấm luôn việc tin vào báo cáo thành công của agent con. Chốt cho đúng: agent review trả về CLEAN hoặc FINDINGS, và FINDINGS không tự động chặn - luật là xem xét cái nào đáng sửa thì sửa. Thứ thật sự chặn nằm ở bước cuối: AI không bao giờ tự commit hay tự mở PR, phải chính người dùng yêu cầu trong lượt đó. Ai muốn quy trình khác thì thêm skill và bổ sung vào dashboard."
|
||||
>
|
||||
<div class="kicker">Quy trình ship</div>
|
||||
<h2>Quy trình ship tính năng có sẵn</h2>
|
||||
<div class="sub" style="margin: 0 0 26px">
|
||||
Một lệnh <span class="mono hl">/agenthub:ship-feature</span> lái lần lượt 5 skill chuẩn,
|
||||
và khai báo tiến độ về dashboard ở từng stage.
|
||||
</div>
|
||||
|
||||
<div class="grid3">
|
||||
<div class="card acc">
|
||||
<div class="t">1 · writing-plans</div>
|
||||
<div class="d">Trao đổi, brainstorming và lên plan trước khi bắt đầu</div>
|
||||
</div>
|
||||
<div class="card acc">
|
||||
<div class="t">2 · test-driven-development</div>
|
||||
<div class="d">Viết testcase trước để lát sau có cái đánh giá kết quả output</div>
|
||||
</div>
|
||||
<div class="card acc">
|
||||
<div class="t">3 · requesting-code-review</div>
|
||||
<div class="d">
|
||||
Nhiều agent chuyên biệt review code: frontend · backend · reviewer
|
||||
<b>context trống</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card acc">
|
||||
<div class="t">4 · verification-before-completion</div>
|
||||
<div class="d">Cấm báo "xong" nếu chưa chạy lại lệnh kiểm tra và đọc kết quả thật</div>
|
||||
</div>
|
||||
<div class="card acc">
|
||||
<div class="t">5 · finishing-a-development-branch</div>
|
||||
<div class="d">Tạo commit, mở Pull Request</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="t">+ Khả năng mở rộng</div>
|
||||
<div class="d">
|
||||
Team cần quy trình khác thì thêm skill và bổ sung vào dashboard - còn một số tính năng
|
||||
nữa em sẽ bổ sung dần theo feedback của anh chị
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cols" style="margin-top: 26px">
|
||||
<ul>
|
||||
<li><b>3 agent review chạy song song</b>: reviewer chung · backend · frontend</li>
|
||||
<li>
|
||||
Mỗi bước kiểm tra do một agent <b>context trống</b> chấm, và phải trả về
|
||||
<b>một dòng kết luận rõ ràng</b>, không được nói chung chung:
|
||||
<div style="font-size: 0.9em; line-height: 1.7; margin-top: 8px; color: var(--mut)">
|
||||
• Chạy test → <b style="color: var(--ok)">pass</b> (thêm mấy test) hay
|
||||
<b style="color: var(--bad)">fail</b> (fail test nào, sai chỗ nào)<br />• Code
|
||||
review → <b style="color: var(--ok)">clean</b> hay
|
||||
<b style="color: var(--warn)">có mấy điểm cần sửa</b><br />• Chốt cuối →
|
||||
<b style="color: var(--ok)">cho đi tiếp</b> hay
|
||||
<b style="color: var(--bad)">chặn lại</b> (còn thiếu gì)
|
||||
</div>
|
||||
</li>
|
||||
<li>Trước khi ship, bắt buộc chạy <b>cập nhật tài liệu dự án</b></li>
|
||||
</ul>
|
||||
<div class="note w" style="margin: 0">
|
||||
⚖ <b>AI không bao giờ tự commit hay tự mở Pull Request.</b><br /><br />Dù mọi bước
|
||||
đã xong xuôi, phải chính người dùng yêu cầu trong lượt đó.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 14 -->
|
||||
<section
|
||||
class="slide"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: backend-reviewer
|
||||
description: Review backend route and hook logic for regressions, data integrity risks, and missing tests.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are a backend reviewer for this repository. Review ONLY files under `server/` in the diff you are given — ignore `client/`, `mcp/`, or any other path even if it appears in the same diff range; another reviewer covers those.
|
||||
|
||||
Focus on:
|
||||
- Hook event lifecycle correctness.
|
||||
- Session/agent state-machine regressions.
|
||||
- API contract compatibility.
|
||||
- Transaction and persistence correctness.
|
||||
- Missing or weak verification coverage.
|
||||
|
||||
## Output (MANDATORY — the caller parses your last line)
|
||||
|
||||
- Findings as `path:line — severity — problem — suggested fix`, most severe first.
|
||||
- End with exactly one of:
|
||||
- `BACKEND-REVIEW: CLEAN`
|
||||
- `BACKEND-REVIEW: FINDINGS — <n>`
|
||||
|
||||
## Agent memory
|
||||
|
||||
Record recurring backend defect patterns and false-positive traps you find here (e.g. hook shapes that look wrong but aren't, common state-machine edge cases). Future backend-reviewer runs read this.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: frontend-reviewer
|
||||
description: Review React UI changes for behavior regressions, state consistency, and UX breakage.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are a frontend reviewer for this repository. Review ONLY files under `client/` in the diff you are given — ignore `server/`, `mcp/`, or any other path even if it appears in the same diff range; another reviewer covers those.
|
||||
|
||||
Focus on:
|
||||
- Routing and navigation consistency.
|
||||
- State updates from websocket and API responses.
|
||||
- Loading/empty/error state correctness.
|
||||
- Breaking visual or interaction regressions.
|
||||
- Missing tests for changed UI behavior.
|
||||
|
||||
## Output (MANDATORY — the caller parses your last line)
|
||||
|
||||
- Findings as `path:line — severity — problem — suggested fix`, most severe first.
|
||||
- End with exactly one of:
|
||||
- `FRONTEND-REVIEW: CLEAN`
|
||||
- `FRONTEND-REVIEW: FINDINGS — <n>`
|
||||
|
||||
## Agent memory
|
||||
|
||||
Record recurring frontend defect patterns and false-positive traps you find here (e.g. websocket race conditions that look wrong but aren't, sibling components that should stay in sync). Future frontend-reviewer runs read this.
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
name: "qc-local"
|
||||
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
|
||||
---
|
||||
|
||||
You are an elite QA automation engineer validating ONE lane's feature on that lane's OWN local stack (isolated FE/API/DB, nothing shared with other lanes), right after the feature branch's e2e passed (the branch is cut from `origin/development`, so this stack IS development + the feature as of the branch point). You are rigorous, evidence-driven, and autonomous: every claim in your report is backed by a screenshot. You never commit, push, or fix code — you verify, capture proof, and report. Your report IS the feature user-flow review the senior gate reads.
|
||||
|
||||
## Context you will be given (by the ship-feature-lane skill)
|
||||
|
||||
- **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 `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.
|
||||
- **Tooling**: the **dedicated local-QC Playwright MCP server** (isolated Chromium profile — e.g. `playwright-qa-local` if the profile names it that). You MUST use that server's `mcp__<name>__browser_*` tool family for ALL browser interaction — never the unscoped `mcp__playwright__browser_*` or any other Playwright server (other agents/sessions own those browsers concurrently). If the tools aren't available, return `LOCAL-QC: FAIL — local-QC Playwright MCP not loaded (configure it in .mcp.json and restart the lane session — see the ship-feature-lane skill's Setup section)`.
|
||||
- **Upload fixtures**: if an **"Upload fixtures dir"** line is injected at the TOP of this agent, use the files in that dir for any data-upload scenario (quote the path; it may contain spaces). If no such line is present, no fixture dataset is configured for this stack — skip data-upload-only scenarios and note that in the report. Put ad-hoc non-fixture files in the lane's `.playwright-mcp/` dir; never source uploads from `/tmp/`.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Load the QC Plan, then execute against it.** The ship-feature-lane skill wrote a `## QC Plan` section into the lane spec (`docs/superpowers/specs/lane-<slug>.md`) at Stage 5 — it is the authoritative scope. Read it first.
|
||||
- **Cover every in-scope scenario** in the plan — skip none.
|
||||
- **Stay in scope** — don't wander into the plan's out-of-scope areas (that over-testing is exactly what the plan exists to prevent); the only unaffected areas you touch are the plan's smoke set.
|
||||
- **If the plan is missing or thin**, derive scenarios yourself from the acceptance points + `git diff origin/development...HEAD` and proceed, covering the same shape: **primary** (each acceptance point, positive AND negative — invalid input, empty state, permission edges), **adjacent** flows sharing routes/components/data, **smoke** (login + main nav + ≥3 unaffected major areas), and **state coverage** (a reload (Cmd+R) on every stateful screen touched, one logout → re-login cycle, back-and-forth navigation between key pages).
|
||||
- **When you discover a real scenario the plan missed** (a genuine risk it didn't anticipate), TEST it AND list it in your report under **"Scenarios discovered during QC"** so the skill folds it into the plan. Do NOT edit the plan file yourself — the skill is its single writer.
|
||||
- **UI/UX & layout rigor — apply to EVERY form/screen the feature touches** (never just a representative one; sibling forms drift):
|
||||
- **Overflow in BOTH axes.** Resize the window NARROWER (width) AND SHORTER (height — e.g. ~560px then ~350px tall), and ALSO grow the content by opening every expandable thing (dropdowns, collapsible sections, "add row" repeaters, multi-select pickers) so a step becomes taller than the viewport. Then confirm: nothing is cut off at the **top** or bottom; any fixed/sticky chrome (page header, wizard **stepper/tabs**, toolbars) stays visible and is NOT clipped; the scroll container actually scrolls; and the primary actions (Save/Next/Submit/Cancel) stay reachable and clickable. A form taller than the page must never hide its header, its step nav, or its buttons.
|
||||
- **Every control has a visible label.** Each input/toggle/select/picker shows a field label. Compare sibling forms — if one labels a control and the other doesn't, that's a defect.
|
||||
- **Visual hierarchy is correct.** Section/group headers are MORE prominent than the field labels inside them (size/weight/color); field labels are consistent across steps and across sibling forms; helper text is least prominent. Flag any inverted or inconsistent hierarchy.
|
||||
- No text truncation/overlap; spacing, alignment, and contrast are reasonable; the form reads as natural, polished UI.
|
||||
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 (`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 (`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)*
|
||||
|
||||
- **Checkpoint as you go:** after EACH scenario, append one line to `proof/<feature-slug>/qc-local/RESULTS.partial.md` (via the Write/Edit tools on `<lane clone>/.playwright-mcp/proof/...`): `<NN> <scenario> — ✅|❌ — <proof file> — <HEAD sha>`. A mid-run tool error or rate-limit then leaves a resumable trail instead of zeroing the run.
|
||||
- **Resume on start:** if `RESULTS.partial.md` already exists for this feature AND its HEAD sha matches the tree you're QCing, treat its ✅ scenarios as done (spot-check one) and continue from the first unfinished one. Sha differs → start fresh (delete the stale partial).
|
||||
- **Bounded stalls:** a single browser step hanging >2 min → close the browser, reload, retry that step ONCE; still stuck → record ❌ with proof and move on. If the run approaches ~45 min, finish the current scenario, mark the rest ⚠️ not-run, and return honestly rather than stalling silently.
|
||||
|
||||
## Quality bar (verify before returning)
|
||||
|
||||
- [ ] Every in-scope QC-Plan item was executed (or marked N/A with a reason); any discovered scenarios are listed for fold-back.
|
||||
- [ ] Every primary scenario has a screenshot of its end state (pass or fail).
|
||||
- [ ] Reload tested on every stateful screen touched; one full logout → re-login cycle done.
|
||||
- [ ] Smoke covered login, navigation, and ≥3 unaffected areas.
|
||||
- [ ] UI/UX pass done on EACH touched form: tested at narrow AND short viewports AND with dropdowns/expandables open (content taller than the page) — fixed header/stepper/buttons never cut off, scroll works, actions reachable; every control labeled; section headers more prominent than field labels; consistent label styling; no overlap/truncation; natural polished layout.
|
||||
- [ ] All screenshots under `proof/<feature-slug>/qc-local/` (check with `ls .playwright-mcp/proof/<feature-slug>/qc-local/`).
|
||||
- [ ] No commits made; password never echoed anywhere.
|
||||
|
||||
## Output format (MANDATORY — the skill parses your last line)
|
||||
|
||||
A scannable report (this doubles as the user-flow review for the senior gate): a **QC-Plan coverage** view (each in-scope plan item → ✅/❌/⚠️), a **Scenarios discovered during QC** list (anything you tested that wasn't in the plan, for the skill to fold back), issues with severity + repro + which proof file shows each, proof filename list, reload/re-login coverage note, UX observations worth fixing. Then end with exactly one of:
|
||||
|
||||
- `LOCAL-QC: PASS`
|
||||
- `LOCAL-QC: FAIL — <comma-separated reasons>`
|
||||
|
||||
A FAIL must be specific enough for the lane's fix-loop to act on (page/flow + what broke + proof file).
|
||||
|
||||
## Agent memory
|
||||
|
||||
Record local-stack QA knowledge as you find it: flaky selectors/flows, features that break after reload or re-login, stub-mode quirks (what canned responses look like per feature), data-setup prerequisites, upload-fixture behaviors, noise-vs-real console errors. Future qc-local runs (any lane) read this.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: "senior-gate-reviewer"
|
||||
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
|
||||
---
|
||||
|
||||
You are the **Senior Gate Reviewer** — the final, independent publish-readiness authority for the ship-feature-lane pipeline. Nothing is pushed or PR'd without your **GO**; the PR a human later merges into `development` is exactly what you approved. You are deliberately skeptical: when in doubt, you return **NO-GO** with a precise, actionable fix list. You do NOT make fixes yourself — you judge, decide, and report.
|
||||
|
||||
## Context you will be given (by the ship-feature-lane skill)
|
||||
|
||||
- **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 **`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`).
|
||||
|
||||
## Your checks
|
||||
|
||||
**Review the PR the reviewers will see.** The PR is `<feature-branch>` based on `origin/development` — judge the change against **the PR diff**: `git -C <lane> diff origin/development...<feature-branch>` (`<feature-branch>` is in your context). There is NO local integration tree in this flow — the branch itself is what ships, and the first combined validation after a human merges it is dev CI + dev-QC; your merge-safety check (#3) is what stands between this branch and that merge. Run checks with `git -C <lane> ...`, `Read`, `Grep`, `make` as needed.
|
||||
|
||||
1. **Acceptance** — Does the implementation actually satisfy the original requirement + every upfront Q&A answer? Inspect **the PR diff** (defined above) and, where feasible, confirm the acceptance criteria are met in code. Missing/partial requirement coverage ⇒ NO-GO.
|
||||
|
||||
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 && 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:
|
||||
- **Overflow** was tested at narrow AND **short** viewports, AND with dropdowns/collapsibles/repeaters **open** so content exceeds the viewport — and no fixed chrome (page header, wizard **stepper/tabs**, action bar) is cut off (especially at the **top**), scroll works, and primary actions stay reachable. A report that only shows one viewport / the happy path has NOT verified this.
|
||||
- Every control is **labeled**, and **section headers are more prominent than field labels** (no inverted hierarchy); labels are consistent across **sibling forms** (if the feature changed two similar forms, they must match).
|
||||
- Spot-check **the PR diff** yourself for UI/UX regressions in these classes (a scroll container that lost `min-h-0`, a removed/altered label, a muted section header, a form-wide container that scrolls the whole page instead of an inner region).
|
||||
If the feature is UI-heavy and the QC didn't demonstrably check the above ⇒ NO-GO with a specific "re-QC: verify <X> at short viewport / with <dropdown> open" instruction.
|
||||
|
||||
5. **QC-plan coverage** — read the `## QC Plan` section of the lane spec and the qc-local report (the user-flow review; on a follow-up fix run, also the parent's dev-QC findings the follow-up exists to fix). Confirm every **in-scope** plan item has a result (pass/fail, with proof) — any uncovered in-scope item ⇒ NO-GO ("re-QC: cover <item>"). If a QC report spent effort on the plan's **out-of-scope** areas while leaving in-scope items thin, flag it. Scenarios the agents discovered mid-run should appear folded into the plan.
|
||||
|
||||
## Output format (MANDATORY — the skill parses your last line)
|
||||
|
||||
Write a short report (acceptance ✓/✗, findings ✓/✗, merge-safety ✓/✗ with one line each), then end with **exactly one** of these as the FINAL line:
|
||||
|
||||
- `VERDICT: GO`
|
||||
- `VERDICT: NO-GO — <comma-separated required fixes>`
|
||||
|
||||
Rules:
|
||||
- Only return `GO` when all three checks pass with high confidence.
|
||||
- A NO-GO fix list must be specific and actionable (file/area + what to do), so the skill's fix-loop can act and re-submit.
|
||||
- Never push, merge, commit, or modify files. You are read-only. Your verdict is the deliverable.
|
||||
+1
-156
@@ -1654,27 +1654,6 @@ async function cmdLanesAdd(args) {
|
||||
console.log(`${c.green("✔")} Created lane #${lane.id}: ${lane.title}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* `agenthub skills install` — copy this checkout's `skills/ship-feature-lane/`
|
||||
* into `~/.claude/skills/ship-feature-lane/`, so `/ship-feature-lane` is
|
||||
* discoverable from a session running inside any lane's own working
|
||||
* directory, not just inside this repo. Pure filesystem action; does not
|
||||
* talk to the dashboard server at all. Reinstall, not merge — overwrites an
|
||||
* existing global copy with the current one.
|
||||
*/
|
||||
function cmdSkillsInstall() {
|
||||
const { installShipFeatureLaneSkill } = require(
|
||||
path.join(REPO_ROOT, "server", "lib", "skills-install.js")
|
||||
);
|
||||
try {
|
||||
const result = installShipFeatureLaneSkill({ repoRoot: REPO_ROOT });
|
||||
console.log(`${c.green("✔")} installed ship-feature-lane skill -> ${result.path}`);
|
||||
} catch (err) {
|
||||
console.error(`✖ ${err.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `agenthub lanes gc [--dry-run]` — reap orphaned Playwright MCP processes
|
||||
* (owning session died) and cap oversized hook logs, across every lane on
|
||||
@@ -2417,25 +2396,6 @@ async function cmdLanesProofLink(args) {
|
||||
);
|
||||
}
|
||||
|
||||
/** `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) {
|
||||
const resolved = await resolveLaneArg(args);
|
||||
if (!resolved) return;
|
||||
const result = await post(
|
||||
`/api/lanes/${resolved.laneId}/agents/install`,
|
||||
{},
|
||||
{ allowError: true }
|
||||
);
|
||||
if (result.status) {
|
||||
console.error(`✖ agents install → ${result.data?.error?.message || result.status}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log(`${c.green("✔")} installed: ${result.installed.join(", ")}`);
|
||||
}
|
||||
|
||||
/** `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. */
|
||||
@@ -2514,8 +2474,7 @@ async function cmdLanesCi(args) {
|
||||
* was display-only: the dashboard rendered a copy button and the operator
|
||||
* pasted it into a session by hand. This runs it, in the lane's own cwd:
|
||||
*
|
||||
* - a slash command (`/superpowers:brainstorming`) starts `claude` with it
|
||||
* as the prompt, exactly like `agenthub ship-full` does with the whole skill;
|
||||
* - a slash command starts `claude` with it as the prompt;
|
||||
* - anything else (`npm test`, `agenthub lanes hook ci-gate`) is spawned directly.
|
||||
*
|
||||
* With no node id it lists the nodes and their commands, which is also the
|
||||
@@ -2678,88 +2637,6 @@ async function cmdLanesShell(args) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a `claude` session in this lane driven by a pipeline's own skill.
|
||||
* Two commands, two depths of work:
|
||||
*
|
||||
* agenthub ship-full → the 16-node `ship-feature` template, `/agenthub:ship-feature-lane`
|
||||
* — autonomous to a published PR, then watched until a
|
||||
* human merges it.
|
||||
* agenthub ship-quick → the 8-node `default` template, `/agenthub:ship-feature`
|
||||
* — in-repo feature work that stops at review/gate.
|
||||
*
|
||||
* Each command puts the lane on its own template, so the choice is the command
|
||||
* you type; the skill string itself lives in the template's `skill` field and
|
||||
* is never duplicated here.
|
||||
*
|
||||
* The prompt is passed as ONE argv element to `claude`, never through a shell —
|
||||
* a requirement containing quotes, `$`, or `;` is text, not code. `--print`
|
||||
* shows the exact command, switches nothing, and runs nothing.
|
||||
*/
|
||||
const SHIP_TEMPLATES = {
|
||||
"ship-full": "ship-feature",
|
||||
"ship-quick": "default",
|
||||
};
|
||||
|
||||
async function cmdShip(command, args) {
|
||||
const templateId = SHIP_TEMPLATES[command];
|
||||
const { flags, positional } = parseArgs(args);
|
||||
const resolved = await resolveLaneArg(args);
|
||||
if (!resolved) return;
|
||||
let { lane } = await get(`/api/lanes/${resolved.laneId}`);
|
||||
|
||||
const { pipelines } = await get("/api/lanes/pipelines");
|
||||
const skill = pipelines.find((p) => p.id === templateId)?.skill;
|
||||
if (!skill) {
|
||||
console.error(
|
||||
`✖ pipeline template "${templateId}" declares no driving skill — ` +
|
||||
`add a "skill" field to it (see docs/LANES.md § Custom template format)`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// `resolveLaneArg` only strips a leading bare lane id; the rest of the
|
||||
// positionals are the requirement, joined back into one prompt line.
|
||||
const requirement = positional
|
||||
.filter((p) => p !== String(resolved.laneId))
|
||||
.join(" ")
|
||||
.trim();
|
||||
const prompt = requirement ? `${skill} ${requirement}` : skill;
|
||||
|
||||
if (flags.print || !requirement) {
|
||||
// `--print` shows what would run, so it switches nothing.
|
||||
if (!requirement) console.error(`usage: agenthub ${command} "<requirement>" [<id>] [--print]`);
|
||||
console.log(`cd ${lane.cwd} && claude ${JSON.stringify(prompt)}`);
|
||||
if (!requirement) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// The command picks the pipeline, not the lane's current setting: put the
|
||||
// lane on the matching template first so the dashboard renders the right
|
||||
// node count from the start rather than after the skill self-corrects.
|
||||
if (lane.pipeline !== templateId) {
|
||||
({ lane } = await patch(`/api/lanes/${lane.id}`, { pipeline: templateId }));
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${c.dim(`lane #${lane.id} · pipeline ${lane.pipeline} (${lane.pipeline_nodes.length} nodes)`)}\n` +
|
||||
`${c.cyan("→")} claude ${JSON.stringify(prompt)}`
|
||||
);
|
||||
const child = spawn("claude", [prompt], { stdio: "inherit", cwd: lane.cwd });
|
||||
await new Promise((resolve) => {
|
||||
child.on("close", (code) => {
|
||||
if (code) process.exitCode = code;
|
||||
resolve();
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
console.error(c.red(`✖ ${err?.message || err}`));
|
||||
process.exitCode = 1;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdFeatureShow(args) {
|
||||
const slug = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!slug) {
|
||||
@@ -2938,16 +2815,6 @@ const COMMAND_GROUPS = [
|
||||
"[<node-id>] [extra…] [<id>] [--print]",
|
||||
"Run ONE pipeline node's own command in the lane's cwd (no id: list every node and its command)",
|
||||
],
|
||||
[
|
||||
"ship-full",
|
||||
'"<requirement>" [<id>] [--print]',
|
||||
"Run the 16-stage lane pipeline: implement → gates → e2e → review → QC → senior gate → PR, watched until a human merges (--print: show the command, run nothing)",
|
||||
],
|
||||
[
|
||||
"ship-quick",
|
||||
'"<requirement>" [<id>] [--print]',
|
||||
"Run the 8-stage in-repo pipeline: plan → implement → tests → review → gate → ship. No PR, no browser QC",
|
||||
],
|
||||
[
|
||||
"lanes shell",
|
||||
"[<id>]",
|
||||
@@ -2976,11 +2843,6 @@ const COMMAND_GROUPS = [
|
||||
"[<id>]",
|
||||
"Converge clone-root proof/ onto .playwright-mcp/proof (idempotent, never automatic)",
|
||||
],
|
||||
[
|
||||
"lanes agents install",
|
||||
"[<id>]",
|
||||
"Write the ship-feature-lane agent templates (qc-local, senior-gate-reviewer) into <lane>/.claude/agents/, git-excluded",
|
||||
],
|
||||
[
|
||||
"lanes mcp sync",
|
||||
"[<id>]",
|
||||
@@ -3001,11 +2863,6 @@ const COMMAND_GROUPS = [
|
||||
"[--dry-run]",
|
||||
"Reap orphaned Playwright MCP processes + cap oversized hook logs, across every lane",
|
||||
],
|
||||
[
|
||||
"skills install",
|
||||
"",
|
||||
"Install skills/ship-feature-lane/ into ~/.claude/skills/ so /ship-feature-lane works from any lane",
|
||||
],
|
||||
[
|
||||
"lock status|acquire|release",
|
||||
"[<name>] [--holder X] [--timeout N]",
|
||||
@@ -3832,9 +3689,6 @@ async function runCommand(argv) {
|
||||
return cmdLanesRuntime(rest[0], rest.slice(1));
|
||||
}
|
||||
if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));
|
||||
if (rest[0] === "agents" && rest[1] === "install") {
|
||||
return cmdLanesAgentsInstall(rest.slice(2));
|
||||
}
|
||||
if (rest[0] === "mcp" && rest[1] === "sync") {
|
||||
return cmdLanesMcpSync(rest.slice(2));
|
||||
}
|
||||
@@ -3857,12 +3711,6 @@ async function runCommand(argv) {
|
||||
return cmdLanesGc(rest.slice(1));
|
||||
}
|
||||
return cmdLanes();
|
||||
case "skills": {
|
||||
if (rest[0] === "install") return cmdSkillsInstall();
|
||||
console.error("usage: agenthub skills install");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
case "lock": {
|
||||
const sub = rest[0];
|
||||
if (sub === "status") return cmdLockStatus(rest.slice(1));
|
||||
@@ -3876,9 +3724,6 @@ async function runCommand(argv) {
|
||||
}
|
||||
case "stage":
|
||||
return cmdStage(rest);
|
||||
case "ship-full":
|
||||
case "ship-quick":
|
||||
return cmdShip(cmd, rest);
|
||||
case "feature": {
|
||||
const sub = rest[0];
|
||||
if (sub === "list") return cmdFeatureList(rest.slice(1));
|
||||
|
||||
@@ -115,7 +115,6 @@ export function AddLaneModal({
|
||||
const [browseOpen, setBrowseOpen] = useState(false);
|
||||
const [setupResult, setSetupResult] = useState<{
|
||||
profile: "scaffolded" | "skipped" | "failed";
|
||||
agents: "ok" | "failed";
|
||||
mcp: "ok" | "failed";
|
||||
router: "ok" | "skipped" | "failed";
|
||||
} | null>(null);
|
||||
@@ -294,7 +293,7 @@ export function AddLaneModal({
|
||||
// POST /worktree returns as soon as the DB row exists (202) — the
|
||||
// actual `git worktree add` runs afterward, in the background, on the
|
||||
// server. Firing setup against the lane's cwd before that finishes
|
||||
// means agents/mcp write into a directory that doesn't exist yet, so
|
||||
// means mcp writes into a directory that doesn't exist yet, so
|
||||
// wait for provisioning to leave the "provisioning" status first.
|
||||
const provisionedLane = await waitForProvisioned(result.lane.id);
|
||||
if (!provisionedLane || provisionedLane.status === "failed") {
|
||||
@@ -303,9 +302,8 @@ export function AddLaneModal({
|
||||
return;
|
||||
}
|
||||
|
||||
const [profileOutcome, agentsOutcome, mcpOutcome, routerOutcome] = await Promise.allSettled([
|
||||
const [profileOutcome, mcpOutcome, routerOutcome] = await Promise.allSettled([
|
||||
api.lanes.profileInit(result.lane.id),
|
||||
api.lanes.agentsInstall(result.lane.id),
|
||||
api.lanes.mcpSync(result.lane.id),
|
||||
applyRouter(result.lane.id),
|
||||
]);
|
||||
@@ -316,7 +314,6 @@ export function AddLaneModal({
|
||||
? "scaffolded"
|
||||
: "skipped"
|
||||
: "failed",
|
||||
agents: agentsOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
router:
|
||||
routerOutcome.status !== "fulfilled" ? "failed" : router === "native" ? "skipped" : "ok",
|
||||
@@ -328,7 +325,6 @@ export function AddLaneModal({
|
||||
console.info("[add-lane] auto-setup result:", {
|
||||
profile:
|
||||
profileOutcome.status === "fulfilled" ? profileOutcome.value : profileOutcome.reason,
|
||||
agents: agentsOutcome.status === "fulfilled" ? agentsOutcome.value : agentsOutcome.reason,
|
||||
mcp: mcpOutcome.status === "fulfilled" ? mcpOutcome.value : mcpOutcome.reason,
|
||||
// Never the profile itself: it would put an endpoint and a lane id in
|
||||
// the browser console. (The key is never sent here at all.)
|
||||
@@ -740,7 +736,6 @@ export function AddLaneModal({
|
||||
ok: setupResult.profile !== "failed",
|
||||
skipped: setupResult.profile === "skipped",
|
||||
},
|
||||
{ label: t("addLaneSetupAgents"), ok: setupResult.agents === "ok" },
|
||||
{ label: t("addLaneSetupMcp"), ok: setupResult.mcp === "ok" },
|
||||
{
|
||||
label: t("addLaneSetupRouter"),
|
||||
|
||||
@@ -271,7 +271,7 @@ export function since(sec: number | null): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Repo tooling (agents install / MCP sync / dev-sync check, and the
|
||||
* Repo tooling (MCP sync / dev-sync check, and the
|
||||
* create/update-readme run, which moved here from the Add-lane modal) for one lane,
|
||||
* collapsed into a single dropdown. Lives outside LaneCard so it can be
|
||||
* placed in the Workspace page header, next to "Add lane" — it polls its
|
||||
@@ -281,16 +281,11 @@ export function since(sec: number | null): string {
|
||||
export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
const { t } = useTranslation(["lanes"]);
|
||||
const [toolsMenuOpen, setToolsMenuOpen] = useState(false);
|
||||
const [laneActionBusy, setLaneActionBusy] = useState<
|
||||
"agents" | "mcp" | "sync-check" | "docs" | null
|
||||
>(null);
|
||||
const [laneActionBusy, setLaneActionBusy] = useState<"mcp" | "sync-check" | "docs" | null>(null);
|
||||
const [laneActionResult, setLaneActionResult] = useState<string | null>(null);
|
||||
const runtime = useLaneRuntime(lane.id, 0);
|
||||
|
||||
const runLaneAction = async (
|
||||
which: "agents" | "mcp" | "sync-check" | "docs",
|
||||
fn: () => Promise<string>
|
||||
) => {
|
||||
const runLaneAction = async (which: "mcp" | "sync-check" | "docs", fn: () => Promise<string>) => {
|
||||
setLaneActionBusy(which);
|
||||
setLaneActionResult(null);
|
||||
try {
|
||||
@@ -302,12 +297,6 @@ export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgentsInstall = () =>
|
||||
runLaneAction("agents", async () => {
|
||||
const result = await api.lanes.agentsInstall(lane.id);
|
||||
return t("actions.agentsInstallResult", { files: result.installed.join(", ") });
|
||||
});
|
||||
|
||||
// A skill only exists inside a CLI session, so there is nothing server-side
|
||||
// to call: this opens the lane's own terminal run with the slash command as
|
||||
// its first message. Claude-only — codex has no slash commands.
|
||||
@@ -363,22 +352,6 @@ export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
role="menu"
|
||||
className="absolute right-0 z-10 mt-1 min-w-44 overflow-hidden rounded-md border border-border-light bg-surface-0 py-1 shadow-lg shadow-black/40"
|
||||
>
|
||||
{/* Claude-only: other CLIs have no agents to install. */}
|
||||
{lane.tool === "claude" && (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="lane-agents-install"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen(false);
|
||||
void handleAgentsInstall();
|
||||
}}
|
||||
className="block w-full px-3 py-1.5 text-left text-fg-secondary hover:bg-surface-2"
|
||||
>
|
||||
{t("actions.agentsInstall")}
|
||||
</button>
|
||||
)}
|
||||
{lane.tool === "claude" && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -42,7 +42,7 @@ const LIVENESS_DOT: Record<Lane["liveness"], string> = {
|
||||
* recordDetection refuses any detection that isn't strictly ahead.
|
||||
*/
|
||||
function detectionLeads(lane: Lane): boolean {
|
||||
if (!lane.detected_stage) return false;
|
||||
if (!lane.detected_stage || !lane.pipeline_nodes.length) return false;
|
||||
const nodes = lane.pipeline_nodes;
|
||||
return (
|
||||
nodes.findIndex((n) => n.id === lane.detected_stage) >=
|
||||
@@ -116,9 +116,9 @@ export default function LaneStripCard({
|
||||
<span className="shrink-0 text-[10px] font-semibold tracking-widest text-fg-muted">
|
||||
#{lane.id}
|
||||
</span>
|
||||
{/* Only claude lanes carry a pipeline; the others show their CLI's
|
||||
tag in place of the stage chip. */}
|
||||
{lane.tool !== "claude" ? (
|
||||
{/* Only claude lanes carry a pipeline, and only while a template is
|
||||
installed; the others show their CLI's tag in place of the stage chip. */}
|
||||
{lane.tool !== "claude" || lane.pipeline_nodes.length === 0 ? (
|
||||
<span className="shrink-0 rounded border border-border bg-surface-2 px-1.5 py-0.5 text-[10px] text-fg-muted">
|
||||
{LANE_TOOL_LABEL[lane.tool]}
|
||||
</span>
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
browse: vi.fn(),
|
||||
get: vi.fn(),
|
||||
profileInit: vi.fn(),
|
||||
agentsInstall: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
routerApply: vi.fn(),
|
||||
},
|
||||
@@ -120,7 +119,6 @@ beforeEach(() => {
|
||||
vi.mocked(api.lanes.profileInit)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ scaffolded: false, reason: "no detectable Node.js project" });
|
||||
vi.mocked(api.lanes.agentsInstall).mockReset().mockResolvedValue({ installed: [] });
|
||||
vi.mocked(api.lanes.mcpSync).mockReset().mockResolvedValue({ servers: [], profilesSeeded: [] });
|
||||
vi.mocked(api.lanes.routerApply).mockReset().mockResolvedValue({
|
||||
tool: "claude",
|
||||
@@ -383,7 +381,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("fires profileInit, agentsInstall, and mcpSync after a successful worktree call", async () => {
|
||||
it("fires profileInit and mcpSync after a successful worktree call", async () => {
|
||||
vi.mocked(api.lanes.branches).mockResolvedValue({
|
||||
branches: ["main"],
|
||||
current: "main",
|
||||
@@ -401,7 +399,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
|
||||
await waitFor(() => expect(api.lanes.worktree).toHaveBeenCalled());
|
||||
await waitFor(() => expect(api.lanes.profileInit).toHaveBeenCalledWith(42));
|
||||
expect(api.lanes.agentsInstall).toHaveBeenCalledWith(42);
|
||||
expect(api.lanes.mcpSync).toHaveBeenCalledWith(42);
|
||||
await waitFor(() => expect(onAdded).toHaveBeenCalled());
|
||||
});
|
||||
@@ -416,7 +413,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
lane: { id: 43, title: "demo2", cwd: "/lanes/demo2", status: "provisioning" } as Lane,
|
||||
});
|
||||
vi.mocked(api.lanes.profileInit).mockRejectedValue(new Error("boom"));
|
||||
vi.mocked(api.lanes.agentsInstall).mockRejectedValue(new Error("boom"));
|
||||
vi.mocked(api.lanes.mcpSync).mockRejectedValue(new Error("boom"));
|
||||
const onAdded = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
@@ -482,7 +478,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
await waitFor(() => expect(onAdded).toHaveBeenCalled());
|
||||
expect(await screen.findByText(/auto-setup was skipped/)).toBeInTheDocument();
|
||||
expect(api.lanes.profileInit).not.toHaveBeenCalled();
|
||||
expect(api.lanes.agentsInstall).not.toHaveBeenCalled();
|
||||
expect(api.lanes.mcpSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
up: vi.fn(),
|
||||
down: vi.fn(),
|
||||
preflight: vi.fn().mockResolvedValue({ blocked: [], warnings: [] }),
|
||||
agentsInstall: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
syncBaseCheck: vi.fn(),
|
||||
routerApply: vi.fn(),
|
||||
@@ -309,17 +308,6 @@ describe("LaneToolsMenu", () => {
|
||||
} as unknown as LaneRuntime);
|
||||
});
|
||||
|
||||
it("clicking Install agents calls the API and shows the result", async () => {
|
||||
vi.mocked(api.lanes.agentsInstall).mockResolvedValue({
|
||||
installed: ["qc-local.md", "senior-gate-reviewer.md"],
|
||||
});
|
||||
render(<LaneToolsMenu lane={makeLane()} />);
|
||||
await userEvent.click(await screen.findByTestId("lane-tools-menu"));
|
||||
await userEvent.click(await screen.findByTestId("lane-agents-install"));
|
||||
await waitFor(() => expect(api.lanes.agentsInstall).toHaveBeenCalledWith(1));
|
||||
expect(await screen.findByTestId("lane-action-result")).toHaveTextContent("qc-local.md");
|
||||
});
|
||||
|
||||
it("clicking Sync MCP calls the API and shows the result", async () => {
|
||||
vi.mocked(api.lanes.mcpSync).mockResolvedValue({
|
||||
servers: ["playwright"],
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"action.stop": "stop",
|
||||
"actionError": "Lane action failed: {{message}}",
|
||||
"actionErrorUnknown": "Unknown error",
|
||||
"actions.agentsInstall": "Install agents",
|
||||
"actions.agentsInstallResult": "Installed: {{files}}",
|
||||
"actions.busy": "…",
|
||||
"actions.mcpSync": "Sync MCP",
|
||||
"actions.mcpSyncResult": "Synced: {{servers}}",
|
||||
@@ -49,7 +47,6 @@
|
||||
"addLaneRouterLoading": "Loading router profiles…",
|
||||
"addLaneRouterNative": "Native",
|
||||
"addLaneRouterProfileLabel": "Router profile",
|
||||
"addLaneSetupAgents": "Agents",
|
||||
"addLaneSetupClose": "Close",
|
||||
"addLaneSetupMcp": "MCP servers",
|
||||
"addLaneSetupProfile": "Profile",
|
||||
|
||||
@@ -63,9 +63,7 @@
|
||||
},
|
||||
"lanesSection": {
|
||||
"title": "Lanes",
|
||||
"description": "Machine-wide housekeeping and one-time setup for the ship-feature-lane pipeline.",
|
||||
"installSkill": "Install ship-feature-lane skill",
|
||||
"installSkillSuccess": "Installed to {{path}}",
|
||||
"description": "Machine-wide lane housekeeping.",
|
||||
"runHousekeeping": "Run housekeeping",
|
||||
"dryRun": "Dry run",
|
||||
"gcResult": "Reaped {{reaped}} process(es), capped {{capped}} log(s)"
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"action.stop": "Dừng",
|
||||
"actionError": "Thao tác làn đường thất bại: {{message}}",
|
||||
"actionErrorUnknown": "Lỗi không xác định",
|
||||
"actions.agentsInstall": "Cài agent",
|
||||
"actions.agentsInstallResult": "Đã cài: {{files}}",
|
||||
"actions.busy": "…",
|
||||
"actions.mcpSync": "Đồng bộ MCP",
|
||||
"actions.mcpSyncResult": "Đã đồng bộ: {{servers}}",
|
||||
@@ -49,7 +47,6 @@
|
||||
"addLaneRouterLoading": "Đang tải danh sách router…",
|
||||
"addLaneRouterNative": "Native",
|
||||
"addLaneRouterProfileLabel": "Profile router",
|
||||
"addLaneSetupAgents": "Agent",
|
||||
"addLaneSetupClose": "Đóng",
|
||||
"addLaneSetupMcp": "MCP server",
|
||||
"addLaneSetupProfile": "Profile",
|
||||
|
||||
@@ -63,9 +63,7 @@
|
||||
},
|
||||
"lanesSection": {
|
||||
"title": "Lanes",
|
||||
"description": "Bảo trì toàn máy và cài đặt một lần cho pipeline ship-feature-lane.",
|
||||
"installSkill": "Cài skill ship-feature-lane",
|
||||
"installSkillSuccess": "Đã cài vào {{path}}",
|
||||
"description": "Dọn dẹp lane trên toàn máy.",
|
||||
"runHousekeeping": "Chạy dọn dẹp",
|
||||
"dryRun": "Chạy thử (dry run)",
|
||||
"gcResult": "Đã dọn {{reaped}} tiến trình, cắt {{capped}} log"
|
||||
|
||||
@@ -1130,8 +1130,6 @@ export const api = {
|
||||
purged_events: number;
|
||||
purged_agents: number;
|
||||
}>("/settings/cleanup", { method: "POST", body: JSON.stringify(params) }),
|
||||
skillsInstall: () =>
|
||||
request<{ installed: true; path: string }>("/skills/install", { method: "POST", body: "{}" }),
|
||||
gc: (body: { dryRun?: boolean } = {}) =>
|
||||
request<{ reaped: number[]; capped: { path: string; sizeBefore: number }[] }>("/lanes/gc", {
|
||||
method: "POST",
|
||||
@@ -2719,11 +2717,6 @@ export const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
},
|
||||
agentsInstall: (id: number) =>
|
||||
request<{ installed: string[] }>(`/lanes/${id}/agents/install`, {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
}),
|
||||
/**
|
||||
* POST /lanes/:id/router/apply - point this lane at an AI Router profile,
|
||||
* or back at `native`. Writes the lane's own config on disk; only the
|
||||
|
||||
@@ -699,12 +699,6 @@ export function Settings() {
|
||||
return res.ok ? t("hooks.success") : t("hooks.failed");
|
||||
});
|
||||
|
||||
const handleSkillsInstall = () =>
|
||||
runAction("skills-install", async () => {
|
||||
const res = await api.settings.skillsInstall();
|
||||
return t("lanesSection.installSkillSuccess", { path: res.path });
|
||||
});
|
||||
|
||||
const handleGc = () =>
|
||||
runAction("gc", async () => {
|
||||
const res = await api.settings.gc({ dryRun: gcDryRun });
|
||||
@@ -1354,23 +1348,6 @@ export function Settings() {
|
||||
<p className="text-xs text-fg-muted mb-4">{t("lanesSection.description")}</p>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<span className="text-xs text-fg-secondary">{t("lanesSection.installSkill")}</span>
|
||||
<button
|
||||
onClick={handleSkillsInstall}
|
||||
disabled={actionLoading !== null}
|
||||
className="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === "skills-install" ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{t("lanesSection.installSkill")}
|
||||
</button>
|
||||
</div>
|
||||
{actionBanner(["skills-install"])}
|
||||
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-fg-secondary">
|
||||
<input
|
||||
|
||||
@@ -523,8 +523,10 @@ export function Workspace() {
|
||||
}, [refreshList]);
|
||||
|
||||
const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null;
|
||||
// Only claude lanes carry a pipeline (codex and antigravity emit no hooks).
|
||||
const noPipeline = !!currentLane && currentLane.tool !== "claude";
|
||||
// Only claude lanes carry a pipeline (codex and antigravity emit no hooks),
|
||||
// and only while a template is installed — none ships, so it is dormant.
|
||||
const noPipeline =
|
||||
!!currentLane && (currentLane.tool !== "claude" || currentLane.pipeline_nodes.length === 0);
|
||||
|
||||
// Feature list follows the selected lane, resets the viewer on lane switch.
|
||||
useEffect(() => {
|
||||
@@ -817,8 +819,9 @@ export function Workspace() {
|
||||
<Pencil className="h-3 w-3 shrink-0 text-fg-muted opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</button>
|
||||
)}
|
||||
{/* Non-claude lanes carry no pipeline: no time-at-stage, no
|
||||
pipeline or feature picker, no map. */}
|
||||
{/* A lane with no pipeline (non-claude, or no template
|
||||
installed): no time-at-stage, no pipeline or feature
|
||||
picker, no map. */}
|
||||
{!noPipeline && (
|
||||
<span className="shrink-0 tabular-nums text-xs text-fg-muted">
|
||||
{since(currentLane.stage_seconds)}
|
||||
@@ -882,7 +885,7 @@ export function Workspace() {
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* Per-lane actions (agents install, mcp sync, sync-base check)
|
||||
{/* Per-lane actions (mcp sync, sync-base check)
|
||||
belong beside the lane they act on, not in a page header. */}
|
||||
<LaneToolsMenu lane={currentLane} />
|
||||
<button
|
||||
|
||||
@@ -8435,44 +8435,11 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-4"
|
||||
>
|
||||
Machine-wide housekeeping and one-time setup for the ship-feature-lane pipeline.
|
||||
Machine-wide lane housekeeping.
|
||||
</p>
|
||||
<div
|
||||
class="card p-5 space-y-4"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-wrap gap-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-fg-secondary"
|
||||
>
|
||||
Install ship-feature-lane skill
|
||||
</span>
|
||||
<button
|
||||
class="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-rotate-ccw w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
||||
/>
|
||||
<path
|
||||
d="M3 3v5h5"
|
||||
/>
|
||||
</svg>
|
||||
Install ship-feature-lane skill
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between flex-wrap gap-3"
|
||||
>
|
||||
|
||||
+12
-4
@@ -135,7 +135,7 @@ POST /api/lanes/ensure
|
||||
```
|
||||
|
||||
```json
|
||||
{ "cwd": "/absolute/path/to/work", "title": "App package", "pipeline": "ship-feature", "tool": "codex" }
|
||||
{ "cwd": "/absolute/path/to/work", "title": "App package", "pipeline": "default", "tool": "codex" }
|
||||
```
|
||||
|
||||
`cwd` is optional — when missing or empty the server falls back to
|
||||
@@ -192,14 +192,22 @@ slugified from its title once, at creation.
|
||||
|
||||
`pipeline` is patchable, which is how a lane moves between templates after
|
||||
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
|
||||
rejected with `400 EBADPIPELINE` rather than stored (`default` is always
|
||||
accepted): `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 `agenthub stage`.
|
||||
|
||||
**Pipelines are dormant until a template is installed.** AgentHub ships no
|
||||
pipeline template; templates come only from `DASHBOARD_PIPELINES_DIR` (see
|
||||
`docs/LANES.md#custom-pipeline-templates`). With none installed, every lane is
|
||||
on an empty `default` pipeline (`pipeline_nodes: []`, `progress: 0`),
|
||||
`GET /api/lanes/pipelines` returns `{ "pipelines": [] }`, and
|
||||
`POST /api/lanes/:id/stage` answers
|
||||
`400 { "error": { "code": "ENOPIPELINE", "message": "no pipeline template is installed, so this lane has no stages to declare (drop a template into DASHBOARD_PIPELINES_DIR to turn stages on)", "stages": [] } }`.
|
||||
|
||||
#### Provision a managed worktree
|
||||
|
||||
```http
|
||||
@@ -1038,7 +1046,7 @@ every number. `status` filters by session lifecycle, same convention as
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{ "name": "agenthub:ship-feature", "plugin": "agenthub", "count": 12, "sessions": 5 },
|
||||
{ "name": "agenthub:repo-onboarding", "plugin": "agenthub", "count": 12, "sessions": 5 },
|
||||
{ "name": "daily-report", "plugin": null, "count": 3, "sessions": 3 }
|
||||
],
|
||||
"plugins": [
|
||||
|
||||
+2
-7
@@ -235,19 +235,14 @@ A lane is a durable unit of parallel agent work — one working directory, many
|
||||
| `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 pipeline [<template-id>] [<id>]` | Show which pipeline template a lane renders against, plus the available templates and their node counts (none until you install one in `DASHBOARD_PIPELINES_DIR`); 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 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. With no pipeline template installed (AgentHub ships none — see [Custom pipeline templates](LANES.md#custom-pipeline-templates)) it answers `400 ENOPIPELINE`. `<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 |
|
||||
|
||||
`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>/.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 |
|
||||
|
||||
+68
-321
@@ -32,9 +32,9 @@ agenthub lanes add --repo /path/to/repo --title "My Feature" --base main --slug
|
||||
|
||||
`--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 (`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.
|
||||
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 (once a pipeline template is installed) — see "Stage detection" 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: `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.
|
||||
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 mcp sync`, and the AI Router pick. 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
|
||||
|
||||
@@ -118,9 +118,8 @@ Codex does not emit Claude Code hooks (sessions, agent events, tool-execution tr
|
||||
|
||||
**These are not bugs; they reflect Codex's architecture.** 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.
|
||||
|
||||
**Codex lanes carry no pipeline.** Nothing infers their stage, nothing types
|
||||
a stage reminder into their pane, and `agenthub lanes agents install` installs
|
||||
nothing on them (`installed: []`). The one thing AgentHub reads from Codex is
|
||||
**Codex lanes carry no pipeline.** Nothing infers their stage and nothing types
|
||||
a stage reminder into their pane. The one thing AgentHub reads from Codex is
|
||||
its running/idle status — see "Stage detection" for the journal poll that
|
||||
mirrors it. Every server start also wipes what older builds left behind on a
|
||||
codex lane — its stage columns, stage log (`lane_stage_events`) and feature
|
||||
@@ -395,8 +394,8 @@ The dashboard web UI merges lanes and runs into a single **Workspace** page acce
|
||||
- **No lanes yet** — the strip is replaced by a single banner (`lane-empty`): icon, one line of what a lane is, the CLI equivalent (`agenthub lanes add --cwd $(pwd)`) and an **Add lane** affordance. The whole banner IS the button — clicking anywhere on it opens the Add-lane modal, so there is no separate button to hit. The strip itself — the dashed add tile plus the lane tiles — only renders once at least one lane exists, because an add tile beside a stray sentence read as a half-drawn row. The banner stays compact on purpose: the run setup form below it is what needs the height.
|
||||
- **Reordering the strip** — drag a tile onto another to move it there, or press **Alt+←/→** with a tile focused (the keyboard path; HTML5 drag-and-drop has no touch support and does not auto-scroll an overflowing strip). The order is a **per-viewer preference**, not a lane fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) exactly like the split-view layout beside it, and writes nothing server-side — `GET /api/lanes` and `agenthub lanes` keep answering in id order, and the order does not follow you to another browser. A lane the stored order has never seen (created since, or created in another tab) lands at the END rather than wherever a stale index would put it; a stored id whose lane is gone is skipped.
|
||||
- **Renaming a lane** — the detail header's name is a button: click it (a pencil appears on hover) to edit in place. **Enter** saves via `PATCH /api/lanes/:id` with `{title}`, **Escape** and clicking away both cancel, and saving an empty name clears the title, which puts the lane's `cwd` back in its place. Renaming is display-only — a managed lane's branch name was slugified from its title once, at creation.
|
||||
- **Lane-detail header row** — one row, and every per-lane control lives on it: liveness + status, the lane's name (no "Lane 23 ·" prefix — the id is on the strip tile), time-on-stage, the pipeline picker, the Model (AI Router) select, the feature picker (there are no stage chips here — the declared stage and the `auto: <stage>` detection both render in the PipelineMap directly below, so the chips were a second, smaller copy of the same two facts), and on the right the **Tools** menu (agents install / mcp sync / sync-base check / create-update readme, hidden when the lane declares no runtime profile), the info toggle, the split-view toggle and Remove. Tools sits here rather than in Settings because every one of its actions runs against one lane's `cwd` — in Settings it would need a lane picker just to say which. **Create/update readme file** (Claude lanes only — Codex has no slash commands) starts the lane's terminal run with `/sg-all:sg-md-writing` as its first message, so the lane opens already writing/refreshing its `CLAUDE.md`/`AGENTS.md` set; it needs the `sg-all` plugin installed in Claude Code, and without it the session just reports an unknown command. It used to be a checkbox in the Add-lane modal: creating a lane and writing its docs are separate decisions, and the second is worth re-running long after the first.
|
||||
- **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.
|
||||
- **Lane-detail header row** — one row, and every per-lane control lives on it: liveness + status, the lane's name (no "Lane 23 ·" prefix — the id is on the strip tile), time-on-stage, the pipeline picker, the Model (AI Router) select, the feature picker (time-on-stage and both pickers only while a pipeline template is installed; there are no stage chips here — the declared stage and the `auto: <stage>` detection both render in the PipelineMap directly below, so the chips were a second, smaller copy of the same two facts), and on the right the **Tools** menu (mcp sync / sync-base check / create-update readme, hidden when the lane declares no runtime profile), the info toggle, the split-view toggle and Remove. Tools sits here rather than in Settings because every one of its actions runs against one lane's `cwd` — in Settings it would need a lane picker just to say which. **Create/update readme file** (Claude lanes only — Codex has no slash commands) starts the lane's terminal run with `/sg-all:sg-md-writing` as its first message, so the lane opens already writing/refreshing its `CLAUDE.md`/`AGENTS.md` set; it needs the `sg-all` plugin installed in Claude Code, and without it the session just reports an unknown command. It used to be a checkbox in the Add-lane modal: creating a lane and writing its docs are separate decisions, and the second is worth re-running long after the first.
|
||||
- **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 Hidden while no pipeline template is installed (see "Pipeline stages and the five node states").
|
||||
- **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. It grabs the keyboard the moment it appears, so typing goes into the pane without clicking it first, and when the pane's process ends — a double Ctrl+C out of `claude`, `exit`, a crash — the console returns to the run setup form on its own, with no "New run" click.
|
||||
- **Clipboard in a pane** — the lane's tmux session is started with the graphical session's own `DISPLAY`/`WAYLAND_DISPLAY`/`XAUTHORITY`/`XDG_RUNTIME_DIR` (read from `systemctl --user show-environment`, this process's own values winning; `server/lib/desktop-env.js`). The dashboard normally runs as a systemd *user* service, which has none of them, so a pane it started had no clipboard at all and `claude` answered "no image found in clipboard" on every image paste while the same CLI pasted fine in a terminal. Only those four names are forwarded, and only into panes this dashboard spawns — an already-running pane keeps the environment it was started with, so it needs a restart to gain one.
|
||||
- **Split view** — a layout toggle (one pane / two side by side / a 2×2 grid, drawn as icons rather than the words, with the count kept as each button's `aria-label` and tooltip) renders that many terminal panes side by side (`grid-cols-2` for 2, a 2×2 grid for 4). EVERY pane is bound to the lane strip's selection — there is no per-pane lane picker — and each pane owns its own tmux session on that lane: pane 0 is the lane's `agenthub-lane-<id>` (its `run_id`), panes 1–3 are `agenthub-lane-<id>-<slot>` (`POST /api/lanes/:id/start` with `slot`). A pane's **Active runs** leaves out the runs the other panes are showing, so one terminal is never attached twice side by side. Only the layout persists to `localStorage` (`agenthub.workspace.splitView`); a stored per-pane lane id from an older build is ignored.
|
||||
@@ -746,7 +745,9 @@ The dashboard web UI shows each lane as a card in a grid, with the selected lane
|
||||
|
||||
## Pipeline stages and the five node states
|
||||
|
||||
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`.
|
||||
> **Pipelines are dormant.** AgentHub ships no pipeline template. Until you install one (drop a template JSON into `DASHBOARD_PIPELINES_DIR` — see "Custom pipeline templates" below), every lane sits on an empty `default` pipeline: no nodes, progress `0`. `GET /api/lanes/pipelines` returns `{"pipelines": []}`, `agenthub stage <x>` (and `POST /api/lanes/:id/stage`) answers `400 ENOPIPELINE`, and the Workspace hides the time-on-stage timer, the pipeline and feature pickers and the PipelineMap — the lane tile shows its tool tag instead of a stage chip. Install a template and all of it comes back; a template with id `default` becomes every lane's default. Further development of pipelines is planned. The rest of this section, and the stage sections after it, describe a lane on an installed template.
|
||||
|
||||
A lane moves through stages defined in a **pipeline template** (see "Custom pipeline templates" below).
|
||||
|
||||
**A lane is created on `default` unless told otherwise.** The "+ Add lane" modal does not ask — it always creates on `default`. Pick another with `agenthub lanes add --pipeline <id>` at creation, or change it afterwards from the *Pipeline template* select in the lane's detail header in Workspace.
|
||||
|
||||
@@ -754,63 +755,31 @@ For a lane that already exists, switch it from the terminal:
|
||||
|
||||
```bash
|
||||
agenthub lanes pipeline # which template this lane uses, and what else exists
|
||||
agenthub lanes pipeline ship-feature # switch it
|
||||
agenthub lanes pipeline ml-training # switch it (any installed template id)
|
||||
```
|
||||
|
||||
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": "/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
|
||||
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
|
||||
agenthub ship-quick "fix the CSV separator"
|
||||
# → lane switched to `default`, then: claude "/agenthub:ship-feature fix the CSV separator"
|
||||
|
||||
agenthub ship-full "add an export button" --print # show the command, switch nothing, run nothing
|
||||
```
|
||||
|
||||
| | `agenthub ship-full` | `agenthub ship-quick` |
|
||||
|---|---|---|
|
||||
| Template | `ship-feature` (16 nodes) | `default` (8 nodes) |
|
||||
| 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 |
|
||||
|
||||
The lane is resolved from the working directory, so no id is needed (pass one to target another lane). Each command puts the lane on its own template *before* spawning, so the dashboard renders the right node count from the first stage rather than after the skill self-corrects; `--print` switches nothing. The requirement is handed to `claude` as ONE argv element — never through a shell — so quotes, `$` and `;` in it are text. A template with no `skill` field makes the command refuse rather than guess.
|
||||
A template may name the slash command that drives it (its optional `skill` field). It rides along on every lane payload as `pipeline_skill`; the dashboard never executes it.
|
||||
|
||||
### Running ONE node
|
||||
|
||||
`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`:
|
||||
When you only want one step — re-run a CI gate, redo a brainstorm — run that node's own `command`:
|
||||
|
||||
```bash
|
||||
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 review # → claude "/code-review"
|
||||
agenthub lanes node gates # → agenthub lanes hook ci-gate
|
||||
agenthub 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 <node-id> # run that node's command in the lane's cwd
|
||||
agenthub lanes node <node-id> extra words # extra words are appended to the prompt
|
||||
agenthub lanes node <node-id> --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 `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 `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`.
|
||||
A node is runnable when its template gives it a `command`. A command 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**.
|
||||
|
||||
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 `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": "/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 `agenthub stage` fixes it.
|
||||
|
||||
### Aggregate pass rates across lanes
|
||||
@@ -859,8 +828,6 @@ offering every other lane even while the counts show only one.
|
||||
|
||||
`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 `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**:
|
||||
|
||||
| State | Color | Meaning |
|
||||
@@ -876,13 +843,13 @@ 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 `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.
|
||||
2. **Implicitly skipped.** On a template whose nodes start `intake`, `plan`, `implement`, 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
|
||||
|
||||
`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.
|
||||
`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, regardless of how much real work happens. With no pipeline template installed there is nothing to declare: the command answers `400 ENOPIPELINE`.
|
||||
|
||||
When a skill — or a plain chat session — reaches a significant milestone, it reports the current stage to the dashboard using `agenthub stage`:
|
||||
|
||||
@@ -904,18 +871,9 @@ agenthub stage <stage> [--lane <id>] [--cwd <path>] [--tool claude|codex] [--sta
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `<stage>` (required): the pipeline stage name or alias. The default pipeline aliases are:
|
||||
- `intake` / `assigned` / `claimed` / `start`
|
||||
- `plan` / `planning` / `brainstorm` / `design`
|
||||
- `implement` / `implementing` / `coding` / `build`
|
||||
- `tests` / `testing` / `unit` / `gates` / `pre-push-gate`
|
||||
- `review` / `reviewing` / `code-review` / `self-review`
|
||||
- `gate` / `verify` / `verification` / `sr-gate` / `gate-blocked`
|
||||
- `ship` / `pr` / `pr-open` / `publishing` / `commit` / `push`
|
||||
- `done` / `complete` / `completed` / `merged`
|
||||
|
||||
Aliases are **per template** — the `ship-feature` pipeline has its own set
|
||||
(see "Pipeline template: ship-feature" below). A name matching no node and no
|
||||
- `<stage>` (required): a node id or one of its aliases, as the lane's
|
||||
pipeline template defines them (see "Custom template format"). Aliases are
|
||||
**per template**. A name matching no node and no
|
||||
alias is still recorded verbatim, but `phaseIdx` then resolves it to nothing:
|
||||
no node renders `current` and progress reads `0`. The CLI prints a warning to
|
||||
stderr and still exits `0` in that case — a typo must not break a
|
||||
@@ -972,8 +930,7 @@ Such keys only exist on lanes that predate validation: `POST
|
||||
/api/lanes/:id/stage` now rejects a stage naming no node of the lane's pipeline
|
||||
with **400 `EBADSTAGE`** and lists the ones that exist. Aliases still resolve,
|
||||
so this catches typos and names borrowed from another template — `agenthub
|
||||
stage test` on a `ship-feature` lane, whose CI node is `gates` — which used to
|
||||
be stored, warned about on stderr, and then counted nowhere.
|
||||
stage test` on a template whose CI node is `gates` — which used to be stored, warned about on stderr, and then counted nowhere.
|
||||
|
||||
Every declaration is also appended to `lane_stage_events`, the append-only log
|
||||
the pass-rate report reads. `lanes.stages` describes the pass in flight and is
|
||||
@@ -1038,14 +995,11 @@ agenthub stage tests --evidence "Pre-push gate: all linters passing" --result pa
|
||||
|
||||
## Custom pipeline templates
|
||||
|
||||
The default pipeline is suitable for feature work: `intake` → `plan` → `implement` → `tests` → `review` → `gate` → `ship` → `done`. For different workflows (data pipelines, research phases, deployment stages), you can provide custom templates.
|
||||
AgentHub ships no template, so a custom template is the only way to turn the pipeline map, stage reporting and stage detection on. A template models whatever workflow you run — feature work, data pipelines, research phases, deployment stages.
|
||||
|
||||
### Where templates come from
|
||||
|
||||
1. **Built-in:** `server/data/pipelines/default.json` (always available)
|
||||
2. **User override:** directory specified by the `DASHBOARD_PIPELINES_DIR` environment variable
|
||||
|
||||
If both exist, **files in the user directory override built-ins** with the same `id`.
|
||||
Every `*.json` file in the directory named by the `DASHBOARD_PIPELINES_DIR` environment variable is loaded once at server start; a malformed file is skipped, never fatal. A template whose `id` is `default` becomes the pipeline of every lane that names no other.
|
||||
|
||||
### Custom template format
|
||||
|
||||
@@ -1070,14 +1024,15 @@ Create a JSON file in your custom pipeline directory (e.g., `~/.agenthub/pipelin
|
||||
|
||||
- **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. `"/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.
|
||||
- **skill** (string, optional): the slash command that drives this pipeline (e.g. `"/my-training-skill"`). Carried on every lane payload as `pipeline_skill`. Never executed by the dashboard.
|
||||
- **nodes** (array, required): list of pipeline stages, in order
|
||||
- **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 (`"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`)
|
||||
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) 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; use `"agenthub stage <id>"` for a node 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`). An alias may also name a **sub-state** — *why* the lane sits on a node — without adding a node to the map. Two constraints: no alias may collide with another node's id or alias (`phaseIdx` takes the FIRST match, so a duplicate silently resolves a declaration onto the wrong node), and all aliases of one node share one record slot (`stageRecords` resolves each declared key onto its node, and the canonical id's record wins), so a sub-state that needs its own `--evidence` has to be a real node.
|
||||
- **detect** (array, optional): stage-detection rules for this node — see "Stage detection → Where the rules live".
|
||||
|
||||
Then set the environment variable:
|
||||
|
||||
@@ -1091,7 +1046,7 @@ When you create a new lane, you can optionally specify which pipeline to use:
|
||||
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.)
|
||||
(The pipeline is selected at creation time. Lanes created without `--pipeline` default to the `default` pipeline — yours if you installed one with that id, otherwise the empty one.)
|
||||
|
||||
|
||||
## Per-feature state and archive
|
||||
@@ -1196,9 +1151,8 @@ leads the declared one (see "Viewing lanes" below), and `LaneStripCard.tsx`'s
|
||||
and the browser never disagree about which one is the headline. Both locate the
|
||||
declaration by the node the server already resolved it onto (the one whose
|
||||
`state` is `current`), never by looking `lane.stage` up as a node id: most of
|
||||
what the ship-feature skill declares is an ALIAS (`tests`, `booting`, `ship`,
|
||||
`gate-blocked`), which is not an id, and an id lookup scores it -1 — every
|
||||
detection, however far behind, would then read as leading.
|
||||
what a skill declares may be an ALIAS, which is not an id, and an id lookup
|
||||
scores it -1 — every detection, however far behind, would then read as leading.
|
||||
|
||||
### Which lane a signal is credited to
|
||||
|
||||
@@ -1241,7 +1195,7 @@ contain whole code blocks:
|
||||
Each rule is tested against these fields **one at a time**, never against a join
|
||||
of them. An `Agent` event really carries `description` + `prompt` +
|
||||
`subagent_type` together, so a rule anchored to the agent's identity —
|
||||
`{"tool": "Agent", "match": "^Explore$"}` in `ship-feature.json` — would never
|
||||
`{"tool": "Agent", "match": "^Explore$"}` — would never
|
||||
fire against a concatenation of all three, and a rule could otherwise match
|
||||
across the seam between two unrelated fields. Write anchored rules against the
|
||||
one field that identifies the tool call.
|
||||
@@ -1257,18 +1211,6 @@ to the flattened input. Either way the result is capped at 120 characters
|
||||
lane's tooltip sometimes shows a file path or a skill name rather than a shell
|
||||
command: it's whichever of the fields above the matching rule's `tool` carried.
|
||||
|
||||
### Superpowers skill invocations
|
||||
|
||||
The built-in `default` pipeline's `intake`, `plan`, `implement`, `review`, and
|
||||
`ship` nodes each carry a `{"tool": "Skill", "match": "..."}` rule matching the
|
||||
workflow skill names they expect (`brainstorming`, `writing-plans`,
|
||||
`executing-plans`/`subagent-driven-development`, Claude Code's own
|
||||
`code-review` plus `requesting-code-review`/`receiving-code-review`,
|
||||
`finishing-a-development-branch`). Invoking one of
|
||||
these skills is a much stronger signal than a matched Bash command, but it is
|
||||
still detection, not declaration — it renders dashed amber and never `done`,
|
||||
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 —
|
||||
@@ -1308,90 +1250,36 @@ read per call, so no restart is needed for a test harness that sets it.
|
||||
### Where the rules live
|
||||
|
||||
Detection rules are not code — they live in the pipeline template itself, as
|
||||
a `detect` array on each node. The built-in `default.json`
|
||||
(`server/data/pipelines/default.json`) ships these rules today, node by node:
|
||||
a `detect` array on each node. AgentHub ships no template, so no rule fires
|
||||
until you install one.
|
||||
|
||||
| Node | Detect rules |
|
||||
|---|---|
|
||||
| `intake` | `Agent` matching subagent `Explore` — a lane sitting at nothing else yet, but already exploring, shows *something* instead of idle; `Skill` matching `brainstorming`, which is intake's own declared command |
|
||||
| `plan` | `Skill` matching `writing-plans`; `Write` matching `docs/.*plan.*\.md`; `Agent` matching `Plan\|Software Architect\|Backend Architect\|UX Architect`; `TaskCreate` (any) |
|
||||
| `implement` | `Skill` matching `executing-plans\|subagent-driven-development\|test-driven-development`; `Edit` (any); `Write` to any path NOT under a `docs/` directory (`^(?!.*(?:^\|/)docs/)`) |
|
||||
| `tests` | `Bash` matching `\b(npm (run )?test\|pytest\|vitest\|jest\|go test\|cargo test\|node\s+--test\|rspec\|phpunit\|dotnet test\|mvn test\|gradle test)\b`; `Agent` matching `Test Results Analyzer\|API Tester\|Performance Benchmarker` |
|
||||
| `review` | `Skill` matching `code-review\|requesting-code-review\|receiving-code-review`; `Bash` matching `\bgh\b[^;&\|]*\bpr diff\b`; `Agent` matching a name containing `reviewer`/`Reviewer` (excluding `senior-gate-reviewer`, a lookbehind), `Evidence Collector`, or `Accessibility Auditor` |
|
||||
| `gate` | `Skill` matching `verification-before-completion`; `Agent` matching `senior-gate-reviewer\|Reality Checker` — entering the gate, never its verdict |
|
||||
| `ship` | `Bash` matching a `git push` **invocation** — `git`, then option groups, then `push` as the first non-flag token — or `gh … pr create` |
|
||||
| `done` | none — see below |
|
||||
A few lessons from rules that ran against real sessions, worth keeping when you
|
||||
write your own:
|
||||
|
||||
**`git diff` is deliberately NOT a review signal.** It was, and measured on a
|
||||
real session four `git diff --stat` runs — used to verify a generated file —
|
||||
outranked 81 test runs and 30 edits and pinned the lane at `review`, because
|
||||
`detect()` takes the LAST matching node and `recordDetection` is forward-only.
|
||||
Reading a diff is constant background activity, so it carries no stage
|
||||
information. `gh pr diff` stays: that one really is a review act.
|
||||
- **Detection is a safety net, not the mechanism.** Because `recordDetection`
|
||||
is forward-only AND never overrides a higher declared stage, a fix-loop
|
||||
re-entry that drops back a node is invisible to detection — only an
|
||||
`agenthub stage` declaration moves the lane back down. What detection adds is
|
||||
the stage an agent forgot to declare and the heartbeat that keeps a working
|
||||
lane from reading STALLED.
|
||||
- **Give a terminal node (`done`, `merged`) no rule.** Inference must never
|
||||
reach a terminal state on its own; that would let a heuristic assert an
|
||||
outcome instead of observing activity. A rule on a gate node should detect
|
||||
that the gate *activity* started, never its verdict.
|
||||
- **`git diff` is not a review signal.** Measured on a real session, four
|
||||
`git diff --stat` runs — used to verify a generated file — outranked 81 test
|
||||
runs and 30 edits and pinned the lane at `review`, because `detect()` takes
|
||||
the LAST matching node and detection is forward-only. Reading a diff is
|
||||
constant background activity; `gh pr diff` really is a review act.
|
||||
- **Match a `git push` *invocation*, not the word `push`.** `git[^;&|]*push`
|
||||
misses `git -c credential.helper='!f() { … }; f' push` (a `;` inside a quoted
|
||||
value), and `git[\s\S]*push` over-matches `git log --oneline -1 && echo "… push …"`.
|
||||
Consume option groups explicitly, treating a quoted option value as one unit,
|
||||
then require `push` as the first non-flag token — that accepts `git push` and
|
||||
`git -c core.hooksPath=/dev/null push` while rejecting `git commit -m "don't push"`,
|
||||
`docker push` and `npm run push-docs`.
|
||||
|
||||
**The `ship` pattern matches a git *invocation*, not the word `push`.** Two
|
||||
weaker forms were tried and both failed against real commands:
|
||||
|
||||
- `git[^;&|]*push` (same shell segment) **misses** this repo's own push, because
|
||||
`git -c credential.helper='!f() { … }; f' push` puts a `;` between the two
|
||||
inside a quoted value. Six real pushes went undetected that way.
|
||||
- `git[\s\S]*push` (anywhere after `git`) **over-matches**, and did so on a live
|
||||
lane within minutes: `git log --oneline -1 && echo "… push …"` pinned the lane
|
||||
at `ship`. Since `ship` sits near the end of the pipeline and detection is
|
||||
forward-only, that single over-read stuck — the same failure `git diff` caused
|
||||
for `review`.
|
||||
|
||||
So the pattern consumes option groups explicitly, treating a quoted option value
|
||||
as one unit, and then requires `push` to be the first non-flag token. That
|
||||
accepts `git push`, `git -c core.hooksPath=/dev/null push` and the credential
|
||||
-helper form, while rejecting `git log … "push"`, `git commit -m "don't push"`,
|
||||
`docker push`, and `npm run push-docs`.
|
||||
|
||||
The `ship-feature` template (`server/data/pipelines/ship-feature.json`) carries
|
||||
its own rules, aimed at the commands and Superpowers skills its driving skill
|
||||
actually runs:
|
||||
|
||||
| Node | Detect rules |
|
||||
|---|---|
|
||||
| `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 `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 `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` |
|
||||
| `watching-pr` | `Bash` matching `gh … pr view` |
|
||||
| `e2e-feature-passed`, `qc-plan`, `reported`, `merged`, `done` | none — declaration-only |
|
||||
|
||||
Two rules are deliberately absent from this template. There is **no `git diff`
|
||||
rule on `review`**, for the reason the `default` template learned the hard way
|
||||
below; the `code-review` skill invocation is the honest signal. And `merged` /
|
||||
`done` carry no rule at all, because inference must never reach a terminal
|
||||
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 `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
|
||||
`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
|
||||
state detection must never reach on its own, since inferring it would let a
|
||||
heuristic assert an outcome instead of observing activity. `intake` and `gate`
|
||||
now carry entry-activity rules (an `Explore` agent, a `senior-gate-reviewer`/
|
||||
`Reality Checker` agent or `verification-before-completion` skill) — these
|
||||
detect that the *activity* of exploring or gate-reviewing started, never a
|
||||
gate's pass/fail verdict, so the "no outcome inference" rule still holds.
|
||||
|
||||
To add a rule, drop a template into the directory named by the
|
||||
To add a rule, put a template in the directory named by the
|
||||
`DASHBOARD_PIPELINES_DIR` environment variable (see "Custom pipeline
|
||||
templates" above) with a `detect` array on the node you want to match:
|
||||
|
||||
@@ -1415,8 +1303,8 @@ Detection never rewinds a lane and never fights a declaration:
|
||||
- **Declared beats detected.** A detection is written only when the lane's
|
||||
*declared* stage index is strictly less than the detection's. A lane
|
||||
already declared at `review` ignores an `implement` detection outright.
|
||||
**One exception: a declaration on the pipeline's LAST node** (`done`, in both
|
||||
built-in templates) stops vetoing. The pipeline is finished there, so a later
|
||||
**One exception: a declaration on the pipeline's LAST node** (typically
|
||||
`done`) stops vetoing. The pipeline is finished there, so a later
|
||||
detection is new work rather than a stale rerun of what the claim covers —
|
||||
without the exception a lane left at `done` rejects every detection for the
|
||||
rest of its life, which reads as "auto-detect is broken". The exception
|
||||
@@ -1637,51 +1525,9 @@ The lane card shows a lock badge when the lane **holds** a named lock (its `lane
|
||||
- **Never delete the lock directory by hand.** A lock that outlives its holder (e.g. after a hard reboot) is cleaned up automatically — the next `acquire` on that name finds it stale (older than `LOCK_MAX_HOLD`) and breaks it. Deleting `LANES_ROOT/.locks/<name>/` yourself only races whoever else's `acquire` is about to do the same check safely.
|
||||
- **Never shrink `LOCK_MAX_HOLD` to force through a wait.** The 300-second floor exists specifically to stop this mistake. If the true holder is gone, the 300-second minimum wait is the price of safety. If the true holder is still running (e.g., a build with a network stall), shortening the timeout from 2700 to 300 does not help — it converts a slow success into a premature timeout, leaving the lock held and every other lane blocked forever.
|
||||
|
||||
## The ship-feature-lane skill (E1)
|
||||
## Lane tooling: QC boot, sync-base, MCP, integrations, CI
|
||||
|
||||
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/` the diff touches — the plugin-qualified `agenthub:backend-reviewer` / `agenthub:frontend-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 `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
|
||||
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 `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 `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
|
||||
|
||||
```bash
|
||||
# Inside a lane's working directory:
|
||||
/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 `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
|
||||
|
||||
**Stages implemented now:**
|
||||
- **0-8**: Intake & frontloaded Q&A → plan → implementation → independent unit tests (1b) → gates+preflight → e2e on feature branch → code review → QC plan → QC → senior gate → publish PR
|
||||
- **10-12**: CI watch on the PR → report → watch PR until merge (gated fix-loop re-entries on review feedback)
|
||||
- **14**: Done
|
||||
|
||||
**Stages currently hardcoded skipped** (pending F's integrations):
|
||||
- **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 `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 `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.
|
||||
The commands below are primitives a driving session (or you) calls explicitly — AgentHub never sequences them.
|
||||
|
||||
### QC boot flag and profile integration
|
||||
|
||||
@@ -1704,7 +1550,7 @@ These environment variables are passed to the profile's `boot` hook during `agen
|
||||
|
||||
### Dev preflight and merge safety: sync-base
|
||||
|
||||
`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:
|
||||
`agenthub lanes sync-base` is the ONE sanctioned merge a session runs from the CLI — `origin/development` into a feature branch. Three modes:
|
||||
|
||||
```bash
|
||||
agenthub lanes sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
|
||||
@@ -1724,27 +1570,9 @@ GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
|
||||
|
||||
`MIGRATIONS_DIR` enables the collision preflight against a numbered-migrations directory. `GENERATED_MERGE_PATHS` gives the listed files a keep-ours merge driver (never hand-merged) and folds the profile's `regen` hook output into the sync commit — the single most common cross-lane conflict, for a repo that generates an API contract/client.
|
||||
|
||||
### 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 `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
|
||||
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 `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 nothing** (`installed: []`, and
|
||||
the Tools menu hides it): Codex never reads `.claude/agents/`, and codex lanes
|
||||
carry no pipeline to drive.
|
||||
|
||||
**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.
|
||||
|
||||
**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet — `ticketer` is invoked by Stage 9 (skipped), and `dev-qc` by Stage 13's dev-QC half (unavailable); `pr-reviewer` isn't referenced anywhere in this skill's text.
|
||||
|
||||
### 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. `agenthub lanes mcp sync` gives it those:
|
||||
A lane needs the same MCP servers (Playwright, a local-QC server) as its source repo to run e2e or browser QC. `agenthub lanes mcp sync` gives it those:
|
||||
|
||||
```bash
|
||||
agenthub lanes mcp sync
|
||||
@@ -1766,11 +1594,11 @@ A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED
|
||||
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 `agenthub lanes ci status` (added — see the command reference below); Stage 13's CI-wait half now honors it for real. The CLI command and `GET /api/lanes/:id/integrations/:name` are the only readouts: the lane card used to carry a row of three badges for these, and it no longer does — two of the three could never be anything but decoration, and a badge that reports a toggle nothing acts on costs a row of card height to say nothing.
|
||||
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. Nothing in AgentHub acts on an enabled `tracker` or `dev_qc` toggle. `ci_wait`'s consumer is `agenthub lanes ci status` (below). The CLI command and `GET /api/lanes/:id/integrations/:name` are the only readouts: the lane card used to carry a row of three badges for these, and it no longer does — two of the three could never be anything but decoration, and a badge that reports a toggle nothing acts on costs a row of card height to say nothing.
|
||||
|
||||
### 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:
|
||||
Check GitHub Actions status for a commit SHA or branch ref — one shot, so a session waiting on CI polls it:
|
||||
|
||||
```bash
|
||||
agenthub lanes ci status <branch-or-sha> [<id>]
|
||||
@@ -1778,87 +1606,6 @@ 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.
|
||||
|
||||
### Pipeline template: ship-feature (16 node stages)
|
||||
|
||||
The skill uses the `ship-feature` pipeline template, which defines the following 16 stages (node IDs):
|
||||
|
||||
1. **intake** — frontloaded Q&A and feature activation
|
||||
2. **plan** — investigation and plan debate
|
||||
3. **implementing** — TDD implementation
|
||||
4. **gates** — CI gates + preflight check
|
||||
5. **e2e-feature** — run e2e on feature branch
|
||||
6. **e2e-feature-passed** — e2e result gate
|
||||
7. **review** — code review
|
||||
8. **qc-plan** — bound QC scope
|
||||
9. **qc** — browser QC (agent-gated until qc-local lands)
|
||||
10. **gate** — senior GO/NO-GO gate (agent-gated until senior-gate-reviewer lands)
|
||||
11. **publishing** — push branch + open/update PR
|
||||
12. **pr-open** — PR open and published
|
||||
13. **reported** — report posted
|
||||
14. **watching-pr** — watch for comments, conflicts, merge
|
||||
15. **merged** — PR merged, post-verify transition
|
||||
16. **done** — pipeline complete
|
||||
|
||||
The Workspace page (`/run`) displays this template with nodes rendered in five states: `failed` (rejected), `current` (now), `done` (with evidence), `passed-no-evidence` (claimed or skipped), and `pending` (not reached).
|
||||
|
||||
### More stage names than nodes
|
||||
|
||||
Each node carries aliases, and they do two different jobs. Some absorb a
|
||||
near-miss (`implement` for `implementing`). Others are **sub-states**: a name
|
||||
the skill declares to say *why* the lane is sitting on a node, without adding a
|
||||
node to the map.
|
||||
|
||||
This is the shape Shipyard converged on — its dashboard renders 13 nodes while
|
||||
its `PHASES` table folds roughly 35 stage names onto them. A pipeline map is
|
||||
read at a glance across many lanes at once, so it stays coarse; the stage name
|
||||
is read one lane at a time, so it can be specific.
|
||||
|
||||
| Node | Near-miss aliases | Sub-states the skill declares |
|
||||
|---|---|---|
|
||||
| `intake` | `assigned`, `claimed`, `start` | `bootstrapping` |
|
||||
| `plan` | `planning`, `brainstorm`, `design` | — |
|
||||
| `implementing` | `implement`, `coding`, `build` | — |
|
||||
| `gates` | `pre-push-gate`, `tests` | `migration-collision`, `sync-conflict` |
|
||||
| `e2e-feature` | `e2e`, `live` | `booting`, `e2e-scoped` |
|
||||
| `e2e-feature-passed` | `e2e-passed` | — |
|
||||
| `review` | `reviewing`, `code-review`, `self-review` | — |
|
||||
| `qc-plan` | — | — |
|
||||
| `qc` | — | — |
|
||||
| `gate` | `sr-gate`, `verify`, `verification` | `gate-blocked` |
|
||||
| `publishing` | `push` | — |
|
||||
| `pr-open` | `ship`, `pr`, `push-conflict` | `push-revalidate` |
|
||||
| `reported` | — | — |
|
||||
| `watching-pr` | — | `pr-comment-fix` |
|
||||
| `merged` | — | — |
|
||||
| `done` | `complete`, `completed` | — |
|
||||
|
||||
50 names over 16 nodes. Every one of them has a **source**: the skill declares
|
||||
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
|
||||
`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.
|
||||
|
||||
Two constraints on this:
|
||||
|
||||
- **No alias may collide** with another node's id or alias. `phaseIdx` takes the
|
||||
FIRST match, so a duplicate would silently resolve a declaration onto the
|
||||
wrong node. A test asserts the whole template is collision-free.
|
||||
- **Aliases of one node share one record slot.** `lane.stages` is keyed by the
|
||||
declared string, and `stageRecords` resolves each key onto its node — so
|
||||
declaring `migration-collision` and then `gates` leaves ONE record for that
|
||||
node (the canonical `gates` one wins). A sub-state that needs to keep its own
|
||||
`--evidence` separately has to be a real node, not an alias.
|
||||
|
||||
`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. 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 AgentHub does NOT do
|
||||
|
||||
**AgentHub does not chain, queue, retry, or evaluate gates.**
|
||||
|
||||
+11
-73
@@ -87,27 +87,22 @@ checkout unnecessary.
|
||||
| The `agenthub` CLI | a launcher written to `~/.local/bin/agenthub` |
|
||||
| The dashboard UI (`/run` and every other client route) | built into the runtime dir by the bootstrap, so it works the moment `claude` starts |
|
||||
| `/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`, `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`) | `agents/` at the repo root, auto-discovered the same way — exposed as `agenthub:<name>` |
|
||||
| The skills (`repo-onboarding`, `debug-live-issue`, `update-project-docs`, `file-headers`) | `skills/` at the repo root — which IS the plugin root, so they are auto-discovered as plugin skills |
|
||||
|
||||
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. `agenthub skills install` survives
|
||||
as the fallback for a plain checkout without the plugin.
|
||||
useless for a lane working in a different repo.
|
||||
|
||||
**The agents ship the same way, from the same files.** `agents/` at the repo
|
||||
root holds all four (`qc-local`, `senior-gate-reviewer`, `backend-reviewer`,
|
||||
`frontend-reviewer`), so the plugin exposes them as
|
||||
`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 `agenthub:` form. On a `codex` lane the same command installs nothing —
|
||||
Codex never reads `.claude/agents/`, and codex lanes carry no pipeline.
|
||||
That is the whole plugin: the hooks, the CLI, the dashboard, three commands and
|
||||
four skills. It ships **no subagents** and no pipeline template, and it neither
|
||||
depends on nor installs any other plugin (Superpowers included). The pipeline
|
||||
slash commands, the `ship-feature` / `ship-feature-lane` skills and the
|
||||
`qc-local` / `senior-gate-reviewer` / `backend-reviewer` / `frontend-reviewer`
|
||||
agents were removed; the stage/pipeline machinery they drove stays in the
|
||||
dashboard, dormant until you install a template (see `docs/LANES.md` §
|
||||
"Pipeline stages and the five node states").
|
||||
|
||||
Because the plugin ships the hooks itself, `npm run install-hooks` is not needed
|
||||
for plugin users — and must not be run alongside it. Events carry no id, so two
|
||||
@@ -315,66 +310,9 @@ courtesy of `Restart=always`.
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `/agenthub-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, **systemd** (`WARN` when the unit names a retired plugin root — the dashboard will not survive a reboot — or when it is installed but stopped; no unit at all is fine), **tmux** (`WARN` when absent — only the run console, `agenthub lanes shell` and session reloads need it), **skill plugins** (see below), UI bundle |
|
||||
| `/agenthub-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, **systemd** (`WARN` when the unit names a retired plugin root — the dashboard will not survive a reboot — or when it is installed but stopped; no unit at all is fine), **tmux** (`WARN` when absent — only the run console, `agenthub lanes shell` and session reloads need it), 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 `/agenthub:ship-feature` and `/agenthub:ship-feature-lane`
|
||||
drive step by step (`brainstorming`, `writing-plans`,
|
||||
`test-driven-development`, `verification-before-completion`,
|
||||
`finishing-a-development-branch`). The `review` node is the exception: it names
|
||||
the bare `/code-review`, Claude Code's own review skill, which carries no
|
||||
`plugin:` prefix and so requires nothing to be installed. Install it
|
||||
with `claude plugin install superpowers@claude-plugins-official`.
|
||||
|
||||
The row is a **WARN, never a FAIL**: the dashboard, hooks and CLI all work
|
||||
without it, and only those two pipeline skills stop partway. A plugin listed but
|
||||
explicitly disabled counts as missing — Claude Code will not resolve its skills.
|
||||
The skills present as plain user skills under `~/.claude/skills/` count as
|
||||
satisfied, so a hand-copied setup is not reported as broken. A custom template
|
||||
in `DASHBOARD_PIPELINES_DIR` naming some other plugin's command is checked the
|
||||
same way.
|
||||
|
||||
Both the derivation and the missing/satisfied rules live in
|
||||
`server/lib/skill-plugins.js`, shared with the bootstrap below so the row and
|
||||
the install can never disagree about what is missing.
|
||||
|
||||
#### Automatic install, and why it is allowlisted
|
||||
|
||||
`plugin-bootstrap.js` installs the missing plugins itself, at the very end of
|
||||
its **detached worker** — never in the 10 s `SessionStart` hook, because the
|
||||
install clones from the network. It runs `claude plugin install
|
||||
superpowers@claude-plugins-official --scope user -y`; if that fails it registers
|
||||
the marketplace (`claude plugin marketplace add anthropics/claude-plugins-official`)
|
||||
and retries exactly once. Failure is logged with the manual command and nothing
|
||||
throws — the dashboard, hooks and CLI 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. `/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
|
||||
the worker is detached and cannot tell the session that started it.
|
||||
|
||||
**Reporting is derived, installing is allowlisted.** The row above reads plugin
|
||||
names out of the templates, which is right for a report. Installing from the
|
||||
same source would not be: a template is a file anyone can drop into
|
||||
`DASHBOARD_PIPELINES_DIR`, so it would turn "edit a JSON file" into "run this
|
||||
vendor's plugin install command on the next session". Auto-install therefore
|
||||
consults `AUTO_INSTALLABLE`, a fixed map in `server/lib/skill-plugins.js`, and
|
||||
skips every other requirement — visible in the doctor, never installed. Adding
|
||||
an entry there is a decision to install someone else's code on a user's machine
|
||||
without asking; it belongs in a review, not in a config file.
|
||||
|
||||
### Where things live
|
||||
|
||||
|
||||
@@ -25,12 +25,6 @@ Print the output verbatim, then add at most three lines of guidance based on the
|
||||
- `FAIL npm` or `FAIL Disk` → nothing else in the report can be trusted until
|
||||
these are fixed; both silently truncate installs.
|
||||
- `WARN agenthub CLI` → print the exact `export PATH=...` line from the output.
|
||||
- `WARN Skill plugins` → the pipelines name slash commands no installed plugin
|
||||
provides. The dashboard is unaffected; `/agenthub:ship-feature(-lane)` is what
|
||||
stops partway. The bootstrap normally installs these itself, so this row also
|
||||
means that attempt has not run or did not succeed — say `/agenthub-update` retries
|
||||
it, and print the `claude plugin install …` line from the output as the manual
|
||||
route.
|
||||
- `WARN systemd` → when it says the unit names a **previous plugin version**,
|
||||
the dashboard is running now but will NOT come back after a reboot. Starting
|
||||
a new Claude Code session repairs it; print the `--force` command from the
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Declare the lane done
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `done` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node done $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Declare e2e passed (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `e2e-passed` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node e2e-passed $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: E2e on the feature branch (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `e2e` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node e2e $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Senior GO/NO-GO gate
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `gate` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node gate $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Implement (TDD)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `implement` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node implement $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Intake — brainstorm the requirement
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `intake` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node intake $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Declare merged (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `merged` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node merged $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Plan — write the implementation plan
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `plan` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node plan $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Open the PR
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `pr-open` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node pr-open $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Publish the branch
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `publishing` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node publishing $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Declare the QC plan (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `qc-plan` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node qc-plan $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Browser QC + proof gallery (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `qc` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node qc $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Declare the run reported (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `reported` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node reported $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Code review
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `review` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node review $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Run the full 16-stage lane pipeline (implement → gates → e2e → review → QC → senior gate → PR, watched until merged)
|
||||
argument-hint: <requirement>
|
||||
allowed-tools: Bash(agenthub lanes pipeline:*)
|
||||
---
|
||||
|
||||
Put this lane on the 16-node `ship-feature` template first, so the dashboard
|
||||
renders the right node count from the first stage rather than after the skill
|
||||
self-corrects:
|
||||
|
||||
!`agenthub lanes pipeline ship-feature`
|
||||
|
||||
Then run the pipeline **in this session** by invoking `/agenthub:ship-feature-lane`
|
||||
with this requirement:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
- No requirement given → ask for one. Never guess what to build.
|
||||
- The switch above failed (no lane owns this directory) → say so and stop; the
|
||||
skill has no lane to report stages against.
|
||||
|
||||
This is the in-session twin of `agenthub ship-full "<requirement>"`, which does the
|
||||
same two steps from a terminal by spawning its own `claude`.
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Run the 8-stage in-repo pipeline (plan → implement → tests → review → gate → ship). No PR, no browser QC
|
||||
argument-hint: <requirement>
|
||||
allowed-tools: Bash(agenthub lanes pipeline:*)
|
||||
---
|
||||
|
||||
Put this lane on the 8-node `default` template first, so the dashboard renders
|
||||
the right node count from the first stage:
|
||||
|
||||
!`agenthub lanes pipeline default`
|
||||
|
||||
Then run the pipeline **in this session** by invoking `/agenthub:ship-feature` with
|
||||
this requirement:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
- No requirement given → ask for one. Never guess what to build.
|
||||
- The switch above failed (no lane owns this directory) → say so and stop; the
|
||||
skill has no lane to report stages against.
|
||||
|
||||
This is the in-session twin of `agenthub ship-quick "<requirement>"`. It stops at
|
||||
review/gate: no PR is opened and no browser QC runs — use `/ship-full` when the
|
||||
work should reach reviewers.
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Tests / pre-push CI gates
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `tests` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node tests $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Watch the PR / CI (ship-feature only)
|
||||
argument-hint: [extra prompt words]
|
||||
allowed-tools: Bash(agenthub lanes node:*)
|
||||
---
|
||||
|
||||
The command this lane pipeline template declares for its `watching-pr` node, resolved
|
||||
against whichever template the lane is actually on (matched by id or alias):
|
||||
|
||||
!`agenthub lanes node watching-pr $ARGUMENTS --print`
|
||||
|
||||
Run that here, in THIS session - never start a nested `claude` process:
|
||||
|
||||
- Line reads `cd <dir> && "claude" "<slash command>"` -> invoke that slash
|
||||
command yourself, passing any extra words as its argument.
|
||||
- Anything else is a shell argv -> run it with Bash from `<dir>`.
|
||||
- Line is an error (`no node ...`, `declares no command`, no lane owns this
|
||||
directory) -> print it verbatim and stop. Do not guess a substitute.
|
||||
|
||||
Do NOT call `agenthub stage` afterwards: a command running is not proof the node
|
||||
passed. Declare the stage when the work is actually done. (The exception is a
|
||||
node whose command already IS `agenthub stage <id>` - there the declaration is the
|
||||
whole step.)
|
||||
@@ -161,10 +161,6 @@ function fastPathOk(rt = runtimeDir()) {
|
||||
if (state.pluginRoot !== PLUGIN_ROOT) return false;
|
||||
if (state.depsHash !== depsHash()) return false;
|
||||
if (!fs.existsSync(path.join(rt, "node_modules"))) return false;
|
||||
// A machine can be fully set up and still be missing the plugins the
|
||||
// pipelines drive. Reading one recorded flag keeps this hot path cheap — the
|
||||
// actual check (manifest + every user SKILL.md) runs once, in the worker.
|
||||
if (!state.skillPluginsSettled) return false;
|
||||
// A live server says nothing about whether it will come back. When our unit
|
||||
// still names a previous plugin cache directory, everything looks healthy
|
||||
// until the next reboot — and then the dashboard is simply gone, from a path
|
||||
@@ -500,46 +496,6 @@ function log(rt, line) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the plugins AgentHub's pipelines drive but this machine cannot resolve —
|
||||
* today `superpowers`, which `/agenthub:ship-feature(-lane)` calls step by step. A
|
||||
* fresh `agenthub` install otherwise runs those skills partway and fails at the
|
||||
* first call with no warning beforehand.
|
||||
*
|
||||
* Off by setting `AGENTHUB_AUTO_INSTALL_PLUGINS` to `0`, `false` or `off`; that
|
||||
* leaves `/agenthub-doctor`'s WARN row as the only notice, which is the right
|
||||
* behaviour for anyone who wants to choose their own plugins.
|
||||
*
|
||||
* One-shot: the outcome is recorded in the runtime state and the fast path
|
||||
* takes it into account, so a machine with no network does not re-attempt a
|
||||
* clone on every session. `/agenthub-update` (`--force`) retries.
|
||||
*
|
||||
* Best-effort throughout. Nothing here may throw or block: the dashboard,
|
||||
* hooks and CLI all work without these plugins.
|
||||
*
|
||||
* @returns {"off"|"nothing-missing"|"installed"|"failed"|"error"} or the previously
|
||||
* recorded outcome when this run had nothing to settle.
|
||||
*/
|
||||
function autoInstallSkillPlugins(rt, { force = false } = {}) {
|
||||
const flag = process.env.AGENTHUB_AUTO_INSTALL_PLUGINS;
|
||||
if (flag === "0" || flag === "false" || flag === "off") return "off";
|
||||
// Return the PREVIOUS outcome, not a marker: writeState records whatever this
|
||||
// returns, and overwriting "installed" with "already-settled" would lose the
|
||||
// only record of what actually happened.
|
||||
const settled = readState(rt)?.skillPluginsSettled;
|
||||
if (!force && settled) return settled;
|
||||
|
||||
try {
|
||||
const { autoInstallSkillPlugins: install } = require("../server/lib/skill-plugins");
|
||||
const summary = install((line) => log(rt, line));
|
||||
if (!summary.attempted.length) return "nothing-missing";
|
||||
return summary.failed.length ? "failed" : "installed";
|
||||
} catch (err) {
|
||||
log(rt, `skill-plugin auto-install skipped: ${err.message}`);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ plugin-cache pruning */
|
||||
|
||||
/** How many retired builds to leave behind, newest first, as a rollback cushion. */
|
||||
@@ -814,21 +770,12 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Last, and deliberately so: it clones from the network, and nothing above
|
||||
// may wait on it. Runs here rather than in the foreground pass because this
|
||||
// whole function is already detached from the 10 s SessionStart hook.
|
||||
const skillPlugins = autoInstallSkillPlugins(rt, { force });
|
||||
|
||||
writeState(
|
||||
{
|
||||
pluginVersion: pluginVersion(),
|
||||
depsHash: wantDeps,
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
claudeHome: getClaudeHome(),
|
||||
// Settles the fast path: whatever the outcome, the next SessionStart
|
||||
// must not spawn a worker just to retry a network clone. Cleared by
|
||||
// `/agenthub-update` (force), which is the retry.
|
||||
skillPluginsSettled: skillPlugins,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
rt
|
||||
@@ -930,6 +877,5 @@ module.exports = {
|
||||
livePids,
|
||||
stopDashboard,
|
||||
startDashboard,
|
||||
autoInstallSkillPlugins,
|
||||
bootstrap,
|
||||
};
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
* declared package, not just that node_modules exists), server liveness,
|
||||
* duplicate hook entries (the one failure that silently doubles every token and
|
||||
* cost figure), the `agenthub` CLI launcher and its PATH, the `tmux` binary
|
||||
* every browser-started run depends on, whether the slash
|
||||
* commands the pipeline templates name as node drivers are actually resolvable
|
||||
* (the `superpowers` plugin, today), and the dashboard bundle — quoting the last
|
||||
* build error when it is missing.
|
||||
* every browser-started run depends on, and the dashboard bundle — quoting the
|
||||
* last build error when it is missing.
|
||||
* Read-only — it diagnoses, it never repairs.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -85,7 +83,6 @@ function diagnose() {
|
||||
add(...cliStatus());
|
||||
add(...systemdStatus());
|
||||
add(...tmuxStatus());
|
||||
add(...skillPluginStatus());
|
||||
|
||||
add(...dashboardUiStatus(rt));
|
||||
|
||||
@@ -190,37 +187,6 @@ function lastBuildError(logPath) {
|
||||
return hit ? hit.trim().slice(0, 200) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* WARN (never FAIL) when a plugin the pipelines depend on cannot be resolved:
|
||||
* the dashboard, hooks and CLI all work without it — only the pipeline
|
||||
* skills that drive it stop partway, so this must not fail a doctor run.
|
||||
*
|
||||
* The requirement derivation and the missing/satisfied rules live in
|
||||
* `server/lib/skill-plugins.js`, shared with the bootstrap that installs them.
|
||||
*/
|
||||
function skillPluginStatus() {
|
||||
const skillPlugins = require("../server/lib/skill-plugins");
|
||||
const required = skillPlugins.requiredSkillPlugins();
|
||||
if (!required.size) return ["ok", "Skill plugins", "no pipeline names an external plugin"];
|
||||
|
||||
let missing;
|
||||
try {
|
||||
missing = skillPlugins.missingSkillPlugins();
|
||||
} catch {
|
||||
return ["warn", "Skill plugins", "could not read the installed-plugin manifest"];
|
||||
}
|
||||
if (!missing.length) {
|
||||
return ["ok", "Skill plugins", `${[...required.keys()].sort().join(", ")} available`];
|
||||
}
|
||||
return [
|
||||
"warn",
|
||||
"Skill plugins",
|
||||
`${missing.map((m) => `${m.name} (needed for ${m.skills.join(", ")})`).join("; ")} — ` +
|
||||
`the pipelines name these commands; install with ` +
|
||||
`claude plugin install ${missing.map((m) => m.autoInstall?.spec || m.name).join(" ")}`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The failure this row exists for is silent by construction: a unit still
|
||||
* naming a retired plugin cache directory keeps a running dashboard looking
|
||||
@@ -275,8 +241,8 @@ function systemdStatus() {
|
||||
* WARN (never FAIL) when `tmux` is absent: every run the dashboard starts is a
|
||||
* tmux session (`server/lib/tmux.js`), so without the binary the run console,
|
||||
* `agenthub lanes shell` and the session-reload keystrokes all fail — while
|
||||
* sessions, analytics, lanes and the CLI keep working. Same reasoning as the
|
||||
* skill-plugins row: a partial feature loss must not fail a doctor run.
|
||||
* sessions, analytics, lanes and the CLI keep working. A partial feature
|
||||
* loss must not fail a doctor run.
|
||||
*
|
||||
* This checks the doctor's own PATH. A server started as a systemd `--user`
|
||||
* service can still see a different one — `GET /api/run/tmux` is the server's
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
* (including the pane the test was launched from). Dropping `TMUX` and
|
||||
* pointing `TMUX_TMPDIR` at a fresh directory gives every tmux call — the
|
||||
* app's own and the tests' direct `execFileSync("tmux", …)` — its own socket.
|
||||
* It also points `DASHBOARD_PIPELINES_DIR` at `server/__tests__/fixtures/
|
||||
* pipelines/`: AgentHub ships no pipeline template any more, and the two old
|
||||
* ones kept there are what the dormant stage/detection code is tested against.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -26,7 +29,11 @@ const files = fs
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const tmuxDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-test-tmux-"));
|
||||
const env = { ...process.env, TMUX_TMPDIR: tmuxDir };
|
||||
const env = {
|
||||
...process.env,
|
||||
TMUX_TMPDIR: tmuxDir,
|
||||
DASHBOARD_PIPELINES_DIR: path.join(dir, "fixtures", "pipelines"),
|
||||
};
|
||||
delete env.TMUX;
|
||||
delete env.TMUX_PANE;
|
||||
|
||||
|
||||
+8
-44
@@ -9,11 +9,7 @@
|
||||
"icon": "📝",
|
||||
"gate": false,
|
||||
"command": "/superpowers:brainstorming",
|
||||
"aliases": [
|
||||
"assigned",
|
||||
"claimed",
|
||||
"start"
|
||||
],
|
||||
"aliases": ["assigned", "claimed", "start"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Agent",
|
||||
@@ -31,11 +27,7 @@
|
||||
"icon": "🧭",
|
||||
"gate": false,
|
||||
"command": "/superpowers:writing-plans",
|
||||
"aliases": [
|
||||
"planning",
|
||||
"brainstorm",
|
||||
"design"
|
||||
],
|
||||
"aliases": ["planning", "brainstorm", "design"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -60,11 +52,7 @@
|
||||
"icon": "🛠",
|
||||
"gate": false,
|
||||
"command": "/superpowers:test-driven-development",
|
||||
"aliases": [
|
||||
"implementing",
|
||||
"coding",
|
||||
"build"
|
||||
],
|
||||
"aliases": ["implementing", "coding", "build"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -85,12 +73,7 @@
|
||||
"icon": "🧪",
|
||||
"gate": true,
|
||||
"command": "npm test",
|
||||
"aliases": [
|
||||
"testing",
|
||||
"unit",
|
||||
"gates",
|
||||
"pre-push-gate"
|
||||
],
|
||||
"aliases": ["testing", "unit", "gates", "pre-push-gate"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Bash",
|
||||
@@ -108,11 +91,7 @@
|
||||
"icon": "👀",
|
||||
"gate": true,
|
||||
"command": "/code-review",
|
||||
"aliases": [
|
||||
"reviewing",
|
||||
"code-review",
|
||||
"self-review"
|
||||
],
|
||||
"aliases": ["reviewing", "code-review", "self-review"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -134,12 +113,7 @@
|
||||
"icon": "🚦",
|
||||
"gate": true,
|
||||
"command": "/superpowers:verification-before-completion",
|
||||
"aliases": [
|
||||
"verify",
|
||||
"verification",
|
||||
"sr-gate",
|
||||
"gate-blocked"
|
||||
],
|
||||
"aliases": ["verify", "verification", "sr-gate", "gate-blocked"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -157,13 +131,7 @@
|
||||
"icon": "🔀",
|
||||
"gate": false,
|
||||
"command": "/superpowers:finishing-a-development-branch",
|
||||
"aliases": [
|
||||
"pr",
|
||||
"pr-open",
|
||||
"publishing",
|
||||
"commit",
|
||||
"push"
|
||||
],
|
||||
"aliases": ["pr", "pr-open", "publishing", "commit", "push"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -181,11 +149,7 @@
|
||||
"icon": "✅",
|
||||
"gate": false,
|
||||
"command": "agenthub stage done",
|
||||
"aliases": [
|
||||
"complete",
|
||||
"completed",
|
||||
"merged"
|
||||
]
|
||||
"aliases": ["complete", "completed", "merged"]
|
||||
}
|
||||
]
|
||||
}
|
||||
+12
-58
@@ -9,12 +9,7 @@
|
||||
"icon": "📝",
|
||||
"gate": false,
|
||||
"command": "/superpowers:brainstorming",
|
||||
"aliases": [
|
||||
"assigned",
|
||||
"claimed",
|
||||
"start",
|
||||
"bootstrapping"
|
||||
],
|
||||
"aliases": ["assigned", "claimed", "start", "bootstrapping"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -36,11 +31,7 @@
|
||||
"icon": "🧭",
|
||||
"gate": false,
|
||||
"command": "/superpowers:writing-plans",
|
||||
"aliases": [
|
||||
"planning",
|
||||
"brainstorm",
|
||||
"design"
|
||||
],
|
||||
"aliases": ["planning", "brainstorm", "design"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -65,11 +56,7 @@
|
||||
"icon": "🛠",
|
||||
"gate": false,
|
||||
"command": "/superpowers:test-driven-development",
|
||||
"aliases": [
|
||||
"implement",
|
||||
"coding",
|
||||
"build"
|
||||
],
|
||||
"aliases": ["implement", "coding", "build"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -91,12 +78,7 @@
|
||||
"icon": "🧪",
|
||||
"gate": true,
|
||||
"command": "agenthub lanes hook ci-gate",
|
||||
"aliases": [
|
||||
"pre-push-gate",
|
||||
"tests",
|
||||
"migration-collision",
|
||||
"sync-conflict"
|
||||
],
|
||||
"aliases": ["pre-push-gate", "tests", "migration-collision", "sync-conflict"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Bash",
|
||||
@@ -114,12 +96,7 @@
|
||||
"icon": "🧪",
|
||||
"gate": false,
|
||||
"command": "agenthub lanes up --qc",
|
||||
"aliases": [
|
||||
"e2e",
|
||||
"e2e-scoped",
|
||||
"booting",
|
||||
"live"
|
||||
],
|
||||
"aliases": ["e2e", "e2e-scoped", "booting", "live"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Bash",
|
||||
@@ -133,9 +110,7 @@
|
||||
"icon": "🧪",
|
||||
"gate": true,
|
||||
"command": "agenthub stage e2e-feature-passed",
|
||||
"aliases": [
|
||||
"e2e-passed"
|
||||
]
|
||||
"aliases": ["e2e-passed"]
|
||||
},
|
||||
{
|
||||
"id": "review",
|
||||
@@ -143,11 +118,7 @@
|
||||
"icon": "👀",
|
||||
"gate": true,
|
||||
"command": "/code-review",
|
||||
"aliases": [
|
||||
"reviewing",
|
||||
"code-review",
|
||||
"self-review"
|
||||
],
|
||||
"aliases": ["reviewing", "code-review", "self-review"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -191,12 +162,7 @@
|
||||
"icon": "🚦",
|
||||
"gate": true,
|
||||
"command": "/superpowers:verification-before-completion",
|
||||
"aliases": [
|
||||
"sr-gate",
|
||||
"verify",
|
||||
"verification",
|
||||
"gate-blocked"
|
||||
],
|
||||
"aliases": ["sr-gate", "verify", "verification", "gate-blocked"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Agent",
|
||||
@@ -214,9 +180,7 @@
|
||||
"icon": "🔀",
|
||||
"gate": false,
|
||||
"command": "/superpowers:finishing-a-development-branch",
|
||||
"aliases": [
|
||||
"push"
|
||||
],
|
||||
"aliases": ["push"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
@@ -234,12 +198,7 @@
|
||||
"icon": "🔀",
|
||||
"gate": false,
|
||||
"command": "gh pr create --base development",
|
||||
"aliases": [
|
||||
"ship",
|
||||
"pr",
|
||||
"push-conflict",
|
||||
"push-revalidate"
|
||||
],
|
||||
"aliases": ["ship", "pr", "push-conflict", "push-revalidate"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Bash",
|
||||
@@ -261,9 +220,7 @@
|
||||
"icon": "👁",
|
||||
"gate": false,
|
||||
"command": "agenthub lanes ci status",
|
||||
"aliases": [
|
||||
"pr-comment-fix"
|
||||
],
|
||||
"aliases": ["pr-comment-fix"],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Bash",
|
||||
@@ -285,10 +242,7 @@
|
||||
"icon": "✅",
|
||||
"gate": false,
|
||||
"command": "agenthub stage done",
|
||||
"aliases": [
|
||||
"complete",
|
||||
"completed"
|
||||
]
|
||||
"aliases": ["complete", "completed"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* @file Tests for lane-agents.js: agent-template installation into a lane's
|
||||
* .claude/agents/, including path resolution, git-exclude idempotence, and
|
||||
* missing-repo rejection.
|
||||
* @file Tests for lane-agents.js's removeStageDoc and the codex pipeline
|
||||
* cleanup it pairs with (clearCodexPipelines).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -18,7 +17,7 @@ const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const lanesLib = require("../lib/lanes");
|
||||
const { installAgents, removeStageDoc } = require("../lib/lane-agents");
|
||||
const { removeStageDoc } = require("../lib/lane-agents");
|
||||
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
|
||||
@@ -48,87 +47,6 @@ function makeRepoDir() {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/** Create a lane record backed by an existing directory. */
|
||||
function makeLane(cwd) {
|
||||
return lanesLib.createLane({ title: "agents-test", cwd, kind: "managed" });
|
||||
}
|
||||
|
||||
describe("installAgents", () => {
|
||||
it("copies agent templates from agents/ dir into lane's .claude/agents/", async () => {
|
||||
const cwd = makeRepoDir();
|
||||
const lane = makeLane(cwd);
|
||||
const result = await installAgents(lane);
|
||||
|
||||
assert.deepEqual([...result.installed].sort(), ["qc-local.md", "senior-gate-reviewer.md"]);
|
||||
assert.ok(fs.existsSync(path.join(cwd, ".claude", "agents", "qc-local.md")));
|
||||
assert.ok(fs.existsSync(path.join(cwd, ".claude", "agents", "senior-gate-reviewer.md")));
|
||||
|
||||
// Verify the source path is new (agents/) not old (data/agent-templates/)
|
||||
const oldPath = path.join(__dirname, "..", "data", "agent-templates", "ship-feature-lane");
|
||||
assert.equal(fs.existsSync(oldPath), false);
|
||||
});
|
||||
|
||||
it("throws ENOTGITREPO when lane cwd has no .git", async () => {
|
||||
const cwd = path.join(SUITE_ROOT, "no-git");
|
||||
fs.mkdirSync(cwd, { recursive: true });
|
||||
const lane = makeLane(cwd);
|
||||
await assert.rejects(
|
||||
() => installAgents(lane),
|
||||
(err) => err.code === "ENOTGITREPO"
|
||||
);
|
||||
});
|
||||
|
||||
it("adds .claude/agents/ line to git info/exclude idempotently", async () => {
|
||||
const cwd = makeRepoDir();
|
||||
const lane = makeLane(cwd);
|
||||
|
||||
await installAgents(lane); // first
|
||||
await installAgents(lane); // reinstall — should not duplicate
|
||||
|
||||
const excludePath = path.join(cwd, ".git", "info", "exclude");
|
||||
const content = fs.readFileSync(excludePath, "utf8");
|
||||
const matches = content.split("\n").filter((l) => l === ".claude/agents/");
|
||||
assert.equal(matches.length, 1);
|
||||
});
|
||||
|
||||
it("overwrites existing agents on reinstall", async () => {
|
||||
const cwd = makeRepoDir();
|
||||
const lane = makeLane(cwd);
|
||||
|
||||
await installAgents(lane);
|
||||
|
||||
// Tamper with the installed file
|
||||
const agentPath = path.join(cwd, ".claude", "agents", "qc-local.md");
|
||||
fs.writeFileSync(agentPath, "tampered");
|
||||
|
||||
// Reinstall
|
||||
await installAgents(lane);
|
||||
const content = fs.readFileSync(agentPath, "utf8");
|
||||
assert.ok(content.includes("#"), "reinstalled file should look like markdown, not 'tampered'");
|
||||
assert.notEqual(content, "tampered");
|
||||
});
|
||||
|
||||
it("creates .claude/agents/ dir if it does not exist", async () => {
|
||||
const cwd = makeRepoDir();
|
||||
const lane = makeLane(cwd);
|
||||
assert.equal(fs.existsSync(path.join(cwd, ".claude", "agents")), false);
|
||||
await installAgents(lane);
|
||||
assert.ok(fs.existsSync(path.join(cwd, ".claude", "agents")));
|
||||
});
|
||||
|
||||
for (const tool of ["codex", "antigravity"]) {
|
||||
it(`installs nothing for a ${tool} lane — no .claude/agents/, no AGENTS.md`, async () => {
|
||||
const cwd = makeRepoDir();
|
||||
const lane = lanesLib.createLane({ title: "cx", cwd, kind: "managed", tool });
|
||||
|
||||
const result = await installAgents(lane);
|
||||
assert.deepEqual(result.installed, []);
|
||||
assert.equal(fs.existsSync(path.join(cwd, ".claude", "agents")), false);
|
||||
assert.equal(fs.existsSync(path.join(cwd, "AGENTS.md")), false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("codex pipeline cleanup", () => {
|
||||
const START = "<!-- agenthub:stage:start -->";
|
||||
const END = "<!-- agenthub:stage:end -->";
|
||||
|
||||
@@ -20,6 +20,11 @@ const SOURCE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-sou
|
||||
const REMOTE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-remote-"));
|
||||
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
||||
process.env.DASHBOARD_PIPELINES_DIR = PIPELINE_DIR;
|
||||
// AgentHub ships no pipeline template; the CLI's stage commands are tested
|
||||
// against the two old ones kept as fixtures.
|
||||
for (const f of ["default.json", "ship-feature.json"]) {
|
||||
fs.copyFileSync(path.join(__dirname, "fixtures", "pipelines", f), path.join(PIPELINE_DIR, f));
|
||||
}
|
||||
process.env.DASHBOARD_REMOTE_SYNC_MS = "0";
|
||||
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
process.env.LANES_ROOT = LANES_ROOT;
|
||||
@@ -675,37 +680,6 @@ describe("agenthub lanes — inferred stage detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("agenthub ship-full / agenthub ship-quick", () => {
|
||||
it("ship-quick prints the 8-stage pipeline's skill, ship-full the 16-stage one", async () => {
|
||||
const quick = await cli(["ship-quick", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(quick.status, 0, quick.stderr);
|
||||
assert.match(quick.stdout, /claude "\/agenthub:ship-feature add an export button"/);
|
||||
|
||||
const full = await cli(["ship-full", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(full.status, 0, full.stderr);
|
||||
assert.match(full.stdout, /claude "\/agenthub:ship-feature-lane add an export button"/);
|
||||
});
|
||||
|
||||
it("--print switches nothing: the lane keeps whatever template it was on", async () => {
|
||||
const before = await get(`/api/lanes`);
|
||||
const lane = before.body.lanes.find((l) => l.cwd === LANE_DIR);
|
||||
const r = await cli(["ship-full", "anything", "--print"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const after = await get(`/api/lanes`);
|
||||
assert.equal(after.body.lanes.find((l) => l.id === lane.id).pipeline, lane.pipeline);
|
||||
});
|
||||
|
||||
it("exits non-zero with the command's own usage when no requirement is given", async () => {
|
||||
const r = await cli(["ship-full"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stderr, /usage: agenthub ship-full/);
|
||||
|
||||
const q = await cli(["ship-quick"], LANE_DIR);
|
||||
assert.notEqual(q.status, 0);
|
||||
assert.match(q.stderr, /usage: agenthub ship-quick/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("agenthub lanes node", () => {
|
||||
it("with no node id, lists every node and its command, and exits non-zero with usage", async () => {
|
||||
const r = await cli(["lanes", "node"], LANE_DIR);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file With NO pipeline template installed (the shipped state since the
|
||||
* built-in templates were removed), lanes must still be created, rendered and
|
||||
* reported on: getPipeline() answers an empty pipeline, `default` stays a
|
||||
* valid id, and a stage declaration is refused with a plain explanation.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "pipelines-dormant-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db");
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data");
|
||||
process.env.CLAUDE_HOME = path.join(ROOT, ".claude");
|
||||
process.env.AGENTHUB_SHELL_PATH = "0";
|
||||
const savedDir = process.env.DASHBOARD_PIPELINES_DIR;
|
||||
delete process.env.DASHBOARD_PIPELINES_DIR;
|
||||
|
||||
const { describe, it, before, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const http = require("node:http");
|
||||
|
||||
const pipelines = require("../lib/pipelines");
|
||||
pipelines.reload();
|
||||
const lanes = require("../lib/lanes");
|
||||
const { createApp } = require("../index");
|
||||
|
||||
let app;
|
||||
let base;
|
||||
const request = (method, p, body) =>
|
||||
fetch(`${base}${p}`, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
}).then(async (r) => ({ status: r.status, body: await r.json().catch(() => null) }));
|
||||
|
||||
before(async () => {
|
||||
app = http.createServer(createApp());
|
||||
await new Promise((r) => app.listen(0, "127.0.0.1", r));
|
||||
base = `http://127.0.0.1:${app.address().port}`;
|
||||
});
|
||||
after(async () => {
|
||||
await new Promise((r) => app.close(r));
|
||||
if (savedDir !== undefined) process.env.DASHBOARD_PIPELINES_DIR = savedDir;
|
||||
pipelines.reload();
|
||||
fs.rmSync(ROOT, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("no pipeline template installed", () => {
|
||||
it("ships no template and answers an empty default pipeline", () => {
|
||||
assert.deepEqual(pipelines.listPipelines(), []);
|
||||
const p = pipelines.getPipeline("default");
|
||||
assert.equal(p.id, "default");
|
||||
assert.deepEqual(p.nodes, []);
|
||||
assert.equal(pipelines.getPipeline("anything").nodes.length, 0);
|
||||
assert.deepEqual(pipelines.nodeStates(p, { stage: "plan", stages: {} }), []);
|
||||
assert.equal(pipelines.progressPct(p, { stage: "plan" }), 0);
|
||||
});
|
||||
|
||||
it("still creates a lane on the default pipeline and renders it", () => {
|
||||
const lane = lanes.createLane({ cwd: path.join(ROOT, "a"), title: "a" });
|
||||
assert.equal(lane.pipeline, "default");
|
||||
const payload = lanes.lanePayload(lane);
|
||||
assert.deepEqual(payload.pipeline_nodes, []);
|
||||
assert.equal(payload.progress, 0);
|
||||
});
|
||||
|
||||
it("refuses a stage declaration with a plain reason", async () => {
|
||||
const lane = lanes.createLane({ cwd: path.join(ROOT, "b"), title: "b" });
|
||||
const r = await request("POST", `/api/lanes/${lane.id}/stage`, { stage: "plan" });
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(r.body.error.code, "ENOPIPELINE");
|
||||
assert.match(r.body.error.message, /no pipeline template is installed/);
|
||||
});
|
||||
|
||||
it("lists no pipelines over the API and still serves the lane list", async () => {
|
||||
assert.deepEqual((await request("GET", "/api/lanes/pipelines")).body, { pipelines: [] });
|
||||
assert.equal((await request("GET", "/api/lanes")).status, 200);
|
||||
});
|
||||
});
|
||||
@@ -185,7 +185,6 @@ describe("fast path", () => {
|
||||
pluginVersion: pkg.version,
|
||||
depsHash: boot.depsHash(),
|
||||
pluginRoot: ROOT,
|
||||
skillPluginsSettled: "nothing-missing",
|
||||
},
|
||||
RT
|
||||
);
|
||||
@@ -206,35 +205,6 @@ describe("fast path", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("skill-plugin auto-install gate", () => {
|
||||
// Both cases here must return WITHOUT reaching server/lib/skill-plugins.js —
|
||||
// that module shells out to `claude plugin install`, and no test may.
|
||||
beforeEach(resetRuntime);
|
||||
after(() => delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS);
|
||||
|
||||
for (const flag of ["0", "false", "off"]) {
|
||||
it(`is off when AGENTHUB_AUTO_INSTALL_PLUGINS=${flag}`, () => {
|
||||
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = flag;
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "off");
|
||||
});
|
||||
}
|
||||
|
||||
it("runs once: a settled state is returned verbatim, not re-attempted", () => {
|
||||
delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS;
|
||||
boot.writeState({ skillPluginsSettled: "installed" }, RT);
|
||||
|
||||
// Verbatim matters: writeState records whatever comes back, so a marker
|
||||
// here would erase the only record of what actually happened.
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "installed");
|
||||
});
|
||||
|
||||
it("is off under the flag even when forced, so /agenthub-update honours the opt-out", () => {
|
||||
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = "0";
|
||||
boot.writeState({ skillPluginsSettled: "failed" }, RT);
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT, { force: true }), "off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bootstrap lock", () => {
|
||||
beforeEach(resetRuntime);
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* @file Tests the `Skill plugins` row of scripts/plugin-doctor.js — the check
|
||||
* that tells a fresh install its pipelines name slash commands no installed
|
||||
* plugin provides (today: `superpowers`, which `/agenthub:ship-feature-lane` drives
|
||||
* step by step). The row must WARN and never FAIL: without that plugin the
|
||||
* dashboard, hooks, CLI and MCP all still work, so failing a doctor run over it
|
||||
* would cry wolf. Every case runs against an injected temp CLAUDE_HOME.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-doctor-skills-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
|
||||
const { diagnose } = require("../../scripts/plugin-doctor");
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(TMP_HOME, { recursive: true, force: true });
|
||||
delete process.env.CLAUDE_HOME;
|
||||
});
|
||||
|
||||
/** Rewrite the injected Claude home, then read back just the row under test. */
|
||||
function rowFor({ plugins = {}, enabledPlugins = {}, userSkills = [], installDirs = [] }) {
|
||||
fs.rmSync(TMP_HOME, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(TMP_HOME, "plugins"), { recursive: true });
|
||||
fs.mkdirSync(path.join(TMP_HOME, "skills"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "plugins", "installed_plugins.json"),
|
||||
JSON.stringify({ plugins })
|
||||
);
|
||||
fs.writeFileSync(path.join(TMP_HOME, "settings.json"), JSON.stringify({ enabledPlugins }));
|
||||
// Created AFTER the wipe above: readPlugins() only counts an instance whose
|
||||
// installPath still exists on disk.
|
||||
for (const dir of installDirs) fs.mkdirSync(dir, { recursive: true });
|
||||
for (const name of userSkills) {
|
||||
fs.mkdirSync(path.join(TMP_HOME, "skills", name), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "skills", name, "SKILL.md"),
|
||||
`---\nname: ${name}\n---\n\nbody\n`
|
||||
);
|
||||
}
|
||||
const row = diagnose().find((r) => r.label === "Skill plugins");
|
||||
assert.ok(row, "the doctor must always emit a Skill plugins row");
|
||||
return row;
|
||||
}
|
||||
|
||||
const SP_DIR = path.join(TMP_HOME, "plugins", "sp");
|
||||
|
||||
/** An installed superpowers instance; `enabled` mirrors settings.json. */
|
||||
function installedSuperpowers(enabled) {
|
||||
return {
|
||||
plugins: { "superpowers@official": [{ installPath: SP_DIR, scope: "user", version: "1.0.0" }] },
|
||||
enabledPlugins: { "superpowers@official": enabled },
|
||||
installDirs: [SP_DIR],
|
||||
};
|
||||
}
|
||||
|
||||
/** The skills the built-in templates name, read from the templates themselves. */
|
||||
function requiredSuperpowersSkills() {
|
||||
const { listPipelines } = require("../lib/pipelines");
|
||||
const names = new Set();
|
||||
for (const p of listPipelines()) {
|
||||
for (const cmd of [p.skill, ...p.nodes.map((n) => n.command)]) {
|
||||
const m = /^\/superpowers:([A-Za-z0-9_-]+)/.exec(cmd || "");
|
||||
if (m) names.add(m[1]);
|
||||
}
|
||||
}
|
||||
return [...names];
|
||||
}
|
||||
|
||||
describe("plugin-doctor: Skill plugins row", () => {
|
||||
it("warns, never fails, when a plugin the pipelines name is not installed", () => {
|
||||
const row = rowFor({});
|
||||
|
||||
assert.equal(row.level, "warn", "a missing skill plugin must not fail the doctor run");
|
||||
assert.match(row.detail, /superpowers/);
|
||||
assert.match(row.detail, /claude plugin install superpowers/);
|
||||
});
|
||||
|
||||
it("names the specific skills that are unavailable, so the reader can verify the claim", () => {
|
||||
const row = rowFor({});
|
||||
|
||||
for (const skill of requiredSuperpowersSkills()) {
|
||||
assert.match(row.detail, new RegExp(skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
}
|
||||
});
|
||||
|
||||
it("passes once the plugin is installed and enabled", () => {
|
||||
const row = rowFor(installedSuperpowers(true));
|
||||
|
||||
assert.equal(row.level, "ok", row.detail);
|
||||
assert.match(row.detail, /superpowers available/);
|
||||
});
|
||||
|
||||
it("treats an explicitly disabled plugin as missing - Claude Code will not resolve its skills", () => {
|
||||
const row = rowFor(installedSuperpowers(false));
|
||||
|
||||
assert.equal(row.level, "warn", row.detail);
|
||||
assert.match(row.detail, /superpowers/);
|
||||
});
|
||||
|
||||
it("accepts the skills copied into ~/.claude/skills instead of installed as a plugin", () => {
|
||||
// A legitimate setup. Reporting it as missing would train the reader to
|
||||
// ignore this row, which is the only failure mode a warning really has.
|
||||
const row = rowFor({ userSkills: requiredSuperpowersSkills() });
|
||||
|
||||
assert.equal(row.level, "ok", row.detail);
|
||||
});
|
||||
|
||||
it("still warns when only SOME of the required skills are present locally", () => {
|
||||
const [first, ...rest] = requiredSuperpowersSkills();
|
||||
const row = rowFor({ userSkills: [first] });
|
||||
|
||||
assert.equal(row.level, "warn", row.detail);
|
||||
assert.ok(!row.detail.includes(`${first},`), `${first} is present and must not be listed`);
|
||||
for (const skill of rest) assert.match(row.detail, new RegExp(skill));
|
||||
});
|
||||
});
|
||||
@@ -242,21 +242,5 @@ describe("plugin marketplace", () => {
|
||||
const listed = new Set(manifest.commands.map((rel) => path.basename(rel)));
|
||||
for (const f of onDisk) assert.ok(listed.has(f), `${f} is not in plugin.json commands`);
|
||||
});
|
||||
|
||||
it("gives every pipeline node of the shipped templates its own slash command", () => {
|
||||
// /intake, /qc, /gate … are how a node is run from inside a session; a
|
||||
// node with no command file can only be reached from a terminal.
|
||||
const { listPipelines } = require("../lib/pipelines");
|
||||
const listed = new Set(manifest.commands.map((rel) => path.basename(rel, ".md")));
|
||||
for (const p of listPipelines().filter((x) => ["default", "ship-feature"].includes(x.id))) {
|
||||
for (const n of p.nodes) {
|
||||
const names = [n.id, ...(n.aliases || [])];
|
||||
assert.ok(
|
||||
names.some((name) => listed.has(name)),
|
||||
`${p.id}/${n.id} has no slash command (tried ${names.join(", ")})`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* @file Tests server/lib/skill-plugins.js — the dependency the ship-feature
|
||||
* pipelines have on other plugins (`superpowers` today): deriving it from the
|
||||
* templates, deciding what is actually missing, and the unattended install the
|
||||
* SessionStart bootstrap performs.
|
||||
*
|
||||
* The security-relevant case is covered first: a pipeline template is a file
|
||||
* anyone may drop into DASHBOARD_PIPELINES_DIR, so a plugin name appearing
|
||||
* there must be REPORTED but never INSTALLED. Every case injects a fake `run`
|
||||
* — no test may reach a real `claude plugin install`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { describe, it, after, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skill-plugins-home-"));
|
||||
const TMP_PIPELINES = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skill-plugins-tpl-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
|
||||
const skillPlugins = require("../lib/skill-plugins");
|
||||
const pipelines = require("../lib/pipelines");
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(TMP_HOME, { recursive: true, force: true });
|
||||
fs.rmSync(TMP_PIPELINES, { recursive: true, force: true });
|
||||
delete process.env.CLAUDE_HOME;
|
||||
delete process.env.DASHBOARD_PIPELINES_DIR;
|
||||
pipelines.reload();
|
||||
});
|
||||
|
||||
/** An empty Claude home: no plugins installed, no user skills. */
|
||||
function emptyHome() {
|
||||
fs.rmSync(TMP_HOME, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(TMP_HOME, "plugins"), { recursive: true });
|
||||
fs.mkdirSync(path.join(TMP_HOME, "skills"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "plugins", "installed_plugins.json"),
|
||||
JSON.stringify({ plugins: {} })
|
||||
);
|
||||
fs.writeFileSync(path.join(TMP_HOME, "settings.json"), JSON.stringify({}));
|
||||
}
|
||||
|
||||
/** Point the loader at a throwaway template naming `command` on one node. */
|
||||
function useTemplate(doc) {
|
||||
fs.writeFileSync(path.join(TMP_PIPELINES, "test.json"), JSON.stringify(doc));
|
||||
process.env.DASHBOARD_PIPELINES_DIR = TMP_PIPELINES;
|
||||
pipelines.reload();
|
||||
}
|
||||
|
||||
function forgetTemplate() {
|
||||
fs.rmSync(path.join(TMP_PIPELINES, "test.json"), { force: true });
|
||||
delete process.env.DASHBOARD_PIPELINES_DIR;
|
||||
pipelines.reload();
|
||||
}
|
||||
|
||||
/** Records the argv of every call and answers with a fixed result. */
|
||||
function fakeRun(results) {
|
||||
const calls = [];
|
||||
const queue = [...results];
|
||||
const run = (args) => {
|
||||
calls.push(args);
|
||||
return queue.length > 1 ? queue.shift() : queue[0];
|
||||
};
|
||||
run.calls = calls;
|
||||
return run;
|
||||
}
|
||||
|
||||
const collect = () => {
|
||||
const lines = [];
|
||||
const log = (line) => lines.push(line);
|
||||
log.lines = lines;
|
||||
return log;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
emptyHome();
|
||||
forgetTemplate();
|
||||
});
|
||||
|
||||
describe("requiredSkillPlugins", () => {
|
||||
it("collects the plugin behind every /plugin:skill a template names, node or pipeline level", () => {
|
||||
const required = skillPlugins.requiredSkillPlugins();
|
||||
|
||||
assert.ok(required.has("superpowers"), "the built-in templates name superpowers");
|
||||
assert.ok(required.get("superpowers").has("writing-plans"));
|
||||
});
|
||||
|
||||
it("ignores agenthub's own commands and anything that is not a /plugin:skill", () => {
|
||||
useTemplate({
|
||||
id: "tpl-shell",
|
||||
skill: "/agenthub:ship-feature",
|
||||
nodes: [
|
||||
{ id: "a", command: "npm test" },
|
||||
{ id: "b", command: "/reload-plugins" },
|
||||
{ id: "c", command: "" },
|
||||
],
|
||||
});
|
||||
|
||||
const required = skillPlugins.requiredSkillPlugins();
|
||||
|
||||
assert.equal(required.has("agenthub"), false, "agenthub is the plugin asking");
|
||||
// Only the built-ins' superpowers requirement survives.
|
||||
assert.deepEqual([...required.keys()], ["superpowers"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("missingSkillPlugins", () => {
|
||||
it("reports a required plugin that is absent, with the skills that need it", () => {
|
||||
const missing = skillPlugins.missingSkillPlugins();
|
||||
const sp = missing.find((m) => m.name === "superpowers");
|
||||
|
||||
assert.ok(sp, "superpowers must be reported missing in an empty home");
|
||||
assert.ok(sp.skills.includes("writing-plans"));
|
||||
assert.equal(sp.autoInstall.spec, "superpowers@claude-plugins-official");
|
||||
});
|
||||
|
||||
it("counts the skills as satisfied when they sit in ~/.claude/skills instead", () => {
|
||||
for (const name of skillPlugins.requiredSkillPlugins().get("superpowers")) {
|
||||
fs.mkdirSync(path.join(TMP_HOME, "skills", name), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "skills", name, "SKILL.md"),
|
||||
`---\nname: ${name}\n---\n\nbody\n`
|
||||
);
|
||||
}
|
||||
|
||||
assert.deepEqual(skillPlugins.missingSkillPlugins(), []);
|
||||
});
|
||||
|
||||
it("counts an installed but explicitly disabled plugin as missing", () => {
|
||||
const dir = path.join(TMP_HOME, "plugins", "sp");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "plugins", "installed_plugins.json"),
|
||||
JSON.stringify({ plugins: { "superpowers@x": [{ installPath: dir, scope: "user" }] } })
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "settings.json"),
|
||||
JSON.stringify({ enabledPlugins: { "superpowers@x": false } })
|
||||
);
|
||||
|
||||
assert.ok(skillPlugins.missingSkillPlugins().some((m) => m.name === "superpowers"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("autoInstallSkillPlugins", () => {
|
||||
it("installs an allowlisted plugin with an argv array, never a shell string", () => {
|
||||
const run = fakeRun([{ ok: true, output: "" }]);
|
||||
const log = collect();
|
||||
|
||||
const summary = skillPlugins.autoInstallSkillPlugins(log, { run });
|
||||
|
||||
assert.deepEqual(summary.installed, ["superpowers"]);
|
||||
assert.deepEqual(run.calls, [
|
||||
["plugin", "install", "superpowers@claude-plugins-official", "--scope", "user", "-y"],
|
||||
]);
|
||||
assert.ok(
|
||||
log.lines.some((l) => l.includes("/reload-plugins")),
|
||||
"the user must be told the current session will not see it yet"
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the marketplace and retries exactly once when the install fails", () => {
|
||||
const run = fakeRun([
|
||||
{ ok: false, output: "unknown marketplace" },
|
||||
{ ok: true, output: "" },
|
||||
{ ok: true, output: "" },
|
||||
]);
|
||||
|
||||
const summary = skillPlugins.autoInstallSkillPlugins(collect(), { run });
|
||||
|
||||
assert.deepEqual(summary.installed, ["superpowers"]);
|
||||
assert.equal(run.calls.length, 3);
|
||||
assert.deepEqual(run.calls[1], [
|
||||
"plugin",
|
||||
"marketplace",
|
||||
"add",
|
||||
"anthropics/claude-plugins-official",
|
||||
"--scope",
|
||||
"user",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports a failure with the manual command, and never throws", () => {
|
||||
const run = fakeRun([{ ok: false, output: "the `claude` CLI is not on PATH" }]);
|
||||
const log = collect();
|
||||
|
||||
const summary = skillPlugins.autoInstallSkillPlugins(log, { run });
|
||||
|
||||
assert.deepEqual(summary.failed, ["superpowers"]);
|
||||
assert.ok(
|
||||
log.lines.some((l) =>
|
||||
l.includes("claude plugin install superpowers@claude-plugins-official")
|
||||
),
|
||||
"a failed auto-install must leave the user a command to run"
|
||||
);
|
||||
});
|
||||
|
||||
// The one that matters. A template is a file on disk; if the plugin name in
|
||||
// it drove the install, dropping a JSON file into DASHBOARD_PIPELINES_DIR
|
||||
// would run an arbitrary vendor's install command on the next session.
|
||||
it("REFUSES to install a plugin a template names but the allowlist does not", () => {
|
||||
useTemplate({
|
||||
id: "tpl-evil",
|
||||
name: "Untrusted",
|
||||
nodes: [{ id: "a", command: "/attacker-plugin:payload" }],
|
||||
});
|
||||
const run = fakeRun([{ ok: true, output: "" }]);
|
||||
|
||||
const summary = skillPlugins.autoInstallSkillPlugins(collect(), { run });
|
||||
|
||||
assert.ok(summary.skipped.includes("attacker-plugin"), "it must be reported as skipped");
|
||||
assert.equal(
|
||||
run.calls.some((args) => args.join(" ").includes("attacker-plugin")),
|
||||
false,
|
||||
"no claude invocation may ever mention a plugin outside AUTO_INSTALLABLE"
|
||||
);
|
||||
// It is still visible to the reader, just never installed.
|
||||
assert.ok(skillPlugins.missingSkillPlugins().some((m) => m.name === "attacker-plugin"));
|
||||
});
|
||||
|
||||
it("does nothing at all when every requirement already resolves", () => {
|
||||
for (const name of skillPlugins.requiredSkillPlugins().get("superpowers")) {
|
||||
fs.mkdirSync(path.join(TMP_HOME, "skills", name), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TMP_HOME, "skills", name, "SKILL.md"),
|
||||
`---\nname: ${name}\n---\n\nbody\n`
|
||||
);
|
||||
}
|
||||
const run = fakeRun([{ ok: true, output: "" }]);
|
||||
|
||||
const summary = skillPlugins.autoInstallSkillPlugins(collect(), { run });
|
||||
|
||||
assert.deepEqual(summary.attempted, []);
|
||||
assert.deepEqual(run.calls, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runClaudePlugin: the env it spawns with", () => {
|
||||
// Every other test in this file injects `run`, so nothing exercises the real
|
||||
// spawn. That gap is how a wrong CLAUDE_CONFIG_DIR pin lived here: the CLI
|
||||
// writes `.claude.json` INSIDE that dir, while cc-discovery reads it beside
|
||||
// ~/.claude by default, so pinning it unconditionally aimed writes at a file
|
||||
// nothing reads. cc-plugin-cli.js's runClaude has the mirror of this test.
|
||||
const PRINT_ENV = 'printf "%s\\n" "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR-<unset>}"';
|
||||
|
||||
function stubClaude(body) {
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skillplug-bin-"));
|
||||
const file = path.join(bin, "claude");
|
||||
fs.writeFileSync(file, `#!/usr/bin/env bash\n${body}\n`);
|
||||
fs.chmodSync(file, 0o755);
|
||||
const originalPath = process.env.PATH;
|
||||
process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
|
||||
return () => {
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(bin, { recursive: true, force: true });
|
||||
};
|
||||
}
|
||||
|
||||
it("leaves CLAUDE_CONFIG_DIR unset when CLAUDE_HOME is not set", () => {
|
||||
const original = process.env.CLAUDE_HOME;
|
||||
delete process.env.CLAUDE_HOME;
|
||||
const restore = stubClaude(PRINT_ENV);
|
||||
try {
|
||||
const res = skillPlugins.runClaudePlugin(["plugin", "list"], 10_000);
|
||||
assert.equal(res.output.trim(), "CLAUDE_CONFIG_DIR=<unset>");
|
||||
} finally {
|
||||
restore();
|
||||
if (original !== undefined) process.env.CLAUDE_HOME = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("pins CLAUDE_CONFIG_DIR to CLAUDE_HOME when the operator set it", () => {
|
||||
const original = process.env.CLAUDE_HOME;
|
||||
process.env.CLAUDE_HOME = "/tmp/some-other-claude-home";
|
||||
const restore = stubClaude(PRINT_ENV);
|
||||
try {
|
||||
const res = skillPlugins.runClaudePlugin(["plugin", "list"], 10_000);
|
||||
assert.equal(res.output.trim(), "CLAUDE_CONFIG_DIR=/tmp/some-other-claude-home");
|
||||
} finally {
|
||||
restore();
|
||||
if (original === undefined) delete process.env.CLAUDE_HOME;
|
||||
else process.env.CLAUDE_HOME = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @file Tests for skills-install.js: skill-copy path resolution (source:
|
||||
* repoRoot/skills/ not repoRoot/.claude/skills/) and error handling.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skills-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { installShipFeatureLaneSkill } = require("../lib/skills-install");
|
||||
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
|
||||
describe("installShipFeatureLaneSkill", () => {
|
||||
it("resolves source from repoRoot/skills/ (new path), not .claude/skills/", () => {
|
||||
// Build a fake repoRoot with the skill only under skills/ship-feature-lane/
|
||||
const repoRoot = path.join(SUITE_ROOT, "mock-repo");
|
||||
const src = path.join(repoRoot, "skills", "ship-feature-lane");
|
||||
fs.mkdirSync(src, { recursive: true });
|
||||
fs.writeFileSync(path.join(src, "SKILL.md"), "# Fake Ship Feature Lane Skill");
|
||||
|
||||
// The old path should NOT exist
|
||||
const oldPath = path.join(repoRoot, ".claude", "skills", "ship-feature-lane", "SKILL.md");
|
||||
assert.equal(fs.existsSync(oldPath), false);
|
||||
|
||||
// Override HOME so we don't touch real ~/.claude
|
||||
const origHome = process.env.HOME;
|
||||
const fakeHome = path.join(SUITE_ROOT, "fake-home");
|
||||
process.env.HOME = fakeHome;
|
||||
|
||||
try {
|
||||
const result = installShipFeatureLaneSkill({ repoRoot });
|
||||
assert.equal(result.installed, true);
|
||||
assert.ok(result.path.startsWith(fakeHome));
|
||||
|
||||
const installedFile = path.join(result.path, "SKILL.md");
|
||||
assert.ok(fs.existsSync(installedFile));
|
||||
assert.equal(fs.readFileSync(installedFile, "utf8"), "# Fake Ship Feature Lane Skill");
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
}
|
||||
});
|
||||
|
||||
it("throws ENOSKILLSRC when skills/ship-feature-lane/ is missing", () => {
|
||||
const repoRoot = path.join(SUITE_ROOT, "empty-repo");
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
assert.throws(
|
||||
() => installShipFeatureLaneSkill({ repoRoot }),
|
||||
(err) => err.code === "ENOSKILLSRC"
|
||||
);
|
||||
});
|
||||
|
||||
it("overwrites an existing installed skill (force: true)", () => {
|
||||
const repoRoot = path.join(SUITE_ROOT, "mock-repo-overwrite");
|
||||
const src = path.join(repoRoot, "skills", "ship-feature-lane");
|
||||
fs.mkdirSync(src, { recursive: true });
|
||||
fs.writeFileSync(path.join(src, "SKILL.md"), "# Updated skill");
|
||||
|
||||
const origHome = process.env.HOME;
|
||||
const fakeHome = path.join(SUITE_ROOT, "fake-home-overwrite");
|
||||
process.env.HOME = fakeHome;
|
||||
|
||||
try {
|
||||
// First install
|
||||
installShipFeatureLaneSkill({ repoRoot });
|
||||
// Tamper with installed copy
|
||||
const existing = path.join(fakeHome, ".claude", "skills", "ship-feature-lane", "SKILL.md");
|
||||
fs.writeFileSync(existing, "# Tampered");
|
||||
// Reinstall — should overwrite
|
||||
installShipFeatureLaneSkill({ repoRoot });
|
||||
assert.equal(fs.readFileSync(existing, "utf8"), "# Updated skill");
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,6 @@ const remoteSourcesRouter = require("./routes/remote-sources");
|
||||
const metricsRouter = require("./routes/metrics");
|
||||
const lanesRouter = require("./routes/lanes");
|
||||
const locksRouter = require("./routes/locks");
|
||||
const skillsRouter = require("./routes/skills");
|
||||
|
||||
const APP_VERSION = (() => {
|
||||
try {
|
||||
@@ -141,7 +140,6 @@ function createApp() {
|
||||
app.use("/api/webhooks", webhooksRouter);
|
||||
app.use("/api/remote-sources", remoteSourcesRouter);
|
||||
app.use("/api/metrics", metricsRouter);
|
||||
app.use("/api/skills", skillsRouter);
|
||||
app.get("/api/openapi.json", (_req, res) => {
|
||||
res.json(openApiSpec);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
/**
|
||||
* @file Installs a `claude` lane's driving instructions: the
|
||||
* ship-feature-lane pipeline's agent templates
|
||||
* (qc-local, senior-gate-reviewer) into a lane's own .claude/agents/, so a
|
||||
* driving session can launch them by the BARE subagent_type name. Source is
|
||||
* the repo's own `agents/` — the same files the agenthub plugin ships, where
|
||||
* they resolve as `agenthub:qc-local`; installing them into the lane is what
|
||||
* makes the unprefixed name work there too. Plain file I/O except
|
||||
* for one git call — resolving where a worktree lane's shared info/exclude
|
||||
* actually lives, the same git-dir/git-common-dir distinction E2's
|
||||
* lane-sync.js had to get right for info/attributes.
|
||||
* @file Cleans up what older builds wrote into a lane: `removeStageDoc` takes
|
||||
* AgentHub's stage block back out of a codex lane's AGENTS.md. Plain file I/O.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { git } = require("./worktree");
|
||||
|
||||
const TEMPLATE_DIR = path.join(__dirname, "..", "..", "agents");
|
||||
const TEMPLATE_FILES = ["qc-local.md", "senior-gate-reviewer.md"];
|
||||
const EXCLUDE_LINE = ".claude/agents/";
|
||||
|
||||
// The block older builds wrote into a codex lane's AGENTS.md. Codex lanes no
|
||||
// longer carry a pipeline, so `removeStageDoc` takes it back out.
|
||||
@@ -52,54 +39,4 @@ function removeStageDoc(cwd) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The dir shared across every worktree of a repo — where info/exclude
|
||||
* lives (same as info/attributes; MERGE_HEAD/HEAD/the index are the only
|
||||
* per-worktree-private state, not this). */
|
||||
async function commonGitDir(cwd) {
|
||||
const result = await git(cwd, ["rev-parse", "--git-common-dir"]);
|
||||
const dir = result.stdout.trim();
|
||||
return path.isAbsolute(dir) ? dir : path.join(cwd, dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy both agent templates into `<lane.cwd>/.claude/agents/` (overwriting
|
||||
* any existing copy — this is a reinstall, not a merge) and idempotently
|
||||
* git-exclude that directory in the shared common git dir.
|
||||
*
|
||||
* A `codex` or `antigravity` lane gets nothing: neither CLI reads
|
||||
* `.claude/agents/`, and those lanes carry no pipeline to drive.
|
||||
*
|
||||
* @param {{cwd: string, tool?: string}} lane
|
||||
* @returns {Promise<{installed: string[]}>}
|
||||
*/
|
||||
async function installAgents(lane) {
|
||||
if (!fs.existsSync(path.join(lane.cwd, ".git"))) {
|
||||
throw Object.assign(new Error(`lane has no .git — not a git repository: ${lane.cwd}`), {
|
||||
code: "ENOTGITREPO",
|
||||
});
|
||||
}
|
||||
|
||||
if (lane.tool && lane.tool !== "claude") return { installed: [] };
|
||||
|
||||
const dest = path.join(lane.cwd, ".claude", "agents");
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
const installed = [];
|
||||
for (const name of TEMPLATE_FILES) {
|
||||
fs.copyFileSync(path.join(TEMPLATE_DIR, name), path.join(dest, name));
|
||||
installed.push(name);
|
||||
}
|
||||
|
||||
const infoDir = path.join(await commonGitDir(lane.cwd), "info");
|
||||
fs.mkdirSync(infoDir, { recursive: true });
|
||||
const excludePath = path.join(infoDir, "exclude");
|
||||
const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
|
||||
const lines = existing.split("\n").filter(Boolean);
|
||||
if (!lines.includes(EXCLUDE_LINE)) {
|
||||
lines.push(EXCLUDE_LINE);
|
||||
fs.writeFileSync(excludePath, lines.join("\n") + "\n");
|
||||
}
|
||||
|
||||
return { installed };
|
||||
}
|
||||
|
||||
module.exports = { installAgents, removeStageDoc };
|
||||
module.exports = { removeStageDoc };
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@
|
||||
|
||||
const { db } = require("../db");
|
||||
const {
|
||||
DEFAULT_PIPELINE_ID,
|
||||
listPipelines,
|
||||
getPipeline,
|
||||
phaseIdx,
|
||||
@@ -111,7 +112,9 @@ function validateTool(tool) {
|
||||
* forever. Reject it at the write, where the caller can still be told.
|
||||
*/
|
||||
function validatePipeline(id) {
|
||||
if (!listPipelines().some((p) => p.id === id)) {
|
||||
// `default` is always valid: with no template installed it is the empty
|
||||
// pipeline every lane sits on (pipelines.js), not a typo.
|
||||
if (id !== DEFAULT_PIPELINE_ID && !listPipelines().some((p) => p.id === id)) {
|
||||
throw Object.assign(new Error(`unknown pipeline: ${id}`), { code: "EBADPIPELINE" });
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* @file Pipeline templates for lanes. A template is a plain JSON list of nodes
|
||||
* (`server/data/pipelines/*.json` plus any override dropped in
|
||||
* `DASHBOARD_PIPELINES_DIR`); this module resolves a lane's declared stage onto
|
||||
* a node through per-node `aliases`, and derives the five render states the
|
||||
* pipeline map draws. Pure functions — no DB, no I/O beyond the one-time
|
||||
* template load, so it stays trivially testable.
|
||||
* dropped in `DASHBOARD_PIPELINES_DIR` (`server/data/pipelines/` is read too,
|
||||
* but AgentHub ships NO template any more — the pipeline feature is dormant
|
||||
* until one is installed); this module resolves a lane's declared stage onto a
|
||||
* node through per-node `aliases`, and derives the five render states the
|
||||
* pipeline map draws. With no template, every lane is on an EMPTY `default`
|
||||
* pipeline: no nodes, no states, progress 0. Pure functions — no DB, no I/O
|
||||
* beyond the one-time template load, so it stays trivially testable.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -14,6 +16,14 @@ const path = require("node:path");
|
||||
const DEFAULT_PIPELINE_ID = "default";
|
||||
const BUILTIN_DIR = path.join(__dirname, "..", "data", "pipelines");
|
||||
|
||||
/** What every lane is on while no template is installed. */
|
||||
const EMPTY_PIPELINE = Object.freeze({
|
||||
id: DEFAULT_PIPELINE_ID,
|
||||
name: "",
|
||||
skill: "",
|
||||
nodes: Object.freeze([]),
|
||||
});
|
||||
|
||||
/** Load every template once. A malformed file is skipped, never fatal. */
|
||||
function loadAll() {
|
||||
const dirs = [BUILTIN_DIR];
|
||||
@@ -77,10 +87,10 @@ function listPipelines() {
|
||||
return [...templates().values()];
|
||||
}
|
||||
|
||||
/** Never throws: an unknown id yields the default template. */
|
||||
/** Never throws: an unknown id yields the default template, else the empty one. */
|
||||
function getPipeline(id) {
|
||||
const t = templates();
|
||||
return t.get(id) || t.get(DEFAULT_PIPELINE_ID);
|
||||
return t.get(id) || t.get(DEFAULT_PIPELINE_ID) || EMPTY_PIPELINE;
|
||||
}
|
||||
|
||||
/** Index of the node matching `stage` by id or alias; -1 when unknown. */
|
||||
@@ -138,7 +148,7 @@ function nodeStates(pipeline, lane) {
|
||||
|
||||
function progressPct(pipeline, lane) {
|
||||
const i = phaseIdx(pipeline, lane.stage);
|
||||
if (i < 0) return 0;
|
||||
if (i < 0 || pipeline.nodes.length < 2) return 0;
|
||||
return Math.round((i / (pipeline.nodes.length - 1)) * 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* @file skill-plugins.js
|
||||
* @description The plugins AgentHub's pipelines depend on: which ones a template
|
||||
* names, which of those are actually resolvable, and the narrow set the
|
||||
* SessionStart bootstrap is allowed to install on the user's behalf.
|
||||
*
|
||||
* Shared by `scripts/plugin-doctor.js` (reports) and
|
||||
* `scripts/plugin-bootstrap.js` (installs) so the two can never disagree about
|
||||
* what is missing. It lives here rather than in either script because
|
||||
* plugin-doctor already requires plugin-bootstrap — the other direction would
|
||||
* be a cycle.
|
||||
*
|
||||
* ## Reporting is derived, installing is allowlisted
|
||||
* `requiredSkillPlugins()` reads the loaded pipeline templates, including any
|
||||
* custom one from `DASHBOARD_PIPELINES_DIR`. That is right for a report: if a
|
||||
* template names `/foo:bar`, the reader should be told `foo` is missing.
|
||||
*
|
||||
* It is NOT right for an install. A template is a file anyone can drop into
|
||||
* that directory, so installing whatever plugin name it happens to contain
|
||||
* would turn "edit a JSON file" into "run this vendor's plugin install command
|
||||
* on the next session" — a supply-chain path AgentHub must not open. Auto-install
|
||||
* therefore consults {@link AUTO_INSTALLABLE}, a fixed map in this file, and
|
||||
* ignores every requirement outside it.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { getClaudeHome } = require("./claude-home");
|
||||
|
||||
/**
|
||||
* Plugins the bootstrap may install unattended, keyed by the plugin name a
|
||||
* template refers to. `spec` is the exact `claude plugin install` argument;
|
||||
* `marketplace` is the `owner/repo` to register first if that spec cannot be
|
||||
* resolved because the marketplace is not known on this machine.
|
||||
*
|
||||
* Adding an entry here is a decision to install someone else's code on a
|
||||
* user's machine without asking — it belongs in a review, not in a config file.
|
||||
*
|
||||
* @type {Record<string, {spec: string, marketplace: string}>}
|
||||
*/
|
||||
const AUTO_INSTALLABLE = {
|
||||
superpowers: {
|
||||
spec: "superpowers@claude-plugins-official",
|
||||
marketplace: "anthropics/claude-plugins-official",
|
||||
},
|
||||
};
|
||||
|
||||
/** Slash commands are addressed `/plugin:skill`; a bare `/command` has no plugin. */
|
||||
const SLASH_COMMAND_RE = /^\/([A-Za-z0-9_-]+):([A-Za-z0-9_-]+)/;
|
||||
|
||||
/**
|
||||
* Every `/plugin:skill` the loaded pipeline templates name — as a pipeline's
|
||||
* own `skill` or as one node's `command` — grouped by the plugin that has to
|
||||
* provide it. Derived rather than hardcoded so editing a template cannot leave
|
||||
* the check behind. `agenthub` is dropped: it is the plugin asking.
|
||||
*
|
||||
* @returns {Map<string, Set<string>>} plugin name → the skill names it must ship.
|
||||
*/
|
||||
function requiredSkillPlugins() {
|
||||
const requirements = new Map();
|
||||
let pipelines = [];
|
||||
try {
|
||||
pipelines = require("./pipelines").listPipelines();
|
||||
} catch {
|
||||
return requirements; // templates unreadable — a different failure, not this one
|
||||
}
|
||||
for (const pipeline of pipelines) {
|
||||
for (const command of [pipeline.skill, ...pipeline.nodes.map((n) => n.command)]) {
|
||||
const match = SLASH_COMMAND_RE.exec(command || "");
|
||||
if (!match) continue; // a shell command, or a bare /command
|
||||
const [, plugin, skill] = match;
|
||||
if (plugin === "agenthub") continue;
|
||||
if (!requirements.has(plugin)) requirements.set(plugin, new Set());
|
||||
requirements.get(plugin).add(skill);
|
||||
}
|
||||
}
|
||||
return requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which required plugins cannot be resolved right now.
|
||||
*
|
||||
* A plugin present but explicitly disabled counts as missing — Claude Code will
|
||||
* not resolve its skills either way. A plugin whose skills all exist as plain
|
||||
* user skills under `~/.claude/skills/` counts as satisfied: hand-copying them
|
||||
* is a legitimate setup, and calling it broken is how a warning teaches its
|
||||
* reader to ignore it.
|
||||
*
|
||||
* @returns {{name: string, skills: string[], autoInstall: {spec: string, marketplace: string}|null}[]}
|
||||
* Empty when everything resolves. `null` autoInstall means report only.
|
||||
* @throws When the installed-plugin manifest cannot be read at all.
|
||||
*/
|
||||
function missingSkillPlugins() {
|
||||
const requirements = requiredSkillPlugins();
|
||||
if (!requirements.size) return [];
|
||||
|
||||
const discovery = require("./cc-discovery");
|
||||
const installed = discovery.readPlugins().plugins;
|
||||
const userSkills = new Set(discovery.readSkills({ scope: "user" }).map((s) => s.name));
|
||||
|
||||
const missing = [];
|
||||
for (const [name, skills] of requirements) {
|
||||
const present = installed.find(
|
||||
(p) => p.name === name && p.installPathExists && p.enabled !== false
|
||||
);
|
||||
if (present) continue;
|
||||
const absent = [...skills].filter((s) => !userSkills.has(s)).sort();
|
||||
if (!absent.length) continue;
|
||||
missing.push({ name, skills: absent, autoInstall: AUTO_INSTALLABLE[name] || null });
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude plugin …`, synchronously. The bootstrap is a synchronous script, so
|
||||
* it cannot use the promise-based `cc-plugin-cli.js` the HTTP routes go
|
||||
* through; the argument validation that module carries is also moot here,
|
||||
* because every argv element below comes from {@link AUTO_INSTALLABLE} — a
|
||||
* constant in this file, never a request or a template.
|
||||
*
|
||||
* Still an argv array, never a shell string. `CLAUDE_CONFIG_DIR` is pinned
|
||||
* only when the operator set `CLAUDE_HOME`, matching `cc-plugin-cli.js`'s
|
||||
* `runClaude()`: left unset, the CLI's own defaults already write where
|
||||
* `cc-discovery` reads, and pinning it unconditionally sent `.claude.json`
|
||||
* to `~/.claude/.claude.json`, which nothing reads.
|
||||
*
|
||||
* @returns {{ok: boolean, output: string}}
|
||||
*/
|
||||
function runClaudePlugin(args, timeoutMs) {
|
||||
const result = runClaudePluginOnce(args, timeoutMs);
|
||||
// An older Claude Code rejects `-y` and never prompts; retry without it.
|
||||
if (!result.ok && args.includes("-y") && /unknown option '-y'/.test(result.output)) {
|
||||
return runClaudePluginOnce(
|
||||
args.filter((a) => a !== "-y"),
|
||||
timeoutMs
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runClaudePluginOnce(args, timeoutMs) {
|
||||
const env = { ...process.env };
|
||||
if (process.env.CLAUDE_HOME) env.CLAUDE_CONFIG_DIR = getClaudeHome();
|
||||
const result = spawnSync("claude", args, {
|
||||
timeout: timeoutMs,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1024 * 1024,
|
||||
// Non-interactive: a would-be prompt must fail fast, not hang out the timeout.
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env,
|
||||
});
|
||||
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
||||
if (result.error) {
|
||||
const enoent = result.error.code === "ENOENT";
|
||||
return {
|
||||
ok: false,
|
||||
output: enoent ? "the `claude` CLI is not on PATH" : String(result.error.message),
|
||||
};
|
||||
}
|
||||
return { ok: result.status === 0, output };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the allowlisted plugins that are missing. Called from the bootstrap's
|
||||
* DETACHED worker, so a slow clone never delays a session; its only channel is
|
||||
* the caller's log.
|
||||
*
|
||||
* Best-effort by design: a failure is logged and the run moves on. The
|
||||
* dashboard, hooks, CLI and MCP all work without these plugins — only the
|
||||
* ship-feature pipelines need them — so nothing here may throw into the
|
||||
* bootstrap.
|
||||
*
|
||||
* @param {(line: string) => void} log
|
||||
* @param {{timeoutMs?: number, run?: typeof runClaudePlugin}} [options] `run` is
|
||||
* a seam for tests: nothing else may reach a real `claude plugin install`.
|
||||
* @returns {{attempted: string[], installed: string[], failed: string[], skipped: string[]}}
|
||||
*/
|
||||
function autoInstallSkillPlugins(log, { timeoutMs = 180_000, run = runClaudePlugin } = {}) {
|
||||
const summary = { attempted: [], installed: [], failed: [], skipped: [] };
|
||||
|
||||
let missing = [];
|
||||
try {
|
||||
missing = missingSkillPlugins();
|
||||
} catch {
|
||||
return summary; // manifest unreadable — the doctor reports it, this stays quiet
|
||||
}
|
||||
|
||||
for (const entry of missing) {
|
||||
if (!entry.autoInstall) {
|
||||
// Reported by /agenthub-doctor, never installed: not on the allowlist.
|
||||
summary.skipped.push(entry.name);
|
||||
continue;
|
||||
}
|
||||
const { spec, marketplace } = entry.autoInstall;
|
||||
summary.attempted.push(entry.name);
|
||||
log(`installing the ${entry.name} plugin — the pipelines drive ${entry.skills.join(", ")}`);
|
||||
|
||||
let result = run(["plugin", "install", spec, "--scope", "user", "-y"], timeoutMs);
|
||||
if (!result.ok) {
|
||||
// The most likely cause on a fresh machine is that the marketplace has
|
||||
// never been registered, which `install` cannot do for us. Register it
|
||||
// and retry exactly once.
|
||||
const added = run(
|
||||
["plugin", "marketplace", "add", marketplace, "--scope", "user"],
|
||||
timeoutMs
|
||||
);
|
||||
if (added.ok) {
|
||||
result = run(["plugin", "install", spec, "--scope", "user", "-y"], timeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
summary.installed.push(entry.name);
|
||||
log(
|
||||
`installed ${spec} — run /reload-plugins to use it in THIS session ` +
|
||||
`(a new session picks it up on its own)`
|
||||
);
|
||||
} else {
|
||||
summary.failed.push(entry.name);
|
||||
log(
|
||||
`could not install ${spec}: ${result.output || "unknown error"} — ` +
|
||||
`install it yourself with: claude plugin install ${spec}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AUTO_INSTALLABLE,
|
||||
requiredSkillPlugins,
|
||||
missingSkillPlugins,
|
||||
autoInstallSkillPlugins,
|
||||
// Exported only so a test can pin the env it spawns with. Every other test
|
||||
// injects `run` and so never reaches the real spawn — which is how an
|
||||
// unconditional CLAUDE_CONFIG_DIR pin could be restored here unnoticed.
|
||||
runClaudePlugin,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* @file Installs skills/ship-feature-lane/ into ~/.claude/skills/, so
|
||||
* /ship-feature-lane is discoverable from a session running inside any
|
||||
* lane's own working directory. This is the FALLBACK path for a plain
|
||||
* checkout: the skill also ships with the `agenthub` plugin (the repo root IS
|
||||
* the plugin root, so `skills/` is the plugin's skills dir), where it is
|
||||
* already global as `/agenthub:ship-feature-lane`. Installing it here just
|
||||
* restores the bare `/ship-feature-lane` name for people who do not run the
|
||||
* plugin. 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>
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
/**
|
||||
* @param {{repoRoot: string}} options - `repoRoot` is this checkout's own
|
||||
* 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}}
|
||||
*/
|
||||
function installShipFeatureLaneSkill({ repoRoot }) {
|
||||
const src = path.join(repoRoot, "skills", "ship-feature-lane");
|
||||
if (!fs.existsSync(src)) {
|
||||
throw Object.assign(new Error(`not found: ${src}`), { code: "ENOSKILLSRC" });
|
||||
}
|
||||
const dest = path.join(os.homedir(), ".claude", "skills", "ship-feature-lane");
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
||||
return { installed: true, path: dest };
|
||||
}
|
||||
|
||||
module.exports = { installShipFeatureLaneSkill };
|
||||
+13
-21
@@ -53,7 +53,6 @@ const {
|
||||
const { runGitAction } = require("../lib/git-ops");
|
||||
const { withLaneLock } = require("../lib/lane-lock");
|
||||
const { checkSync, mergeSync, continueSync } = require("../lib/lane-sync");
|
||||
const { installAgents } = require("../lib/lane-agents");
|
||||
const { syncMcp } = require("../lib/lane-mcp");
|
||||
const { HOOKS, runHook, resolveProfile, isIntegrationEnabled } = require("../lib/lane-profile");
|
||||
const { slotDirs } = require("../lib/lane-slots");
|
||||
@@ -510,6 +509,19 @@ router.post("/:id/stage", sameOriginGuard, (req, res) => {
|
||||
const requested = req.body && req.body.stage;
|
||||
if (requested !== undefined && requested !== null && requested !== "") {
|
||||
const pipeline = getPipeline(current.pipeline);
|
||||
// No template installed: the pipeline feature is dormant, so there is no
|
||||
// stage to declare — say so instead of listing zero nodes.
|
||||
if (!pipeline.nodes.length) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: "ENOPIPELINE",
|
||||
message:
|
||||
"no pipeline template is installed, so this lane has no stages to declare " +
|
||||
"(drop a template into DASHBOARD_PIPELINES_DIR to turn stages on)",
|
||||
stages: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (phaseIdx(pipeline, requested) === -1) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
@@ -631,26 +643,6 @@ router.post("/:id/proof-link", sameOriginGuard, (req, res) => {
|
||||
res.json(proofLib.ensureProofLink(lane));
|
||||
});
|
||||
|
||||
/**
|
||||
* Install the ship-feature-lane pipeline's agent templates (qc-local,
|
||||
* senior-gate-reviewer) into this lane's own .claude/agents/. Static file
|
||||
* copy — no templating, no credentials to inject (this repo has no
|
||||
* seed-account system yet; see the E3 design spec's Decisions table).
|
||||
* Never automatic, same as proof-link: a session calls this explicitly.
|
||||
*/
|
||||
router.post("/:id/agents/install", sameOriginGuard, async (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
try {
|
||||
res.json(await installAgents(lane));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOTGITREPO") {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
res.status(500).json({ error: { code: err.code || "ERUNTIME", message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Give this lane the same MCP servers as its source repo — relocates the
|
||||
* source repo's already-configured mcpServers (from ~/.claude.json) into
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* @file Machine-wide (not lane-scoped) skill installation.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
const { Router } = require("express");
|
||||
const path = require("node:path");
|
||||
const { installShipFeatureLaneSkill } = require("../lib/skills-install");
|
||||
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/agenthub.js's REPO_ROOT
|
||||
* already does from its own location. */
|
||||
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
router.post("/install", sameOriginGuard, (req, res) => {
|
||||
try {
|
||||
res.json(installShipFeatureLaneSkill({ repoRoot: REPO_ROOT }));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOSKILLSRC") {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
res.status(500).json({ error: { code: err.code || "ERUNTIME", message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
name: ship-feature-lane
|
||||
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>`. Brainstorms with the user and frontloads ALL clarifying questions once (`AskUserQuestion`), stops for their approval of the plan, then runs unattended: implement (TDD) → independent unit-test agent → pre-push CI gates + dev preflight → e2e on the feature branch (agent-run) → code review (fresh-context agent) → local QC → senior GO/NO-GO gate → the user's acceptance → 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, the senior gate says GO and the user accepts — it's finalized when reviewers see it. A NO-GO loops back to the gates, or to replanning when the approach itself was wrong. 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 (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."
|
||||
---
|
||||
|
||||
# Ship Feature Lane (AgentHub lane pipeline)
|
||||
|
||||
You are running the autonomous feature pipeline for **one AgentHub lane**. The human's interactive touchpoints are exactly three — **Stage 0 (frontloaded Q&A)**, **Stage 0b (approving the plan)** and **Stage 7 (accepting the work before the PR is published)** — plus **merging the PR on GitHub**; everything between them runs to completion or to a `blocked` escalation, reporting progress through `agenthub stage` (which the dashboard renders). Those three are not optional politeness: they are the points where being wrong is cheapest to fix (before a plan), and where being wrong becomes public (before a PR).
|
||||
|
||||
## Current status
|
||||
|
||||
**Stages currently hardcoded skipped** (pending F's integrations):
|
||||
- **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 `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.
|
||||
|
||||
## Setup — do this first, every run
|
||||
|
||||
```bash
|
||||
LANE_DIR="$(pwd)" # the lane clone IS your cwd — AgentHub resolves the lane from this, never a hardcoded path
|
||||
```
|
||||
- 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.
|
||||
- **Pipeline template is this skill's contract, not whatever the lane started on.** Before your first `agenthub stage` call, run `agenthub lanes pipeline ship-feature` (no id needed — resolves from `cwd` like `agenthub stage`; idempotent, a no-op if already set) so every stage below resolves against the 16-node `ship-feature` map. The human is never expected to pick this in Add Lane or anywhere else — you force it to match the skill actually running.
|
||||
- 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: 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.
|
||||
- **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
|
||||
|
||||
Long pipelines outlive the context window. When context is compacted (summarized), re-derive these before continuing:
|
||||
|
||||
```bash
|
||||
LANE_DIR="$(pwd)" # lane = cwd, resolved fresh
|
||||
```
|
||||
|
||||
Then check your current position:
|
||||
- **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 `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 `agenthub` commands are idempotent).
|
||||
- **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.
|
||||
- **Agents preflight:** if this is the lane's first run through this pipeline, run `agenthub lanes agents install` once — it writes the `qc-local` and `senior-gate-reviewer` agent definitions into the lane's own `.claude/agents/` (idempotent; re-running just overwrites with the current definitions). That copy is what makes the bare `subagent_type: qc-local` resolve inside the lane. If the install isn't possible (not a git repo, or the CLI is unavailable), the `agenthub` plugin ships the same two agents, addressed as `agenthub:qc-local` / `agenthub:senior-gate-reviewer` — use those names instead. Only if NEITHER exists are Stages 6/7 unable to launch.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Publish ONLY after the senior-gate-reviewer returns `VERDICT: GO` AND the human accepts it (Stage 7).** "Publish" = push the feature branch + open/update the PR (Stage 8). Nothing reviewer-visible exists before both, and nothing else authorizes it.
|
||||
- **Testing and reviewing run in a FRESH-CONTEXT agent — never in this session.** The session that wrote the code is its worst judge: it already believes the code is correct and it remembers the reasoning the tests are supposed to challenge. Every stage whose job is to *judge* the work — Stage 0b (plan debate), Stage 1b (unit tests), Stage 3 (e2e run + triage), Stage 4 (code review), Stage 6 (browser QC), Stage 7 (senior gate) — is dispatched with the Agent tool to a subagent that starts with an EMPTY context and receives only artifacts it can verify itself: the lane's working directory, the lane spec file, the diff range, the commands to run, and the other agents' reports verbatim. The stages you keep are the ones that *do* rather than judge: intake, implementing, the mechanical gates (Stage 2), publishing (Stage 8), the CI/PR watch (Stages 10–12), and applying every fix the judges ask for. Rules: **never `subagent_type: fork`** for these (a fork inherits this session's context, which defeats the whole point); never "save a round-trip" by doing the pass yourself; never paste your own conclusions ("I already checked X", "this is safe because…") into the brief — hand facts and paths, not verdicts. You implement and you apply fixes; you do not grade your own work.
|
||||
- Launch them **FOREGROUND** (they gate the pipeline, and their completion re-invokes you, which satisfies the turn-liveness rule).
|
||||
- Each returns ONE parseable final line (`UNIT-TESTS:` / `E2E:` / `REVIEW:` / `LOCAL-QC:` / `VERDICT:`). Parse that line; never infer a pass from prose.
|
||||
- **The fix-loop:** any failure in stages 2–7 (gates/preflight, e2e, review, QC, senior gate), any red PR CI that's genuinely yours (Stage 10), and any worth-fixing review comment (Stage 12) → fix on the **feature branch**, and re-run **from Stage 2 through Stage 8** (gates+preflight → e2e → review → QC plan → QC → senior gate → publish/update PR), then the Stage 10 CI watch. Never skip a gate — the full process applies; no shortcuts because "it's just review feedback".
|
||||
- **Re-entry that changes non-test source re-runs Stage 1b first** (fresh unit-test agent on the new diff), then Stage 2 onward. A re-entry that only touches test files skips 1b — the tests are already what changed.
|
||||
- **EXCEPTION — test-only re-entry (browser-QC fast-path).** If the re-entry's change is ENTIRELY test files — `git diff --name-only` since the last browser-QC'd commit matches only test paths — the app's runtime behavior/UI is unchanged from the last QC'd pass. Run this EXACT stage set, nothing else:
|
||||
- **Always run:** Stage 2 (gates + preflight), Stage 7 (senior gate), Stage 8 (publish/update PR), and the Stage 10 tail (CI watch).
|
||||
- **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: `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.** This binds whoever actually runs the hook — normally Stage 3's e2e agent, so pass this paragraph into its brief verbatim; you only follow it yourself for an ad-hoc run. A hung suite never fires the completion re-invoke, stranding the lane at `stage=e2e-feature`. When running `agenthub lanes hook e2e`: 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. The ONE legitimate exception is a turn that ends on an `AskUserQuestion` at a scheduled touchpoint (Stage 0, 0b, 7): the human's answer is the re-invoker, and a pipeline waiting on a question it actually asked is not a stalled one. 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 `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 (`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
|
||||
|
||||
### 0 — Intake & frontloaded Q&A *(the only interactive part)*
|
||||
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) so your questions are grounded in what actually exists.
|
||||
- **The brainstorm is mandatory, not conditional.** Run the **superpowers:brainstorming** skill here on every run — not only "if the requirement is fuzzy". A one-line requirement always leaves scope, edge cases and acceptance criteria unstated, and this stage is the only place left to find that out cheaply.
|
||||
- **Ask only what the repo cannot answer.** Read first, ask second: a question whose answer is in the code, the tests, `CLAUDE.md` or the existing UI spends the human's one interactive round on something you could have looked up. What belongs to them is intent — scope, priorities, trade-offs, what "done" means — never facts.
|
||||
- **Frontloaded Q&A — asked with `AskUserQuestion`, and you WAIT for the answers.** Put the questions to the human with the `AskUserQuestion` tool (batched, up to 4 per round, each with real options — the way plan mode asks), not as prose they may not answer. Ask **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. You may not answer them on the human's behalf and you may not go on to Stage 0b until they are answered — deciding for them here is what makes a whole autonomous pipeline build the wrong feature. Only if the human explicitly says to decide yourself do you proceed, and then you write the assumptions into the lane spec. **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`. After this the human is asked exactly twice more — to approve the plan (Stage 0b) and to accept the work before the PR is published (Stage 7) — plus any `blocked` escalation. Nothing else is interactive.
|
||||
|
||||
### 0b — Investigate & plan *(autonomous)*
|
||||
- `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) — fresh context, never `fork`.** Spawn a SEPARATE sub-agent (Agent tool — `Plan` or `general-purpose`, FOREGROUND) to critique the plan + investigation. Hand it the requirement, the Stage-0 answers and the plan as text plus the paths to read; do NOT hand it your reasoning for why the plan is right — that is exactly what it is there to test. Ask for: 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.
|
||||
- Write the Q&A answers **and the agreed plan** to a lane spec file `docs/superpowers/specs/lane-<slug>.md` (gitignored, or add it to `.gitignore` if this is the first one) — the acceptance contract the senior gate checks against.
|
||||
- **STOP — the human approves the plan before any code exists.** This is the second of the three scheduled interactive touchpoints. Show them the plan: what changes, which files, what is deliberately out of scope, the test strategy, and how each acceptance criterion is met — plus the lane spec path. Ask with `AskUserQuestion`: approve / revise / change approach. "Revise" means edit the plan and the lane spec and ask again. **Stage 1 does not start without an approval in this turn** — an adversarial agent agreeing with your plan is not the human agreeing with it. Announce "Plan approved — going autonomous now" and only then continue.
|
||||
- A later stage that turns out to need a materially different approach comes BACK here for a fresh plan and a fresh approval, rather than drifting under the old one.
|
||||
|
||||
### 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: `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 (`agenthub lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
|
||||
- `agenthub stage implementing`
|
||||
|
||||
### 1b — Independent unit tests *(fresh-context agent)*
|
||||
Your TDD tests in Stage 1 were written by the same mind that wrote the code, so they inherit its blind spots. This stage buys a second, uncontaminated set.
|
||||
|
||||
- `agenthub stage tests --status running` (an alias of `gates` — the map holds; the stage name says these minutes are the independent test pass).
|
||||
- Launch a fresh-context agent (Agent tool, `subagent_type: general-purpose`, FOREGROUND — **never `fork`**). Hand it ONLY: the lane's working directory, the lane spec path (`docs/superpowers/specs/lane-<slug>.md`), the diff range `git diff origin/development...feat/<slug>`, the repo's unit-test commands, and this brief:
|
||||
- Read the acceptance criteria and the diff. Write the unit tests the implementation is **missing** — one per real behavior or edge case (happy path, negative/error path, boundary), in the repo's existing test style and location. Test the contract in the spec, not the shape of the implementation.
|
||||
- Do NOT modify non-test files. Do NOT weaken, skip, or delete an existing test. No coverage padding, no asserting constants or framework internals.
|
||||
- Run the suite. Commit **test files only**.
|
||||
- Final line: `UNIT-TESTS: PASS — <n> tests added` (all green) or `UNIT-TESTS: FAIL — <failing test> :: <the defect it exposes>`.
|
||||
- `FAIL` → a real defect: fix the **source** on the feature branch (root cause — **systematic-debugging**; never edit the agent's test to make it pass unless the test itself is provably wrong, and say so in the evidence), then re-run this stage.
|
||||
- `PASS` → `agenthub stage tests --evidence "<n> independent unit tests added, green"` → Stage 2.
|
||||
|
||||
### 2 — Pre-push CI gates + dev preflight (on the feature branch)
|
||||
- `agenthub stage gates --status running` — declare it FIRST. This is also the fix-loop's re-entry point, and the declaration is what moves the lane BACK down the pipeline: detection alone can't (`recordDetection` is forward-only and never overrides a higher declared stage), so a re-entry that skips this line leaves the dashboard showing the stage you already left.
|
||||
- `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. Exit 5 on a migration-number collision: declare `agenthub stage migration-collision --status running` (an alias of `gates` — the map stays put, the lane's stage names WHY it is sitting there), 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.
|
||||
|
||||
### 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:
|
||||
|
||||
- `agenthub stage booting --status running` — an alias of `e2e-feature`; the boot below can take minutes and this says which minutes they are.
|
||||
- `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 stage e2e-feature --status running`, then launch a fresh-context agent (Agent tool, `subagent_type: general-purpose`, FOREGROUND — **never `fork`**) to RUN and triage the suite. You booted the stack; you do not run or read the suite. Hand it ONLY: the lane's working directory, the feature branch, the acceptance points (lane spec path), and the e2e discipline verbatim — `agenthub lanes hook e2e` with `run_in_background: true`, a backgrounded `sleep 90` beside it, `agenthub lanes logs <id> e2e` for the tail on each wake, kill-and-FAIL past ~22 min, and NEVER a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout and the heartbeat). 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.
|
||||
- It reports one final line: `E2E: PASS` or `E2E: FAIL — <spec/test> :: <root cause, and whether it looks like a product defect or an infra flake>`.
|
||||
- On `FAIL`: fix on the feature branch, commit, re-run from Stage 1b (source changed) or Stage 2 (test-only change).
|
||||
- **Iterating on a failing spec:** declare `agenthub stage e2e-scoped` (alias of `e2e-feature`) so the dashboard shows this is a narrowed run, not the gate, and tell the e2e agent to run scoped through the hook — `agenthub lanes hook e2e -- <spec file/filter>` (same agent, same discipline; the narrowing is in ITS brief, not in you taking the run back). 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)*
|
||||
- `agenthub stage review --status running`, then launch a fresh-context reviewer agent (Agent tool, `subagent_type: Code Reviewer` — FOREGROUND, **never `fork`**; a reviewer who already watched you write the code is not a reviewer). Hand it ONLY: the lane's working directory, the lane spec path (requirement + acceptance criteria), the diff commands, and the brief below. Do NOT summarize your own implementation for it, and do NOT pre-empt findings — let it read the diff cold.
|
||||
- Run **Claude Code's own `code-review` skill** (the official plugin's `/code-review`) **at effort `high`** on the feature diff vs `origin/development` — the deterministic gate, not an ad-hoc read, and not a reviewer summarizing the diff in its own words. No PR is 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 in its session, fall back to a manual review of that diff (correctness, security, tests, migration/contract safety, this repo's `CLAUDE.md` + `.claude/rules/` boundaries).
|
||||
- Final line: `REVIEW: CLEAN` or `REVIEW: FINDINGS — <n>`, with the findings listed above it as `path:line — severity — problem — suggested fix`.
|
||||
- **Scale the pass to the diff.** A handful of lines in one layer gets the `code-review` pass alone — three agents re-reading a 20-line change buys one opinion at three times the tokens, and this stage re-runs on every fix-loop lap. Add the domain reviewer(s) once the change has seams: multiple files in a layer, a route/schema/migration/hook touched, or anything you would want a second reader on.
|
||||
- **Domain reviewers, in parallel with the above (same message, separate Agent calls).** Check `git diff --name-only origin/development...feat/<slug>` for touched layers and launch every matching one — FOREGROUND, fresh context, **never `fork`**, addressed as `agenthub:backend-reviewer` / `agenthub:frontend-reviewer` (plugin-qualified — no lane install needed, unlike `qc-local`/`senior-gate-reviewer`):
|
||||
- Any path under `server/` → `agenthub:backend-reviewer`.
|
||||
- Any path under `client/` → `agenthub:frontend-reviewer`.
|
||||
- Neither touched (root/`skills/`/`agents/`/`scripts/`/docs-only change) → skip both; the generic Code Reviewer above is the only gate.
|
||||
- Hand each ONLY the lane's working directory and the diff range `git diff origin/development...feat/<slug>` — each scopes itself to its own domain's files even though the diff range is the same for both.
|
||||
- Each ends with its own parseable line: `BACKEND-REVIEW:` / `FRONTEND-REVIEW:` — `CLEAN` or `FINDINGS — <n>`.
|
||||
- Triage ALL findings — the generic review plus every domain reviewer that ran — together with **receiving-code-review** judgment (verify each, neither blind agreement nor blind rejection) and apply what's worth applying on the feature branch. A finding you reject is answered with a reason, never with silence. Changed source → re-run **from Stage 1b**; changed only tests → **from Stage 2**. Either way the re-entry passes through THIS stage again: review ends when a pass over the current diff comes back clean (or with only findings you explicitly and defensibly rejected), not when one pass has been performed. Keep the findings + your resolutions — the senior gate reads them.
|
||||
- The Stage-6 `qc-local` report covers the user-flow review for the senior gate.
|
||||
- `agenthub stage review --evidence "<total findings count across all reviewers that ran> findings, resolved"`
|
||||
|
||||
### 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:
|
||||
- **In-scope scenarios** (numbered): each acceptance point with positive AND negative cases; adjacent flows sharing routes/components/data with the change; required **state coverage** (reload on each stateful screen touched, one logout→re-login, back/forth nav); and the required **UI/UX layout checks** for every form/screen the feature touches (narrow AND short viewport, expandables open so content exceeds the viewport, fixed chrome not clipped, every control labelled, section headers more prominent than field labels).
|
||||
- **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.
|
||||
- `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:
|
||||
- `agenthub stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local`, or `agenthub:qc-local` if the lane copy isn't installed — FOREGROUND, fresh context, **never `fork`**; it gates the pipeline; the Setup section's agents preflight covers both names). 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 — and NOTHING about how you built it. No "the button is at the top-right", no "this part already works", no known-issue list: a QC pass that starts from your mental model only re-checks what you already believed. 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.
|
||||
|
||||
### 7 — Senior GO/NO-GO gate *(authorizes the publish)*
|
||||
- Launch the **senior-gate-reviewer** agent (Agent tool, `subagent_type: senior-gate-reviewer`, or `agenthub:senior-gate-reviewer` if the lane copy isn't installed — FOREGROUND, fresh context, **never `fork`**; installed the same way as `qc-local`, see Stage 6). Give it: the lane's working directory, the requirement + Stage-0 answers (the lane spec file), the feature branch, the Stage-4 code-review findings + resolutions **as the other agents wrote them** (verbatim/by path — never your paraphrase of why each was fine), the Stage-6 `qc-local` report (the user-flow review), and the raw results of gates/e2e/QC. The agent inspects the local diff with `git diff origin/development...feat/<slug>` — no open PR is required (and none exists yet).
|
||||
- Parse its final line:
|
||||
- `VERDICT: GO` → the agent gate is satisfied; now the human's.
|
||||
- `VERDICT: NO-GO — <fixes>` → `agenthub stage gate-blocked --evidence "NO-GO — <reason>"` (an alias of `gate`, so the map holds while the lane's stage name says the gate refused), then **re-enter by the SIZE of what is wrong**, not always at the same place:
|
||||
- A defect, a missed case, a wrong detail → fix on the feature branch and re-run **from Stage 2** (or Stage 1b if non-test source changed).
|
||||
- Wrong approach, wrong scope, or a requirement that turns out to mean something else → back to **Stage 0b**: replan, and take the new plan through its approval stop again before any further code.
|
||||
- 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. Two NO-GOs on the same root issue means the loop is not converging: stop and ask the human instead of a third lap.
|
||||
- **Then ask the human to accept, before anything reviewer-visible exists.** Publishing is outward-facing and irreversible in the social sense — a PR reviewers have seen cannot be un-seen. Present: what was built, what was verified and how (gates/e2e/QC with their real outcomes), what the reviews found and what you did about it, and anything you could not run. Ask with `AskUserQuestion`: publish the PR / send back for fixes / rethink the approach. `VERDICT: GO` is the agent's opinion about evidence; it is not the human's decision to publish. Not accepted → re-enter exactly as for a NO-GO above.
|
||||
- `agenthub stage gate --evidence "GO; human accepted"` (or `NO-GO — <reason>`)
|
||||
|
||||
### 8 — Publish: push branch + open/update PR *(GATED — only on GO)*
|
||||
- `agenthub stage publishing --status running`
|
||||
- `agenthub stage push-revalidate` (alias of `pr-open`) — then 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.
|
||||
- `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.
|
||||
- `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 `agenthub lanes integration tracker`, this stage gets a real implementation. (Check `agenthub lanes integration tracker` in the heartbeat: even when on, no ticketer agent exists yet to act on it.)
|
||||
|
||||
### 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 `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.
|
||||
5. **Still red after your fix, on the same job:** you have now mis-triaged it once — hand the triage to a fresh-context agent (`general-purpose`, FOREGROUND, never `fork`) with the run URL, the failing job's logs and the diff range, and NOT your theory of the cause. Calling your own failure a flake twice is how a real defect gets pushed past the gate.
|
||||
|
||||
### 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).
|
||||
- `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)*
|
||||
- `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 (`agenthub stage watching-pr`).
|
||||
- `MERGED` → a human merged it: go to **Stage 13** (post-merge verification).
|
||||
- `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`. **Base guard first:** check the PR's actual base — `gh pr view <pr_url> --json baseRefName -q .baseRefName`. If it is NOT `development` (the base drifted — a human retargeted the PR, or it predates this pipeline), do NOT auto-merge anything: `--status blocked --note "PR base is not development — human decision"` and STOP. If the base IS `development`, declare `agenthub stage sync-conflict --status running` (an alias of `gates`, which is also where the re-entry below lands — one declaration, honest about both) and resolve it as real work:
|
||||
- `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.
|
||||
- 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):
|
||||
- **Worth fixing** (reviewer-requested change, real bug, test/doc gap): this is a NEW CHANGE — `agenthub stage pr-comment-fix --status running` (alias of `watching-pr`, so the lane reads as *acting on a comment* rather than idly polling), then apply it on the feature branch and re-enter the pipeline **from Stage 2 through Stage 8** (+ Stage 10 CI watch). The full process applies; no shortcuts because "it's just review feedback". **After the fix is pushed, reply to the comment** confirming resolution — what changed + the commit/PR ref — by writing the reply to a file and posting `gh pr comment <pr_url> --body-file <path>` referencing the comment. **Never inline `--body "..."`** — bodies carry backticks/`file:line`/`$(...)` that bash reads as command substitution inside double quotes, which corrupts the comment and trips an approval prompt. Then come back here and keep watching.
|
||||
- **Question / discussion**: answer it via `gh pr comment <pr_url> --body-file <path>` (same file-not-inline rule) — no code change.
|
||||
- **Not worth fixing** (out of scope, working as intended, deferred): **reply with the reasoning** so the reviewer knows why it wasn't actioned (don't just skip it).
|
||||
- **Sign every reply** with a distinct attribution — end each posted body, on its own line, with: `— 🤖 ship-feature-lane pipeline`.
|
||||
- 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 *(CI-wait works; dev-QC still unavailable)*
|
||||
- `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:
|
||||
- `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.
|
||||
|
||||
### 14 — Done
|
||||
- Post the final report: PR merged, that dev-QC (deployed-site QC) is unavailable pending F; CI-wait already ran in Stage 13.
|
||||
- `agenthub stage done --status passed --note "merged; dev-QC 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 `agenthub stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
|
||||
@@ -1,321 +0,0 @@
|
||||
---
|
||||
name: ship-feature
|
||||
description: Implement a feature safely end-to-end in this repository. Use when adding or changing functionality across backend, frontend, or MCP with required verification and documentation updates. Drives the Superpowers workflow skills and declares each phase with `agenthub stage` so the dashboard's pipeline map follows along. Brainstorms WITH the user at intake, stops for their approval of the plan before any code, reviews with Claude Code's own `code-review` plus only the domain reviewers the diff actually needs, and stops again at the gate for their acceptance — a NO-GO loops back to implement or plan rather than shipping.
|
||||
---
|
||||
|
||||
# Ship Feature
|
||||
|
||||
Use this workflow for medium or large implementation tasks. It does not restate
|
||||
how to plan, test, or review — the Superpowers skills own that. What lives here
|
||||
is the phase order, this repository's own rules, and the stage declaration at
|
||||
each boundary.
|
||||
|
||||
For a feature inside a AgentHub **lane**, use `ship-feature-lane` instead: it adds
|
||||
the branch/e2e/QC/senior-gate/PR half this skill deliberately leaves out.
|
||||
|
||||
## Declaring the stage
|
||||
|
||||
Each phase below starts with `agenthub stage <node>`, which is what puts the phase
|
||||
on the lane's pipeline map. The nodes are the `default` template's:
|
||||
`intake → plan → implement → tests → review → gate → ship → done`.
|
||||
|
||||
Set the template yourself, don't rely on how the lane was created: before your
|
||||
first `agenthub stage` call, run `agenthub lanes pipeline default` (no id needed —
|
||||
resolves from `cwd`; a no-op if the lane is already on it). This is what makes
|
||||
the choice invisible to the human — whichever of `ship-feature` or
|
||||
`ship-feature-lane` actually runs is what decides the template, not a picker
|
||||
they have to get right in advance.
|
||||
|
||||
`agenthub stage` needs a lane owning the current directory. If it reports no lane,
|
||||
this repo was never adopted (`agenthub lanes add --cwd $(pwd)` fixes it) — carry on
|
||||
with the workflow and skip the stage calls; they are reporting, not control
|
||||
flow. Never skip a phase because its stage call failed.
|
||||
|
||||
Declaring beats detection. The dashboard also *infers* these stages from the
|
||||
Superpowers skill invocations below, but an inference never renders `done` and
|
||||
never overrides a declaration — and detection cannot move a lane BACKWARD past
|
||||
a declared stage, so a rework loop is invisible unless you re-declare the phase
|
||||
you dropped back to.
|
||||
|
||||
## Waiting for an agent or a long command
|
||||
|
||||
A phase that dispatches a foreground agent (tests, review, gate) or runs a long
|
||||
command is **already** wired to wake you: you are re-invoked the moment a
|
||||
backgrounded Agent or helper finishes. So:
|
||||
|
||||
- Do not poll for it, and do not invent a timer. **`ScheduleWakeup` is a
|
||||
`/loop`-only primitive** — this pipeline is not a `/loop` session, so it
|
||||
neither paces nor sustains anything here, and in `auto` permission mode it can
|
||||
simply be refused (the safety classifier is a model call, which fails when
|
||||
that model is briefly unavailable). Never let a phase's progress depend on it.
|
||||
- Need to pace a wait anyway (a CI run, an external queue)? Background the wait:
|
||||
`sleep <secs>` with `run_in_background: true`.
|
||||
- Never narrate a wait you are not actually running. If you cannot sustain one,
|
||||
say so plainly and stop.
|
||||
|
||||
## The shape of a run
|
||||
|
||||
Forward: `intake → plan → implement → tests → review → gate → ship → done`.
|
||||
Two back-edges, and they are the point of the pipeline, not its failure mode:
|
||||
|
||||
- **`tests`/`review` → `implement`** — a failure or a finding sends you to the
|
||||
code, then forward through `tests` and `review` again.
|
||||
- **`gate` → `plan`** — a NO-GO about the APPROACH (wrong scope, wrong shape,
|
||||
the requirement meant something else) replans and re-earns the user's
|
||||
approval. A NO-GO about a DEFECT goes to `implement` instead.
|
||||
|
||||
Going back is a declaration: re-run `agenthub stage <node>` for the node you
|
||||
dropped to. Detection cannot move a lane backward on its own, so a silent
|
||||
rework leaves the dashboard showing a stage you already left, and the nodes
|
||||
ahead keep the previous pass's evidence.
|
||||
|
||||
## Right-size the run before you start it
|
||||
|
||||
This pipeline is not free: every phase that dispatches an agent costs a model
|
||||
call and minutes of wall clock, and the fix-loop pays it again on each lap.
|
||||
Match its weight to the work, and say in one line which weight you picked.
|
||||
|
||||
| Work | What runs |
|
||||
|---|---|
|
||||
| **Trivial** — typo, rename, one-line fix, a question | Not this skill. Do it, run the touched suite, say what you did. |
|
||||
| **Small** — one layer, a few files, no new interface | `intake` questions → `implement` → `tests` → `review` with `code-review` alone → `gate` (the user's acceptance IS the gate). Skip the plan-debate agent, the independent-test agent and the gate agent. |
|
||||
| **Medium / large** — multi-file, new route/schema/interface, cross-layer, or anything you would defend in a review | Every phase below, in full. |
|
||||
|
||||
Sizing is a claim you make out loud, not a shortcut you take quietly. When
|
||||
torn, take the heavier one. A small task that grows re-sizes upward mid-run —
|
||||
re-declare the phase you re-enter and pick up the skipped agents from there.
|
||||
|
||||
## Dispatching a judgement (used by `tests`, `review`, `gate`)
|
||||
|
||||
One contract, written once, so the phases below don't each re-argue it:
|
||||
|
||||
- **Fresh context, FOREGROUND, never `fork`.** A fork inherits your context —
|
||||
precisely the bias the dispatch exists to escape. Foreground because the phase
|
||||
gates on the answer, and the agent's completion is what re-invokes you.
|
||||
- **Hand only what the agent can verify for itself**: the repo path, the
|
||||
requirement/plan, the `git diff` range, the commands to run. Never your
|
||||
conclusions — "I already checked X", "this is safe because…" — those are the
|
||||
very claims it is there to test.
|
||||
- **One parseable final line** is the verdict (`UNIT-TESTS:`, `REVIEW:`,
|
||||
`BACKEND-REVIEW:`, `FRONTEND-REVIEW:`, `GATE:`). Read that line; never infer a
|
||||
pass from prose around it.
|
||||
- **Never dispatch what a command answers.** Running a suite, reading a log,
|
||||
grepping a path — do it yourself. An agent is for judgement, not for typing.
|
||||
- Agents for the same phase go out in **one message** so they run concurrently.
|
||||
|
||||
## Resuming after a compaction
|
||||
|
||||
A long run outlives the context window. Re-derive your position, don't guess it:
|
||||
|
||||
- `agenthub lanes` — the lane's declared stage, status and notes.
|
||||
- `git status` + `git diff --stat <base>...` — what is actually built.
|
||||
- The plan file — what was actually approved.
|
||||
|
||||
Resume at the declared stage and re-run it from the top; every `agenthub`
|
||||
command is idempotent. **A phase whose evidence you cannot find did not
|
||||
happen** — re-run it rather than assuming a past self did.
|
||||
|
||||
## Phases
|
||||
|
||||
### intake — brainstorm WITH the user, always
|
||||
- `agenthub stage intake --status running`
|
||||
- Restate the requirement and its success criteria.
|
||||
- Explore the impacted modules: `Explore` agent for breadth, then read the key
|
||||
files yourself. Identify which layers are hit (server / client / docs /
|
||||
scripts) — `repo-onboarding` if the area is unfamiliar.
|
||||
- **superpowers:brainstorming is MANDATORY here, not a fallback for a fuzzy
|
||||
requirement.** Run it before any plan exists. Skipping it because the ask
|
||||
"seems clear" is the single most common way this pipeline builds the wrong
|
||||
thing: a requirement typed in one line always leaves scope, edge cases and
|
||||
acceptance criteria unstated.
|
||||
- **Ask only what the repo cannot answer.** Read first, ask second: a question
|
||||
whose answer is in the code, the tests, `CLAUDE.md` or the existing UI costs
|
||||
the user's time and earns you a worse answer than reading would. What belongs
|
||||
to the user is intent — scope, priorities, trade-offs, what "done" means —
|
||||
never facts you could have looked up.
|
||||
- **Ask the user, and wait for the answer.** Put the open questions to them with
|
||||
`AskUserQuestion` — batched in one round (up to 4 at a time), each with real
|
||||
options, exactly the way plan mode asks. Questions that change what you build:
|
||||
scope boundaries, which surfaces are in/out, behavior on the edge cases you
|
||||
found, what "done" means. You may not answer them on the user's behalf, and
|
||||
you may not move to `plan` until they are answered. Answering yourself and
|
||||
proceeding IS the bug this phase exists to prevent.
|
||||
- The one exception: the user explicitly said to skip the questions and decide
|
||||
yourself ("cứ làm đi", "no questions", "you decide"). Then state the
|
||||
assumptions you are proceeding under, in writing, and go on.
|
||||
- **Leave when:** the user's answers are in hand (or their explicit "decide
|
||||
yourself" is), and you can state the scope, the non-goals and what "done"
|
||||
means in your own words without a guess in any of them.
|
||||
|
||||
### plan — a written plan, challenged, then APPROVED by the user
|
||||
- `agenthub stage plan`
|
||||
- **superpowers:writing-plans**: approach, files to change, the test strategy
|
||||
(which behavior each test pins), and how each success criterion is met.
|
||||
- If the task is a bug rather than a feature: **superpowers:systematic-debugging**
|
||||
first. Root cause, not symptom — and grep every caller of the function you
|
||||
are about to change, not just the path the report names.
|
||||
- Have the plan **challenged by a fresh-context agent** (`Plan` or
|
||||
`general-purpose`, never `fork`): missed requirements, wrong assumptions, a
|
||||
simpler approach, unhandled edge cases. Give it the requirement and the plan,
|
||||
not your reasons for believing the plan. Apply what survives
|
||||
**superpowers:receiving-code-review** scrutiny.
|
||||
- **STOP. The plan is presented to the user and implementation does not start
|
||||
until they approve it in this turn.** Show the plan (or its path plus a short
|
||||
summary: what changes, which files, what is deliberately out of scope, how it
|
||||
will be verified) and ask with `AskUserQuestion` — approve / revise / change
|
||||
approach. "Revise" means edit the plan and ask again; the loop is the point.
|
||||
Writing a plan and immediately implementing it is exactly the behavior this
|
||||
phase forbids, however obvious the plan looks to you.
|
||||
- Approval covers the plan as presented. Work that turns out to need a
|
||||
materially different approach comes BACK here (`agenthub stage plan` again)
|
||||
for a fresh approval, rather than drifting under the old one.
|
||||
- **Leave when:** a written plan exists, it survived the challenge, and the user
|
||||
approved THIS version of it in this turn.
|
||||
|
||||
### implement — smallest coherent diff
|
||||
- `agenthub stage implementing`
|
||||
- Entered only from an **approved** plan (or from a gate NO-GO sending work
|
||||
back here). No approval in this turn → you are still in `plan`.
|
||||
- **superpowers:test-driven-development**: failing test → minimal code → green.
|
||||
- Smallest diff that satisfies the requirement. Every changed line traces to it.
|
||||
- **file-headers** applies to every source file you create or edit.
|
||||
- Work the approved plan task by task, in its order. Before leaving this phase,
|
||||
re-read the plan's acceptance criteria and check each one against what you
|
||||
actually built — that list, not your memory of it, is what `gate` will ask
|
||||
about.
|
||||
- **The plan turning out wrong is a result, not a detour.** If implementing
|
||||
shows the approach cannot work, or the requirement means something else, stop
|
||||
and go back to `plan` (re-declare it, present the new plan, get it approved
|
||||
again). Quietly building a different thing than the one approved is the
|
||||
failure this phase has to avoid.
|
||||
- Long implementation? Re-declare `agenthub stage implementing` as you go — it
|
||||
doubles as the heartbeat, so the dashboard doesn't read a working lane as
|
||||
stalled.
|
||||
- **Leave when:** every plan task is done, every acceptance criterion has
|
||||
something concrete behind it, and the diff contains nothing that traces to
|
||||
neither.
|
||||
|
||||
### tests — this repo's verification, not a claim
|
||||
- `agenthub stage tests --status running`
|
||||
- **Dispatch the judgement, run the commands yourself.** Only work that needs an
|
||||
unbiased pair of eyes is worth an agent; a deterministic command is not.
|
||||
- **Independent unit tests → a fresh-context agent** (`general-purpose`; see
|
||||
*Dispatching a judgement*). It reads the requirement
|
||||
and the diff cold and writes the tests the implementation is *missing* —
|
||||
happy, negative and boundary, in the repo's existing style. It may not touch
|
||||
non-test files nor weaken existing tests. Final line:
|
||||
`UNIT-TESTS: PASS — <n> added` or `UNIT-TESTS: FAIL — <test> :: <defect>`.
|
||||
- **Suite run → plain Bash, here.** Run the suite(s) the diff actually needs:
|
||||
backend changed → `npm run test:server`; frontend changed →
|
||||
`npm run test:client`; both → both. A docs-only change needs neither, and
|
||||
running the other layer's suite proves nothing about this diff. Nothing is judged, so nothing is
|
||||
delegated: an agent for this buys a second model's tokens to read output you
|
||||
can read for free. A UI snapshot diff is reported for review, **never
|
||||
blindly regenerated** (`cd client && npx vitest run -u` is yours to run,
|
||||
only after you read the diff).
|
||||
- `FAIL` → fix the **source** (root cause), not the agent's test — then re-run
|
||||
this phase. Rewriting a failing independent test to pass is the one move that
|
||||
makes this whole phase worthless.
|
||||
- Record the outcome: `agenthub stage tests --evidence "<what passed>"`, or
|
||||
`--result fail` with what failed. A step you could not run is reported as not
|
||||
run, never as passed.
|
||||
- **Leave when:** the suites the diff needs are green in output you actually
|
||||
read, and the independent-test agent (when the size called for one) returned
|
||||
`PASS`.
|
||||
|
||||
### review — the native review, plus the domains this diff actually touched
|
||||
- `agenthub stage review --status running`
|
||||
- **Read the diff first, then decide who reviews it.** `git diff --name-only`
|
||||
against the base this task started from is what picks the reviewers. Spawning
|
||||
a fixed set regardless of what changed wastes a pass on files nobody touched,
|
||||
and worse, reads as coverage that never happened.
|
||||
- **Scale the pass to the diff.** A handful of lines in one layer gets the
|
||||
`code-review` pass alone — three agents re-reading a 20-line change costs
|
||||
three times the tokens for one opinion. Add the domain reviewer(s) once the
|
||||
change is big enough to have seams: multiple files in a layer, a route or
|
||||
schema or hook touched, or anything you would want a second reader on.
|
||||
- **Always: Claude Code's own `code-review` skill** (`/code-review`, on the
|
||||
working diff — it is also what the `review` node declares). Not optional, and
|
||||
not replaced by an agent reviewing "in its own words": run it in a
|
||||
fresh-context `Code Reviewer` agent whose job is to invoke that skill and
|
||||
report what it found (a reviewer that watched you write the code is not a
|
||||
reviewer). See **superpowers:requesting-code-review** for what to hand over.
|
||||
Final line: `REVIEW: CLEAN` or `REVIEW: FINDINGS — <n>`.
|
||||
- **Domain reviewers, conditional on the touched layers**, in the same message
|
||||
as the above so they run concurrently:
|
||||
- Any path under `server/` (routes, lib, hooks, db, CLI-facing code) →
|
||||
`agenthub:backend-reviewer`. No backend path changed → do not launch it.
|
||||
- Any path under `client/` (components, pages, i18n, styles) →
|
||||
`agenthub:frontend-reviewer`. No frontend path changed → do not launch it.
|
||||
- Both touched → both, in the same message so they run concurrently.
|
||||
- Neither touched (docs-only, scripts-only, config-only) → neither; the
|
||||
`code-review` pass above is the whole review.
|
||||
- Hand each ONLY the repo path and the working diff — each scopes itself to
|
||||
its own domain's files. Each ends with its own parseable line:
|
||||
`BACKEND-REVIEW:` / `FRONTEND-REVIEW:` — `CLEAN` or `FINDINGS — <n>`.
|
||||
- Apply what is worth applying — `code-review` plus every domain reviewer that
|
||||
ran — with **superpowers:receiving-code-review** judgment: verify each point,
|
||||
neither blind agreement nor blind rejection. A point you reject is answered
|
||||
with a reason, not silence.
|
||||
- **Changed code in response? Re-run `tests`, then re-run THIS phase.** Review
|
||||
is a loop, not a checkpoint: it ends when a pass over the current diff comes
|
||||
back clean (or with only findings you have explicitly and defensibly
|
||||
rejected), not when one pass has been performed.
|
||||
- Record it: `agenthub stage review --evidence "<n> findings across <who ran>,
|
||||
resolved"`.
|
||||
- **Leave when:** the latest pass over the CURRENT diff is clean or
|
||||
argued-through — not when a pass has merely happened.
|
||||
|
||||
### gate — evidence, then the user's acceptance
|
||||
- **superpowers:verification-before-completion**. Commands actually run, output
|
||||
actually read. This is the phase that stops "should work" from shipping.
|
||||
- At medium/large size the gate is a **fresh-context agent** too
|
||||
(`general-purpose`): hand it the requirement, the diff range and the
|
||||
verification commands — not your evidence — and let it re-run and re-check
|
||||
them. Final line: `GATE: GO` or `GATE: NO-GO — <what is missing>`. At small
|
||||
size skip it; the user's acceptance below is the gate.
|
||||
- **The gate is not yours to wave through. Ask the user.** Present, in a few
|
||||
lines: what was built, what was verified and how (the commands and their real
|
||||
outcome), what review found and what you did about it, and anything you could
|
||||
not run. Then ask with `AskUserQuestion`: accept / send back for fixes /
|
||||
rethink the approach. Silence is not acceptance, and neither is `GATE: GO` —
|
||||
the agent gate checks the evidence, the user decides whether this is what they
|
||||
wanted.
|
||||
- **NO-GO, or the user does not accept → go BACK, do not proceed.** Pick the
|
||||
node by the size of what is wrong, and re-declare it so the dashboard follows
|
||||
the rework (a declaration on an earlier node clears the nodes ahead of it —
|
||||
that is what makes a loop visible instead of leaving stale `done` evidence):
|
||||
- A defect, a missed case, a wrong detail → `agenthub stage implementing`, fix
|
||||
at the root cause, then forward through `tests` and `review` again.
|
||||
- Wrong approach, wrong scope, a requirement that turns out to mean something
|
||||
else → `agenthub stage plan`, replan, and get the plan approved again (the
|
||||
`plan` phase's approval step applies in full, every time through).
|
||||
- Either way the phases after it run again. Never carry a NO-GO forward into
|
||||
`ship` with a note about it.
|
||||
- Only once the user has accepted: `agenthub stage gate --evidence "<what was
|
||||
verified, and that the user accepted it>"`
|
||||
- **Leave when:** the verification really ran, its output was really read, and
|
||||
the user said yes. Two of three is a NO-GO.
|
||||
|
||||
### ship — docs, then the user's call
|
||||
- `agenthub stage ship`
|
||||
- **update-project-docs** — mandatory for any change to behavior, config,
|
||||
interfaces, events, schema, CLI commands, or features. Not optional, not
|
||||
deferred, not "if asked".
|
||||
- Commit / push / PR **only when the user asks in this turn**. Finishing an
|
||||
implementation is not authorization to commit.
|
||||
- **Leave when:** the docs that the change made wrong are right again, and you
|
||||
have reported plainly what shipped, what was verified, and what you skipped.
|
||||
- `agenthub stage done --status passed --evidence "<what shipped>"`
|
||||
|
||||
## This repository's own rules
|
||||
|
||||
Not restated here. `CLAUDE.md` is loaded in every session and already binds
|
||||
them — backward-compatible API/WebSocket contracts, fail-safe hooks,
|
||||
migration-safe schema changes, the destructive-lane and git-argv guards, and
|
||||
the lane boundaries (the console never writes a stage; the runtime never writes
|
||||
`stage`/`status`/`notes`). A second copy here would only drift out of sync with
|
||||
the first. Read `CLAUDE.md` and `.claude/rules/` for the area you are touching.
|
||||
|
||||
## References
|
||||
- Checklist template: `references/feature-checklist.md`
|
||||
@@ -1,21 +0,0 @@
|
||||
# Feature Checklist
|
||||
|
||||
- Scope
|
||||
- Problem and success criteria are explicit.
|
||||
- Impacted layers identified (server/client/docs/scripts).
|
||||
|
||||
- Implementation
|
||||
- Input validation and error handling are explicit.
|
||||
- Existing behavior preserved where not in scope.
|
||||
- Safety controls preserved.
|
||||
|
||||
- Verification
|
||||
- Backend: `npm run test:server` when backend changed.
|
||||
- Frontend: `npm run test:client` when UI changed.
|
||||
|
||||
- Documentation
|
||||
- `README.md`, `ARCHITECTURE.md`, `SETUP.md`, `INSTALL.md` updated as needed.
|
||||
- Commands in docs match `package.json`.
|
||||
|
||||
- Delivery
|
||||
- Known risks and unrun checks are clearly stated.
|
||||
Reference in New Issue
Block a user