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
+212
View File
@@ -0,0 +1,212 @@
/**
* @file cc-watcher.js
* @description Best-effort file watcher for the Claude Code config surfaces
* surfaced by the Config Explorer page. Watches ~/.claude/ recursively (if
* the platform supports it) plus ~/.claude.json and emits a debounced
* `cc_config_changed` over the dashboard websocket so the UI can refetch
* without polling.
*
* Aggressively filters fs.watch events: ~/.claude/ contains lots of churn
* (`projects/*.jsonl` transcripts, `file-history/`, our own
* `cc-config-backups/`) that has nothing to do with the Config Explorer.
* Only paths matching real config surfaces fire a broadcast. Without this
* filter the watcher fires multiple times per second while a claude session
* is active and the page becomes a perpetual loading spinner.
*
* Failures here are non-fatal — `fs.watch` is platform-quirky, and the
* Config Explorer still has a manual Refresh button.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
const { getClaudeHome } = require("./claude-home");
const DEBOUNCE_MS = 500;
// Subpaths inside ~/.claude/ that ARE config surfaces and should trigger a
// refetch. Anything else (transcripts, file history, our own backups) is
// ignored. Match is by prefix on the relative path.
const RELEVANT_PREFIXES = [
"settings.json",
"settings.local.json",
"keybindings.json",
"statusline.py",
"statusline-command.sh",
"known_marketplaces.json",
"agents",
"commands",
"skills",
"output-styles",
"hooks",
"plugins",
"CLAUDE.md",
];
// Subpaths to explicitly ignore even if they match RELEVANT_PREFIXES by
// accident. Important: our own backup dir lives at ~/.claude/cc-config-backups/
// and writing backups would re-trigger the watcher in a loop without this.
const IGNORED_PREFIXES = [
"cc-config-backups",
"backups", // Claude Code's own ~/.claude/backups/.claude.json.backup.* churn
"projects",
"file-history",
"todos",
"shell-snapshots",
"ide",
"logs",
"statsig",
];
// Config surfaces that are DIRECTORIES — watched recursively so nested changes
// (e.g. skills/<x>/SKILL.md) still fire. We deliberately watch ONLY these,
// never the whole of ~/.claude/, so the recursive watcher never registers
// interest in high-churn dirs (backups/, projects/, logs/) whose transient
// files crash Node's Linux userland recursive watcher mid-stat.
const WATCH_SUBDIRS = ["agents", "commands", "skills", "output-styles", "hooks", "plugins"];
let started = false;
let timer = null;
let pendingPaths = new Set();
const watchers = [];
function isRelevantUnderHome(home, fullPath) {
const rel = path.relative(home, fullPath);
if (!rel || rel.startsWith("..")) return false;
// First segment of the relative path
const head = rel.split(path.sep)[0];
if (IGNORED_PREFIXES.includes(head)) return false;
if (!RELEVANT_PREFIXES.includes(head)) return false;
return true;
}
function scheduleEmit(broadcast, p) {
if (p) pendingPaths.add(p);
if (timer) return;
timer = setTimeout(() => {
timer = null;
const paths = Array.from(pendingPaths);
pendingPaths = new Set();
if (paths.length === 0) return;
try {
broadcast("cc_config_changed", { source: "fs", paths });
} catch {
/* ignore */
}
}, DEBOUNCE_MS);
}
function safeWatchHome({ home, broadcast }) {
if (!fs.existsSync(home)) return;
// Watch ~/.claude/ itself NON-recursively. Catches top-level config files
// (settings.json, keybindings.json, CLAUDE.md, statusline.*, *.json) plus the
// creation/removal of subdirs. Non-recursive does NOT walk-and-stat children,
// so it never trips the recursive-watcher ENOENT race on churn dirs.
try {
const w = fs.watch(home, (_event, filename) => {
if (!filename) return;
const full = path.join(home, filename);
if (!isRelevantUnderHome(home, full)) return;
scheduleEmit(broadcast, full);
});
w.on("error", () => {});
watchers.push(w);
} catch {
/* platform limitation — best effort only */
}
// Recursively watch ONLY the relevant config subdirs (never backups/, projects/,
// logs/, …) so nested changes still fire without watching the high-churn trees.
for (const sub of WATCH_SUBDIRS) {
const dir = path.join(home, sub);
try {
if (!fs.existsSync(dir)) continue;
const w = fs.watch(dir, { recursive: true }, (_event, filename) => {
const full = filename ? path.join(dir, filename) : dir;
if (!isRelevantUnderHome(home, full)) return;
scheduleEmit(broadcast, full);
});
w.on("error", () => {});
watchers.push(w);
} catch {
/* platform limitation — best effort only */
}
}
}
function safeWatchFile({ target, broadcast }) {
try {
if (!fs.existsSync(target)) return;
const w = fs.watch(target, () => scheduleEmit(broadcast, target));
w.on("error", () => {});
watchers.push(w);
} catch {
/* ignore */
}
}
// Belt-and-suspenders: Node's recursive fs.watch (userland impl on Linux) stats
// changed paths and can throw ENOENT/EPERM when a file vanishes mid-event. That
// throw escapes the watcher's `error` event as an uncaughtException. This watcher
// is explicitly best-effort and must NEVER take down the server, so swallow
// exactly that class of error and let every other uncaught exception crash as
// normal (print + non-zero exit, matching Node's default).
let crashGuard = null;
function installWatchCrashGuard() {
if (crashGuard) return;
crashGuard = (err) => {
const stack = (err && err.stack) || "";
const transientWatch =
err &&
(err.code === "ENOENT" || err.code === "EPERM") &&
err.syscall === "stat" &&
/fs[\\/](recursive_watch|watchers)/.test(stack);
if (transientWatch) return; // vanished file under a watched tree — ignore
// Not ours: preserve default crash behavior.
console.error(err);
process.exit(1);
};
process.on("uncaughtException", crashGuard);
}
function uninstallWatchCrashGuard() {
if (!crashGuard) return;
process.removeListener("uncaughtException", crashGuard);
crashGuard = null;
}
/**
* Start watching the Claude Code config surfaces. Idempotent: subsequent
* calls are no-ops.
*/
function startCcWatcher({ broadcast }) {
if (started) return;
started = true;
installWatchCrashGuard();
const home = getClaudeHome();
safeWatchHome({ home, broadcast });
// ~/.claude.json sits beside ~/.claude/, not inside it.
safeWatchFile({ target: path.join(os.homedir(), ".claude.json"), broadcast });
}
function stopCcWatcher() {
if (timer) {
clearTimeout(timer);
timer = null;
}
for (const w of watchers) {
try {
w.close();
} catch {
/* ignore */
}
}
watchers.length = 0;
pendingPaths = new Set();
uninstallWatchCrashGuard();
started = false;
}
module.exports = { startCcWatcher, stopCcWatcher, isRelevantUnderHome };