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
+150
View File
@@ -0,0 +1,150 @@
/**
* @file Preflight checks before destructive lane operations (reset, remove, purge).
* Queries the current state without mutations: git status, unpushed commits, sessions
* to be purged, and blockers (adopted lanes, missing directories, unpushed work).
* Every result is read-only; the route and action layer decide what to do with blocks.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const { db } = require("../db");
const wt = require("./worktree");
const lanesLib = require("./lanes");
/**
* Preflight for reset, remove, or purge. Returns an object describing what will happen:
* - reset/remove: {action, lane, kind, branch, dirty, untracked, unpushed, head, blocked, warnings}
* - purge: {action, lane, sessions, events, tokenRows, bytesEstimate, activeSessionSkipped}
*
* blocked[] holds only conditions that genuinely prevent the action: "adopted" (not
* managed), "missing" (dir gone), "unreadable" (git failed against the directory), and
* "unpushed-commits" (unpushed > 0 — the only one a `force: true` overrides). warnings[]
* holds purely informational facts that never gate the action, starting with "no-remote"
* (no git remote configured — nothing is backed up, but the action proceeds). Both are
* data, not an exception.
*
* @param {object} lane - The lane to check
* @param {string} action - One of "reset", "remove", "purge"
* @returns {Promise<object>} Preflight report
*/
async function preflight(lane, action) {
const blocked = [];
const warnings = [];
// Check if lane is adopted (not managed)
if (lane.kind === "adopted") {
blocked.push("adopted");
}
// For reset/remove, return git status and blockers
if (action === "reset" || action === "remove") {
let dirty = 0;
let untracked = 0;
let unpushed = 0;
let head = null;
// First check if directory exists
if (!fs.existsSync(lane.cwd)) {
blocked.push("missing");
} else {
// Directory exists, try to read git status
try {
// Get status counts
const status = await wt.statusCounts(lane.cwd);
dirty = status.dirty;
untracked = status.untracked;
head = status.head;
// Get unpushed count — measured against the lane's own base branch when
// there is no remote, so it counts this lane's work, not the whole repo.
unpushed = await wt.unpushedCount(lane.cwd, lane.base_branch);
if (unpushed > 0) {
blocked.push("unpushed-commits");
}
// Detect no remotes configured at all — informational only, never a blocker:
// a perfectly ordinary local-only managed lane has no remote at all.
const noRemote = await wt.hasNoRemotes(lane.cwd);
if (noRemote) {
warnings.push("no-remote");
}
} catch (err) {
// Directory exists but git failed (corrupt repo, permission denied, etc.)
blocked.push("unreadable");
}
}
return {
action,
lane: lane.id,
kind: lane.kind,
branch: lane.branch,
dirty,
untracked,
unpushed,
head: head || null,
blocked,
warnings,
};
}
// For purge, count sessions and events to be deleted
if (action === "purge") {
const counts = countPurgeSessions(lane);
return {
action,
lane: lane.id,
sessions: counts.sessions,
events: counts.events,
tokenRows: counts.tokenRows,
bytesEstimate: (counts.events + counts.tokenRows) * 512,
activeSessionSkipped: counts.activeSessionSkipped,
};
}
// Unknown action should never reach here (route validates)
throw Object.assign(new Error(`unknown action: ${action}`), { code: "EBADACTION" });
}
/**
* Count sessions, events, and token_usage rows that would be deleted by purgeLaneSessions.
* Uses the shared purgeCandidateSessions helper to ensure the counts match exactly what
* gets deleted, so the confirmation dialog's numbers are truthful.
*/
function countPurgeSessions(lane) {
const result = { sessions: 0, events: 0, tokenRows: 0, activeSessionSkipped: false };
// Use the shared helpers so the path matching (and its LIKE escaping) has
// exactly one definition, in server/lib/lanes.js.
const sessionsToDelete = lanesLib.purgeCandidateSessions(lane);
result.activeSessionSkipped = lanesLib.hasActiveLaneSession(lane);
if (sessionsToDelete.length === 0) {
return result;
}
// Count events for those sessions
const sessionIds = sessionsToDelete.map((s) => s.id);
const eventsCount = db
.prepare(
`SELECT COUNT(*) as count FROM events WHERE session_id IN (${sessionIds.map(() => "?").join(",")})`
)
.get(...sessionIds);
result.events = eventsCount ? eventsCount.count : 0;
// Count orphaned token_usage rows
const tokenCount = db
.prepare(
`SELECT COUNT(*) as count FROM token_usage WHERE session_id IN (${sessionIds.map(() => "?").join(",")})`
)
.get(...sessionIds);
result.tokenRows = tokenCount ? tokenCount.count : 0;
// Count sessions (for completeness)
result.sessions = sessionsToDelete.length;
return result;
}
module.exports = { preflight };