diff --git a/CLAUDE.md b/CLAUDE.md index 9b0d35b7..7f1688e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 51edc81c..55610d78 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/README.vi.md b/README.vi.md index 032aef73..73414011 100644 --- a/README.vi.md +++ b/README.vi.md @@ -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 diff --git a/client/README.md b/client/README.md index c25d1a04..7fa2bc7a 100644 --- a/client/README.md +++ b/client/README.md @@ -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() { } /> {/* legacy path - Claude config now lives in a Settings tab */} } /> - } /> + {/* legacy paths - lanes and runs are one page now */} + } /> + } /> + } /> } /> } /> diff --git a/server/README.md b/server/README.md index 7d0c86c0..506773ea 100644 --- a/server/README.md +++ b/server/README.md @@ -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 `/.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-`, 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