feat: workspace aliases, tab groups of split panes, native path picker (v2.1.0)
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note. REPL `help lanes|workspace|workspaces` resolves through the aliases. - A workspace holds up to ten sessions (slots 0-9); every console tab is a group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout); parseGroups keeps the stored active tab when an earlier one is dropped. - Browse button beside path fields: POST /api/lanes/pick-path opens the OS picker on the server's desktop — loopback only and no forwarding header (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is killed when the request closes, a folder field falls back to the in-app browser on any failure. - Changes rail counts chip, image viewer (/git/raw), status bar session counts and per-day cost (`date=` on /api/pricing/cost). - /agenthub-open and /agenthub-doctor run from the INSTALLED build (supersededBy), doctor offers fixes behind two confirmations; plugin-open treats a locked bootstrap as non-fatal. - New-terminal shell waits for the first attach (issue #55). - repo-onboarding / debug-live-issue skills removed. - OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history, /api/workspaces mount.
This commit is contained in:
+54
-13
@@ -108,6 +108,13 @@ https://dashboard.example.com
|
||||
|
||||
### Workspaces
|
||||
|
||||
Every `/api/lanes…` route below also answers under `/api/workspaces…` — the
|
||||
same router mounted twice (`server/index.js`), so `GET /api/workspaces/:id` is
|
||||
`GET /api/lanes/:id`. Both are permanent: `/api/workspaces` matches the UI's
|
||||
word, but exists only on servers newer than v2.0.1, so a script that must also
|
||||
reach an older install keeps `/api/lanes`. Responses keep their `lane`/`lanes`
|
||||
keys either way, and the paths below are written with `/api/lanes`.
|
||||
|
||||
Workspace mutations (`POST /api/lanes/ensure`, `POST /api/lanes/worktree`,
|
||||
`PATCH /api/lanes/:id`, confirmed
|
||||
actions, the runtime routes `POST /api/lanes/:id/up`, `/down` and
|
||||
@@ -170,6 +177,32 @@ Two concurrent calls for the same path always yield **one** workspace: the
|
||||
`lanes.cwd` UNIQUE constraint decides the winner, and the loser re-reads and
|
||||
returns the winning workspace rather than reporting a conflict.
|
||||
|
||||
#### Pick a path with the OS's own dialog
|
||||
|
||||
```http
|
||||
POST /api/lanes/pick-path
|
||||
Content-Type: application/json
|
||||
|
||||
{ "kind": "dir", "start": "/home/me/projects" }
|
||||
```
|
||||
|
||||
Opens the operating system's folder (`kind: "dir"`, the default) or file
|
||||
(`kind: "file"`) picker **on the server's desktop** and waits for the user —
|
||||
zenity, else kdialog, on Linux; `osascript` on macOS; PowerShell's WinForms
|
||||
dialogs on Windows (`server/lib/native-dialog.js`; macOS and Windows untested).
|
||||
`start` opens it there (its folder when it names a file; home when missing).
|
||||
Answers `{ "path": "<absolute>" }` or `{ "cancelled": true }`. Same-origin
|
||||
guarded, and only a socket from loopback carrying no `X-Forwarded-For` /
|
||||
`Forwarded` header may ask (a reverse proxy connects from loopback too):
|
||||
`403 EREMOTE` otherwise, `501 ENODIALOG` with no graphical session, no picker
|
||||
program, or one that could not reach the display, `409 EBUSY` while one is
|
||||
already open, `500 EDIALOG` when the program failed. The dialog is closed when
|
||||
the request goes away (a closed tab, an SSH tunnel's browser leaving). A folder
|
||||
**Browse** button in the UI (`client/src/components/PathPickButton.tsx`) falls
|
||||
back on ANY of those to the in-app folder browser
|
||||
`GET /api/lanes/browse?path=<abs>`; a file field falls back to typing the path
|
||||
on `EREMOTE`/`ENODIALOG` and shows the error otherwise.
|
||||
|
||||
#### Update a workspace
|
||||
|
||||
```http
|
||||
@@ -193,7 +226,7 @@ to showing the workspace's `cwd`. Renaming is display-only: a workspace's branch
|
||||
slugified from its title once, at creation.
|
||||
|
||||
`pipeline` is patchable, which is how a workspace moves between templates after
|
||||
creation (`agenthub lanes pipeline <template-id>`). An id no template defines is
|
||||
creation (`agenthub workspace pipeline <template-id>`). An id no template defines is
|
||||
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
|
||||
@@ -281,8 +314,8 @@ When the session's tool is Codex, three fields control Codex-specific behavior:
|
||||
- **`router: "<name>"`** — the AI Router profile THIS session runs on (omitted = native, or for a resume the router its session last ran on); see § AI Router profiles. `400 EBADROUTER` for an unknown name, `400 EUNSUPPORTED` on a tool other than Claude/Codex.
|
||||
- **`worktree: true`** (Claude workspaces, no `resumeSessionId`) — a NEW session in a git worktree of its own, Claude Code's way: the pane runs `claude --worktree=ah-<id>-<base36 time>` (the `=` form, since `-w [name]` would take the prompt as the name; the name is made by the server), so Claude itself creates `<repo>/.claude/worktrees/<name>` on branch `worktree-<name>`, copies `.worktreeinclude`, runs the user's `WorktreeCreate` hook and cleans up on exit. `/.claude/worktrees/` is added to the repo root's `info/exclude`; the run is recorded with `worktree: 1` (and the run handle carries `worktree: true`). `400 EUNSUPPORTED` on any other tool. A `resumeSessionId` (or fork) of a session that ran in such a worktree starts the pane IN that worktree; one whose worktree is gone is `400 ENOWORKTREE`.
|
||||
|
||||
- **`slot`** — which Workspace tab's session to start, `0`–`3` or `"auto"` (default `0`, else `400 EBADSLOT`). `"auto"` lets the server pick the first slot with no live session, under the workspace lock (a client's own pick may come from a stale list); when all four are busy it answers `409 ERUNLIVE` "every tab of this workspace is busy", while a specific busy slot answers `409 ERUNLIVE` "that tab already has a live session". Slot 0 is the workspace's own `agenthub-lane-<id>` and is what `run_id` records; slots 1–3 are extra sessions on the same workspace, `agenthub-lane-<id>-<slot>`, and never touch `run_id`/`status`. The response adds `run_id`: the session this start opened. `stop`, `reset` and `remove` kill every slot's session, not only `run_id`.
|
||||
- **`shell`** — `true` opens a plain shell in that slot instead of an AI tool (the Workspace's **New terminal** tab): tmux's default shell in the workspace's folder, recorded as tool `shell`. Every other field is ignored, nothing comes from the request but the slot, and the workspace's `run_id`/`status` stay as they are (no agent is at work). A slot with a live session answers `409 ERUNLIVE`.
|
||||
- **`slot`** — which Workspace session to start, `0`–`9` (ten per workspace) or `"auto"` (default `0`, else `400 EBADSLOT`). `"auto"` lets the server pick the first slot with no live session, under the workspace lock (a client's own pick may come from a stale list); when all ten are busy it answers `409 ERUNLIVE` "every tab of this workspace is busy", while a specific busy slot answers `409 ERUNLIVE` "that tab already has a live session". Slot 0 is the workspace's own `agenthub-lane-<id>` and is what `run_id` records; slots 1–9 are extra sessions on the same workspace, `agenthub-lane-<id>-<slot>`, and never touch `run_id`/`status`. The response adds `run_id`: the session this start opened. `stop`, `reset` and `remove` kill every slot's session, not only `run_id`.
|
||||
- **`shell`** — `true` opens a plain shell in that slot instead of an AI tool (the Workspace's **New terminal** tab): the login shell tmux would run (`$SHELL -l`) in the workspace's folder, started only once a client is attached, so at the browser's size (`#{session_attached}` polled, at most 2 s — a prompt printed at tmux's 80 columns and then resized left the cursor on top of it, issue #55), recorded as tool `shell`. Every other field is ignored, nothing comes from the request but the slot, and the workspace's `run_id`/`status` stay as they are (no agent is at work). A slot with a live session answers `409 ERUNLIVE`.
|
||||
|
||||
Error `503 ETMUXMISSING` is returned when the host has no `tmux` binary. Every run the dashboard starts is a tmux session, so this is a missing dependency rather than a bad request — the message carries the install command, and the same code comes back from `POST /api/run`. Nothing else in the dashboard is affected.
|
||||
|
||||
@@ -526,14 +559,15 @@ that file.
|
||||
GET /api/lanes/:id/git/log?limit=30[&file=<path>]
|
||||
GET /api/lanes/:id/git/file?path=<path>[&rev=work|HEAD|<sha>]
|
||||
PUT /api/lanes/:id/git/file {path, content, baseSha}
|
||||
GET /api/lanes/:id/git/raw?path=<image>[&rev=work|HEAD|<sha>]
|
||||
GET /api/lanes/:id/git/ls?q=<substring>
|
||||
GET /api/lanes/:id/git/tree?path=<dir>
|
||||
GET /api/lanes/:id/git/tree-all
|
||||
```
|
||||
|
||||
The reads behind the Changes panel's **History**, whole-file view, quick-open
|
||||
and file explorer. All four are read-only and unguarded, and all four answer
|
||||
`{available:false}` with HTTP **200** for a cwd git cannot read.
|
||||
and file explorer. The GETs are read-only and unguarded, and the JSON ones
|
||||
answer `{available:false}` with HTTP **200** for a cwd git cannot read.
|
||||
|
||||
- **`/git/log`** → `{available, commits:[{sha, short, subject, author, date,
|
||||
refs}]}`, newest first, `limit` clamped to 200. `file=` narrows it to the
|
||||
@@ -547,6 +581,13 @@ and file explorer. All four are read-only and unguarded, and all four answer
|
||||
overwrite. `403 ESECRET` for `settings.local.json`; `400 EBADPATH`,
|
||||
`ENOFILE`, `EBADCONTENT`, `ETOOBIG`, `EBINARY`. Answers the file re-read,
|
||||
fresh `sha` included. CRLF endings and the final newline are preserved.
|
||||
- **`/git/raw`** → the bytes of one IMAGE file (png, jpg, jpeg, gif, webp,
|
||||
avif, bmp, ico, svg — by extension, which also sets the Content-Type), for
|
||||
the viewer's `<img>`; the Changes panel shows an image file this way instead
|
||||
of text or a binary diff. Same path/rev/secret guards as `/git/file`, 20 MB
|
||||
cap; `400 ENOTIMAGE` for any other extension, `ETOOBIG`, `ENOFILE`,
|
||||
`EBADPATH`, `EBADREV`, `403 ESECRET`. Served with a `sandbox` CSP and
|
||||
`nosniff`, so an SVG opened from this URL runs no script.
|
||||
- **`/git/file`** → `{available, path, rev, binary, truncated, lines, sha?}`
|
||||
(`sha` only for a whole, textual `work` file: what a save sends back).
|
||||
`rev=work` (default) is the file on disk — the one branch that reads the
|
||||
@@ -994,7 +1035,7 @@ Delete proof at one of three granularities: request body `{ "group": "phase-name
|
||||
POST /api/lanes/:id/proof-link
|
||||
```
|
||||
|
||||
Converge the clone-root `proof/` directory (if it exists at `<lane.cwd>/proof`) onto the canonical `.playwright-mcp/proof`. This is an explicit operation, never automatic — exposed only as `agenthub lanes proof-link`. Idempotent.
|
||||
Converge the clone-root `proof/` directory (if it exists at `<lane.cwd>/proof`) onto the canonical `.playwright-mcp/proof`. This is an explicit operation, never automatic — exposed only as `agenthub workspace proof-link`. Idempotent.
|
||||
|
||||
**Response:**
|
||||
|
||||
@@ -1110,7 +1151,7 @@ every number. `status` filters by session lifecycle, same convention as
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{ "name": "agenthub:repo-onboarding", "plugin": "agenthub", "count": 12, "sessions": 5 },
|
||||
{ "name": "agenthub:update-project-docs", "plugin": "agenthub", "count": 12, "sessions": 5 },
|
||||
{ "name": "daily-report", "plugin": null, "count": 3, "sessions": 3 }
|
||||
],
|
||||
"plugins": [
|
||||
@@ -1941,10 +1982,10 @@ curl -X DELETE http://localhost:4820/api/pricing/gpt-5.1-codex
|
||||
#### Total Cost
|
||||
|
||||
```http
|
||||
GET /api/pricing/cost?tz_offset=<minutes>&sources=<csv>&lane=<id>
|
||||
GET /api/pricing/cost?tz_offset=<minutes>&sources=<csv>&lane=<id>&date=<YYYY-MM-DD>
|
||||
```
|
||||
|
||||
The total cost with a per-model `breakdown`, plus `daily_costs: [{date, cost}]` by local date (`tz_offset` is the browser's `getTimezoneOffset()`; the status bar's **Today** is the entry for today). A day is the day the tokens were SPENT: each session's total is split over the local days of its `token_usage_hourly` rows (one per UTC hour, from each Claude transcript message's timestamp), and whatever the total holds beyond those rows — a session recorded before that table and whose transcript is gone, sub-agent files, Codex and Antigravity sessions — is booked to the session's start date. The days always add up to the total. `lane=<id>` narrows it to the sessions that workspace owns (`sessions.lane_id`, or held by one of its `dashboard_runs` and not stamped yet — the rule of `GET /api/sessions?lane=`; a non-numeric id is `400 INVALID_INPUT`); the status bar shows it as **Workspace $…** on the Workspace page, beside **Session $…** from `GET /api/pricing/cost/:sessionId` for the replayed session or the focused pane's.
|
||||
The total cost with a per-model `breakdown`, plus `daily_costs: [{date, cost}]` by local date (`tz_offset` is the browser's `getTimezoneOffset()`). `date=<YYYY-MM-DD>` prices only that local day — `total_cost`, `breakdown` and `daily_costs` all cover just it (any other shape is `400 INVALID_INPUT`); the status bar's **Today** asks for the local date and shows the day's token total beside it, listing tokens and cost per model on hover (as it does for **Workspace** and **Session**). A day is the day the tokens were SPENT: each session's total is split over the local days of its `token_usage_hourly` rows (one per UTC hour, from each Claude transcript message's timestamp), and whatever the total holds beyond those rows — a session recorded before that table and whose transcript is gone, sub-agent files, Codex and Antigravity sessions — is booked to the session's start date. The days always add up to the total. `lane=<id>` narrows it to the sessions that workspace owns (`sessions.lane_id`, or held by one of its `dashboard_runs` and not stamped yet — the rule of `GET /api/sessions?lane=`; a non-numeric id is `400 INVALID_INPUT`); the status bar shows it as **Workspace $…** on the Workspace page, beside **Session $…** from `GET /api/pricing/cost/:sessionId` for the replayed session or the focused pane's.
|
||||
|
||||
```bash
|
||||
curl "http://localhost:4820/api/pricing/cost?tz_offset=-420"
|
||||
@@ -2276,7 +2317,7 @@ GET /api/run List all live runs (computed fresh from tm
|
||||
GET /api/run/tmux { available: bool } — whether tmux is on PATH
|
||||
GET /api/run/binary?tool= { found, path } for the `claude`, `codex` or `agy` binary
|
||||
GET /api/run/cwds Suggested cwds (dashboard, home, recent)
|
||||
GET /api/run/history?limit=&laneId= Persisted run history; laneId narrows to one lane's runs
|
||||
GET /api/run/history?limit=&laneId=&tool= Persisted run history; laneId narrows to one lane's runs, tool (with laneId) to one tool's — `shell` = Terminal tabs
|
||||
GET /api/run/files?cwd=&q= Fuzzy file search inside cwd for the @-file autocomplete
|
||||
POST /api/run/drop-image Raw image body (png/jpeg/gif/webp, ≤20 MB) → {path}: saved 0600 under <tmpdir>/agenthub-drops-<uid>/ with a random name, pruned after a day; the terminal pastes the path
|
||||
(skips node_modules, .git, dist, build, .next, .cache, coverage, vendor)
|
||||
@@ -2295,7 +2336,7 @@ DELETE /api/run/:id Kill (SIGTERM → SIGKILL after 5 s)
|
||||
|
||||
`permissionMode` (`auto` default, or `acceptEdits` / `default` / `plan` / `bypassPermissions`) is the mode the pane starts in — `auto` is the CLI's own classifier mode: it asks only for what it reads as risky. The pane's argv also carries `--allow-dangerously-skip-permissions`, which only makes **bypass permissions** selectable in the pane's `shift+tab` cycle — it does not enable it.
|
||||
|
||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub workspace shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||
|
||||
Every history row carries `lane_id` (the workspace whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). A run's `id` is its tmux pane name, which each Start in that pane reuses, so the run it replaces is kept as its own row with `id` = `<pane name>@<its started_at>` (status `abandoned` if it never recorded an end). A row still marked `running` is flipped to `abandoned` once its tmux session is gone — checked at server start and on the 10 s run-heal beat (`DASHBOARD_RUN_HEAL_MS`); a pane that survived a server restart stays `running`. `GET /api/run/history?laneId=<n>` returns only that workspace's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, and the Dashboard's Analytics tab (Workflow sub-tab included) automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
||||
|
||||
@@ -2368,7 +2409,7 @@ The mascot (Sagi) can wear a pet: right-click it for **Hide pet**, **Size** (48
|
||||
|
||||
A **Codex pet** is the `.codex-pet.zip` format Codex Desktop uses (the bundles shared on codex-pets.net and in `mileson/codex-pets`): `pet.json` (`id`, `displayName`, `description`, `spritesheetPath`) and `spritesheet.webp`, a 1536×1872 atlas of 8 columns × 9 rows of 192×208 frames — idle, run right, run left, waving, jumping, failed, waiting, running, review, with 6/8/8/4/5/8/6/6/6 frames. The mascot plays the row of its mood (an agent working → running, waiting for you → waiting, an error → failed, a finished turn → jumping, idle/asleep → idle). Only the two files are read, at the archive root or one folder deep; an entry's size is checked from the zip's directory before it is inflated; the sheet must be a WebP of exactly that size (`400 EBADPET` otherwise). An uploaded image is shown still and bobs; SVG is refused because an SVG served from the dashboard's origin runs script.
|
||||
|
||||
**Featured pets are downloaded, never bundled.** The four offered (Goku, Bac Ho, Border Collie, 11) are other people's uploads to codex-pets.net with no licence to redistribute them, so AgentHub ships none of their art: `POST /api/pets/featured/:id` takes only an id from the fixed list in `server/lib/pets.js` (`404 ENOPET` for any other, with no request made) and fetches `https://codex-pets.net/api/pets/<id>` and `/spritesheet` — no URL from the request, no redirects followed. A pet id is always made by the server (`p-<12 hex>` for an upload, `cp-<featured id>`), every write is same-origin (`403 EBADORIGIN`), and at most 50 pets are kept (`409 ETOOMANY`). Other errors: `413 ETOOBIG`, `502 EFETCH` (codex-pets.net unreachable or refusing).
|
||||
**Featured pets are downloaded, never bundled.** The three offered (Goku, Border Collie, 11) are other people's uploads to codex-pets.net with no licence to redistribute them, so AgentHub ships none of their art: `POST /api/pets/featured/:id` takes only an id from the fixed list in `server/lib/pets.js` (`404 ENOPET` for any other, with no request made) and fetches `https://codex-pets.net/api/pets/<id>` and `/spritesheet` — no URL from the request, no redirects followed. A pet id is always made by the server (`p-<12 hex>` for an upload, `cp-<featured id>`), every write is same-origin (`403 EBADORIGIN`), and at most 50 pets are kept (`409 ETOOMANY`). Other errors: `413 ETOOBIG`, `502 EFETCH` (codex-pets.net unreachable or refusing).
|
||||
|
||||
---
|
||||
|
||||
@@ -2550,7 +2591,7 @@ Sent when a notification is created.
|
||||
|
||||
#### /ws-pty/:runId — PTY frames
|
||||
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub lanes shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`, else 80×24): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and re-asserts it whenever the terminal regains focus, because `window-size latest` lets a second attach client (another tab, `agenthub lanes shell`) resize the tmux window out from under this one. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub workspace shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`, else 80×24): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and re-asserts it whenever the terminal regains focus, because `window-size latest` lets a second attach client (another tab, `agenthub workspace shell`) resize the tmux window out from under this one. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
|
||||
#### cc_config_changed
|
||||
|
||||
|
||||
+17
-15
@@ -229,15 +229,17 @@ Manage the remote (SSH) machines this dashboard pulls Claude Code history from
|
||||
|
||||
A workspace is a durable unit of parallel agent work — one working directory, many sessions over time. Full guide: [`docs/LANES.md`](LANES.md).
|
||||
|
||||
`agenthub workspace …` (or `agenthub workspaces` for the list) is the name to use. `agenthub lanes …` and `--lane <id>` are the old names and keep working — the same commands as `agenthub workspace …` and `--workspace <id>` — so scripts and skills written before the rename need no change.
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `agenthub lanes` | List workspaces with stage, status, liveness and progress |
|
||||
| `agenthub lanes add --cwd <path> --title <text> [--tool <id>]` | Adopt an existing directory as a workspace; `--tool` is an AI Tools id (default `claude`) |
|
||||
| `agenthub lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>] [--tool <id>]` | Provision a dashboard-managed git worktree as a new workspace |
|
||||
| `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 workspace 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 workspace'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 workspace 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 workspace's database |
|
||||
| `agenthub workspaces` | List workspaces with stage, status, liveness and progress |
|
||||
| `agenthub workspace add --cwd <path> --title <text> [--tool <id>]` | Adopt an existing directory as a workspace; `--tool` is an AI Tools id (default `claude`) |
|
||||
| `agenthub workspace add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>] [--tool <id>]` | Provision a dashboard-managed git worktree as a new workspace |
|
||||
| `agenthub workspace 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 workspace pipeline [<template-id>] [<id>]` | Show which pipeline template a workspace 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 workspace's current stage unresolvable warns |
|
||||
| `agenthub workspace 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 workspace id) |
|
||||
| `agenthub workspace 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 workspace's database |
|
||||
| `agenthub stage <stage> [--tool claude\|codex] [--evidence <text>] [--note <text>] [--result pass\|fail]` | Declare the workspace'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` workspace sharing one directory |
|
||||
| `agenthub feature list [<id>]` | List every feature this workspace 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 |
|
||||
@@ -247,19 +249,19 @@ A workspace is a durable unit of parallel agent work — one working directory,
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| `agenthub lanes up [<id>] [--no-build]` | Boot the stack through the profile's `boot` + `health` hooks, then poll until it is healthy or the boot fails. `--no-build` reuses an existing build |
|
||||
| `agenthub lanes down [<id>]` | Stop the stack. Idempotent, and a no-op for a workspace that was never up |
|
||||
| `agenthub lanes runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the workspace's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
||||
| `agenthub lanes logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
||||
| `agenthub lanes hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
||||
| `agenthub lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
| `agenthub workspace up [<id>] [--no-build]` | Boot the stack through the profile's `boot` + `health` hooks, then poll until it is healthy or the boot fails. `--no-build` reuses an existing build |
|
||||
| `agenthub workspace down [<id>]` | Stop the stack. Idempotent, and a no-op for a workspace that was never up |
|
||||
| `agenthub workspace runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the workspace's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
||||
| `agenthub workspace logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
||||
| `agenthub workspace hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
||||
| `agenthub workspace proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||
| `agenthub lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||
| `agenthub lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-workspace named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling workspace (`lane<slot>`) |
|
||||
| `agenthub lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||
|
||||
Omit `<id>` and the command addresses the workspace owning the current directory, so a session running inside a workspace never needs to know its own id. Only a leading all-digits argument is read as an id — `agenthub lanes logs web --tail 4096` addresses the workspace by directory, not workspace 4096.
|
||||
Omit `<id>` and the command addresses the workspace owning the current directory, so a session running inside a workspace never needs to know its own id. Only a leading all-digits argument is read as an id — `agenthub workspace logs web --tail 4096` addresses the workspace by directory, not workspace 4096.
|
||||
|
||||
Resolution order, shared by every cwd-resolving command: `--lane`/leading id → `AGENTHUB_LANE_ID` (exported into every tmux pane the dashboard starts) → `--tool <id>`, else the harness's marker (`CLAUDECODE` means claude; Codex has no equivalent marker, so a codex session outside a dashboard pane passes `--tool codex`) → the workspace owning the directory. One directory can back a `claude` workspace and a `codex` workspace at once, so when two candidates remain the command fails and names both instead of picking one — see [Which workspace a CLI command addresses](LANES.md#which-workspace-a-cli-command-addresses).
|
||||
Resolution order, shared by every cwd-resolving command: `--workspace` (old name `--lane`)/leading id → `AGENTHUB_LANE_ID` (exported into every tmux pane the dashboard starts) → `--tool <id>`, else the harness's marker (`CLAUDECODE` means claude; Codex has no equivalent marker, so a codex session outside a dashboard pane passes `--tool codex`) → the workspace owning the directory. One directory can back a `claude` workspace and a `codex` workspace at once, so when two candidates remain the command fails and names both instead of picking one — see [Which workspace a CLI command addresses](LANES.md#which-workspace-a-cli-command-addresses).
|
||||
|
||||
Because a workspace's services are fully detached, restarting the dashboard (or `agenthub update`) never stops a running stack.
|
||||
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ graph TB
|
||||
- Node.js >= 20.0.0
|
||||
- npm >= 9.0.0
|
||||
- git — every lane operation shells out to it
|
||||
- tmux — only for the browser run console and `agenthub lanes shell`; both
|
||||
- tmux — only for the browser run console and `agenthub workspace shell`; both
|
||||
execute the binary directly and have no fallback. Per-OS install commands and
|
||||
the two-sided check (your PATH vs the server's) are in
|
||||
[INSTALL.md → Step 0](../INSTALL.md#step-0--system-packages).
|
||||
|
||||
+79
-78
@@ -14,10 +14,10 @@ A workspace persists across session restarts, which is what makes a pipeline vie
|
||||
|
||||
## Creating a workspace
|
||||
|
||||
Create a workspace bound to a working directory with `agenthub lanes add`:
|
||||
Create a workspace bound to a working directory with `agenthub workspace add`:
|
||||
|
||||
```bash
|
||||
agenthub lanes add --cwd /path/to/repo --title "My Feature"
|
||||
agenthub workspace add --cwd /path/to/repo --title "My Feature"
|
||||
```
|
||||
|
||||
The workspace is now ready to track work. When you run a Claude Code session in that directory (or a subdirectory), the dashboard will bind the session to the workspace automatically by matching the cwd.
|
||||
@@ -27,21 +27,21 @@ The workspace is now ready to track work. When you run a Claude Code session in
|
||||
To provision a dashboard-managed git worktree instead of adopting an existing directory, use a source repository:
|
||||
|
||||
```bash
|
||||
agenthub lanes add --repo /path/to/repo --title "My Feature" --base main --slug my-feature
|
||||
agenthub workspace add --repo /path/to/repo --title "My Feature" --base main --slug my-feature
|
||||
```
|
||||
|
||||
`--title`, `--base`, and `--slug` are optional. The CLI waits for background provisioning to finish and reports either the ready workspace 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 (once a pipeline template is installed) — see "Stage detection" below. Once adopted, that workspace's own card lists every managed-worktree workspace whose `source_repo` matches its `cwd`, each a clickable link to jump to that workspace.
|
||||
Adopting your main repo (`agenthub workspace 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 workspace's own card lists every managed-worktree workspace whose `source_repo` matches its `cwd`, each a clickable link to jump to that workspace.
|
||||
|
||||
Adding a workspace through the dashboard's "+ Add workspace" 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 workspace from being created or from each other — a workspace 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 workspace's own card, or the CLI) if the automatic attempt didn't apply.
|
||||
Adding a workspace through the dashboard's "+ Add workspace" flow also auto-runs, best-effort, in parallel: `agenthub workspace 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 workspace mcp sync`, and the AI Router pick. None of the three blocks the workspace from being created or from each other — a workspace 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 workspace's own card, or the CLI) if the automatic attempt didn't apply.
|
||||
|
||||
### The "+ Add workspace" modal
|
||||
|
||||
The modal has a segmented toggle mirroring the CLI's two modes:
|
||||
|
||||
- **Repo** — adopts the given directory as-is (maps to `agenthub lanes add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
|
||||
- **Worktree** (default) — provisions a managed worktree (maps to `agenthub lanes add --repo`).
|
||||
- **Repo** — adopts the given directory as-is (maps to `agenthub workspace add --cwd`). No branch fields; the auto-setup summary above does not run (adopting is instant, nothing to wait on).
|
||||
- **Worktree** (default) — provisions a managed worktree (maps to `agenthub workspace add --repo`).
|
||||
|
||||
In Worktree mode the branch question has three answers, because git's own rule
|
||||
decides which is available (issue #34):
|
||||
@@ -83,13 +83,13 @@ this field wants had to be retyped every time.
|
||||
The modal has **no pipeline picker**: every workspace it creates is born on
|
||||
`default` (the server's own default), and the template is changed afterwards
|
||||
from the workspace's own detail header in Workspace, or at creation from the
|
||||
terminal with `agenthub lanes add --pipeline <id>`. *Model endpoint* is the one
|
||||
terminal with `agenthub workspace add --pipeline <id>`. *Model endpoint* is the one
|
||||
per-workspace extra the form still asks for, and it sits in the form itself rather
|
||||
than behind a disclosure — a disclosure around a single field only hid it.
|
||||
|
||||
A workspace has no AI Router of its own: each NEW session picks one. The **AI Router** select in the workspace home's prompt box lists the profiles saved in Settings › AI Router (9Router, OpenRouter, a self-hosted gateway) plus **Native** (the vendor's own API); it shows Native alone, disabled, when the session's AI tool cannot use a router (only Claude and Codex can), and it remembers the last pick (`agenthub.session.router`). The start sends `router: "<name>"`; a Claude pane then runs with `--settings=<data dir>/router-settings/<name>.json`, which outranks the workspace's own settings, and a Codex pane with `-p <name>` plus its key through tmux `-e`. So two sessions of one workspace can run on two routers, a worktree session gets its router too, and switching never touches a running session. A resume goes back to the router its session last ran on (`dashboard_runs.router`). Editing a profile in Settings rewrites those files (a rotated key reaches live Claude sessions); deleting it removes them. Older builds wrote a workspace's router into its `.claude/settings.local.json`; every server start takes those eight keys back out. See `docs/API.md` § AI Router profiles.
|
||||
|
||||
Both modes' path field has a **Browse** button next to it, opening a small folder browser (`GET /api/lanes/browse?path=<abs>`) instead of a native OS picker — a browser cannot hand a web page an absolute filesystem path from a native dialog, so this dashboard (local-first, server and browser on the same machine) lists directories server-side instead: click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field. Git repos are marked in the listing.
|
||||
Both modes' path field has a **Browse** button next to it that opens the operating system's own folder picker (`POST /api/lanes/pick-path`). A browser cannot hand a web page an absolute path from its own dialog, so the SERVER opens the dialog on its desktop (zenity/kdialog on Linux, `osascript` on macOS, PowerShell on Windows) — which is your screen only when the browser runs on the same machine. A browser on another machine, or a server with no desktop or no zenity/kdialog, gets the small in-app folder browser instead (`GET /api/lanes/browse?path=<abs>`): click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field; git repos are marked. The same button sits beside the workspace home's **Folder** field, Import history's folder, the project-memory folder, CLAUDE_HOME/CODEX_HOME and a remote source's SSH identity file (a file picker, no in-app fallback).
|
||||
|
||||
## Workspace tool: Claude or Codex
|
||||
|
||||
@@ -132,9 +132,9 @@ the file if nothing else is in it).
|
||||
Reset a managed worktree, remove one, or purge the workspace's eligible session history with the CLI:
|
||||
|
||||
```bash
|
||||
agenthub lanes reset <id> [--force] [--keep-db] --yes
|
||||
agenthub lanes remove <id> [--force] --yes
|
||||
agenthub lanes purge <id> --yes
|
||||
agenthub workspace reset <id> [--force] [--keep-db] --yes
|
||||
agenthub workspace remove <id> [--force] --yes
|
||||
agenthub workspace purge <id> --yes
|
||||
```
|
||||
|
||||
Each command first fetches and prints its preflight counts. `reset` and `remove` show `head`, `dirty`, `untracked`, and `unpushed` (plus `database`, the name a data-isolated workspace's **reset** will drop — `remove` drops no database at all; see "Data isolation" below); `purge` shows `sessions`, `events`, and `tokenRows`. The action refuses to run without `--yes`, and sends those exact facts back as its confirmation. Use `--force` only when the preflight reports unpushed commits — it applies to `reset` alone, since `remove` deletes nothing that could hold them. `--keep-db` (reset only) skips the whole drop-recreate-migrate-reseed sequence, leaving the workspace's database exactly as it was. An adopted workspace points at a directory you own, so the CLI refuses to `reset` it. `remove` is allowed for **every** workspace kind, adopted or managed, because it drops only the dashboard's record and leaves the working tree, its branch, and its database (if any) untouched — deleting those is yours to do by hand.
|
||||
@@ -143,8 +143,8 @@ Each command first fetches and prints its preflight counts. `reset` and `remove`
|
||||
|
||||
### The two workspace kinds
|
||||
|
||||
- **`managed`** — a git worktree the dashboard itself created (`agenthub lanes add --repo`), rooted under `LANES_ROOT`. The dashboard may reset that worktree to its base branch. It does NOT delete it: removing the workspace forgets the dashboard's record and leaves the directory and its branch on disk.
|
||||
- **`adopted`** — an existing directory you already had (`agenthub lanes add --cwd`). The dashboard only ever tracks it; it never resets or deletes that worktree. "Removing" an adopted workspace drops the dashboard's record of it and leaves the directory untouched.
|
||||
- **`managed`** — a git worktree the dashboard itself created (`agenthub workspace add --repo`), rooted under `LANES_ROOT`. The dashboard may reset that worktree to its base branch. It does NOT delete it: removing the workspace forgets the dashboard's record and leaves the directory and its branch on disk.
|
||||
- **`adopted`** — an existing directory you already had (`agenthub workspace add --cwd`). The dashboard only ever tracks it; it never resets or deletes that worktree. "Removing" an adopted workspace drops the dashboard's record of it and leaves the directory untouched.
|
||||
|
||||
### The three independent safety checks
|
||||
|
||||
@@ -194,9 +194,9 @@ Every destructive `POST /api/lanes/:id/:action` must echo back an `expect` objec
|
||||
|
||||
### Limitation: a fresh worktree has no dependencies
|
||||
|
||||
`agenthub lanes add --repo` runs `git worktree add` only. It does not run `npm install` (or any other package manager), and it does not copy the source repo's untracked local files (`.env`, IDE config, etc.) into the new worktree. A managed workspace is ready for `git` immediately but needs its own dependency install and local env setup before a session can run the project inside it.
|
||||
`agenthub workspace add --repo` runs `git worktree add` only. It does not run `npm install` (or any other package manager), and it does not copy the source repo's untracked local files (`.env`, IDE config, etc.) into the new worktree. A managed workspace is ready for `git` immediately but needs its own dependency install and local env setup before a session can run the project inside it.
|
||||
|
||||
A repository can close the dependency half of that gap itself by declaring a `bootstrap` hook in its profile (below) and running `agenthub lanes hook bootstrap`. Seeding a workspace's `.env` and its database is handled automatically once the profile opts in — see "Data isolation" below.
|
||||
A repository can close the dependency half of that gap itself by declaring a `bootstrap` hook in its profile (below) and running `agenthub workspace hook bootstrap`. Seeding a workspace's `.env` and its database is handled automatically once the profile opts in — see "Data isolation" below.
|
||||
|
||||
## Workspace runtime: running a workspace's own stack
|
||||
|
||||
@@ -238,15 +238,15 @@ AgentHub resolves the profile from the **workspace's own working copy first**, t
|
||||
### Scaffolding a profile automatically
|
||||
|
||||
```bash
|
||||
agenthub lanes profile init <repo> [--force] # detect + write .agenthub/profile/
|
||||
agenthub lanes profile check [<path>] # validate one (path defaults to cwd)
|
||||
agenthub workspace profile init <repo> [--force] # detect + write .agenthub/profile/
|
||||
agenthub workspace profile check [<path>] # validate one (path defaults to cwd)
|
||||
```
|
||||
|
||||
`profile init` currently detects **Node.js only**, in exactly two layouts: a root `package.json` (single-service), or `backend/package.json` **and** `frontend/package.json` both present (monorepo, checked first). Anything else — Python/Go/Ruby, a pnpm/yarn/turborepo workspace, non-standard directory names — is refused with an actionable message rather than guessed at; write `.agenthub/profile/` by hand for those, following the reference above.
|
||||
|
||||
Detection reads `package.json` (which of `start`/`dev`/`preview` exists) and `docker-compose.yml` (a service matching `/postgres/i` or `/redis/i`), never executes anything, and never guesses a migration tool: a detected database gets `migrate.sh`/`seed.sh` scaffolded as an explicit, always-`exit 0` TODO stub, not a guessed Prisma/Knex/TypeORM command. `profile check` is the hard gate — it fails (non-zero exit, every problem listed) on any leftover `TODO:`, any missing or non-executable hook, or a declared port already in use; it only *warns* when `~/.agenthub/secrets.env` doesn't exist yet for a database-declaring profile.
|
||||
|
||||
`profile check` takes a **path**, not a workspace id or `--lane`/`--cwd` flag like every other `lanes` subcommand — it is meant to run against a bare repository right after `init`, before any workspace or worktree exists for it.
|
||||
`profile check` takes a **path**, not a workspace id or `--workspace`/`--cwd` flag like every other `workspace` subcommand — it is meant to run against a bare repository right after `init`, before any workspace or worktree exists for it.
|
||||
|
||||
|
||||
### Slots and ports
|
||||
@@ -296,12 +296,12 @@ Runnable hook names are a fixed allowlist: `bootstrap`, `boot`, `health`, `migra
|
||||
### Up, down, and surviving a restart
|
||||
|
||||
```bash
|
||||
agenthub lanes up # boot the lane owning this directory
|
||||
agenthub lanes up 3 --no-build # boot lane 3, reusing an existing build
|
||||
agenthub lanes runtime # slot, ports, service liveness, last boot error
|
||||
agenthub lanes logs api # tail one service or hook log
|
||||
agenthub lanes down # stop the stack
|
||||
agenthub lanes hook ci-gate # run any allowlisted hook
|
||||
agenthub workspace up # boot the lane owning this directory
|
||||
agenthub workspace up 3 --no-build # boot lane 3, reusing an existing build
|
||||
agenthub workspace runtime # slot, ports, service liveness, last boot error
|
||||
agenthub workspace logs api # tail one service or hook log
|
||||
agenthub workspace down # stop the stack
|
||||
agenthub workspace hook ci-gate # run any allowlisted hook
|
||||
```
|
||||
|
||||
`up` runs `boot` then `health`. It deliberately does **not** run `bootstrap` — installing dependencies on every boot would make a routine restart minutes long. A failing health check **leaves the processes running**: their logs are what tell you which service never came up, and killing them to report a tidy failure destroys the evidence.
|
||||
@@ -378,7 +378,7 @@ Parsed with the same literal `KEY=VALUE` reader as `profile.env` — never sourc
|
||||
|
||||
**When each step runs:**
|
||||
|
||||
- **Provisioning** a new managed worktree (`agenthub lanes add --repo`) — after the worktree is created: seed `.env`, run `bootstrap`, create the database, `migrate`, `seed`. Runs once.
|
||||
- **Provisioning** a new managed worktree (`agenthub workspace add --repo`) — after the worktree is created: seed `.env`, run `bootstrap`, create the database, `migrate`, `seed`. Runs once.
|
||||
- **`up`** — repair `.env` (a hand-edited or never-seeded file gets fixed), ensure the database exists (cheap when it already does), `migrate` on every boot (a workspace's schema drifts while it sits idle), and `seed` only on the boot that actually created the database.
|
||||
- **`reset`** — refresh `.env` with `--force` (preserving `ENV_PRESERVE` keys from the workspace's own file — swapping in the source's `JWT_SECRET` would 401 a running workspace's tokens until reboot), re-run `bootstrap` (a reset can land on a branch with new dependencies), clear the declared `LANE_DIRS`, then drop, recreate, migrate and reseed the database — unless `--keep-db`, which skips that whole block.
|
||||
- **`remove`** — drops **nothing**. `removeLaneData` still exists in `server/lib/lane-runtime.js` (and still refuses an adopted workspace), but the `remove` action no longer calls it: a workspace's database is the user's data and outlives the dashboard's record of it. Drop it yourself when you actually want it gone.
|
||||
@@ -391,32 +391,32 @@ The dashboard web UI merges workspaces and runs into a single **Workspace** page
|
||||
|
||||
- **No page header.** The page title and the four counters (`lanes`, `running`, `needs you`, `dead`) are gone: the sidebar's workspace list shows every workspace with its own status dot, and the status bar at the bottom of every page counts running and need-you workspaces. `GET /api/lanes` still returns `counts` for other callers.
|
||||
- **The sidebar's workspace list picks the workspace** — on every page, below the nav (`client/src/components/WorkspaceTree.tsx`), shaped like Claude Desktop's project list: every workspace on ONE level (no repo grouping, no worktree rows), in your stored order. A row (`WorkspaceRow.tsx`) is a chevron, a status dot (green = active, pulsing while running; amber row = needs you; red = dead or error; grey = idle), the title or folder name and the tool's icon, with `+` (open it for a new session) and `⋯` on hover. Clicking the name opens `/workspace?lane=<id>` and expands the row: the URL is the selection, so a notification link, F5 and the back button all land on the same workspace, and a bare `/workspace` reopens the last one shown (`agenthub.workspace.selected`) or the first. The header has a **tool filter** (shown once workspaces of two tools exist; kept in `agenthub.sidebar.projects` → `tool`, a stored tool no workspace has filters nothing), search and `+` (Add workspace). Search matches workspace names and paths — such a workspace keeps all its sessions — and session names: a workspace whose name does not match still shows when one of its sessions does, with only the matching sessions under it (every list stays mounted, hidden, to load and count). In the collapsed rail the list is a single icon that opens the sidebar.
|
||||
- **A workspace's sessions** — the chevron expands the row to the sessions the workspace OWNS and has not archived (`WorkspaceSessionList.tsx`: `GET /api/sessions?lane=<id>&archived=0`, the newest 20, re-read when the workspace's status or session moves; which rows are expanded is kept in `agenthub.sidebar.projects` → `expanded`). A live AI tab whose session has no row yet — a tab opened bare from `+`, before its first prompt — is listed at the top as **New session** straight from the run (its session id is known from the start), so a new tab shows in its workspace at once; the row becomes the real session when its first hook lands. The sidebar's live panes follow the Workspace page's own run-list poll (`LIVE_RUNS_EVENT`), since a tab in slots 1–3 moves no workspace column. A workspace owns a session its pane held — `bindRunSession` stamps `sessions.lane_id` from the pane's run, first writer wins, a nested `claude -p` excluded — and lists nothing else: a `claude` typed into a terminal in the same folder, or history from before the workspace existed, belongs to no workspace and is found on the Sessions page. Sessions older than this ownership column are not guessed: only those `dashboard_runs` still named were backfilled. A session started by a tool with no hooks (Codex without its hooks, any catalog tool) is never stamped. A row's dot is the session's state (`sessionDot`: amber waiting for you, red failed, green a live pane, hollow at rest); double-click renames it (`PATCH {name}`), and its hover button **archives** it (`PATCH {archived: true}` — hidden from the workspace, nothing deleted; the home lists the archived ones folded with **Unarchive**). A filled dot marks a session that still has a live pane (a `GET /api/run` handle carrying its session id — every pane the dashboard starts carries `AGENTHUB_RUN_ID=<its tmux name>`, the hook handler forwards it as `run_id`, and `bindRunSession` in `server/routes/hooks.js` records the hook's session on that run: the first session always, afterwards — for a Claude pane — only a `SessionStart` that is not a fresh `startup` (`/clear`, `/resume`), so a `claude -p` the agent runs inside the pane never takes it over; the rule is Claude-only, and a Codex or Antigravity pane follows every new session): clicking it opens `/workspace?lane=<id>&run=<runId>`, which ATTACHES to that tmux session (`attachRequest` on `LaneConsolePane`) — it starts nothing and resumes nothing. The session opens on its own tab (the tab of its tmux slot): in the pane already showing it (focused), else in the focused pane; **Ctrl/Cmd+click** opens it in a split beside the focused pane (see **Split panes** below). Any other session opens in its workspace the way Claude Desktop opens an old one: `/workspace?lane=<id>&session=<id>` shows its conversation, read-only (`SessionReplay.tsx`, the console hidden — never unmounted — behind it), with a prompt box under it. Opening it resumes nothing; **sending** a message resumes it (`POST /api/lanes/:id/start {slot, resumeSessionId, prompt}` in the first free pane slot, on the tool the session ran on) and opens that pane — a resume re-reads the whole transcript into context, so it waits for that explicit send. The session's analysis page (`/sessions/<id>`: agents, events, cost, transcript) is the **Analyst** button in the session view's right panel, beside Files and Changes (disabled while the pane's run has reported no session yet).
|
||||
- **Row menu** — right-click a row, or its `⋯` button: Start/Stop (slot 0), Rename, the Model (AI Router) select, New workspace here…, Copy path, Reset worktree… (managed only) and Delete workspace…, each the call the rest of the UI already makes. Delete is record-only exactly as before (see "Removing a workspace").
|
||||
- **A workspace's sessions** — the chevron expands the row to the sessions the workspace OWNS and has not archived (`WorkspaceSessionList.tsx`: `GET /api/sessions?lane=<id>&archived=0`, the newest 20, re-read when the workspace's status or session moves; which rows are expanded is kept in `agenthub.sidebar.projects` → `expanded`). A live AI tab whose session has no row yet — a tab opened bare from `+`, before its first prompt — is listed at the top as **New session** straight from the run (its session id is known from the start), so a new tab shows in its workspace at once; the row becomes the real session when its first hook lands. The sidebar's live panes follow the Workspace page's own run-list poll (`LIVE_RUNS_EVENT`), since a session in slots 1–9 moves no workspace column. A workspace owns a session its pane held — `bindRunSession` stamps `sessions.lane_id` from the pane's run, first writer wins, a nested `claude -p` excluded — and lists nothing else: a `claude` typed into a terminal in the same folder, or history from before the workspace existed, belongs to no workspace and is found on the Sessions page. Sessions older than this ownership column are not guessed: only those `dashboard_runs` still named were backfilled. A session started by a tool with no hooks (Codex without its hooks, any catalog tool) is never stamped. A row's dot is the session's state (`sessionDot`: amber waiting for you, red failed, green a live pane, hollow at rest); double-click renames it (`PATCH {name}`), and its hover button **archives** it (`PATCH {archived: true}` — hidden from the workspace, nothing deleted; the home lists the archived ones folded with **Unarchive**). A filled dot marks a session that still has a live pane (a `GET /api/run` handle carrying its session id — every pane the dashboard starts carries `AGENTHUB_RUN_ID=<its tmux name>`, the hook handler forwards it as `run_id`, and `bindRunSession` in `server/routes/hooks.js` records the hook's session on that run: the first session always, afterwards — for a Claude pane — only a `SessionStart` that is not a fresh `startup` (`/clear`, `/resume`), so a `claude -p` the agent runs inside the pane never takes it over; the rule is Claude-only, and a Codex or Antigravity pane follows every new session): clicking it opens `/workspace?lane=<id>&run=<runId>`, which ATTACHES to that tmux session (`attachRequest` on `LaneConsolePane`) — it starts nothing and resumes nothing. The session opens on its own tab (the tab of its tmux slot): in the pane already showing it (focused), else in the focused pane; **Ctrl/Cmd+click** opens it in a split beside the focused pane (see **Split panes** below). Any other session opens in its workspace the way Claude Desktop opens an old one: `/workspace?lane=<id>&session=<id>` shows its conversation, read-only (`SessionReplay.tsx`, the console hidden — never unmounted — behind it), with a prompt box under it. Opening it resumes nothing; **sending** a message resumes it (`POST /api/lanes/:id/start {slot, resumeSessionId, prompt}` in the first free pane slot, on the tool the session ran on) and opens that pane — a resume re-reads the whole transcript into context, so it waits for that explicit send. The session's analysis page (`/sessions/<id>`: agents, events, cost, transcript) is the **Analyst** button in the session view's right panel, beside Files and Changes (disabled while the pane's run has reported no session yet).
|
||||
- **Row menu** — right-click a row, or its `⋯` button: Rename, the Model (AI Router) select, New workspace here…, Copy path, Reset worktree… (managed only) and Delete workspace…, each the call the rest of the UI already makes. Delete is record-only exactly as before (see "Removing a workspace").
|
||||
- **Right-click menus** — every item that has actions opens them on a right-click too, in one shell (`client/src/components/ContextMenu.tsx`: placed at the pointer, kept inside the viewport, closed by Escape, a click outside or an item). The buttons stay; the menu repeats what they do and nothing more. A session row — in the sidebar, the home's Recents and the archived fold (`lanes/SessionMenu.tsx`) — offers Open, Open in split (live), Rename and Archive (sidebar), Unarchive (archived), Session details, Copy session ID and Close tab (live); the sidebar's **New session** row Open, Open in split and Close tab. A console tab offers Open, Open in split, Session details, Copy session ID and Close tab; a pane header (split view) Show <session> here, Split right/down with a new terminal and Close pane. In the Changes panel a changed file (overview row or review header) offers Open, Stage/Unstage, Discard — two clicks, as on its trash button — or the conflict's Keep mine / Take theirs / Mark resolved, and Copy relative path; a Files tree entry Open and Copy relative path; a History commit View commit, Copy commit SHA and Copy commit message. The home's title opens the row menu, and a Context file or extra folder offers Copy path and Remove. Outside the workspace the same shell serves the Sessions table, the Dashboard's agent cards and recent events, the Activity feed and Workflow runs (`components/SessionRowMenu.tsx`: Open session details, Open in its workspace when it has one, Copy session ID, Copy folder path), an attached prompt image (View / Remove), the managed workspace card (Reset), every Settings list (Git credentials, AI Router, Pricing, alert rules, webhooks, remote sources, AI tools) and every Claude Code / Codex config list (skills, agents, commands, output styles and memory files via `FileItemMenu` in `components/config/MdItemList.tsx`; plugins, MCP servers, marketplaces, auth profiles). A delete or remove there takes the button's own confirm — a modal, or two clicks. Text areas (the terminal, a conversation, a diff) keep the browser's own menu. The shell stops the menu's clicks from reaching the card it was opened from.
|
||||
- **The workspace home** — `/workspace?lane=<id>` opens on the workspace's home (`client/src/components/lanes/WorkspaceHome.tsx`), shaped like a Claude Desktop project page: the name (rename from its `⋯` menu, which is the sidebar row menu) with the repository it belongs to under it (root, the main worktree's branch, the worktree count, from `GET /api/projects` — the facts Settings → Projects used to show), a prompt box — the one place a session starts WITH a prompt (a pane has no run form; the session view's tab bar `+` only starts a bare CLI or a shell) — whose fields run in order **AI** (the session's own tool: every installed, enabled one, the workspace's by default), **AI Router** (Native or a saved profile; Native alone, disabled, for a tool that cannot use one), **Model** (the tool's own list, or on a router that router's models), **Permission** (Claude's permission mode; Codex's sandbox and approval; a catalog tool's per-session bypass switch) and **Effort** (Claude and Codex), and whose **Start** opens a NEW session in the first pane slot with no live run (0–3, `POST /api/lanes/:id/start {slot}`; all four busy is refused) and then shows it, and **Recents** — the workspace's own sessions (`?lane=<id>`) with their cost and time, a live one opening its pane and any other its conversation (`&session=`), exactly like the sidebar. The transcript page (`/sessions/<id>`) of a Claude session started in a workspace's own folder carries an explicit **Resume** (`client/src/components/lanes/SessionResumeButton.tsx`, the one place that resumes) and beside it **Fork**, which resumes the history into a NEW session id (`{forkSession: true}` → `--fork-session`, the original untouched, offered even while the original has a live pane): it warns that a resume re-reads the whole transcript into context, re-reads the live panes, then starts it in the workspace's first free pane (`POST /api/lanes/:id/start {slot, resumeSessionId}`) and opens it. A session from a SUBFOLDER gets no Resume — the pane starts in the workspace's folder and Claude finds a session by the folder it ran in — and a session that still has a live pane shows **Open pane** instead. On the right sit the workspace's own context: **Instructions** (appended to every new session's system prompt), **Memory** (read-only: the folder's CLAUDE.md / AGENTS.md family and Claude's auto memory), **Context** (uploaded files, ≤ 10 MB each, stored by AgentHub) and **Folder** (the workspace's own folder plus extra ones, with the repo's untracked worktrees suggested). Only Claude and Codex panes can take them (`--add-dir`, `--append-system-prompt-file=` / `-c developer_instructions=`); for any other tool the page says so and its panes start without them. See `docs/API.md` → Workspace context. The home HIDES the session view with CSS — the console never unmounts behind it.
|
||||
- **The workspace home** — `/workspace?lane=<id>` opens on the workspace's home (`client/src/components/lanes/WorkspaceHome.tsx`), shaped like a Claude Desktop project page: the name (rename from its `⋯` menu, which is the sidebar row menu) with the repository it belongs to under it (root, the main worktree's branch, the worktree count, from `GET /api/projects` — the facts Settings → Projects used to show), a prompt box — the one place a session starts WITH a prompt (a pane has no run form; the session view's tab bar `+` only starts a bare CLI or a shell) — whose fields run in order **AI** (the session's own tool: every installed, enabled one, the workspace's by default), **AI Router** (Native or a saved profile; Native alone, disabled, for a tool that cannot use one), **Model** (the tool's own list, or on a router that router's models), **Permission** (Claude's permission mode; Codex's sandbox and approval; a catalog tool's per-session bypass switch) and **Effort** (Claude and Codex), and whose **Start** opens a NEW session in the first slot with no live run (0–9, `POST /api/lanes/:id/start {slot}`; all ten busy is refused) and then shows it in a tab of its own, and **Recents** — the workspace's own sessions (`?lane=<id>`) with their cost and time, a live one opening its pane and any other its conversation (`&session=`), exactly like the sidebar. The transcript page (`/sessions/<id>`) of a Claude session started in a workspace's own folder carries an explicit **Resume** (`client/src/components/lanes/SessionResumeButton.tsx`, the one place that resumes) and beside it **Fork**, which resumes the history into a NEW session id (`{forkSession: true}` → `--fork-session`, the original untouched, offered even while the original has a live pane): it warns that a resume re-reads the whole transcript into context, re-reads the live panes, then starts it in the workspace's first free pane (`POST /api/lanes/:id/start {slot, resumeSessionId}`) and opens it. A session from a SUBFOLDER gets no Resume — the pane starts in the workspace's folder and Claude finds a session by the folder it ran in — and a session that still has a live pane shows **Open pane** instead. On the right sit the workspace's own context: **Instructions** (appended to every new session's system prompt), **Memory** (read-only: the folder's CLAUDE.md / AGENTS.md family and Claude's auto memory), **Context** (uploaded files, ≤ 10 MB each, stored by AgentHub) and **Folder** (the workspace's own folder plus extra ones, with the repo's untracked worktrees suggested). Only Claude and Codex panes can take them (`--add-dir`, `--append-system-prompt-file=` / `-c developer_instructions=`); for any other tool the page says so and its panes start without them. See `docs/API.md` → Workspace context. The home HIDES the session view with CSS — the console never unmounts behind it.
|
||||
- **A session in its own worktree** — Claude Desktop's worktree toggle: the row under the home's prompt box has a **Local / New worktree** select (Claude workspaces in a git repo). Claude Code itself makes a worktree only when started with `--worktree` or when asked mid-session — and a worktree it enters mid-session is invisible to AgentHub, which reads a session's folder once, at its start, and the new session's pane runs `claude --worktree=<name>`, so CLAUDE CODE makes `<repo>/.claude/worktrees/<name>` on branch `worktree-<name>`, copies `.worktreeinclude`, runs your `WorktreeCreate` hook, and on exit removes a worktree with no changes (or asks). AgentHub adds `/.claude/worktrees/` to the repo's `info/exclude` (Claude does not, and the main repo would list it). The session stays this workspace's own — listed with a branch icon and the worktree's name — and the session view's Changes panel works in ITS worktree (every git route with `?session=`, resolved from the session row: owned by the workspace, registered by `git worktree list`; `ENOWORKTREE` otherwise, never the main folder). Resume and Fork start in that worktree. **Archive** of a worktree session with no live pane asks first and removes the worktree like Claude Desktop — `git worktree remove` without force (git refuses unsaved work, and then nothing is archived) and `git branch -d` (unmerged work keeps its branch); a lock held by a live process refuses, a stale one is cleared. The workspace's own folder is never removed.
|
||||
- **The session view** — `/workspace?lane=<id>&run=<runId>` (a live session picked in the sidebar or Recents, or one just started) shows the console's tabs and the Files / Changes panel, with that run's tab active. The sidebar's workspace row opens a workspace's tabs when it has live ones — on the pane it last focused (`agenthub.workspace.paneLayout`) — and its home when it has none; the **Home** button at the left end of the tab bar opens the home of a workspace whose tabs stay running. Every workspace keeps its own tabs, panes and sessions: another workspace's tab is never shown. A browser notification for a workspace with a live pane opens it here directly.
|
||||
- **The session view** — `/workspace?lane=<id>&run=<runId>` (a live session picked in the sidebar or Recents, or one just started) shows the console's tabs and the Files / Changes panel, with that run's tab active. The sidebar's workspace row always opens the workspace's home, live tabs or not (a live tab opens from its session row); the **Home** button at the left end of the tab bar opens the home of a workspace whose tabs stay running. Every workspace keeps its own tabs, panes and sessions: another workspace's tab is never shown. A browser notification for a workspace with a live pane opens it here directly.
|
||||
- **Untracked worktrees** are no longer listed in the sidebar; Settings → Git → **Worktrees** (`showUntracked`, hidden sources `claude` = `<repo>/.claude/worktrees/*` and `agenthub` = `LANES_ROOT`) now decides which of them the home's Add folder suggests.
|
||||
- **No workspaces yet** — the page shows a single banner (`lane-empty`): icon, one line of what a workspace is, the CLI equivalent (`agenthub lanes add --cwd $(pwd)`) and an **Add workspace** affordance. The whole banner IS the button — clicking anywhere on it opens the Add-workspace modal. The banner stays compact on purpose: the run setup form below it is what needs the height.
|
||||
- **Reordering** — drag a row onto another, or press **Alt+↑/↓** with a row focused. The order is a **per-viewer preference**, not a workspace fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) 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 workspace the stored order has never seen lands at the END; a stored id whose workspace is gone is skipped.
|
||||
- **No workspaces yet** — the page shows a single banner (`lane-empty`): icon, one line of what a workspace is, the CLI equivalent (`agenthub workspace add --cwd $(pwd)`) and an **Add workspace** affordance. The whole banner IS the button — clicking anywhere on it opens the Add-workspace modal. The banner stays compact on purpose: the run setup form below it is what needs the height.
|
||||
- **Reordering** — drag a row and drop it anywhere in the list (a line shows where it lands, by the pointer's position — on another row, on a session list, above the first or below the last), or press **Alt+↑/↓** with a row focused. The order is a **per-viewer preference**, not a workspace fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) and writes nothing server-side — `GET /api/lanes` and `agenthub workspaces` keep answering in id order, and the order does not follow you to another browser. A workspace the stored order has never seen lands at the END; a stored id whose workspace is gone is skipped.
|
||||
- **Renaming a workspace** — double-click its sidebar row (or Rename in the row menu) 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 folder name back in its place. Renaming is display-only — a managed workspace's branch name was slugified from its title once, at creation.
|
||||
- **No header row** — the session view has no header of its own: the console's tab bar is it (status, the workspace's name and the back arrow are gone — the name is on the sidebar row and the home). Only a workspace with an installed pipeline template shows its time-on-stage, pipeline picker and feature picker above the console; there are no stage chips — the declared stage and the `auto: <stage>` detection both render in the PipelineMap. While one pane is shown, its split right/down menus sit at the right end of the tab bar; each offers **New terminal** and the same AI tools as the `+` menu. The detail block (the workspace card, the PipelineMap, the proof gallery) is always shown, and Delete lives in the sidebar row menu. There is no Tools menu any more: MCP sync and the sync-base check are the CLI's `agenthub lanes mcp sync` and `agenthub lanes sync-base`, and a readme refresh is a skill typed into a session.
|
||||
- **No header row** — the session view has no header of its own: the console's tab bar is it (status, the workspace's name and the back arrow are gone — the name is on the sidebar row and the home). Only a workspace with an installed pipeline template shows its time-on-stage, pipeline picker and feature picker above the console; there are no stage chips — the declared stage and the `auto: <stage>` detection both render in the PipelineMap. While one pane is shown, its split right/down menus sit at the right end of the tab bar; each offers **New terminal** and the same AI tools as the `+` menu. The detail block (the workspace card, the PipelineMap, the proof gallery) is always shown, and Delete lives in the sidebar row menu. There is no Tools menu any more: MCP sync and the sync-base check are the CLI's `agenthub workspace mcp sync` and `agenthub workspace sync-base`, and a readme refresh is a skill typed into a session.
|
||||
- **Detail panel** — the selected workspace'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. A pane has **no run form of its own**: a session with a prompt starts from the workspace home's prompt box. There is no **New run** button: when a pane's process ends (a double Ctrl+C out of `claude`, `exit`, a crash) its pane closes; the last one shows the next open tab, or the home when none is left. A pane with nothing to show offers **New session**, which opens the home too.
|
||||
Keyboard: the terminal keeps Tab and Shift+Tab (`claude` cycles its permission mode on Shift+Tab), so **Ctrl+M** toggles *Tab moves focus* — VS Code's way out of its terminal — and Tab then leaves the pane; **Cmd+F** (macOS) / **Ctrl+Shift+F** opens a find bar. URLs open in a new tab; a selection tmux copies (OSC 52) lands in the browser clipboard, and that path is write-only — nothing in the pane can read your clipboard. `agenthub.terminal.screenReader` = `"1"` in `localStorage` turns on xterm's screen-reader mode. While the socket is down a **Reconnecting…** overlay says keystrokes are not reaching the pane; it re-attaches by itself. In the session view, outside a terminal or a text field, `Alt+1`–`Alt+4` focuses pane 1–4, `Alt+\` splits the focused pane right with a new terminal, and `Alt+W` closes the focused pane (its session stays a tab).
|
||||
- **Clipboard in a pane** — the workspace'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.
|
||||
- **Scrollbar in a pane** — every pane gets tmux's `mouse on` and `pane-scrollbars on` with a visible style (`pane-scrollbars-style fg=brightwhite,bg=black,width=2`, `enableMouse` in `server/lib/tmux.js`; tmux 3.7+). tmux keeps no history and draws no scrollbar on the alternate screen, so a `claude` pane is started with `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1` (`spawnRun` in `server/lib/pty-run.js`) and renders inline even when `~/.claude/settings.json` says `"tui": "fullscreen"`. That setting still applies to `claude` in your own terminals; a pane started before this change keeps fullscreen until it is restarted.
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`), one tab per live tmux session of the workspace — at most four, its slots 0–3: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–3 are `agenthub-lane-<id>-<slot>`. A tab shows its AI tool's icon and name (or **Terminal**); the active tab is the focused pane's, which is the URL's `?run=`, so the Changes panel and Analyst follow it, and a tab another pane shows carries a grey underline. A click shows a tab in the focused pane (or focuses the pane already showing it), **Ctrl/Cmd+click** — on a tab or on a `+` item — opens it in a split, and a tab drags onto the panes. Every tab with a live run stays mounted and those no pane shows are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops its session (`DELETE /api/run/<runId>`) and closes its pane; with no pane left it moves to the next tab, or the home. The **+** menu opens a new tab in the first free slot, which the server picks (`slot: "auto"`, under the workspace lock): **New terminal** (your own default shell in the workspace folder — `POST /api/lanes/:id/start {slot, shell: true}`, recorded as tool `shell`; it is not an agent, so the workspace's `run_id`/`status` are left alone), each installed, enabled AI tool (started bare, on its default model, no prompt), and **Agent settings…** (Settings → Agents). With four tabs open **+** is disabled. EVERY tab is bound to the sidebar's selection. A tab has no header of its own (no title, no **Active runs** list, no **New run**): other sessions are opened from the sidebar or the home's Recents. (The old fixed 1/2/4 split layout and its `agenthub.workspace.splitView` key are gone; **Split panes** replaces them.)
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`) over the workspace's live tmux sessions — at most ten, its slots 0–9: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–9 are `agenthub-lane-<id>-<slot>`. Every tab is a **combo**: a group of one to four split panes, each session in exactly one tab (`PaneGroups` in `client/src/lib/paneLayout.ts`). A one-pane tab shows its AI tool's icon and name (or **Terminal**); a combo tab shows every pane's icon and name, and the most urgent state dot. The active tab is the one on screen and its focused pane is the URL's `?run=`, so the Changes panel and Analyst follow it. A click shows a tab; **Ctrl/Cmd+click** on a one-pane tab or a `+` item brings it into the tab on screen, split to the right (a full tab — four panes — opens it as a tab of its own instead), and a one-pane tab dragged onto a pane joins that tab there. Any tab drags along the bar to reorder it; a pane's header dragged onto the bar takes that pane out into a tab of its own at that place. Every live session stays mounted and those of other tabs are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops every session in it (`DELETE /api/run/<runId>`, after a confirm when one is an AI tool); with no tab left it moves to the home. The **+** menu opens a new tab in the first free slot, which the server picks (`slot: "auto"`, under the workspace lock): **New terminal** (your own default shell in the workspace folder — `POST /api/lanes/:id/start {slot, shell: true}`, recorded as tool `shell`; it is not an agent, so the workspace's `run_id`/`status` are left alone), each installed, enabled AI tool (started bare, on its default model, no prompt), and **Agent settings…** (Settings → Agents). With ten sessions open **+** is disabled. EVERY tab is bound to the sidebar's selection. A tab has no header of its own (no title, no **Active runs** list, no **New run**): other sessions are opened from the sidebar or the home's Recents. (The old fixed 1/2/4 split layout and its `agenthub.workspace.splitView` key are gone; **Split panes** replaces them.)
|
||||
|
||||
- **Split panes** — the console splits like VS Code (`client/src/components/run/PaneGrid.tsx`, model in `client/src/lib/paneLayout.ts`): a binary tree of splits, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes, one per live session, a session never shown twice. **Ctrl/Cmd+click** a tab, a `+` item or a sidebar session row opens it to the right of the focused pane (the intent rides on the navigation's state, never in the URL); with more than one pane each gets a slim header: **drag** it (or a tab) onto another pane — its edge splits there, its middle swaps the two —, its **▾** shows another live session in it (swapping with the pane that showed it), **Split right / Split down** puts a not-shown session or a **New terminal** beside it, and **×** closes the pane only (its session stays a tab). Dividers drag (or move by arrow keys) between 10% and 90%. Clicking in (or tabbing into) a pane focuses it, `?run=` follows — so the right panel, Analyst and the sidebar read the focused session — and the keyboard goes with it: only the focused pane's terminal takes it when it appears. A pane closes when its terminal reports that its session ended (exited, closed from another browser); the run list itself only prunes a remembered layout on a workspace's first read, and a pane opened here whose session never got listed — one empty read, which the server returns on any tmux error, never wipes a split. A session ending while the home or a replay is shown only closes its pane; the URL stays. The layout is a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`), never on the server. Every pane is an absolutely placed sibling keyed by its slot, so splitting, swapping or resizing never re-parents — and so never remounts — a live terminal; the session view itself fills the page edge to edge, its tab bar and the right panel's activity bar one header strip.
|
||||
- **Split panes** — the console splits like VS Code (`client/src/components/run/PaneGrid.tsx`, model in `client/src/lib/paneLayout.ts`): one binary tree of splits PER TAB, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes in a tab, a session never shown twice. **Ctrl/Cmd+click** a one-pane tab, a `+` item or a sidebar session row brings it into the tab on screen, to the right of the focused pane (the intent rides on the navigation's state, never in the URL); with more than one pane each gets a slim header: **drag** it (or a tab) onto another pane — its edge splits there, its middle swaps the two —, its **▾** shows another live session in it (swapping with the pane that showed it), **Split right / Split down** puts a not-shown session or a **New terminal** beside it, and **×** takes the pane out of this tab into a tab of its own (its session keeps running). A session dropped or swapped in from another tab leaves that tab. Dividers drag (or move by arrow keys) between 10% and 90%. Clicking in (or tabbing into) a pane focuses it, `?run=` follows — so the right panel, Analyst and the sidebar read the focused session — and the keyboard goes with it: only the focused pane's terminal takes it when it appears. A pane closes when its terminal reports that its session ended (exited, closed from another browser); the run list itself only prunes a remembered layout on a workspace's first read, and a pane opened here whose session never got listed — one empty read, which the server returns on any tmux error, never wipes a split. A session ending while the home or a replay is shown only closes its pane; the URL stays. The tabs and their layouts are a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`, `{groups, active}`; a stored pre-tab single layout reads as one tab), never on the server. Every pane is an absolutely placed sibling keyed by its slot, so splitting, swapping or resizing never re-parents — and so never remounts — a live terminal; the session view itself fills the page edge to edge, its tab bar and the right panel's activity bar one header strip.
|
||||
- **Workspace grid** — one card per workspace, 1 column, 2 at `md`, 3 at `xl`. Each card carries the workspace id, liveness dot and status, title, declared stage and time-on-stage, the `auto:` chip when detection leads, the kind and CI tags, the working-copy facts from `GET /api/lanes/:id/git`, and the action row. The needs-you message (`needs_action`) is on the workspace list tile only — the detail card no longer repeats it as a banner.
|
||||
|
||||
Run history is per workspace, queryable via `GET /api/run/history?laneId=<n>`.
|
||||
|
||||
**A console pane follows the workspace it shows.** Selecting another workspace on the workspace list swaps every pane's cwd, run history and terminal over to the new workspace. If the new workspace already has a live session for that pane's slot in `GET /api/run`, the pane re-attaches to it immediately, so each pane sticks to its own `agenthub-lane-<id>[-<slot>]` tmux session; if it has none, the pane is empty. Nothing of the previous workspace (a half-typed prompt, its terminal) carries over.
|
||||
|
||||
**Start and Resume are create-or-reuse, and never silently swallow the request.** When the workspace's `agenthub-lane-<id>` tmux session does not exist, it is created with the full argv. When it exists but its pane is sitting at a **shell prompt** (a `agenthub lanes shell` you opened, or a `claude` that has since exited), the argv is typed into that pane — so `--resume` really runs, and an initial prompt really lands, in the session you are already looking at. Only when the pane is running something (a live `claude`, an editor, a build) is the request adopted as-is: attaching shows you what is running rather than typing over it. Before this, an existing session was always adopted silently, so the first Resume after a `agenthub lanes shell` answered `200` while doing nothing at all.
|
||||
**Start and Resume are create-or-reuse, and never silently swallow the request.** When the workspace's `agenthub-lane-<id>` tmux session does not exist, it is created with the full argv. When it exists but its pane is sitting at a **shell prompt** (a `agenthub workspace shell` you opened, or a `claude` that has since exited), the argv is typed into that pane — so `--resume` really runs, and an initial prompt really lands, in the session you are already looking at. Only when the pane is running something (a live `claude`, an editor, a build) is the request adopted as-is: attaching shows you what is running rather than typing over it. Before this, an existing session was always adopted silently, so the first Resume after a `agenthub workspace shell` answered `200` while doing nothing at all.
|
||||
|
||||
**A workspace whose `run_id` still points at a live run refuses a second Start/Resume outright** — `POST /api/lanes/:id/start` answers `409 ERUNLIVE` rather than typing a new prompt or `--resume` over a session that is still in flight. The dashboard starts a session only in a slot with no live run (the home, a resume and the tab bar's `+` all send `slot: "auto"`, so the SERVER picks the first free one under the workspace lock; `409 ERUNLIVE` when all four are busy), and a tab's **×** is what frees a slot (`DELETE /api/run/:id`); the workspace's own `run_id`/`status` self-heal to idle the next time anything reads the workspace (`healRunId` in `routes/lanes.js`), so no separate workspace-side call is needed.
|
||||
|
||||
@@ -630,7 +630,8 @@ reads the workspace's git surface in five modes:
|
||||
renamed), and a folder holding any of them gets an amber dot. The colours
|
||||
come with the tree read, so **Refresh** re-reads them.
|
||||
Opening a file entry shows the whole file (`/git/file`) for reading, at every
|
||||
panel width. **Clicking its text** (or Enter on it; any range but a commit)
|
||||
panel width; an image file (png, jpg, gif, webp, avif, bmp, ico, svg) is shown
|
||||
as the picture (`/git/raw`), with no editor. **Clicking its text** (or Enter on it; any range but a commit)
|
||||
turns that view into a text editor with the caret on the clicked line and
|
||||
the scroll position kept — a click that only ends a drag-selection does not,
|
||||
so copying never drops you into the editor. **Cancel** returns to reading.
|
||||
@@ -731,7 +732,7 @@ expensive shapes. Diffs are capped server-side (300 files, 200 KB per file,
|
||||
List all workspaces with their current status:
|
||||
|
||||
```bash
|
||||
agenthub lanes
|
||||
agenthub workspaces
|
||||
```
|
||||
|
||||
This shows each workspace's:
|
||||
@@ -755,13 +756,13 @@ The dashboard web UI shows each workspace as a card in a grid, with the selected
|
||||
|
||||
A workspace moves through stages defined in a **pipeline template** (see "Custom pipeline templates" below).
|
||||
|
||||
**A workspace is created on `default` unless told otherwise.** The "+ Add workspace" 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 workspace's detail header in Workspace.
|
||||
**A workspace is created on `default` unless told otherwise.** The "+ Add workspace" modal does not ask — it always creates on `default`. Pick another with `agenthub workspace add --pipeline <id>` at creation, or change it afterwards from the *Pipeline template* select in the workspace's detail header in Workspace.
|
||||
|
||||
For a workspace that already exists, switch it from the terminal:
|
||||
|
||||
```bash
|
||||
agenthub lanes pipeline # which template this lane uses, and what else exists
|
||||
agenthub lanes pipeline ml-training # switch it (any installed template id)
|
||||
agenthub workspace pipeline # which template this lane uses, and what else exists
|
||||
agenthub workspace pipeline ml-training # switch it (any installed template id)
|
||||
```
|
||||
|
||||
or from the dashboard: the pipeline-template select next to the workspace'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).
|
||||
@@ -773,10 +774,10 @@ A template may name the slash command that drives it (its optional `skill` field
|
||||
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 <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
|
||||
agenthub workspace node # list every node of this lane's pipeline and its command
|
||||
agenthub workspace node <node-id> # run that node's command in the lane's cwd
|
||||
agenthub workspace node <node-id> extra words # extra words are appended to the prompt
|
||||
agenthub workspace node <node-id> --print # show the argv, run nothing
|
||||
```
|
||||
|
||||
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 workspace comes from the working directory as usual, and the command always runs in the workspace's own `cwd`. The node is matched by **id or alias**.
|
||||
@@ -832,7 +833,7 @@ offering every other workspace even while the counts show only one.
|
||||
|
||||
## Attaching a real terminal to a workspace
|
||||
|
||||
`agenthub lanes shell` attaches a real terminal to the exact tmux session the dashboard's Start/Resume buttons use for this workspace (`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.
|
||||
`agenthub workspace shell` attaches a real terminal to the exact tmux session the dashboard's Start/Resume buttons use for this workspace (`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.
|
||||
|
||||
The dashboard renders every node in the pipeline in one of five **states**:
|
||||
|
||||
@@ -872,7 +873,7 @@ This command:
|
||||
### Full command signature
|
||||
|
||||
```bash
|
||||
agenthub stage <stage> [--lane <id>] [--cwd <path>] [--tool <id>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]
|
||||
agenthub stage <stage> [--workspace <id>] [--cwd <path>] [--tool <id>] [--status <s>] [--evidence <text>] [--note <text>] [--result pass|fail]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
@@ -892,7 +893,7 @@ agenthub stage <stage> [--lane <id>] [--cwd <path>] [--tool <id>] [--status <s>]
|
||||
The check is skipped for `--result fail`, which paints the node `failed`
|
||||
rather than `current`.
|
||||
|
||||
- `--lane <id>` (optional): the numeric workspace ID. If omitted, the command resolves the workspace by `cwd`.
|
||||
- `--workspace <id>` (optional): the numeric workspace ID. If omitted, the command resolves the workspace by `cwd`.
|
||||
|
||||
- `--cwd <path>` (optional): working directory to match against a workspace's cwd. If omitted, uses the current working directory. Useful when calling from outside the repo.
|
||||
|
||||
@@ -954,7 +955,7 @@ Every cwd-resolving `agenthub` command (`stage`, `lanes pipeline`, `feature
|
||||
activate`, `lanes up/down/logs`, …) shares one resolver, `resolveLaneArg` in
|
||||
`bin/agenthub.js`. It answers in this order:
|
||||
|
||||
1. `--lane <id>`, or a leading numeric positional.
|
||||
1. `--workspace <id>`, or a leading numeric positional.
|
||||
2. `AGENTHUB_LANE_ID` in the environment — every tmux pane the dashboard starts
|
||||
carries it (`tmux new-session -e`, or an assignment prefix when the argv is
|
||||
typed into a pane already at a shell prompt). A session inside a
|
||||
@@ -973,7 +974,7 @@ directory and no signal from steps 1-3, the command **fails** and names both
|
||||
workspaces rather than picking one:
|
||||
|
||||
```
|
||||
2 lanes own /home/me/repo — pass --tool <claude|codex> or --lane <id>: #23 (claude), #33 (codex)
|
||||
2 workspaces own /home/me/repo — pass --tool <claude|codex> or --workspace <id>: #23 (claude), #33 (codex)
|
||||
```
|
||||
|
||||
That is deliberate. Silently taking the first would write a codex session's
|
||||
@@ -1036,7 +1037,7 @@ Create a JSON file in your custom pipeline directory (e.g., `~/.agenthub/pipelin
|
||||
- **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 (`"/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 workspace card and as copyable text per node in Dashboard → Analytics → Workflow, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
|
||||
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) or a shell one (`"agenthub workspace 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 workspace card and as copyable text per node in Dashboard → Analytics → Workflow, and run by `agenthub workspace 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 workspace 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".
|
||||
|
||||
@@ -1049,7 +1050,7 @@ export DASHBOARD_PIPELINES_DIR=~/.agenthub/pipelines
|
||||
When you create a new workspace, you can optionally specify which pipeline to use:
|
||||
|
||||
```bash
|
||||
agenthub lanes add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-training
|
||||
agenthub workspace add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-training
|
||||
```
|
||||
|
||||
(The pipeline is selected at creation time. Workspaces created without `--pipeline` default to the `default` pipeline — yours if you installed one with that id, otherwise the empty one.)
|
||||
@@ -1194,7 +1195,7 @@ key back onto its node, so a stage declared by alias keeps its record — and it
|
||||
`--evidence` — instead of reading as an inference or losing its `done`. Where a
|
||||
node has both a canonical record and an alias record, the canonical one wins.
|
||||
|
||||
The same rule governs `agenthub lanes`: the inferred stage is printed only when it
|
||||
The same rule governs `agenthub workspaces`: the inferred stage is printed only when it
|
||||
leads the declared one (see "Viewing workspaces" below), and the PipelineMap's
|
||||
amber detection badge follows the identical condition — the terminal and the
|
||||
browser never disagree about which one is the headline. Both locate the
|
||||
@@ -1263,7 +1264,7 @@ command: it's whichever of the fields above the matching rule's `tool` carried.
|
||||
Detection only ever attributes to a workspace whose `cwd` matches the hook's
|
||||
session `cwd` (see "Which workspace a signal is credited to" above). A session
|
||||
working directly in a source repo that was never itself adopted as a workspace —
|
||||
run `agenthub lanes add --cwd $(pwd)` from that repo to fix that — gets no
|
||||
run `agenthub workspace add --cwd $(pwd)` from that repo to fix that — gets no
|
||||
detection at all, because no workspace owns that `cwd`.
|
||||
|
||||
### Detection expires
|
||||
@@ -1395,12 +1396,12 @@ Set the environment variable to change when a watcher is considered dead:
|
||||
export LANE_DEAD_SEC=600 # 10 minutes instead of 5
|
||||
```
|
||||
|
||||
## Housekeeping: agenthub lanes gc
|
||||
## Housekeeping: agenthub workspace gc
|
||||
|
||||
Runs machine-wide, not against one workspace — safe to run any time, running workspaces and live sessions are unaffected:
|
||||
|
||||
```bash
|
||||
agenthub lanes gc [--dry-run]
|
||||
agenthub workspace gc [--dry-run]
|
||||
```
|
||||
|
||||
Two things, both scoped to what's actually stale:
|
||||
@@ -1408,7 +1409,7 @@ Two things, both scoped to what's actually stale:
|
||||
- **Reaps orphaned Playwright MCP processes.** A `.playwright-mcp`-scoped process whose owning Claude Code session died gets reparented to pid 1 — a live session's MCP keeps its real parent and is left alone. Kills the whole process tree so a spawned browser doesn't leak too.
|
||||
- **Caps oversized hook logs.** Any `LANES_ROOT/.state/lane<N>/logs/*.log` over 10MB is truncated in place to its last 2MB (same inode — a concurrent writer's file descriptor stays valid).
|
||||
|
||||
`--dry-run` prints what would happen without doing it. This is a deliberately narrower port of Shipyard's `lane-gc.sh` — it does NOT auto-remove stale worktrees by age (that would violate this repo's own never-automatic-destroy rule; use `agenthub lanes reset|remove|purge` explicitly instead), archive feature state (AgentHub's workspace/feature state lives in the database, not flat files), or sweep scratch debris (AgentHub doesn't generate the files Shipyard's own scripts did). See `docs/superpowers/specs/2026-08-05-lane-gc-design.md` for the full scoping rationale.
|
||||
`--dry-run` prints what would happen without doing it. This is a deliberately narrower port of Shipyard's `lane-gc.sh` — it does NOT auto-remove stale worktrees by age (that would violate this repo's own never-automatic-destroy rule; use `agenthub workspace reset|remove|purge` explicitly instead), archive feature state (AgentHub's workspace/feature state lives in the database, not flat files), or sweep scratch debris (AgentHub doesn't generate the files Shipyard's own scripts did). See `docs/superpowers/specs/2026-08-05-lane-gc-design.md` for the full scoping rationale.
|
||||
|
||||
## Workspace actions
|
||||
|
||||
@@ -1416,7 +1417,7 @@ The web UI and CLI provide these actions on a workspace:
|
||||
|
||||
### start
|
||||
|
||||
Start a tmux-backed run attached to the workspace. The Workspace page's "Start Run" button calls `POST /api/run` with the workspace's id; you can also attach from a real terminal using `agenthub lanes shell`.
|
||||
Start a tmux-backed run attached to the workspace. The Workspace page's "Start Run" button calls `POST /api/run` with the workspace's id; you can also attach from a real terminal using `agenthub workspace shell`.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4820/api/run \
|
||||
@@ -1424,7 +1425,7 @@ curl -X POST http://localhost:4820/api/run \
|
||||
-d '{"laneId": 5, "initialPrompt": "continue the work"}'
|
||||
```
|
||||
|
||||
The `initialPrompt` field is optional — if omitted, the tmux session is created/attached with no initial input, and you type into the terminal directly. Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The same tmux session persists across attach/detach cycles, so you can switch between the browser Workspace and `agenthub lanes shell` seamlessly.
|
||||
The `initialPrompt` field is optional — if omitted, the tmux session is created/attached with no initial input, and you type into the terminal directly. Returns `{ id, laneId, status, cwd, model, permissionMode, effort, resumeSessionId, sessionId, startedAt, promptPreview }` where `id` is the tmux session name. The same tmux session persists across attach/detach cycles, so you can switch between the browser Workspace and `agenthub workspace shell` seamlessly.
|
||||
|
||||
**The model picker offers aliases, not pinned ids.** The Run setup's Model select sends `fable` / `opus` / `sonnet` / `haiku` (plus `Custom…` for a literal id, and the empty value that inherits `settings.json`). `claude --model` documents those as *"an alias for the latest model"* and resolves each itself — 2.1.278 maps `fable` to `claude-fable-5-1` — so a new model release reaches the dashboard the moment the CLI is updated, with no list to edit by hand. Cost maths is unaffected: pricing keys off the model id the transcript reports, which is always the resolved full name. Codex has no such aliases, so `RUN_MODEL_CHOICES_CODEX` stays a pinned list.
|
||||
|
||||
@@ -1516,7 +1517,7 @@ Proof files live inside the workspace's own `cwd`, not under `LANES_ROOT`:
|
||||
|
||||
### Linking proof directories (the `ensure_proof_link` primitive)
|
||||
|
||||
An MCP server's `--output-dir` flag may point to different locations across runs. To converge all proof onto one canonical path regardless, AgentHub provides the `ensure_proof_link` operation, exposed only as the explicit `agenthub lanes proof-link` CLI command — it is **never automatic**. This matches the "AgentHub does not orchestrate" rule: a session decides when to link, not the dashboard.
|
||||
An MCP server's `--output-dir` flag may point to different locations across runs. To converge all proof onto one canonical path regardless, AgentHub provides the `ensure_proof_link` operation, exposed only as the explicit `agenthub workspace proof-link` CLI command — it is **never automatic**. This matches the "AgentHub does not orchestrate" rule: a session decides when to link, not the dashboard.
|
||||
|
||||
`ensure_proof_link` works as follows:
|
||||
|
||||
@@ -1583,7 +1584,7 @@ The commands below are primitives a driving session (or you) calls explicitly
|
||||
When the workspace stack needs to run under QC conditions (deterministic, mocked externals), use the `--qc` flag:
|
||||
|
||||
```bash
|
||||
agenthub lanes up --qc
|
||||
agenthub workspace up --qc
|
||||
```
|
||||
|
||||
This flag activates the workspace's `QC_BOOT_ENV` profile declaration. Define it in the profile's `profile.env` file as space-separated `KEY=value` pairs (the same format as the `PORTS` declaration):
|
||||
@@ -1595,16 +1596,16 @@ PORT_BASE_app=3000
|
||||
QC_BOOT_ENV="MOCK_PAYMENTS=1 STUB_EMAIL=1"
|
||||
```
|
||||
|
||||
These environment variables are passed to the profile's `boot` hook during `agenthub lanes up --qc`, ensuring QC test runs are deterministic.
|
||||
These environment variables are passed to the profile's `boot` hook during `agenthub workspace up --qc`, ensuring QC test runs are deterministic.
|
||||
|
||||
### Dev preflight and merge safety: sync-base
|
||||
|
||||
`agenthub lanes sync-base` is the ONE sanctioned merge a session runs from the CLI — `origin/development` into a feature branch. Three modes:
|
||||
`agenthub workspace 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
|
||||
agenthub lanes sync-base feat/<slug> # merge origin/development into the feature branch
|
||||
agenthub lanes sync-base --continue feat/<slug> # finish after a manually resolved conflict
|
||||
agenthub workspace sync-base --check feat/<slug> # read-only preflight: fetch + collision check + DEV_DELTA/DEV_OVERLAP
|
||||
agenthub workspace sync-base feat/<slug> # merge origin/development into the feature branch
|
||||
agenthub workspace sync-base --continue feat/<slug> # finish after a manually resolved conflict
|
||||
```
|
||||
|
||||
Exit codes: `0` clean, `4` merge conflict (left in place — resolve, commit, then `--continue`), `5` migration-number collision (nothing merged — rename the printed file, re-run).
|
||||
@@ -1621,10 +1622,10 @@ GENERATED_MERGE_PATHS="api/openapi.json api/client.ts"
|
||||
|
||||
### Syncing MCP servers: mcp sync
|
||||
|
||||
A workspace 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:
|
||||
A workspace needs the same MCP servers (Playwright, a local-QC server) as its source repo to run e2e or browser QC. `agenthub workspace mcp sync` gives it those:
|
||||
|
||||
```bash
|
||||
agenthub lanes mcp sync
|
||||
agenthub workspace mcp sync
|
||||
```
|
||||
|
||||
Reads the source repo's already-configured `mcpServers` from `~/.claude.json` (normal Claude Code project-scope config — set this up for the source repo once, the same way you would for any project), relocates any absolute path under the source repo to the workspace's own directory, pins a `@playwright/mcp` server's `--output-dir` to the workspace's `.playwright-mcp` (so proof screenshots land where the proof gallery reads them), and writes `<lane>/.mcp.json`. Also seeds the workspace's Chromium browser profiles from the source repo's own — preserves saved logins, and never overwrites a profile that already exists at the destination.
|
||||
@@ -1635,22 +1636,22 @@ A workspace whose source repo has no `mcpServers` configured gets a clear `ENOMC
|
||||
|
||||
Restart the workspace's Claude session after syncing — MCP config is read at session start.
|
||||
|
||||
### Checking integration toggles: agenthub lanes integration
|
||||
### Checking integration toggles: agenthub workspace integration
|
||||
|
||||
A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
|
||||
|
||||
```bash
|
||||
agenthub lanes integration tracker # exit 0 = on, 1 = off
|
||||
agenthub workspace 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. 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 workspace 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 workspace ci status` (below). The CLI command and `GET /api/lanes/:id/integrations/:name` are the only readouts: the workspace 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
|
||||
### Post-merge CI status: agenthub workspace ci status
|
||||
|
||||
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>]
|
||||
agenthub workspace 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 workspace if provided; if omitted, it's not needed.
|
||||
@@ -1689,10 +1690,10 @@ The workspace card UI currently has no prompt/message input field. When you clic
|
||||
## Related commands
|
||||
|
||||
```bash
|
||||
agenthub lanes # List all lanes
|
||||
agenthub lanes add --cwd <path> --title <text> # Adopt an existing directory
|
||||
agenthub lanes add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]
|
||||
agenthub lanes reset|remove|purge <id> [--force] --yes
|
||||
agenthub workspaces # List all workspaces (old name: agenthub lanes, still works)
|
||||
agenthub workspace add --cwd <path> --title <text> # Adopt an existing directory
|
||||
agenthub workspace add --repo <path> [--title <text>] [--base <branch>] [--slug <slug>]
|
||||
agenthub workspace reset|remove|purge <id> [--force] --yes
|
||||
agenthub stage <stage> [--evidence "..."] # Report the current stage
|
||||
```
|
||||
|
||||
@@ -1709,7 +1710,7 @@ agenthub stage <stage> [--evidence "..."] # Report the current stage
|
||||
|
||||
## Workspaces on any AI tool
|
||||
|
||||
A workspace can run any AI coding CLI in the AI Tools catalog (`server/data/ai-tools/*.json` — ten built in: Claude Code, Codex, Gemini CLI, Antigravity, GitHub Copilot CLI, OpenCode, Cursor CLI (`cursor-agent`), Oh My Pi, Hermes and OpenClaw —, plus any `*.json` in `DASHBOARD_AI_TOOLS_DIR`, which replaces a built-in file with the same `id` but may not set `launcher`). Settings → Agents → **AI tools** (`/settings/agents`; the old `/settings#ai-tools` redirects there) lists what is installed (found on PATH; **Refresh** re-reads the login shell's PATH first) with an enable switch, **Set default** and **Update** (for every tool whose catalog file names an updater — no version lookup, the updater itself reports "up to date"), and, folded behind a disclosure, what is available to install with a homepage link. The Add-workspace modal shows the chosen tool as one button (the default until changed); clicking it lists every installed, enabled tool as buttons to pick from; `agenthub lanes add --tool <id>` does the same from a terminal. The run form's **Model** picker lists the workspace tool's own models (`GET /api/ai-tools/:id/models`; Claude, Codex, and any catalog tool whose file declares `model: { flag }` — the id is validated and passed as `<flag> <id>`), plus **Custom…**, and starts on the tool's default model when one is set.
|
||||
A workspace can run any AI coding CLI in the AI Tools catalog (`server/data/ai-tools/*.json` — ten built in: Claude Code, Codex, Gemini CLI, Antigravity, GitHub Copilot CLI, OpenCode, Cursor CLI (`cursor-agent`), Oh My Pi, Hermes and OpenClaw —, plus any `*.json` in `DASHBOARD_AI_TOOLS_DIR`, which replaces a built-in file with the same `id` but may not set `launcher`). Settings → Agents → **AI tools** (`/settings/agents`; the old `/settings#ai-tools` redirects there) lists what is installed (found on PATH; **Refresh** re-reads the login shell's PATH first) with an enable switch, **Set default** and **Update** (for every tool whose catalog file names an updater — no version lookup, the updater itself reports "up to date"), and, folded behind a disclosure, what is available to install with a homepage link. The Add-workspace modal shows the chosen tool as one button (the default until changed); clicking it lists every installed, enabled tool as buttons to pick from; `agenthub workspace add --tool <id>` does the same from a terminal. The run form's **Model** picker lists the workspace tool's own models (`GET /api/ai-tools/:id/models`; Claude, Codex, and any catalog tool whose file declares `model: { flag }` — the id is validated and passed as `<flag> <id>`), plus **Custom…**, and starts on the tool's default model when one is set.
|
||||
|
||||
A catalog file:
|
||||
|
||||
|
||||
+6
-6
@@ -87,7 +87,7 @@ 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/` |
|
||||
| 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 |
|
||||
| The skills (`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.
|
||||
@@ -172,8 +172,8 @@ So the bootstrap now asks first (`supersededBy`): if Claude Code's
|
||||
`~/.claude/plugins/installed_plugins.json` records a **sibling** directory as
|
||||
the installed build of this plugin, this build logs one line and returns
|
||||
`superseded` without touching dependencies, the unit, or the server. `--force`
|
||||
does not override it — the self-update chain runs `--force` from the NEW
|
||||
directory, which is never superseded, so the only `--force` this can refuse is
|
||||
does not override it — the self-update chain and `/agenthub-update` run
|
||||
`--force` from the NEW directory, which is never superseded, so the only `--force` this can refuse is
|
||||
one that would deliberately downgrade.
|
||||
|
||||
It cannot hand over to the new build either: the process it would have to
|
||||
@@ -338,9 +338,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), 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 |
|
||||
| `/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 workspace shell` and session reloads need it), UI bundle. On any `FAIL`/`WARN` it asks whether to fix them, then lists the exact steps (one `plugin-bootstrap.js --force` from the installed build for deps/server/hooks/CLI launcher/stale unit, Node ≥ 22.5 through the user's version manager, the tmux package, the PATH line, …) and runs them only after a second yes; disk, a foreign unit, tmux built from source and an older running tmux server are reported, never touched. `plugin-doctor.js` itself stays read-only |
|
||||
| `/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 with `plugin-bootstrap.js --force` run from the build that is INSTALLED now (`supersededBy()` of this session's root, else that root — the old root's bootstrap would refuse as `superseded`), and run the doctor. The dashboard is on the new version at once; only this session's own commands and skills wait for `/reload-plugins` or a new session. |
|
||||
| `/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. `plugin-open.js` exits 1 when the build fails or that bootstrap does not answer `ok`, and the command then runs the `/agenthub-doctor` flow and retries the build once after the approved fixes |
|
||||
|
||||
### Where things live
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Retiring the old name "lane" from the CLI and API
|
||||
|
||||
_Phase 0 (`docs/plans/workspace-redesign/00-rename-lane-to-workspace.md`)
|
||||
changed the word in the UI and docs only. This plan takes the user-facing
|
||||
NAMES of the CLI and API the same way, in four steps spread over releases so
|
||||
nobody is broken by an update and nothing stored is touched._
|
||||
|
||||
**R** = the first release after v2.0.1 that ships steps 1–3 (write its
|
||||
version here when it is cut: R = `v2.1.0`).
|
||||
|
||||
## Steps
|
||||
|
||||
| # | What | When | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Aliases: `/api/workspaces` = `/api/lanes` (same two routers mounted twice, `server/index.js` `LANE_MOUNTS`); `agenthub workspace`/`workspaces` = `agenthub lanes` (`COMMAND_ALIASES`/`canonicalArgv` in `bin/agenthub.js`); `--workspace <id>` = `--lane <id>` (`resolveLaneArg`). | R | done |
|
||||
| 2 | Help, usage and hint text, the UI's command hints and the user docs say `agenthub workspace …` / `--workspace`; `docs/API.md` documents both paths. | R | done |
|
||||
| 3 | Deprecation: `agenthub lanes` or `--lane` prints ONE stderr line naming the new spelling (`warnOldNames`), stdout unchanged; the help footer and `docs/CLI.md` call them the old names. | R | done |
|
||||
| 4 | Remove the old CLI names (below). | **not before R+3** | open |
|
||||
|
||||
Three releases is the window for an install to update gradually: anyone who
|
||||
runs the CLI in that time sees the step-3 note on every old-name call, and a
|
||||
release that skips straight from < R to ≥ R+3 still gets a clear error (4a),
|
||||
never a silent change of behaviour.
|
||||
|
||||
## Step 4 — what it does
|
||||
|
||||
**4a. CLI (do this).**
|
||||
- `agenthub lanes …` no longer runs; it exits 1 with
|
||||
`renamed: use agenthub workspace …` (not "Unknown command", and not help).
|
||||
- `--lane <id>` exits 1 with `renamed: use --workspace <id>`.
|
||||
- The handler is renamed `workspace` in `runCommand`; `COMMAND_ALIASES` keeps
|
||||
only `workspaces → workspace`; `warnOldNames` goes away.
|
||||
- Tests: `lanes-cli.test.js` "agenthub workspaces" suite flips to assert the
|
||||
refusal; every remaining `cli(["lanes", …])` call in the server suites moves
|
||||
to `workspace`.
|
||||
|
||||
**4b. Client and CLI requests move to `/api/workspaces` (do this).**
|
||||
- `client/src/lib/api.ts` (and the ~105 `/lanes…` request paths) and the 29
|
||||
`/api/lanes` calls in `bin/agenthub.js` switch to `/api/workspaces`. Safe at
|
||||
R+3 because every server ≥ R answers it — a CLI updated before its server
|
||||
restarted (`installed_not_running`) is talking to a server ≥ R by then.
|
||||
|
||||
**4c. The `/api/lanes` mount — decide at R+3, default KEEP.**
|
||||
Removing it saves one array entry and breaks every user script, bookmark and
|
||||
cached browser tab that still names it. Keep it as a silent, undocumented
|
||||
compatibility path unless there is a concrete reason. If it is removed,
|
||||
answer `410 Gone` with `{error:{code:"ERENAMED", message:"use /api/workspaces"}}`
|
||||
for one more release before dropping it.
|
||||
|
||||
## What step 4 must NEVER touch (no data loss)
|
||||
|
||||
Step 4 renames only how a request is ROUTED. Nothing below is renamed,
|
||||
moved or migrated — they are where users' data and running work live:
|
||||
|
||||
- SQLite: tables `lanes`, `lane_sources`, `lane_features`,
|
||||
`lane_stage_events`; columns `lane_id`, `needs_action_session`, …; no
|
||||
table rename, no column rename, no migration.
|
||||
- JSON keys in API responses (`lane`, `lanes`, `lane_id`, `counts`) and the
|
||||
WebSocket message types — an older client or script reads them.
|
||||
- tmux session names `agenthub-lane-<id>` / `agenthub-lane-<id>-<slot>` —
|
||||
renaming orphans every live pane (Stop/Reset could no longer reach it).
|
||||
- Env vars `AGENTHUB_LANE_ID`, `LANES_ROOT`, `LANE_DEAD_SEC`,
|
||||
`LANE_BRANCH_PREFIX` — a pane started before the update still carries them.
|
||||
- On-disk paths: `LANES_ROOT` (`~/.claude/agenthub-lanes/`, managed
|
||||
worktrees), `LANES_ROOT/.state/lane<slot>/`,
|
||||
`<data dir>/workspace-context/<lane id>/`,
|
||||
`<data dir>/workspace-prompts/<lane id>.md`, `.agenthub/profile/`.
|
||||
- Browser storage keys (`agenthub.workspace.*`, `agenthub.sidebar.*`) — a
|
||||
rename resets everyone's layout and order.
|
||||
- Error codes (`EBADLANE`, `ERUNLIVE`, …) and file/component names.
|
||||
|
||||
## Release checklist for step 4
|
||||
|
||||
1. `package.json` version ≥ R+3 (count R from the table above).
|
||||
2. Release notes, EN + VI, top of the list: "`agenthub lanes` and `--lane`
|
||||
are removed — use `agenthub workspace` and `--workspace`. Your workspaces,
|
||||
sessions and history are unchanged."
|
||||
3. `grep -rn "agenthub lanes\|--lane\b" skills plugins docs README* INSTALL.md`
|
||||
finds nothing outside `docs/plans/` (a shipped skill calling the old name
|
||||
would break the moment the plugin updates).
|
||||
4. Pipeline templates are user files (`DASHBOARD_PIPELINES_DIR`) and may run
|
||||
`agenthub lanes hook …` as a node command: the release note says so, and
|
||||
the 4a error names the replacement, so the failing node tells the user
|
||||
what to edit.
|
||||
5. Both suites green; open an existing install's DB before and after the
|
||||
update and compare `SELECT count(*) FROM lanes` / `sessions` / `events`
|
||||
— step 4 must not change a single row.
|
||||
Reference in New Issue
Block a user