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,169 @@
/**
* @file Regression: a BACKGROUND subagent's tool events must not clear a
* genuine 'notification' waiting flag held by the MAIN agent (blocked on the
* user via AskUserQuestion / permission). Before the fix, PreToolUse/PostToolUse
* cleared awaiting_input_since unconditionally, so a session that was truly
* "waiting for you" oscillated back to active on every subagent tool event —
* AI-Deck (12s poll) and deck-web (5s WS) then disagreed about the same session.
*
* The guard reuses the existing subagent-actor heuristic (findDeepestWorkingAgent
* while main is 'waiting'): when a subagent is the actor, only PASSIVE waits
* (stop/session_start/interrupted) are cleared; a 'notification' wait is
* preserved. When MAIN is the actor, clearing is unconditional (keeps the
* documented permission-mid-tool path intact).
*
* This test lives in the fork's own suite so a future upstream merge that
* silently reverts the guard fails loudly here (home-network monitor fork,
* see decisions/).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const os = require("os");
const http = require("http");
const TEST_DB = path.join(os.tmpdir(), `awaiting-guard-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
process.env.DASHBOARD_LIVENESS_PROBE = "0";
const { createApp, startServer } = require("../index");
let server;
let BASE;
function fetch(urlPath, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(urlPath, BASE);
const req = http.request(
{
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method || "GET",
headers: { "Content-Type": "application/json", ...options.headers },
},
(res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => {
let parsed;
try {
parsed = JSON.parse(body);
} catch {
parsed = body;
}
resolve({ status: res.statusCode, body: parsed });
});
}
);
req.on("error", reject);
if (options.body) req.write(JSON.stringify(options.body));
req.end();
});
}
const post = (p, body) => fetch(p, { method: "POST", body });
const hook = (hook_type, data) => post("/api/hooks/event", { hook_type, data });
const sessionOf = async (id) => (await fetch(`/api/sessions/${id}`)).body.session;
/**
* Drive a session into: main agent 'waiting' with the given reason, AND a live
* working subagent (so findDeepestWorkingAgent returns it). Returns nothing;
* asserts each precondition so a harness regression is obvious.
*/
async function sessionWaitingWithWorkingSubagent(sid, { notification }) {
await hook("SessionStart", { session_id: sid });
// UserPromptSubmit clears the session_start wait and promotes main → working.
await hook("UserPromptSubmit", { session_id: sid, prompt: "go" });
// Main (working) spawns a subagent → subagent row inserted with status working.
await hook("PreToolUse", {
session_id: sid,
tool_name: "Agent",
tool_input: { subagent_type: "reviewer", prompt: "review" },
});
// Now block the MAIN agent. A waiting-for-user Notification stamps
// reason='notification'; a Stop stamps the passive reason='stop'.
if (notification) {
await hook("Notification", { session_id: sid, message: "Claude is waiting for your input" });
} else {
await hook("Stop", { session_id: sid });
}
const sess = await sessionOf(sid);
assert.ok(sess.awaiting_input_since, "precondition: session should be awaiting");
assert.equal(
sess.awaiting_reason,
notification ? "notification" : "stop",
"precondition: expected awaiting_reason"
);
}
before(async () => {
server = await startServer(createApp(), 0);
BASE = `http://127.0.0.1:${server.address().port}`;
});
after(() => {
if (server) server.close();
});
describe("awaiting guard: subagent tool events vs. main-agent waiting", () => {
it("PRESERVES a 'notification' wait when a background subagent fires PreToolUse", async () => {
const sid = "guard-notif-pre";
await sessionWaitingWithWorkingSubagent(sid, { notification: true });
// Subagent (deepest working, main is waiting) runs a tool. This must NOT
// clear the main agent's genuine "waiting for you" flag.
await hook("PreToolUse", { session_id: sid, tool_name: "Bash" });
const sess = await sessionOf(sid);
assert.ok(sess.awaiting_input_since, "notification wait must survive subagent PreToolUse");
assert.equal(sess.awaiting_reason, "notification");
});
it("PRESERVES a 'notification' wait when a background subagent fires PostToolUse", async () => {
const sid = "guard-notif-post";
await sessionWaitingWithWorkingSubagent(sid, { notification: true });
await hook("PostToolUse", { session_id: sid, tool_name: "Bash" });
const sess = await sessionOf(sid);
assert.ok(sess.awaiting_input_since, "notification wait must survive subagent PostToolUse");
assert.equal(sess.awaiting_reason, "notification");
});
it("CLEARS a passive 'stop' wait when a background subagent fires a tool event", async () => {
// Passive-clear is desirable: a backgrounded subagent's activity should flip
// a merely-Stopped session back to active ("done/idle only while no agent works").
const sid = "guard-stop-pre";
await sessionWaitingWithWorkingSubagent(sid, { notification: false });
await hook("PreToolUse", { session_id: sid, tool_name: "Bash" });
const sess = await sessionOf(sid);
assert.equal(
sess.awaiting_input_since,
null,
"passive stop wait should be cleared by subagent activity"
);
assert.equal(sess.awaiting_reason, null);
});
it("CLEARS a 'notification' wait when MAIN (no working subagent) resumes with a tool", async () => {
// Control: main was waiting on the user, no subagent running. A PreToolUse
// means main itself resumed — clearing is correct (unchanged behaviour).
const sid = "guard-notif-mainactor";
await hook("SessionStart", { session_id: sid });
await hook("UserPromptSubmit", { session_id: sid, prompt: "go" });
await hook("Notification", { session_id: sid, message: "Claude is waiting for your input" });
let sess = await sessionOf(sid);
assert.ok(sess.awaiting_input_since && sess.awaiting_reason === "notification", "precondition");
await hook("PreToolUse", { session_id: sid, tool_name: "Bash" });
sess = await sessionOf(sid);
assert.equal(sess.awaiting_input_since, null, "main resuming must clear its own wait");
assert.equal(sess.awaiting_reason, null);
});
});