Files
AgentHub/CLAUDE.md
T
nntrivi2001 8a812a174b fix(lanes): keep stage detection honest across rollbacks and feature switches
Three defects let a lane's pipeline map claim progress that was not this
pass's work, all of them on the "going back" path a new request takes:

- `setStage` cleared the detection columns on a real transition but never
  pruned the `stages` records AHEAD of the new stage. A lane that ran through
  `done` and was then declared back at `plan` rendered `plan` as `current`
  while `tests`/`review`/`done` still read `done`, carrying the previous
  pass's evidence, and contradicting `progress` (computed from the declared
  index alone). A backwards declaration now drops every record it can prove
  is ahead; a key matching no node is unplaceable and left alone.
- `activateFeature` rewrote the live lane row for the incoming feature but
  carried the outgoing one's `detected_*` across. A never-seen slug starts at
  `stage='idle'`/`stages={}`, exactly the shape `inferredDisplayStage` renders
  from detection, so the new feature opened painted with the old one's
  progress — and forward-only rejected every real detection for it until the
  carried-over one aged past DETECTION_TTL_MS. Now cleared, like every other
  real transition; deliberately not archived, since detection is live-only.
- `codex-ingest` classified a rollout journal by raw `now - mtime`, which goes
  negative for a future mtime (clock stepped back, NTP correction, a restore
  that preserved timestamps) and read as "written milliseconds ago". A
  finished thread's journal could replay as live work; a file claiming to be
  from the future is now idle.

Also fixes `check-headers.sh`, which audited via `find` and so reported local
gitignored runtime state (a lane's own `.ccam/profile/hooks/*.sh`) that is not
this repo's source and can never be made compliant here. It now lists files
through git (tracked + untracked-but-not-ignored), so a file an agent just
wrote is still caught before it is staged while ignored state is not.

Tests: 1359 pass (4 new). Header audit exits 0.
2026-09-03 15:29:08 +07:00

15 KiB

Claude Code Working Guide

Last synced: 13b2bc4 (2026-09-03). 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).
  • Keep MCP integration production-ready for local use (mcp/).

Repo map

  • server/: Express API, hook ingestion, SQLite access, websocket broadcast (includes optional git upstream checks and routes/updates.js, plus lib/workflow-ingest.js which ingests on-disk Workflow-tool run journals — fleets that emit no hooks — and lib/codex-ingest.js, the same idea for codex lanes: it tails Codex's own rollout journals because Codex emits no hooks at all).
  • client/: React + Vite UI.
  • scripts/: hook installer/handler, import, seed, cleanup utilities. (Update detection lives server-side in server/lib/update-check.js. POST /api/updates/apply (only when the checkout is fast-forwardable) pulls, rebuilds, and self-restarts via server/lib/self-restart.js + the detached scripts/restart-helper.js; otherwise users run the printed manual command, surfaced in the UI and by ccam update-check.)
  • mcp/: local MCP server exposing dashboard operations as tools. mcp/build/ is committed on purpose — plugin MCP servers start before any bootstrap could build them; scripts/check-mcp-build.js (content hash in mcp/build/.srchash, run by pre-commit and /ccam-doctor) keeps it honest. Rebuild with npm run mcp:build, never hand-edit mcp/build/.
  • skills/: this repo's skills. Because the repo root is the ccam plugin root, this is also the plugin's skills directory — every skill here ships to anyone who installs the plugin, addressed as /ccam:<name> (bare /<name> works as an alias). Put a skill here only if it should reach plugin users; ccam skills install remains a fallback that copies ship-feature-lane into ~/.claude/skills/ for plain checkouts. - agents/: the same story for subagents — plugin root, so all five (qc-local, senior-gate-reviewer, backend-reviewer, frontend-reviewer, mcp-reviewer) ship as ccam:<name>. This is also the source ccam lanes agents install copies the two pipeline agents from into a lane's .claude/agents/, which is what keeps the bare subagent_type: qc-local working there. One file per agent, never a second copy.
  • .claude-plugin/: the marketplace plus the root ccam plugin manifest ("source": "./" — the whole repo is the plugin). Its hooks are inline in plugin.json; its commands live in plugins/ccam/commands/, which is NOT a subdirectory plugin. scripts/plugin-bootstrap.js runs from SessionStart and owns the writable runtime under ~/.claude/agent-dashboard/runtime/ — it never writes into the plugin cache, which Claude Code replaces on every update. See docs/PLUGINS.md.

Lanes

A lane is a durable unit of parallel agent work — one working directory, many Claude Code sessions over time. Lanes are keyed by cwd, not session_id, so they survive session restarts and allow the dashboard to show a pipeline that persists across runs.

Key points:

  • Sessions are bound to lanes by longest path-boundary prefix match on their cwd (set in hook data from claude CLI).
  • A lane's stage is declared by the driving Claude session (via ccam stage <stage> [--evidence "..."]), not inferred by the dashboard.
  • A CLI command resolves its lane by (cwd, tool), never cwd alone. resolveLaneArg (bin/ccam.js) answers in order: --lane/leading id → CCAM_LANE_ID (every tmux pane the dashboard starts carries it, via tmux new-session -e) → --tool, else the harness marker (CLAUDECODE ⇒ claude; Codex has NO reliable env marker, so it passes --tool codex) → the owning lane. Two candidates at one directory and no signal is an ERROR that names both, never a pick — writing a codex session's stage onto the claude lane's card is a lie about which agent did the work.
  • The console never writes a lane's stage. Declared stages come from ccam stage (a skill calling the CLI command), and inferred stages come from tool-event detection only. The UI's run console and the Workspace page never call POST /api/lanes/:id/stage — that boundary exists because the console observes tool calls but cannot see their outcomes. A lane's stage is the session's truth about progress; the console's observation of tool execution would not be proof of completion.
  • Going back is a declaration, and it takes the nodes ahead with it. A declaration landing on a node BEFORE the lane's current one (a fix-loop re-entry, or a new request on a lane that already reached done) clears the detection columns like every real transition AND drops the stages records of every node ahead of it — those belong to the previous pass, and left in place they render done with the old work's evidence beside a current ring further back, contradicting progress (computed from the declared index alone). Records under a key matching no node are left alone: unplaceable, so unprovably ahead. ccam feature activate <slug> is the other half of this — a feature switch archives the outgoing pipeline and clears detection too, because a detection belongs to the feature that produced it.
  • A detection expires, an evidence rule does not. recordDetection skips its forward-only comparison once detected_at is older than DETECTION_TTL_MS (default 5 min), so a lane can move backwards between work sessions. That window changes only WHICH detection is current — it never relaxes declared-wins (an agent's own claim has no expiry) and never lets an inferred node render done. Declared-wins has exactly ONE exception: a declaration on the pipeline's LAST node (done) stops vetoing, because the pipeline is over there and a later detection is new work — without it a lane left at done rejects every detection for the rest of its life.
  • Working-copy facts live at GET /api/lanes/:id/git, never inside GET /api/lanes. That endpoint shells out to git three times; the lane list is polled and re-broadcast on every hook. A cwd that is not a readable repo returns {available:false} with HTTP 200 — a normal state, not a fault. Cards fetch it themselves every 30s and fail silently.
  • The Workspace console collapses with CSS, never by unmounting. Unmounting RunConsole disposes the run subscription and drops a live run's rendered history.
  • CCAM does not orchestrate: no chaining, no queue, no retry logic, no gate evaluation. The session in control makes all decisions; the dashboard records the claimed stage and shows evidence. The runtime layer (ccam lanes up|down|hook) does not change this — it offers primitives a session calls; nothing in the dashboard sequences them.
  • The runtime never writes stage, status or notes — only slot and ports. status=running means an AGENT is working, not that a server is listening; merging the two would corrupt classifyLiveness. Boot failures live in LANES_ROOT/.state/lane<slot>/last-error.json and surface via GET /api/lanes/:id/runtime. Same boundary as the console-never-writes-stage rule above.
  • A lane's stack is up or down as a computed fact, never a stored one. runtimeFacts re-derives it from pid files and port probes on every read. A process dies to OOM, a stray kill, a reboot — caching a truth CCAM does not control buys ghost state. Slot allocation is the opposite (fully controlled, must be race-free), so that one does live in the DB, under withLaneLock plus a partial unique index.
  • A profile's profile.env is parsed, never sourced. Hooks are executed deliberately; config is only read. Sourcing arbitrary shell from a user's repository into the dashboard process would be a code-execution path. Hook names always come from the fixed allowlist in server/lib/lane-profile.js, never from a request.
  • Pipeline templates are JSON files (server/data/pipelines/) with node definitions; custom templates override built-ins when DASHBOARD_PIPELINES_DIR is set.
  • Nodes render in five states: failed (rejected), current (now), done (with evidence), passed-no-evidence (claimed or skipped, amber), pending (not reached).
  • 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.
  • A cwd can back at most one lane per tool, not one lane overall. Uniqueness is the composite idx_lanes_cwd_tool (cwd, tool), not cwd alone — Repo/Adopt mode (POST /api/lanes/ensure) lets the same directory carry a claude lane and a codex lane side by side, shown as two separate cards. resolveLaneByCwd(cwd, tool) takes an optional tool filter for exactly this; hook ingestion (touchLaneFromHook, resolveLaneForWork) always pins it to "claude" because Codex has no hook mechanism in this codebase, so a hook event must never touch a codex lane sharing the path. Worktree mode is unaffected — it already allocates a unique directory per lane.

See docs/LANES.md for full guide: stage reporting, custom templates, lane actions, and the five node states.

Non-negotiable engineering rules

  • Preserve existing behavior unless explicitly asked to change it.
  • Prefer minimal, reversible diffs.
  • Never silently weaken safety controls around destructive actions.
  • Keep docs updated when behavior, commands, file locations, or workflows change — apply the update-project-docs skill automatically at the end of every change-set that alters behavior, config, interfaces, events, schema, CLI commands, or features (do not wait to be asked).
  • Every applicable source file you create or update (.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css) must start with the copyright/authorship header — file overview + the exact line @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>. Formats and audit script: skills/file-headers/ (verify with bash skills/file-headers/scripts/check-headers.sh). This binds every coding agent (Claude Code, Codex, or others).

Commands you should know

  • Setup: npm run setup
  • Dev: npm run dev
  • Prod build/start: npm run build then npm start
  • Server tests: npm run test:server
  • Client tests: npm run test:client
  • MCP install/build/start: npm run mcp:install, npm run mcp:build, npm run mcp:start
  • MCP build freshness: npm run mcp:check-build (must pass whenever mcp/src changes)
  • MCP typecheck: npm run mcp:typecheck
  • CLI (after setup): ccam <command> — terminal access to the full dashboard surface (bin/ccam.js; ccam help lists commands)

Testing and verification policy

  • Backend changes: run npm run test:server before finishing.
  • Frontend changes: run npm run test:client when relevant. This includes per-screen render snapshots (client/src/pages/__tests__/screens.snapshot.test.tsx). If a UI change is intentional, review the snapshot diff and regenerate baselines with cd client && npx vitest run -u; never blindly update snapshots to make tests pass.
  • MCP changes: run npm run mcp:typecheck and npm run mcp:build.
  • If you cannot run a verification step, state exactly what was not run and why.

Change guidelines by area

  • API routes: preserve response shapes unless change is requested and documented.
  • Database: avoid schema changes without migration-safe logic.
  • Hooks: keep fail-safe and non-blocking behavior.
  • WebSocket: keep message types stable and backward-compatible.
  • Documentation: include exact commands and paths; keep markdown examples runnable.

Agent behavior

  • Explore first, then implement.
  • For larger tasks, propose/check a short plan before broad edits.
  • Use file-specific rules in .claude/rules/ when working in scoped areas.
  • Use project skills from skills/ for repeatable workflows (they ship with the ccam plugin: /ccam:<name>, or the bare /<name> alias).
  • Use agents/ subagents for focused review or investigation passes (they ship with the plugin: ccam:backend-reviewer, etc.).
  • Declare lane stage even in plain chat, not just inside skills. ccam stage (see docs/LANES.md § Reporting a stage) is a reporting command, not a skill-only ritual — any Claude session working inside an adopted lane's cwd should call it on real stage transitions (starting to plan, starting to implement, running tests, opening the PR, etc.), whether or not a skill is driving. Tool-event detection (server/lib/stage-detect.js) only ever paints the amber "detected" badge, never the blue current ring — a lane worked entirely through plain chat with no ccam stage calls will show a stale current stage no matter how much real work happens. Skip it only when ccam stage reports no lane owns the cwd (not adopted).
  • Prefer ccam:ship-feature(-lane) over invoking superpowers:brainstorming/writing-plans/executing-plans/subagent-driven-development/test-driven-development directly for implementation work inside a lane's cwd. Those Superpowers skills know nothing about CCAM lanes and never call ccam stage themselves — going straight to them (skipping the CCAM wrapper) is exactly how a lane's current stage goes stale mid-implementation while ccam stage is never called for real transitions. ccam:ship-feature/ship-feature-lane drive the same underlying Superpowers skills phase-by-phase and declare ccam stage at every boundary — pick one of them first for any multi-phase feature/bugfix inside a lane; reach for a bare Superpowers skill directly only for work with no lane cwd to report against (e.g. a scratch repo, or explicitly told to skip the CCAM wrapper).