feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
@@ -0,0 +1,204 @@
# Worktree-lanes: final-review follow-ups
Base: bf312c6 (branch `feat/worktree-lanes`). One commit for the whole set.
## Follow-up 1: unreadable worktrees were genuinely unremovable
### (a) Made an unreadable managed lane genuinely removable
`server/lib/worktree.js`: `removeWorktree` unconditionally called
`git worktree remove --force`, which git refuses outright — even with a
second `--force` — when the worktree's OWN `.git` pointer fails its own
validation (a corrupt/garbage `.git` file). All three `assertDestroyable`
checks pass for this shape (the directory exists, resolves inside
`LANES_ROOT`, and is still listed by the source repo), so the lane was stuck:
`removeWorktree` threw, the route 500'd, the lane row survived.
Fix: wrapped the `git worktree remove --force` call in a try/catch. On
failure, `findWorktreeAdminDir(sourceRepo, cwd)` locates the worktree's
administrative directory under the source repo's common dir
(`<common>/worktrees/<name>`) by reading each entry's `gitdir` file — that
file's content is the absolute path to the worktree's own `.git` file, read
from the *source repo's* side, so it still resolves correctly even though the
worktree's own `.git` file is corrupt. If found, `fs.rmSync` deletes only
that administrative directory (never the worktree directory itself), which
deregisters the worktree from `git worktree list`. Branch delete and lane-row
deletion then proceed exactly as for every other remove. If no matching
admin directory is found (paranoid case — should not happen for a lane that
passed all three checks), the original git error is rethrown rather than
silently continuing, so the failure is never swallowed.
This is a genuine deregistration, not a workaround: it never touches the
worktree's own files, and after it runs, `git worktree remove --force` on
the same path correctly reports `'...' is not a working tree` — proof the
repo no longer thinks it manages that directory.
### Experiment (raw output)
Built a real corrupt worktree under a throwaway repo and ran the exact
sequence a reviewer had already reproduced, then the new fallback:
```
=== remove --force (expect fail) ===
fatal: validation failed, cannot remove working tree: '/tmp/tmp.4kZLeHvThU/lanes/src__corrupt/.git' is not a .git file, error code 5
exit=128
=== manually delete admin dir ===
(no output)
=== worktree list after manual admin removal ===
worktree /tmp/tmp.4kZLeHvThU/src
HEAD 0a1ed2bb01a8ac88b758180d569997b8c0bb22e8
branch refs/heads/main
=== branch still exists? ===
feat/corrupt
=== worktree directory + corrupted .git file still present? ===
total 16
drwxrwxr-x 2 smartgiftailab smartgiftailab 4096 ... .
drwxrwxr-x 3 smartgiftailab smartgiftailab 4096 ... ..
-rw-rw-r-- 1 smartgiftailab smartgiftailab 8 ... .git
-rw-rw-r-- 1 smartgiftailab smartgiftailab 6 ... README.md
garbage
=== git worktree remove again now (expect: not a working tree, confirms deregistered) ===
fatal: '/tmp/tmp.4kZLeHvThU/lanes/src__corrupt' is not a working tree
exit=128
```
This confirms: (1) `remove --force` genuinely refuses a corrupt worktree,
matching the earlier report exactly (error code 5 here vs 7 in the original
report — git version difference, same refusal); (2) deleting only
`.git/worktrees/<name>` deregisters the worktree from `git worktree list`
without touching the worktree directory or its files; (3) after
deregistration git itself confirms the path is no longer a working tree.
The actual code path (`removeWorktree` against a real corrupt worktree
inside `LANES_ROOT`, through `findWorktreeAdminDir`) was then exercised by
the new server test below and produced the same result end to end.
### (b) Reworded the false promise
`destructive.notice.unreadable` in `client/src/i18n/locales/{en,zh,vi,ko}/lanes.json`
no longer promises forced success. New English text: "The lane directory
cannot be read as a Git worktree. Removal is attempted; if Git itself
refuses, its worktree record is cleared directly instead. Either way, the
directory itself is never touched." zh/vi/ko were translated with the same
meaning (not machine-transliterated word-for-word), matching each locale's
existing terminology for "lane"/"worktree"/"record" already used elsewhere
in the same file.
`docs/API.md` (blocked/remove paragraph) and `docs/LANES.md` (the three
safety checks section) both had the same "force-removes an unreadable one"
claim; both now describe the attempt-then-fallback behavior and name the
corrupt-`.git`-pointer case explicitly.
### (c) Added the two missing modal tests
`client/src/components/lanes/__tests__/DestructiveLaneModal.test.tsx`:
- `disables RESET when the worktree directory is unreadable` — pins
`unreadable` staying in `HARD_BLOCKERS.reset`.
- `ENABLES remove when the worktree directory is unreadable` — pins
`unreadable` staying absent from `HARD_BLOCKERS.remove`, and that the
reworded notice text renders and the modal still echoes back the exact
`expect` block on confirm.
### Regression test for (a)
`server/__tests__/worktree.test.js`: new test builds a real worktree, writes
garbage into its `.git` file (reproducing the exact corruption), asserts
`git status` inside it fails (sanity), calls `removeWorktree` directly, then
asserts: the source repo no longer lists it, its branch is gone, **and** the
directory plus a file written into it still exist afterward untouched.
## Follow-up 2: `start` was atomic by accident, not by invariant
`server/routes/lanes.js`: the `start` case read `lane.run_id` and later wrote
a new one with no `await` in between — atomic today only because nothing
yields the event loop in that stretch. Wrapped the whole check-then-spawn in
`withLaneLock(lane.id, async () => {...})`, re-fetching the lane inside the
lock (a concurrent `remove` could have deleted the row while queued, so a
`missing` case now returns 404 `ENOLANE` instead of dereferencing a null
lane — a real edge case introduced by the lock itself, not present before).
The `409 ERUNLIVE` code and message are unchanged.
Regression test (`server/__tests__/lane-lifecycle.test.js`,
`"start is serialized behind the per-lane lock..."`): holds the same lane's
lock directly from the test (`withLaneLock(lane.id, () => new Promise(...))`),
fires `POST .../start` while that lock is held, confirms the request has NOT
settled 50ms later and that no run_id was written, then releases the held
lock and confirms `start` only proceeds (spawns, returns 200) after release.
This proves the route genuinely shares the per-lane lock rather than proving
only that the current zero-await code happens to be atomic.
## Follow-up 3: openapi.yaml drift check in CI
`.github/workflows/ci.yml`: added a step to the existing "🧹 Check Formatting"
job — `npm run openapi:yaml` (regenerate) followed by
`git diff --exit-code openapi.yaml`. No new job, no git hook (the pre-commit
hook is already slow, per instruction). Verified locally: regenerating
produces zero diff against the currently committed file.
## Follow-up 4: missing `sameOriginGuard` on two mutating routes
`server/routes/lanes.js`: added `sameOriginGuard` to `POST /api/lanes/`
(lane creation) and `POST /api/lanes/:id/stage` (stage reporting) —
previously the only two mutating lane routes without it.
Checked both callers before changing anything:
- `bin/ccam.js`'s `post()` helper (backing `ccam lanes add` and `ccam stage`)
only ever sends a `Content-Type` header, never `Origin` or `Referer` — the
guard passes any request with no Origin header through unconditionally
(same rule already relied on by every other guarded lane route). Verified
end to end by re-running `server/__tests__/lanes-cli.test.js` (13/13) after
the change — both `ccam lanes add --repo` and the destructive-lifecycle
CLI flows (which call `stage` indirectly via `clear`) still pass.
- `grep -rn "api/lanes" mcp/ scripts/` — no hits. Nothing else calls these
routes.
No legitimate caller was broken; no need to weaken the guard or stop and
ask.
Regression tests added to `server/__tests__/lanes-api.test.js`:
`"rejects cross-origin lane creation"` and `"rejects cross-origin stage
reporting"`, mirroring the existing cross-origin PATCH/DELETE tests exactly
(assert `403 EBADORIGIN`, and for stage, that the lane's stage was not
changed).
## Commands run, with tallies
- `node --test server/__tests__/worktree.test.js` → 19/19 (was 18)
- `node --test server/__tests__/lane-lifecycle.test.js` → 29/29 (was 28)
- `node --test server/__tests__/lanes-api.test.js` → 22/22 (was 20)
- `node --test server/__tests__/lanes-cli.test.js` → 13/13 (unchanged, re-run
as a caller check for follow-up 4)
- `npm run test:server`**861/861** (baseline 857)
- `cd client && npx vitest run src/components/lanes/__tests__/DestructiveLaneModal.test.tsx`
→ 16/16 (was 14)
- `npm run test:client`**299/299** (baseline 297)
- `npm run build` → succeeded (client build, `tsc -b && vite build`)
- `bash .claude/skills/file-headers/scripts/check-headers.sh` → exit 0
- `node scripts/generate-openapi-yaml.js && git diff --exit-code openapi.yaml`
→ exit 0, no diff
- `npm run format:check` → all files pass
## Left out / deferred, with reasons
- Did not touch the "Each verb → remove" bullet in `docs/LANES.md` (a
different paragraph from the one named in the brief) — it only describes
the ordinary managed-removal steps and makes no claim about the
corrupt-`.git` case, so it was not inaccurate and editing it would have
been unrequested scope.
- Did not add a CLI-level test reproducing the corrupt-`.git` removal
through `ccam lanes remove` — the brief asked for "a test proving (a)"
which the new `worktree.test.js` case already does directly against
`removeWorktree` (the same function the route and CLI both call); adding a
second, slower end-to-end CLI version would duplicate coverage without
proving anything new.
- Did not add a real concurrent-`claude`-process test for follow-up 2 (two
genuine `POST /start` calls racing against real spawned processes) — the
codebase's own existing tests avoid ever spawning `claude` twice
concurrently for exactly this reason (slow, and the real `claude` binary's
behavior isn't what's under test). The lock-holding test proves the same
invariant deterministically without that cost.