docs: sync CLAUDE.md and READMEs with codex lanes and the tmux run surface

Add the codex-lane facts and doc-sync marker to CLAUDE.md, document the
per-lane tool picker in README/README.vi, and bring server/README.md and
client/README.md's API and routing tables up to date with the tmux-backed
run endpoints and the /workspace route.
This commit is contained in:
2026-08-27 10:58:01 +07:00
parent d81b229f79
commit f8fba0a51d
5 changed files with 49 additions and 20 deletions
+3
View File
@@ -1,5 +1,7 @@
# Claude Code Working Guide
_Last synced: d884d19 (2026-08-26)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker.
## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
- Preserve real-time behavior (hooks -> API -> SQLite -> WebSocket -> UI).
@@ -33,6 +35,7 @@ A **lane** is a durable unit of parallel agent work — one working directory, m
- Liveness: a silent **watcher** (stage matching `/watch|poll/`) is dead after `LANE_DEAD_SEC` seconds (default 300); a silent **idle** lane is at rest, not dead.
- **Never `rm -rf` a lane.** Destructive lane operations (`reset`, `remove`, `purge`) go through `server/lib/worktree.js`/`server/lib/lanes.js`, never a raw filesystem delete — that's what keeps the three-check destroy guard and the DB bookkeeping in lockstep.
- **Never build a git command as a shell string.** Lane git operations use `execFile("git", [...args])` with an explicit argument array (see `server/lib/worktree.js`'s `git()` helper), never a concatenated/interpolated string passed to a shell.
- **A lane's `tool` is a lane fact, immutable, and never read from a request.** `lanes.tool` (`claude` | `codex`, default `claude`) is set by `createLane` only — it is deliberately absent from `PATCHABLE`, so no PATCH can flip the CLI under a live pane. Every spawn path (`POST /api/lanes/:id/start`, `POST /api/run`) takes the tool from the lane row and ignores whatever the body says; the run panel's Codex-only fields (`sandbox`, `approval`, `resumeLast`) are validated against the fixed allowlists in `server/lib/pty-run.js` before they reach an argv, and the bypass flags (`--dangerously-bypass-approvals-and-sandbox`, `--dangerously-bypass-hook-trust`) are never emitted. `GET /api/run/binary?tool=` resolves the probed binary name from that same allowlist — the query string reaches `which`.
- **Adopted lanes are not destroyable.** A lane with `kind === "adopted"` may never have its worktree reset or removed; "removing" one only drops the dashboard's own record of it. This is enforced in code (`assertDestroyable`'s first check) — do not add a path that bypasses it.
See `docs/LANES.md` for full guide: stage reporting, custom templates, lane actions, and the five node states.
+12 -4
View File
@@ -17,10 +17,11 @@ Internal build — all rights reserved.
- **Stage detection.** The stage is inferred from the tool stream, so a session
that never calls `ccam stage` still shows progress — rendered dashed amber and
never as done, because an inference is not evidence.
- **Run Claude from the browser.** A real terminal (tmux + a real PTY,
rendered with xterm.js) attached to a lane's directory — the exact TUI you'd
see locally, fully interactive, resumable, and attachable from a real
terminal too via `ccam lanes shell`.
- **Run Claude Code or Codex from the browser.** A real terminal (tmux + a real
PTY, rendered with xterm.js) attached to a lane's directory — the exact TUI
you'd see locally, fully interactive, resumable, and attachable from a real
terminal too via `ccam lanes shell`. Which CLI it starts is the lane's own
setting.
- **Analytics (in-dashboard tab), alerts, and a workflow view**, plus an MCP server and a CLI.
## Requirements
@@ -101,6 +102,13 @@ ccam lanes add --cwd /path/to/repo --title "My feature" # adopt
ccam lanes add --repo /path/to/repo --slug my-feature # managed worktree
```
Every lane also carries the CLI that drives it — `claude` (the default) or
`codex` — picked in the Add-lane modal and fixed for the life of the lane. A
Codex lane's run panel offers Codex's own sandbox, approval and reasoning-effort
choices, and resumes with `codex resume --last`. Codex emits none of this
dashboard's hooks, so a Codex lane records its runs but stays absent from
Sessions, Analytics and stage detection.
The declared stage comes from `ccam stage`. The inferred stage comes from tool
events and expires after `DETECTION_TTL_MS` (default 5 minutes), so a lane can
move backwards between work sessions. Detection never writes the declared stage,
+13 -3
View File
@@ -18,9 +18,12 @@ Internal build — all rights reserved.
session không bao giờ gọi `ccam stage` vẫn hiển thị progress — render dạng
viền chấm màu hổ phách (amber) và không bao giờ hiện `done`, vì suy luận
không phải bằng chứng.
- **Chạy Claude từ trình duyệt.** Spawn session trong thư mục của lane, stream
output, gửi follow-up, resume session cũ bất kỳ.
- **Analytics, alert, Kanban và workflow view**, cộng thêm MCP server và CLI.
- **Chạy Claude Code hoặc Codex từ trình duyệt.** Một terminal thật (tmux + PTY
thật, render bằng xterm.js) gắn vào thư mục của lane — đúng cái TUI bạn thấy ở
máy, tương tác đầy đủ, resume được, và attach được từ terminal thật qua
`ccam lanes shell`. Chạy CLI nào là thiết lập riêng của từng lane.
- **Analytics (tab trong dashboard), alert và workflow view**, cộng thêm MCP
server và CLI.
## Yêu cầu
@@ -99,6 +102,13 @@ ccam lanes add --cwd /path/to/repo --title "My feature" # adopt
ccam lanes add --repo /path/to/repo --slug my-feature # managed worktree
```
Mỗi lane còn mang theo CLI điều khiển nó — `claude` (mặc định) hoặc `codex` —
chọn trong modal Add lane và cố định suốt vòng đời của lane. Lane Codex có panel
chạy với các lựa chọn riêng của Codex (sandbox, approval, reasoning effort) và
resume bằng `codex resume --last`. Codex không bắn hook của dashboard này, nên
lane Codex vẫn ghi lịch sử chạy nhưng không xuất hiện ở Sessions, Analytics và
stage detection.
Stage khai báo (declared) đến từ `ccam stage`. Stage suy luận (inferred) đến
từ tool event và hết hạn sau `DETECTION_TTL_MS` (mặc định 5 phút), nên lane có
thể lùi lại giữa các work session. Detection không bao giờ ghi đè stage khai
+8 -3
View File
@@ -449,11 +449,13 @@ graph TB
Feed["/activity (redirect)"]
Workflows["/workflows"]
CcConfig["/cc-config (redirect)"]
Run["/run"]
Lanes["/lanes (redirect)"]
Run["/run (redirect)"]
Workspace["/workspace"]
Settings["/settings (tabs: dashboard | claude-code | ...)"]
NF["/* (NotFound)"]
Root --> Dashboard & Sessions & Detail & Feed & Workflows & CcConfig & Run & Settings & NF
Root --> Dashboard & Sessions & Detail & Feed & Workflows & CcConfig & Lanes & Run & Workspace & Settings & NF
style Dashboard fill:#3B82F6
style Detail fill:#3B82F6
@@ -479,7 +481,10 @@ function App() {
<Route path="workflows" element={<Workflows />} />
{/* legacy path - Claude config now lives in a Settings tab */}
<Route path="cc-config" element={<Navigate to="/settings?tool=claude-code" replace />} />
<Route path="run" element={<Run />} />
{/* legacy paths - lanes and runs are one page now */}
<Route path="lanes" element={<Navigate to="/workspace" replace />} />
<Route path="run" element={<Navigate to="/workspace" replace />} />
<Route path="workspace" element={<Workspace />} />
<Route path="settings" element={<SettingsHub />} />
<Route path="*" element={<NotFound />} />
</Route>
+13 -10
View File
@@ -651,22 +651,25 @@ Reads — and carefully gated mutations for low-risk text-file artifacts — for
| `PUT` | `/api/cc-config/file` | Create or overwrite a text-file artifact (skills/agents/commands/output-styles/memory). Body: `{ scope, type, name?, content }`. Auto-backs-up if file exists. Atomic temp + rename. 256 KB cap. Per-project file-based memory is also editable via `{ scope: "auto-memory", type: "auto-memory", project, name }` — backups land under `<memory-dir>/.cc-config-backups/auto-memory/`, and an invalid project slug returns `EBADPROJECT` |
| `DELETE` | `/api/cc-config/file` | Backup-then-delete a text-file artifact. Skill dirs are backed up whole before recursive removal |
### Run Claude (`/api/run`)
### Run a lane's CLI (`/api/run`)
HTTP surface for spawning and supervising `claude` subprocesses from the dashboard. Every route enforces a same-origin / loopback-Origin guard against browser CSRF.
HTTP surface for the tmux-backed terminal run: one session per lane, named `ccam-lane-<laneId>`, running the CLI that lane declares (`claude` or `codex`). There is no in-memory handle map — every read is computed from `tmux list-sessions`, so a session killed out-of-band self-corrects on the next call. Every route enforces a same-origin / loopback-Origin guard against browser CSRF.
| Method | Path | Description |
| -------- | ----------------------------- | ----------- |
| `GET` | `/api/run` | List handles + `maxConcurrent` + `activeCount` |
| `GET` | `/api/run/binary` | Probe whether `claude` is on `PATH` |
| `GET` | `/api/run` | `{ items }` — every live run handle, recomputed from tmux |
| `GET` | `/api/run/history?limit=&laneId=` | Persisted run history from `dashboard_runs` (default 50, max 500, newest first); each row carries `isLive` and the `tool` it was spawned with. `laneId` narrows to one lane |
| `GET` | `/api/run/binary[?tool=]` | Probe whether the CLI is on `PATH`. `?tool=codex` probes Codex; omitted or `?tool=claude` probes Claude; anything else is `400 EBADTOOL`. Returns `{ found, path }` |
| `GET` | `/api/run/tmux` | `{ available }` — whether the `tmux` binary exists |
| `GET` | `/api/run/cwds` | Suggested cwds (dashboard, home, recent from sessions) |
| `GET` | `/api/run/files?cwd=…&q=…` | Fuzzy file search inside `cwd` for the Run page's `@`-file autocomplete. Skips `node_modules`, `.git`, `dist`, `build`, `.next`, `.cache`, `coverage`, `vendor`, etc. Cwd is required and must exist; results are capped and ranked by basename match |
| `POST` | `/api/run` | Spawn. Body: `{ prompt, mode, cwd?, model?, permissionMode?, resumeSessionId?, effort? }`. `effort` (`low`/`medium`/`high`) maps to `--effort`. When `resumeSessionId` is set in conversation mode, `prompt` may be empty — the spawner skips the initial stdin write and `claude --resume` idles until the client POSTs a follow-up to `/api/run/:id/message`. Spawner always passes `--output-format stream-json --verbose --include-partial-messages` for character-by-character streaming. Concurrency is effectively uncapped by default (ceiling 10000, override with `RUN_MAX_CONCURRENT`) — the terminal TUI has no cap and neither does the dashboard; the ceiling is sanity-only to prevent fork-bomb footguns |
| `POST` | `/api/run/:id/message` | Send follow-up turn (conversation mode only). Body: `{ text }` |
| `GET` | `/api/run/:id` | Handle state. `?envelopes=1` includes the in-memory envelope log for re-attach |
| `DELETE` | `/api/run/:id` | Stop (SIGTERM → SIGKILL after 5 s) |
| `GET` | `/api/run/files?cwd=…&q=…` | Fuzzy file search inside `cwd` for the run panel's `@`-file autocomplete. Skips `node_modules`, `.git`, `dist`, `build`, `.next`, `.cache`, `coverage`, `vendor`, etc. Cwd is required and must exist; results are capped and ranked by basename match |
| `POST` | `/api/run` | Create-or-adopt the lane's tmux session. Body: `{ laneId, cwd, initialPrompt?, model?, permissionMode?, effort?, resumeSessionId? }`. The CLI comes from the lane row, never from the body. An existing session whose pane sits at a shell prompt gets the argv typed into it; a pane already running a program is adopted unchanged |
| `GET` | `/api/run/:id` | One run handle, or 404 once its tmux session is gone |
| `DELETE` | `/api/run/:id` | Kill the tmux session |
WebSocket message types added: `run_stream` (parsed stream-json envelope, including `stream_event` deltas from `--include-partial-messages`), `run_status` (status transitions), `run_input_ack` (stdin write confirmed), and `cc_config_changed` (broadcast by `lib/cc-watcher.js` on `fs.watch` events under `~/.claude/` and by `routes/cc-config.js` after every successful PUT/DELETE — debounced at 500 ms, payload `{ source: "dashboard"|"fs", action?, scope?, type?, name?, paths? }`).
Codex-only spawn options (`sandbox`, `approval`, `resumeLast`) are accepted by the lane action `POST /api/lanes/:id/start`, not by `POST /api/run` — see [`docs/API.md`](../docs/API.md).
The pane itself is not JSON: its bytes stream over `/ws-pty/:runId` as binary WebSocket frames. The JSON WebSocket surface adds `cc_config_changed` (broadcast by `lib/cc-watcher.js` on `fs.watch` events under `~/.claude/` and by `routes/cc-config.js` after every successful PUT/DELETE — debounced at 500 ms, payload `{ source: "dashboard"|"fs", action?, scope?, type?, name?, paths? }`).
### Import History