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
+40
View File
@@ -0,0 +1,40 @@
/**
* @file Per-lane serialization lock. Ensures that only one async operation runs
* on a lane at a time, preventing concurrent git checkout races and other
* worktree collisions. Implemented as a chain of promises keyed by lane ID;
* acquiring a lock waits for the previous holder to settle (success or throw),
* then runs the new work, and releases for the next waiter.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const chains = new Map();
/**
* Acquire the per-lane lock, run fn, and release.
* The lock serialises work per lane_id: the next waiter runs only after the
* previous holder settles (success or error). If fn throws, the exception
* propagates to the caller; the chain is *not* poisoned — the next waiter
* still gets a fresh attempt.
*
* @param {number|string} id - Lane ID, converted to string for deduplication.
* @param {() => Promise<T>} fn - Async function to run while holding the lock.
* @returns {Promise<T>} The result of fn, or its thrown error.
*/
function withLaneLock(id, fn) {
const key = String(id);
const prev = chains.get(key) || Promise.resolve();
const run = prev.then(fn, fn); // run regardless of how the previous holder settled
// Keep the chain alive but never let a rejection poison the next waiter.
const settled = run.then(
() => {},
() => {}
);
chains.set(key, settled);
// Clean up the chain entry when it settles to prevent unbounded map growth.
settled.then(() => {
if (chains.get(key) === settled) chains.delete(key);
});
return run;
}
module.exports = { withLaneLock };