@tekmidian/pai 0.66.9 → 0.66.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auto-route-DoFrncW7.mjs → auto-route-D5vCdnrO.mjs} +2 -2
- package/dist/{auto-route-DoFrncW7.mjs.map → auto-route-D5vCdnrO.mjs.map} +1 -1
- package/dist/auto-route-DMz24Gwp.mjs +7 -0
- package/dist/{chain-BL4khGgy.mjs → chain-DyVKYcIZ.mjs} +3 -3
- package/dist/{chain-BL4khGgy.mjs.map → chain-DyVKYcIZ.mjs.map} +1 -1
- package/dist/cli/index.mjs +7 -7
- package/dist/cli/program.mjs +7 -7
- package/dist/{context-handover-cache-5qnqzWqp.mjs → context-handover-cache-CDvnUdcN.mjs} +4 -4
- package/dist/{context-handover-cache-5qnqzWqp.mjs.map → context-handover-cache-CDvnUdcN.mjs.map} +1 -1
- package/dist/daemon/index.mjs +6 -6
- package/dist/{daemon-C-2mo7y_.mjs → daemon-BjF97v22.mjs} +5 -5
- package/dist/{daemon-C-2mo7y_.mjs.map → daemon-BjF97v22.mjs.map} +1 -1
- package/dist/{daemon-4VZOwUXe.mjs → daemon-CqTgE1vm.mjs} +6 -6
- package/dist/daemon-mcp/index.mjs +4 -4
- package/dist/{fallback-2yWDDYiT.mjs → fallback-ctWuL9Ie.mjs} +4 -3
- package/dist/{fallback-2yWDDYiT.mjs.map → fallback-ctWuL9Ie.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs.map +1 -1
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{pai-marker-BFOlchq2.mjs → pai-marker-lwp4pDRv.mjs} +4 -4
- package/dist/pai-marker-lwp4pDRv.mjs.map +1 -0
- package/dist/{planner-CspP59ya.mjs → planner-4nCXw4TS.mjs} +4 -4
- package/dist/{planner-CspP59ya.mjs.map → planner-4nCXw4TS.mjs.map} +1 -1
- package/dist/{program-D_Ks5nkL.mjs → program-CBo62E7P.mjs} +11 -9
- package/dist/program-CBo62E7P.mjs.map +1 -0
- package/dist/{run-BZNdpUGQ.mjs → run-BHLXvURR.mjs} +3 -3
- package/dist/{run-BZNdpUGQ.mjs.map → run-BHLXvURR.mjs.map} +1 -1
- package/dist/{worktree-CwMRTa0H.mjs → worktree-DxsZ85ni.mjs} +20 -2
- package/dist/worktree-DxsZ85ni.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/auto-route-UD9PySwP.mjs +0 -7
- package/dist/pai-marker-BFOlchq2.mjs.map +0 -1
- package/dist/program-D_Ks5nkL.mjs.map +0 -1
- package/dist/worktree-CwMRTa0H.mjs.map +0 -1
|
@@ -752,7 +752,7 @@ function provisionDeps(logDir, id, cwd, wtDir) {
|
|
|
752
752
|
git(wtDir, [
|
|
753
753
|
"check-ignore",
|
|
754
754
|
"-q",
|
|
755
|
-
"node_modules"
|
|
755
|
+
"node_modules/"
|
|
756
756
|
]);
|
|
757
757
|
} catch {
|
|
758
758
|
return;
|
|
@@ -953,6 +953,7 @@ function salvageUncommitted(wtDir, label, timeoutMs) {
|
|
|
953
953
|
const toStage = [];
|
|
954
954
|
for (const p of paths) if (missing.has(p)) skipped.push(`${p} (missing, not committed)`);
|
|
955
955
|
else if (symlinkEscapesWorktree(wtDir, p)) skipped.push(`symlink ${p} -> ${readlinkSync(join(wtDir, p))} (points outside the worktree)`);
|
|
956
|
+
else if (p === "node_modules" || p.startsWith("node_modules/")) skipped.push(`${p} (node_modules is never committed)`);
|
|
956
957
|
else toStage.push(p);
|
|
957
958
|
if (!toStage.length) return {
|
|
958
959
|
committed: [],
|
|
@@ -989,6 +990,21 @@ function salvageUncommitted(wtDir, label, timeoutMs) {
|
|
|
989
990
|
skipped
|
|
990
991
|
};
|
|
991
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Refuse a merge whose branch adds or changes a top-level `node_modules` of
|
|
995
|
+
* any type (a worker's own commit of its symlink included): merging it would
|
|
996
|
+
* replace the checkout's real directory. Both merge paths call this.
|
|
997
|
+
*/
|
|
998
|
+
function assertNoNodeModules(cwd, from, branch, id) {
|
|
999
|
+
if (git(cwd, [
|
|
1000
|
+
"diff",
|
|
1001
|
+
"--name-only",
|
|
1002
|
+
from,
|
|
1003
|
+
branch,
|
|
1004
|
+
"--",
|
|
1005
|
+
"node_modules"
|
|
1006
|
+
])) throw new Error(`worker ${id}: branch ${branch} commits node_modules, which would replace the checkout's real directory. Remove it on the branch (git rm -r --cached node_modules, amend), then re-run merge`);
|
|
1007
|
+
}
|
|
992
1008
|
/** A salvage commit that deletes more tracked files than this is refused. */
|
|
993
1009
|
const MAX_SALVAGE_DELETES = 5;
|
|
994
1010
|
/**
|
|
@@ -1116,6 +1132,7 @@ function mergeSnapshotWorker(logDir, id, st, noCommit = false) {
|
|
|
1116
1132
|
snapshot,
|
|
1117
1133
|
branch
|
|
1118
1134
|
]).split("\n").filter(Boolean);
|
|
1135
|
+
assertNoNodeModules(st.cwd, snapshot, branch, id);
|
|
1119
1136
|
assertNoSnapshotDrift(st.cwd, snapshot, changedPaths, id);
|
|
1120
1137
|
const diff = execFileSync("git", [
|
|
1121
1138
|
"-C",
|
|
@@ -1339,6 +1356,7 @@ function mergeWorker(logDir, id, opts = {}) {
|
|
|
1339
1356
|
mergeBase,
|
|
1340
1357
|
st.branch
|
|
1341
1358
|
]).split("\n").filter(Boolean);
|
|
1359
|
+
assertNoNodeModules(st.cwd, mergeBase, st.branch, id);
|
|
1342
1360
|
assertNoDirtyOverlap(st.cwd, incomingPaths, id, st.branch);
|
|
1343
1361
|
if (opts.noCommit) return applyNoCommit(st.cwd, id, st.branch, st.worktreeDir, mergeBase, incomingPaths);
|
|
1344
1362
|
try {
|
|
@@ -1575,4 +1593,4 @@ function gcWorktreesThrottled(logDir, everyMin = 60, now = Date.now()) {
|
|
|
1575
1593
|
|
|
1576
1594
|
//#endregion
|
|
1577
1595
|
export { newWorkerId as A, shortText as B, contextPercent as C, isLive as D, isChatPane as E, setWorkerLabel as F, panesDir as G, ensureNoMcpConfig as H, waitForTerminalStatus as I, routingStatePath as K, appendSystemPromptArgs as L, ownsPid as M, parseSleepSecs as N, loadStatus as O, saveStatus as P, longInlinePromptHint as R, contextLabel as S, elapsedOf as T, eventsPath as U, stripPromptValues as V, ledgerPath as W, appendLedger as _, git as a, ageOf as b, recordWorktree as c, worktreeSystemPrompt as d, worktreeWanted as f, pendingResults as g, pendingGate as h, gcWorktreesThrottled as i, nowStamp as j, loadStatuses as k, salvageOnExit as l, formatPending as m, discardWorker as n, inPlaceSystemPrompt as o, worktreesDir as p, workersLogDir as q, gcWorktrees as r, mergeWorker as s, addWorktree as t, verifyWorker as u, ledgerSummary as v, describeTool as w, alive as x, UNLABELED as y, parseRunnerArgs as z };
|
|
1578
|
-
//# sourceMappingURL=worktree-
|
|
1596
|
+
//# sourceMappingURL=worktree-DxsZ85ni.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-DxsZ85ni.mjs","names":[],"sources":["../src/workers/paths.ts","../src/workers/args.ts","../src/workers/status.ts","../src/workers/ledger.ts","../src/workers/pending.ts","../src/workers/worktree.ts"],"sourcesContent":["/**\n * paths.ts — where worker run artefacts live.\n *\n * Everything a run writes (event mirror, status file, pane registry, routing\n * state, the empty MCP config) sits under one logDir so the whole tree is\n * disposable and configurable: `workers.logDir`, default ~/.claude/logs/workers.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { expandHome, DEFAULT_LOG_DIR, type WorkersConfig } from \"./config.js\";\nimport { paiHomePath, resolvePaiFile } from \"../config/pai-home.js\";\n\n/**\n * Absolute logDir for the given config. An explicit `logDir` in workers.yaml\n * is honored verbatim (the user chose it). Otherwise this resolves like every\n * other PAI_HOME file: PAI_HOME/logs/workers if it exists, else the old\n * ~/.claude/logs/workers (with a one-time notice), else the new path.\n */\nexport function workersLogDir(config: WorkersConfig): string {\n const expanded = expandHome(config.logDir);\n if (config.logDir !== DEFAULT_LOG_DIR) return expanded;\n return resolvePaiFile(paiHomePath(\"logs\", \"workers\"), [expanded], \"pai config migrate --logs\");\n}\n\n/** The strict empty MCP config for headless runs, written on demand. */\nexport function ensureNoMcpConfig(logDir: string): string {\n const path = noMcpConfigPath(logDir);\n if (!existsSync(path)) {\n mkdirSync(logDir, { recursive: true });\n writeFileSync(path, '{ \"mcpServers\": {} }\\n', \"utf8\");\n }\n return path;\n}\n\nexport function statusPath(logDir: string, id: string): string {\n return join(logDir, `${id}.status`);\n}\n\nexport function eventsPath(logDir: string, id: string): string {\n return join(logDir, `${id}.jsonl`);\n}\n\nexport function ledgerPath(logDir: string): string {\n return join(logDir, \"ledger.log\");\n}\n\nexport function routingStatePath(logDir: string): string {\n return join(logDir, \"routing-state.json\");\n}\n\nexport function panesDir(logDir: string): string {\n return join(logDir, \"panes\");\n}\n\n/**\n * The strict empty MCP config handed to headless workers. Written into the\n * logDir on demand (never into the user's vendor config directory — this is\n * PAI state, not vendor state).\n */\nexport function noMcpConfigPath(logDir: string): string {\n return join(logDir, \"no-mcp.json\");\n}\n","/**\n * args.ts — parse the claude-args tail that `pai worker run` receives.\n *\n * The runner needs a few things out of the caller's argument vector: the\n * prompt (for the default label), the requested --output-format (so the final\n * print matches what plain `claude -p` would have produced), whether the\n * caller already chose a --model or an --mcp-config (both suppress the\n * defaults the runner would otherwise force), any --mcp allowlist names, any\n * --allowedTools grants (their mcp__server__… entries decide which servers\n * load), and whether they appended their own system prompt (the worker\n * contract is then added alongside, not instead). Everything is passed\n * through untouched — the runner never rewrites the caller's task.\n */\n\nexport interface ParsedRunnerArgs {\n /** Prompt string after -p/--print, when the run is headless. */\n prompt: string | null;\n /** Caller's --output-format: text (default), json, or stream-json. */\n outputFormat: \"text\" | \"json\" | \"stream-json\";\n /** Args to hand to claude (minus --output-format/--verbose, which we add). */\n rest: string[];\n /** Headless: a -p/--print flag is present. */\n headless: boolean;\n /** Caller passed --model (or --model=…): do not force the provider model. */\n callerModel: boolean;\n /** Caller passed --mcp-config (or --mcp-config=…): keep their MCP setup. */\n callerMcpConfig: boolean;\n /** Caller passed --append-system-prompt: the contract is added alongside. */\n callerSystemPrompt: boolean;\n /** The caller's --append-system-prompt texts, pulled out of `rest` (claude keeps only the last flag). */\n callerSystemPrompts: string[];\n /** Caller passed --tools (or --tools=…): a project's `pai project tools` pin does not apply. */\n callerTools: boolean;\n /** --mcp values (repeatable, comma-separated inside one flag). */\n mcp: string[];\n /** --allowedTools values (repeatable, comma-separated inside one flag). */\n allowedTools: string[];\n}\n\nexport function parseRunnerArgs(argv: string[]): ParsedRunnerArgs {\n let prompt: string | null = null;\n let outputFormat: ParsedRunnerArgs[\"outputFormat\"] = \"text\";\n const rest: string[] = [];\n let headless = false;\n let callerModel = false;\n let callerMcpConfig = false;\n let callerSystemPrompt = false;\n const callerSystemPrompts: string[] = [];\n let callerTools = false;\n const mcp: string[] = [];\n const allowedTools: string[] = [];\n\n let i = 0;\n while (i < argv.length) {\n const a = argv[i];\n if (a === \"-p\" || a === \"--print\") {\n headless = true;\n rest.push(a);\n if (i + 1 < argv.length && !argv[i + 1].startsWith(\"-\")) {\n prompt = argv[i + 1];\n rest.push(prompt);\n i += 1;\n }\n } else if (a === \"--output-format\") {\n const v = argv[i + 1];\n if (v === \"json\" || v === \"stream-json\") outputFormat = v;\n i += 1; // dropped; the runner prints the result in this format itself\n } else if (a.startsWith(\"--output-format=\")) {\n const v = a.slice(\"--output-format=\".length);\n if (v === \"json\" || v === \"stream-json\") outputFormat = v;\n } else if (a === \"--verbose\") {\n // dropped; re-added by the runner\n } else if (a === \"--append-system-prompt\") {\n callerSystemPrompt = true;\n if (i + 1 < argv.length) callerSystemPrompts.push(argv[++i]);\n } else if (a.startsWith(\"--append-system-prompt=\")) {\n callerSystemPrompt = true;\n callerSystemPrompts.push(a.slice(\"--append-system-prompt=\".length));\n } else if (a === \"--mcp\") {\n const v = argv[i + 1];\n if (v !== undefined && !v.startsWith(\"-\")) {\n mcp.push(v);\n i += 1;\n }\n } else if (a.startsWith(\"--mcp=\")) {\n mcp.push(a.slice(\"--mcp=\".length));\n } else if (a === \"--allowedTools\") {\n // captured for the MCP derivation, but still claude's flag: it passes through\n rest.push(a);\n const v = argv[i + 1];\n if (v !== undefined && !v.startsWith(\"-\")) {\n allowedTools.push(v);\n rest.push(v);\n i += 1;\n }\n } else if (a.startsWith(\"--allowedTools=\")) {\n allowedTools.push(a.slice(\"--allowedTools=\".length));\n rest.push(a);\n } else {\n if (a === \"--model\") callerModel = true;\n if (a.startsWith(\"--model=\")) callerModel = true;\n if (a === \"--mcp-config\") callerMcpConfig = true;\n if (a.startsWith(\"--mcp-config=\")) callerMcpConfig = true;\n if (a === \"--tools\") callerTools = true;\n if (a.startsWith(\"--tools=\")) callerTools = true;\n if (\n prompt === null && !a.startsWith(\"-\") && rest.length > 0 &&\n (rest[rest.length - 1] === \"-p\" || rest[rest.length - 1] === \"--print\")\n ) {\n prompt = a;\n }\n rest.push(a);\n }\n i += 1;\n }\n\n return {\n prompt,\n outputFormat,\n rest,\n headless,\n callerModel,\n callerMcpConfig,\n callerSystemPrompt,\n callerSystemPrompts,\n callerTools,\n mcp,\n allowedTools,\n };\n}\n\n/**\n * The one --append-system-prompt flag for a spawn: claude keeps only the LAST\n * occurrence, so every piece is joined (blank line between) into a single flag.\n * Empty parts are dropped; no parts, no flag.\n */\nexport function appendSystemPromptArgs(parts: string[]): string[] {\n const text = parts.filter((p) => p.trim()).join(\"\\n\\n\");\n return text ? [\"--append-system-prompt\", text] : [];\n}\n\n/**\n * Turn `-p \"<prompt>\"` into bare `-p` (same for --print): for stream-json\n * stdin runs the prompt moves to the first user message on stdin, so the\n * value must not stay on the command line. Only prompt values directly\n * following the flag are touched; everything else passes through.\n */\nexport function stripPromptValues(argv: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n const isPrint = a === \"-p\" || a === \"--print\";\n out.push(a);\n if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith(\"-\")) {\n i += 1; // drop the prompt value; the flag itself stays\n }\n }\n return out;\n}\n\n/**\n * A one-line stderr hint (never blocking) for an inline -p prompt long or\n * multi-line enough to be a shell-quoting risk — write it to a file and pass\n * --spec instead. null when the prompt is short/simple enough to be fine.\n */\nexport function longInlinePromptHint(prompt: string | null): string | null {\n if (!prompt) return null;\n const newlines = (prompt.match(/\\n/g) ?? []).length;\n if (prompt.length > 600 || newlines > 3) {\n return \"hint: long inline prompts break on shell quoting — write the spec to a file and use --spec <file>\";\n }\n return null;\n}\n\n/** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */\nexport function shortText(s: unknown, n: number): string {\n const t = String(s ?? \"\").split(/\\s+/).filter(Boolean).join(\" \");\n return t.length <= n ? t : t.slice(0, n - 1) + \"…\";\n}\n","/**\n * status.ts — the live per-worker status file.\n *\n * One JSON file per run, rewritten atomically on every turn, read by `ps`,\n * `follow`, the status line and the pane logic. Same field set the Python\n * runner wrote, plus `provider` (routing is multi-provider now) and `session`\n * (the AIBroker identity of the launching session, see scope.ts).\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { shortText } from \"./args.js\";\nimport { statusPath } from \"./paths.js\";\n\nexport interface WorkerSessionRef {\n /** AIBroker/iTerm session id (the iTerm UUID). */\n id: string;\n /** AIBroker name of that session, when it has one. */\n name: string;\n}\n\nexport interface WorkerStatus {\n id: string;\n pid: number;\n label: string;\n cwd: string;\n /** iTerm session id of the launching terminal (\"\" outside iTerm). */\n term: string;\n provider: string;\n model: string;\n state: \"running\" | \"done\" | \"failed\" | \"killed\" | \"lost\";\n started: string;\n updated: string;\n turns: number;\n tools: number;\n last: string;\n rc: number | null;\n secs: number | null;\n /** Launching session resolved through AIBroker, when it was. */\n session?: WorkerSessionRef | null;\n /** Claude Code session id (system/init) — what `resume` continues. */\n claudeSession?: string | null;\n /**\n * Claude Code session id of the orchestrator whose Bash launched this run,\n * when the status line's session map knew it (see scope.ts). Distinct from\n * claudeSession, which is the run's own session.\n */\n spawnerSession?: string | null;\n /** Context meter: tokens of the last assistant turn (input+cache+output). */\n contextTokens?: number | null;\n /** Context meter: window size (init model info or provider default). */\n contextWindow?: number | null;\n /** Path the prompt was read from via --spec (\"-\" for stdin), when it was. */\n spec?: string | null;\n /**\n * Caller's --output-format, when the run was headless. A json/stream-json\n * caller reads its own result and is notified on process exit by the\n * harness — supervision.ts uses this to skip the duplicate relay send.\n */\n outputFormat?: \"text\" | \"json\" | \"stream-json\";\n /** Chain this stage belongs to (the chain id), when it is a chain stage. */\n parent?: string;\n /** Class name of the chain stage (\"draft\", \"implement\", …). */\n stage?: string;\n /** Worktree this run executed in, when it got one (see worktree.ts). */\n worktreeDir?: string | null;\n /** Branch the worker committed on (worker/<id>), set on worktree runs. */\n branch?: string | null;\n /** Commit the branch started from (worktree base) for the commits count. */\n worktreeBase?: string | null;\n /** Whether `worktreeBase` is a snapshot of uncommitted checkout state, not HEAD. */\n worktreeSnapshot?: boolean | null;\n /** Commits the worker made on its branch; set with `branch` on success. */\n commits?: number | null;\n /** Set by `pai worker merge` once the branch landed in the original checkout. */\n merged?: boolean;\n /** Set by `pai worker gc`: the hidden ref (refs/pai-archive/<id>) holding the leftover worktree's state. */\n archived?: string | null;\n /**\n * How the run came to be: \"spawn\" (a `pai worker run` subagent) or \"chat\"\n * (the terminal's interactive pane itself, tracked like a worker). Absent\n * on statuses written before the flag existed — read as a spawn.\n */\n origin?: \"spawn\" | \"chat\";\n /** Which contract/parser this run used for its final report (see report.ts). */\n reportFormat?: \"json\" | \"ag2\";\n /** Whether the final AG2 report passed `aibroker agentish check`; unset for json reports or when no validator ran. */\n reportValid?: boolean;\n /** Validation error messages, when reportValid is false. */\n reportErrors?: string[];\n /** Whether the headless prompt got the end-of-turn \"final message must be…\" trailer line. */\n promptTrailer?: boolean;\n /** Whether an invalid AG2 final message triggered the one bounded re-ask (see run.ts). */\n reportRetried?: boolean;\n /**\n * Consecutive failed tool executions in this run, maintained by the runner's\n * stream loop (0 after any success). Absent on old status files — every\n * reader must treat that as \"no signal\".\n */\n consecFails?: number | null;\n /**\n * Seconds of the most recent single long sleep observed in a Bash tool_use\n * (see parseSleepSecs), cleared when the command completes. Absent on old\n * status files.\n */\n sleepSec?: number | null;\n /** Epoch ms the run is stopped at (deadline.ts); absent = no limit. */\n deadlineAt?: number | null;\n /** Limit in minutes the deadline was derived from. */\n maxMinutes?: number | null;\n /** The deadline stopped this run (state failed, `last` says why). */\n timedOut?: boolean;\n /** Launching process (the parent of `pai worker run`) at start, and its command name. */\n parentPid?: number;\n parentComm?: string;\n parentBackground?: boolean;\n /** Why the run ended (endreason.ts): exit, deadline, pai-worker-kill, parent-gone, external-signal. */\n endReason?: string;\n /** Who asked for the stop, when known (kill caller, daemon:<reason>). */\n endBy?: string;\n /** Detail for the reason (signal, sender note). */\n endNote?: string;\n /** Ever granted clickr desktop controls (see controls.ts) — the runner returns them best-effort on exit. */\n controlsHeld?: boolean;\n}\n\n/** Label a worker gets when launched with neither --label nor a prompt. */\nexport const UNLABELED = \"unlabeled\";\n\n/**\n * Whether a status is the terminal's interactive chat pane, not a spawned\n * task worker. `origin` is the real discriminator; the fallback catches\n * running entries from before the flag existed (unlabeled, no turns yet).\n * Removable once every pane runs code that writes `origin`.\n */\nexport function isChatPane(s: Pick<WorkerStatus, \"origin\" | \"label\" | \"turns\">): boolean {\n return (\n s.origin === \"chat\" ||\n (!s.origin && (s.label === UNLABELED || s.label === \"(no prompt)\") && s.turns === 0)\n );\n}\n\n/** Context-meter percentage 0–100, null when the numbers are missing. */\nexport function contextPercent(s: Pick<WorkerStatus, \"contextTokens\" | \"contextWindow\">): number | null {\n if (!s.contextTokens || !s.contextWindow) return null;\n return Math.round((s.contextTokens / s.contextWindow) * 100);\n}\n\n/** Compact token count: 84k, 200k, 900. */\nexport function fmtContextK(n: number): string {\n return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);\n}\n\n/**\n * The one context-meter label, shared by every renderer so identical\n * numbers always render identically: `ctx 84k/200k (42%)` — used-style\n * percent. Empty when the numbers are missing (callers drop the part).\n */\nexport function contextLabel(\n s: Pick<WorkerStatus, \"contextTokens\" | \"contextWindow\">\n): string {\n const pct = contextPercent(s);\n if (pct === null) return \"\";\n return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;\n}\n\n/** Timestamp format shared by status files and the ledger. */\nexport function nowStamp(d: Date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +\n `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n );\n}\n\n// second-resolution ids repeat when two workers/chains start together in one\n// process; a repeat gets a monotonic suffix so files never collide\nlet lastId = \"\";\nlet idSeq = 0;\n\nexport function newWorkerId(d: Date = new Date(), pid = process.pid): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;\n if (base === lastId) {\n idSeq += 1;\n return `${base}-${String(idSeq).padStart(2, \"0\")}`;\n }\n lastId = base;\n idSeq = 0;\n return base;\n}\n\n// Temp names must be unique per writer, not just per worker: two processes\n// legitimately write the same id at the same moment (`pai worker kill` marking\n// a run killed while the run's own SIGTERM handler writes its terminal state).\n// With one shared `<id>.status.tmp` they clobber each other's temp file and the\n// loser's rename fails with ENOENT — observed live on 2026-09-19.\nlet tmpSeq = 0;\n\n/** A temp path only this call owns — never the same string twice. */\nexport function statusTmpPath(logDir: string, id: string): string {\n return `${statusPath(logDir, id)}.${process.pid}.${tmpSeq++}.tmp`;\n}\n\n// The label this process first wrote for a given worker id: run.ts holds one\n// unchanging label in memory for the whole run and passes it to every\n// periodic saveStatus call, so a later call whose label still equals this\n// baseline is that unmodified write, never an intentional change — on-disk\n// then wins, so an external `pai worker goal` relabel between two of a\n// worker's own writes survives instead of being reverted by the next one. A\n// call whose label differs from the baseline (or the first call for an id in\n// this process, which has no baseline yet) IS the intentional change and\n// always wins — exactly the one-shot `pai worker goal` process itself.\nconst firstWrittenLabel = new Map<string, string>();\n\n/** Write status atomically (temp + rename) and stamp `updated`. */\nexport function saveStatus(logDir: string, status: WorkerStatus, d: Date = new Date()): void {\n status.updated = nowStamp(d);\n const path = statusPath(logDir, status.id);\n const baseline = firstWrittenLabel.get(status.id);\n if (baseline === undefined) {\n firstWrittenLabel.set(status.id, status.label);\n }\n const onDisk = baseline === undefined ? undefined : loadStatus(logDir, status.id);\n if (onDisk) {\n if (status.label === baseline && onDisk.label !== status.label) status.label = onDisk.label;\n // Set by another process (`pai worker controls`); a true flag is never dropped by a stale in-memory copy.\n if (onDisk.controlsHeld) status.controlsHeld = true;\n }\n const tmp = statusTmpPath(logDir, status.id);\n if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });\n try {\n writeFileSync(tmp, JSON.stringify(status), \"utf8\");\n renameSync(tmp, path);\n } catch (e) {\n try {\n rmSync(tmp, { force: true });\n } catch {\n // nothing to clean up\n }\n throw e;\n }\n}\n\n/**\n * Set a worker's goal (the operator's `--label`), atomically, through the\n * same saveStatus path everything else writes with. Used by `pai worker\n * goal` and the `--goal` option of `say`: a fresh call in a fresh process, so\n * it always carries no baseline yet and always wins over the worker's own\n * next periodic write (see saveStatus above).\n */\nexport function setWorkerLabel(logDir: string, id: string, label: string): WorkerStatus {\n const status = loadStatus(logDir, id);\n if (!status) throw new Error(`no worker named \"${id}\"`);\n status.label = label;\n saveStatus(logDir, status);\n return status;\n}\n\n/** Load every status file in the logDir, oldest id first, skipping damage. */\nexport function loadStatuses(logDir: string): WorkerStatus[] {\n if (!existsSync(logDir)) return [];\n const out: WorkerStatus[] = [];\n for (const name of readdirSync(logDir).sort()) {\n if (!name.endsWith(\".status\")) continue;\n try {\n out.push(JSON.parse(readFileSync(join(logDir, name), \"utf8\")) as WorkerStatus);\n } catch {\n // a half-written or damaged status file is not worth a crash\n }\n }\n return out;\n}\n\nexport function loadStatus(logDir: string, id: string): WorkerStatus | null {\n const path = statusPath(logDir, id);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as WorkerStatus;\n } catch {\n return null;\n }\n}\n\n/**\n * Wait for a worker to record its own terminal state (the run's signal\n * handler writes killed/rc/secs). Returns that status, or null when it is\n * still \"running\" after `timeoutMs` — the caller then writes the state itself.\n */\nexport async function waitForTerminalStatus(\n logDir: string,\n id: string,\n timeoutMs = 2000,\n stepMs = 50\n): Promise<WorkerStatus | null> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const s = loadStatus(logDir, id);\n if (s && s.state !== \"running\") return s;\n if (Date.now() >= deadline) return null;\n await new Promise((r) => setTimeout(r, stepMs));\n }\n}\n\nexport function alive(pid: number | null | undefined): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `status.pid` still names the process that was started as this\n * worker, not a later, unrelated process the OS handed the same pid to after\n * the worker exited. `alive()` alone can't tell these apart — a pid observed\n * live on 2026-09-19 (worker 20260919-083549-90913) had been reused, and\n * `pai worker kill` signalled the wrong, unrelated process. `ps -o lstart=`\n * reads the running process's actual start time and compares it against the\n * status file's own `started` stamp; a reused pid almost never starts within\n * 120s of the original, so a wider drift means \"different process\".\n */\nexport function ownsPid(status: Pick<WorkerStatus, \"pid\" | \"started\">): boolean {\n if (status.pid <= 0 || !alive(status.pid)) return false;\n try {\n const out = execFileSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(status.pid)], {\n encoding: \"utf8\",\n env: { ...process.env, LC_ALL: \"C\" },\n }).trim();\n const psStart = Date.parse(out);\n const started = Date.parse(status.started.replace(\" \", \"T\"));\n if (Number.isNaN(psStart) || Number.isNaN(started)) return alive(status.pid);\n return Math.abs(psStart - started) <= 120_000;\n } catch {\n // pid exited between the alive() check and the ps call, or ps failed for\n // another reason — the pid-existence check is still better than nothing.\n return alive(status.pid);\n }\n}\n\n/** Whether a status is both marked \"running\" and still owns its recorded pid. */\nexport function isLive(status: Pick<WorkerStatus, \"pid\" | \"started\" | \"state\">): boolean {\n return status.state === \"running\" && ownsPid(status);\n}\n\n/** \"42s\" under 90s, \"7m\" above — the coarse age the table and bar show. */\nexport function ageOf(ts: string, now: Date = new Date()): string {\n const t = Date.parse(ts.replace(\" \", \"T\"));\n if (Number.isNaN(t)) return \"?\";\n const s = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;\n}\n\n/** Precise elapsed time: \"45s\", \"1m 23s\", \"12m 05s\", \"1h 02m 05s\" — seconds zero-padded after a larger unit. */\nexport function elapsedOf(ts: string, now: Date = new Date()): string {\n const t = Date.parse(ts.replace(\" \", \"T\"));\n if (Number.isNaN(t)) return \"?\";\n const secs = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n const hours = Math.floor(secs / 3600);\n const mins = Math.floor((secs % 3600) / 60);\n const sec = secs % 60;\n if (hours > 0) return `${hours}h ${String(mins).padStart(2, \"0\")}m ${String(sec).padStart(2, \"0\")}s`;\n if (mins > 0) return `${mins}m ${String(sec).padStart(2, \"0\")}s`;\n return `${sec}s`;\n}\n\n/** Shorter sleeps are legitimate polling — not worth a supervision event. */\nexport const SLEEP_FLOOR_SECS = 60;\n\nconst SLEEP_RE = /^\\s*sleep\\s+([0-9]+(?:\\.[0-9]+)?)([smhd]?)\\s*(?:$|[;&|])/;\n\n/** s/m/h/d suffix to seconds multiplier, shared by every `sleep N<unit>` parse below. */\nfunction sleepUnitMultiplier(unit: string): number {\n return unit === \"m\" ? 60 : unit === \"h\" ? 3600 : unit === \"d\" ? 86_400 : 1;\n}\n\n/**\n * Seconds of a `sleep N` (N plain or with an s/m/h/d suffix) that stands alone\n * or leads a compound command (`sleep 150; cat f`, `sleep 2m && tail f`,\n * `sleep 90 || true`, `sleep 60 &`), or null when no long sleep leads: anchored\n * to the start of the command, so a filename containing \"sleep\" or an\n * `echo sleep 500` never trips it, and floored at SLEEP_FLOOR_SECS so ordinary\n * polling stays invisible.\n */\nexport function parseSleepSecs(command: string): number | null {\n const m = command.match(SLEEP_RE);\n if (!m) return null;\n const secs = Number(m[1]) * sleepUnitMultiplier(m[2]);\n return Number.isFinite(secs) && secs >= SLEEP_FLOOR_SECS ? secs : null;\n}\n\n/**\n * Every command-segment boundary a shell recognises before a new simple\n * command can start: the very start of the string, `;`, `&`/`&&`, `|`/`||`,\n * a newline, an opening `(` or `{` (also covers `$(...)`, which ends in `(`),\n * or the keywords `do`/`then`. Segment end is the mirror image plus a closing\n * `)`, so `sleep 90` inside `$(sleep 90)` or `(sleep 90)` still counts.\n */\n// The terminator is a lookahead, not a consumed match: two sleeps sharing one\n// separator (`sleep 65; sleep 590`) both need that `;` available as their own\n// boundary — matchAll never revisits a character a previous match consumed.\nconst SLEEP_SEGMENT_RE =\n /(?:^|[;&|\\n(){]|\\bdo\\b|\\bthen\\b)\\s*sleep\\s+([0-9]+(?:\\.[0-9]+)?)([smhd]?)(?=\\s*(?:$|[;&|)\\n]))/g;\n\n/**\n * The longest `sleep N` found as its OWN command segment anywhere in\n * `command` — not just leading it, unlike {@link parseSleepSecs} — or null\n * when none reaches SLEEP_FLOOR_SECS. Used by the PreToolUse Bash gate\n * (block-sleep-poll.ts), which must catch a sleep buried after `&&`, inside a\n * `do ... done` loop, or in a subshell, not only one that leads the command.\n */\nexport function longestSleepSecs(command: string): number | null {\n let longest: number | null = null;\n for (const m of command.matchAll(SLEEP_SEGMENT_RE)) {\n const secs = Number(m[1]) * sleepUnitMultiplier(m[2]);\n if (!Number.isFinite(secs) || secs < SLEEP_FLOOR_SECS) continue;\n if (longest === null || secs > longest) longest = secs;\n }\n return longest;\n}\n\nconst LOOP_RE = /\\b(for|while|until)\\b([^;\\n]*)[;\\n]\\s*do\\b([\\s\\S]*?)\\bdone\\b/g;\n\nfunction sleepSum(text: string): number {\n let sum = 0;\n for (const m of text.matchAll(SLEEP_SEGMENT_RE)) {\n const secs = Number(m[1]) * sleepUnitMultiplier(m[2]);\n if (Number.isFinite(secs)) sum += secs;\n }\n return sum;\n}\n\n/** Iterations of a `for x in <words>` header, or null when not provably small. */\nfunction forIterations(header: string): number | null {\n const words = header.replace(/^\\s*\\w+\\s+in\\s+/, \"\").trim();\n if (words === header.trim() || /[$`*?]/.test(words)) return null;\n const range = /^\\{(-?\\d+)\\.\\.(-?\\d+)\\}$/.exec(words);\n if (range) return Math.abs(Number(range[2]) - Number(range[1])) + 1;\n return words ? words.split(/\\s+/).length : 0;\n}\n\n/**\n * Total sleep of one Bash command: every `sleep N` segment counted once, plus\n * loop repetition — `for x in a b c` multiplies its body by the word count;\n * `while`/`until` and any unbounded `for` count as at least SLEEP_FLOOR_SECS,\n * except a `kill -0` job-wait (the sanctioned wait-for-a-job loop). Used by\n * the PreToolUse gate so `sleep 58; sleep 58` is caught as the 116 s it is.\n */\n// ponytail: loops are not nested-aware (body ends at the first `done`); upgrade to a real shell parse if that bites.\nexport function totalSleepSecs(command: string): number {\n let total = sleepSum(command);\n for (const m of command.matchAll(LOOP_RE)) {\n const body = sleepSum(m[3]);\n if (body === 0) continue;\n if (m[1] === \"for\") {\n const n = forIterations(m[2]);\n total += n === null ? Math.max(SLEEP_FLOOR_SECS, body) : body * (n - 1);\n } else if (!/\\bkill\\s+-0\\b/.test(m[2])) {\n total += SLEEP_FLOOR_SECS;\n }\n }\n return total;\n}\n\n/** One-line description of a tool call, e.g. \"Bash: npm test\". */\nexport function describeTool(name: string, inp: unknown): string {\n if (typeof inp !== \"object\" || inp === null) return name;\n const i = inp as Record<string, unknown>;\n const get = (k: string) => (typeof i[k] === \"string\" ? (i[k] as string) : \"\");\n if (name === \"Bash\") return `Bash: ${shortText(get(\"command\"), 70)}`;\n if (name === \"Read\" || name === \"Edit\" || name === \"Write\" || name === \"MultiEdit\") {\n const file = get(\"file_path\").split(\"/\").pop() ?? \"\";\n return `${name}: ${file}`;\n }\n if (name === \"Grep\" || name === \"Glob\") return `${name}: ${shortText(get(\"pattern\"), 50)}`;\n if (name === \"WebSearch\") return `${name}: ${shortText(get(\"query\"), 50)}`;\n if (name === \"WebFetch\") return `${name}: ${shortText(get(\"url\"), 60)}`;\n return name;\n}\n","/**\n * ledger.ts — the routing ledger, one append-only log for every worker event.\n *\n * Line shapes (whitespace-aligned so `tail` reads as a table):\n *\n * 2026-09-17 12:00:00 WORKER-START id=<id> provider=<p> mode=headless model=<m> cwd=<cwd> label=<label>\n * 2026-09-17 12:01:00 WORKER-END id=<id> provider=<p> mode=headless model=<m> rc=0 secs=60 turns=3 tools=8 label=<label>\n * 2026-09-17 12:00:20 WORKER-REROUTE from=<a> to=<b> reason=quota\n * 2026-09-17 12:00:00 DENIED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>\n * 2026-09-17 12:00:00 ALLOWED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>\n *\n * `pai worker log` (and glm-log before it) counts these; keep the tags stable.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport function ledgerStamp(d: Date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +\n `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n );\n}\n\nexport interface LedgerLine {\n stamp: string;\n event: string;\n fields: Record<string, string>;\n}\n\n/** Parse `2026-09-17 12:00:00 TAG key=value …` into its parts. */\nexport function parseLedgerLine(line: string): LedgerLine | null {\n const m = line.match(/^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) (\\S+)(?: (.*))?$/);\n if (!m) return null;\n const fields: Record<string, string> = {};\n // values are whitespace-collapsed by the writers, so spaces safely delimit\n for (const part of (m[3] ?? \"\").split(\" \")) {\n if (!part) continue;\n const eq = part.indexOf(\"=\");\n if (eq <= 0) continue;\n fields[part.slice(0, eq)] = part.slice(eq + 1);\n }\n return { stamp: m[1], event: m[2], fields };\n}\n\nexport function parseLedger(text: string): LedgerLine[] {\n const out: LedgerLine[] = [];\n for (const line of text.split(\"\\n\")) {\n if (!line.trim()) continue;\n const parsed = parseLedgerLine(line);\n if (parsed) out.push(parsed);\n }\n return out;\n}\n\n/** Append one ledger line. `kv` order is the caller's; values are flattened. */\nexport function appendLedger(\n path: string,\n event: string,\n kv: Record<string, string | number | null | undefined>,\n now: Date = new Date()\n): void {\n const parts: string[] = [];\n for (const [k, v] of Object.entries(kv)) {\n if (v === undefined || v === null) continue;\n parts.push(`${k}=${String(v).replace(/\\s+/g, \" \")}`);\n }\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n const tail = parts.length ? \" \" + parts.join(\" \") : \"\";\n appendFileSync(path, `${ledgerStamp(now)} ${event}${tail}\\n`, \"utf8\");\n}\n\nexport interface LedgerSummary {\n scope: string;\n started: number;\n endedOk: number;\n endedFailed: number;\n denied: number;\n allowed: number;\n reroutes: number;\n lastLines: string[];\n}\n\n/** The counts `pai worker log` prints, over today's lines or the whole file. */\nexport function ledgerSummary(\n path: string,\n scope: \"today\" | \"all\",\n lastN = 15,\n now: Date = new Date()\n): LedgerSummary | null {\n if (!existsSync(path)) return null;\n const text = readFileSync(path, \"utf8\");\n const today = ledgerStamp(now).slice(0, 10);\n const lines = text.split(\"\\n\").filter((l) => l.trim());\n const scoped =\n scope === \"all\" ? lines : lines.filter((l) => l.startsWith(today));\n const parsed = scoped.map(parseLedgerLine).filter((x): x is LedgerLine => x !== null);\n const count = (ev: string) => parsed.filter((p) => p.event === ev).length;\n const ends = parsed.filter((p) => p.event === \"WORKER-END\");\n const ok = ends.filter((p) => p.fields.rc === \"0\").length;\n return {\n scope: scope === \"all\" ? \"all time\" : `today ${today}`,\n started: count(\"WORKER-START\"),\n endedOk: ok,\n endedFailed: ends.length - ok,\n denied: count(\"DENIED-ANTHROPIC-AGENT\"),\n allowed: count(\"ALLOWED-ANTHROPIC-AGENT\"),\n reroutes: count(\"WORKER-REROUTE\"),\n lastLines: lines.slice(-lastN),\n };\n}\n","/**\n * Pending worker results: finished workers whose worktree branch still holds\n * work that never reached the repo (commits not in HEAD, or uncommitted\n * files). Surfaced by `pai worker ps/pending`, the SessionStart hook, the\n * status line and the prepublish gate so an unmerged fix cannot be forgotten.\n */\n\nimport { existsSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { isLive, loadStatuses } from \"./status.js\";\nimport { statusPath } from \"./paths.js\";\nimport { git, uncommittedPaths } from \"./worktree.js\";\n\nexport interface PendingResult {\n id: string;\n label: string;\n repo: string;\n /** ms since the worker's status file last changed (its end). */\n ageMs: number;\n age: string;\n /** commits on the branch not in the repo's HEAD (patch-id aware). */\n commits: number;\n /** uncommitted paths in the worktree, node_modules excluded. */\n dirty: number;\n}\n\n/** Stale after this long: `pai worker gc` (archive) instead of merge. */\nexport const STALE_HOURS = 24;\n\nfunction toplevel(dir: string): string | null {\n try {\n return git(dir, [\"rev-parse\", \"--show-toplevel\"]);\n } catch {\n return null;\n }\n}\n\nfunction fmtAge(ms: number): string {\n const h = Math.floor(ms / 3_600_000);\n return h >= 48 ? `${Math.floor(h / 24)}d` : h >= 1 ? `${h}h` : `${Math.floor(ms / 60_000)}m`;\n}\n\n/** Finished workers with unmerged work; `repoRoot` limits to one repo, else every repo. */\nexport function pendingResults(logDir: string, repoRoot?: string, now = Date.now()): PendingResult[] {\n const want = repoRoot ? toplevel(repoRoot) : null;\n if (repoRoot && !want) return [];\n const out: PendingResult[] = [];\n for (const st of loadStatuses(logDir)) {\n if (st.archived || st.merged || isLive(st)) continue;\n if (!st.worktreeDir || !existsSync(st.worktreeDir)) continue;\n const repo = toplevel(st.cwd);\n if (!repo || (want && repo !== want)) continue;\n let commits = 0;\n let dirty = 0;\n try {\n if (st.branch) {\n commits = git(repo, [\"cherry\", \"HEAD\", `refs/heads/${st.branch}`])\n .split(\"\\n\")\n .filter((l) => l.startsWith(\"+\")).length;\n }\n dirty = uncommittedPaths(st.worktreeDir).filter((p) => !/(^|\\/)node_modules(\\/|$)/.test(p)).length;\n } catch {\n continue; // branch or worktree unreadable: nothing verifiable to report\n }\n if (!commits && !dirty) continue;\n let ageMs = 0;\n try {\n ageMs = Math.max(0, now - statSync(statusPath(logDir, st.id)).mtimeMs);\n } catch {\n // status vanished mid-scan: age unknown, keep the entry\n }\n out.push({ id: st.id, label: st.label, repo, ageMs, age: fmtAge(ageMs), commits, dirty });\n }\n return out;\n}\n\n/** One human line per result, with the exact next command. */\nexport function formatPending(results: PendingResult[], showRepo = false): string {\n return results\n .map((r) => {\n const next = r.ageMs > STALE_HOURS * 3_600_000 ? \"pai worker gc\" : `pai worker merge ${r.id}`;\n const repo = showRepo ? ` ${r.repo}` : \"\";\n return `${r.id} ${r.age} ${r.commits} commit(s), ${r.dirty} dirty ${r.label}${repo} -> ${next}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * Release gate: exit code and message for `pai worker pending --gate`.\n * PAI_ALLOW_PENDING=1 overrides.\n */\nexport function pendingGate(\n results: PendingResult[],\n env: NodeJS.ProcessEnv = process.env\n): { code: number; message: string } {\n if (!results.length || env.PAI_ALLOW_PENDING === \"1\") return { code: 0, message: \"\" };\n return {\n code: 1,\n message:\n `${results.length} unmerged worker result(s) in this repo — merge or discard before publishing:\\n` +\n `${formatPending(results)}\\n` +\n \"Override deliberately with PAI_ALLOW_PENDING=1.\",\n };\n}\n\n/** Cached count of pending results for `cwd`'s repo (statusline/hook: cheap on repeat). */\nexport function pendingCountCached(logDir: string, cwd: string, ttlMs = 60_000, now = Date.now()): number {\n const file = join(logDir, \"pending-cache.json\");\n let cache: Record<string, { at: number; n: number }> = {};\n try {\n cache = JSON.parse(readFileSync(file, \"utf8\"));\n } catch {\n // no cache yet\n }\n const hit = cache[cwd];\n if (hit && now - hit.at < ttlMs) return hit.n;\n const n = pendingResults(logDir, cwd, now).length;\n try {\n writeFileSync(file, JSON.stringify({ ...cache, [cwd]: { at: now, n } }));\n } catch {\n // read-only log dir: recompute next time\n }\n return n;\n}\n\n/** Invalidate pending cache entries for a repo and its subdirectories. */\nexport function invalidatePendingCache(logDir: string, repoRoot: string): void {\n const file = join(logDir, \"pending-cache.json\");\n let cache: Record<string, { at: number; n: number }> = {};\n try {\n cache = JSON.parse(readFileSync(file, \"utf8\"));\n } catch {\n // no cache yet\n return;\n }\n const resolved = repoRoot.endsWith(\"/\") ? repoRoot : `${repoRoot}/`;\n let changed = false;\n for (const key of Object.keys(cache)) {\n if (key === repoRoot || key.startsWith(resolved)) {\n delete cache[key];\n changed = true;\n }\n }\n if (!changed) return;\n try {\n writeFileSync(file, JSON.stringify(cache));\n } catch {\n // read-only log dir: best effort\n }\n}\n","/**\n * worktree.ts — one git worktree per writing worker.\n *\n * A run whose class edits files (implement, complex, plan) and whose cwd is a\n * git repository gets its own worktree by default: `git worktree add\n * <logDir>/worktrees/<id> -b worker/<id>` from the current HEAD. The worker\n * commits its own work on that branch — the no-commit rule applies to the\n * main branch only — and the parent (or the operator) merges the result back:\n *\n * pai worker merge <id> git merge --no-ff worker/<id> + remove worktree + delete branch\n * pai worker discard <id> remove worktree and branch, keep nothing\n *\n * `ps` marks a worker with an unmerged branch `⎇`. Draft and review run in\n * place; `--no-worktree` opts out, `--worktree` forces one on.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, lstatSync, mkdirSync, readFileSync, mkdtempSync, writeFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve, sep } from \"node:path\";\nimport { tmpdir, platform } from \"node:os\";\nimport { isLive, loadStatus, loadStatuses, saveStatus, UNLABELED, type WorkerStatus } from \"./status.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, statusPath } from \"./paths.js\";\nimport { invalidatePendingCache } from \"./pending.js\";\n\nexport function worktreesDir(logDir: string): string {\n return join(logDir, \"worktrees\");\n}\n\nexport function worktreeBranch(id: string): string {\n return `worker/${id}`;\n}\n\nexport function worktreePath(logDir: string, id: string): string {\n return join(worktreesDir(logDir), id);\n}\n\n/** Run git in `cwd` with an optional env override, trimmed stdout, stderr on failure. */\nfunction runGit(cwd: string, args: string[], env?: NodeJS.ProcessEnv, timeout = 30_000): string {\n try {\n return execFileSync(\"git\", [\"-C\", cwd, ...args], {\n encoding: \"utf8\",\n timeout,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n ...(env ? { env } : {}),\n }).trim();\n } catch (e) {\n const err = e as { stderr?: Buffer | string; message?: string };\n const why =\n (typeof err.stderr === \"string\" ? err.stderr : err.stderr?.toString(\"utf8\")) ||\n err.message ||\n String(e);\n throw new Error(`git ${args.join(\" \")} in ${cwd}: ${why.trim()}`);\n }\n}\n\n/** Run git in `cwd`, returning trimmed stdout; throws with stderr on failure. */\nexport function git(cwd: string, args: string[], timeoutMs?: number): string {\n return runGit(cwd, args, undefined, timeoutMs);\n}\n\n/** Is `cwd` inside a git repository (a .git dir — worktrees: a .git file)? */\nexport function isGitRepo(cwd: string): boolean {\n try {\n return git(cwd, [\"rev-parse\", \"--git-dir\"]) !== \"\";\n } catch {\n return false;\n }\n}\n\n/** Tools that can write to the checkout (Bash can, via the shell). */\nconst WRITING_TOOLS = [\"Edit\", \"Write\", \"MultiEdit\", \"NotebookEdit\", \"Bash\"];\n\n/** Can a run with these --allowedTools write? No restriction at all means yes. */\nexport function toolsCanWrite(allowedTools: string[]): boolean {\n const names = allowedTools.flatMap((e) => e.split(\",\")).map((s) => s.trim().replace(/\\(.*$/, \"\")).filter(Boolean);\n return !names.length || names.some((n) => WRITING_TOOLS.includes(n));\n}\n\n/** How the run flags decide the worktree question; undefined = decide by default. */\nexport type WorktreeFlag = boolean | undefined;\n\n/** Should this run get a worktree? Explicit flag first, then: can it write? */\nexport function worktreeWanted(flag: WorktreeFlag, opts: { cwd: string; allowedTools: string[] }): boolean {\n if (flag !== undefined) return flag;\n return toolsCanWrite(opts.allowedTools) && isGitRepo(opts.cwd);\n}\n\nexport interface WorktreeInfo {\n dir: string;\n branch: string;\n base: string;\n /** `base` is a snapshot commit of uncommitted checkout state, not HEAD itself. */\n snapshot: boolean;\n}\n\n/**\n * Commit `cwd`'s current index + working tree (including untracked files) as\n * a floating commit on top of HEAD, without touching the real index, working\n * tree or HEAD: a temporary `GIT_INDEX_FILE` collects the snapshot, `git\n * commit-tree` writes the commit object directly (it updates no ref). The\n * caller then branches a worktree from that commit, so the worktree sees\n * exactly what was on disk, uncommitted or not.\n */\nfunction snapshotUncommitted(cwd: string, id: string, head: string): string {\n const idxDir = mkdtempSync(join(tmpdir(), \"pai-worktree-idx-\"));\n const idx = join(idxDir, \"index\");\n try {\n const env = { ...process.env, GIT_INDEX_FILE: idx };\n runGit(cwd, [\"read-tree\", head], env);\n runGit(cwd, [\"add\", \"-A\"], env);\n const tree = runGit(cwd, [\"write-tree\"], env);\n return runGit(cwd, [\n \"commit-tree\",\n tree,\n \"-p\",\n head,\n \"-m\",\n `worker ${id}: snapshot of uncommitted checkout`,\n ]);\n } finally {\n rmSync(idxDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Create the worktree and branch for `id` from `cwd`'s HEAD — or, when the\n * checkout has uncommitted changes, from a snapshot commit of that dirty\n * state (see `snapshotUncommitted`), so a worktree worker sees the same\n * files the operator sees, not just what was last committed. Throws when git\n * refuses (no commits yet, branch exists, …) — the caller decides whether to\n * degrade to an in-place run.\n */\nexport function addWorktree(logDir: string, id: string, cwd: string): WorktreeInfo {\n const dir = worktreePath(logDir, id);\n const branch = worktreeBranch(id);\n const head = git(cwd, [\"rev-parse\", \"HEAD\"]);\n const dirty = dirtyPaths(cwd).length > 0;\n const base = dirty ? snapshotUncommitted(cwd, id, head) : head;\n git(cwd, [\"worktree\", \"add\", dir, \"-b\", branch, base]);\n provisionDeps(logDir, id, cwd, dir);\n return { dir, branch, base, snapshot: dirty };\n}\n\n/**\n * Give the worktree its own dependencies so the worker never has to improvise\n * (symlinking node_modules to the main checkout, building in a /tmp copy).\n * A real node_modules directory in the main checkout is APFS-cloned\n * (`cp -c`, copy-on-write, near instant); elsewhere the lockfile installs it.\n * Never a symlink. Time-boxed and best effort: a failure is logged, the run\n * goes on.\n */\nexport function provisionDeps(logDir: string, id: string, cwd: string, wtDir: string): void {\n const note = (n: string) => appendLedger(ledgerPath(logDir), \"WORKER-NOTE\", { id, note: n });\n try {\n const root = git(cwd, [\"rev-parse\", \"--show-toplevel\"]);\n const main = join(root, \"node_modules\");\n const dest = join(wtDir, \"node_modules\");\n if (!existsSync(main) || lstatSync(main).isSymbolicLink() || existsSync(dest)) return;\n try {\n // an unignored node_modules would be committed by salvage; the trailing\n // slash makes git test a directory (the path does not exist yet), so a\n // dir-only `node_modules/` rule matches too\n git(wtDir, [\"check-ignore\", \"-q\", \"node_modules/\"]);\n } catch {\n return;\n }\n const t0 = Date.now();\n const opts = { cwd: wtDir, stdio: \"ignore\" as const, timeout: 180_000 };\n if (platform() === \"darwin\") {\n try {\n execFileSync(\"cp\", [\"-cR\", main, dest], opts);\n note(`node_modules cloned (apfs) in ${Date.now() - t0}ms`);\n return;\n } catch {\n rmSync(dest, { recursive: true, force: true }); // half-copied\n }\n }\n if (existsSync(join(wtDir, \"bun.lock\"))) execFileSync(\"bun\", [\"install\", \"--frozen-lockfile\"], opts);\n else if (existsSync(join(wtDir, \"package-lock.json\"))) execFileSync(\"npm\", [\"ci\"], opts);\n else return;\n note(`node_modules installed from lockfile in ${Date.now() - t0}ms`);\n } catch (e) {\n try {\n note(`node_modules provisioning failed: ${(e as Error).message}`);\n } catch {\n /* ledger is best effort too */\n }\n }\n}\n\n/** Commits the branch collected on top of its base. */\nexport function commitsSince(dir: string, base: string): number {\n try {\n return parseInt(git(dir, [\"rev-list\", \"--count\", `${base}..HEAD`]), 10) || 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Record the worktree result in the status file: branch and commit count on\n * success, the worktree cleaned up on failure. Returns the updated status.\n */\nexport function recordWorktree(\n logDir: string,\n status: WorkerStatus,\n info: WorktreeInfo,\n ok: boolean\n): WorkerStatus {\n const s = { ...status };\n if (ok) {\n s.branch = info.branch;\n s.commits = commitsSince(info.dir, info.base);\n s.worktreeDir = info.dir;\n s.worktreeBase = info.base;\n s.worktreeSnapshot = info.snapshot;\n } else if (safeCommitsSince(info.dir, info.base) > 0) {\n // a failed or killed run that committed (or was salvaged) keeps its work:\n // branch and worktree stay for `pai worker merge` / `discard`\n s.branch = info.branch;\n s.commits = commitsSince(info.dir, info.base);\n s.worktreeDir = info.dir;\n s.worktreeBase = info.base;\n s.worktreeSnapshot = info.snapshot;\n } else {\n // a failed run with no commits leaves nothing to merge; the branch dies with the worktree\n removeWorktree(s.cwd, info.dir, true);\n try {\n git(s.cwd, [\"branch\", \"-D\", info.branch]);\n } catch {\n // already gone or never created\n }\n s.branch = null;\n s.worktreeDir = null;\n s.worktreeBase = null;\n s.commits = null;\n s.worktreeSnapshot = null;\n }\n saveStatus(logDir, s);\n invalidatePendingCache(logDir, s.cwd);\n return s;\n}\n\nfunction safeCommitsSince(dir: string, base: string): number {\n try {\n return commitsSince(dir, base);\n } catch {\n return 0;\n }\n}\n\n/**\n * Salvage for an exit path (run finaliser, kill signal): commit whatever the\n * worker left uncommitted, log WORKER-SALVAGE, never throw. Bounded by\n * `timeoutMs` per git call. Returns the committed paths.\n */\nexport function salvageOnExit(\n ledger: string,\n id: string,\n label: string,\n wtDir: string,\n timeoutMs?: number\n): string[] {\n try {\n const { committed, skipped } = salvageUncommitted(wtDir, label, timeoutMs);\n if (committed.length) appendLedger(ledger, \"WORKER-SALVAGE\", { id, paths: committed.join(\",\"), skipped: skipped.length });\n if (skipped.length) appendLedger(ledger, \"WORKER-NOTE\", { id, note: `salvage skipped: ${skipped.join(\"; \")}` });\n return committed;\n } catch (e) {\n try {\n appendLedger(ledger, \"WORKER-NOTE\", { id, note: `salvage failed: ${(e as Error).message}` });\n } catch {\n /* best effort */\n }\n return [];\n }\n}\n\n/** Remove a worktree directory from git's books and the filesystem. */\nfunction removeWorktree(cwd: string, dir: string, force: boolean): void {\n try {\n git(cwd, [\"worktree\", \"remove\", ...(force ? [\"--force\"] : []), dir]);\n return;\n } catch {\n // fall through to the manual cleanup\n }\n if (existsSync(dir)) {\n try {\n rmSync(dir, { recursive: true, force: true });\n git(cwd, [\"worktree\", \"prune\"]);\n } catch {\n // best effort: a leftover directory is visible in the logDir\n }\n }\n}\n\n/**\n * Tracked changes (staged or not) plus untracked files — everything a\n * `git add -A` in `wtDir` would commit. The same two calls the old patch\n * carry used; name-only, not porcelain: a worktree-only change renders as\n * \" M path\" and the shared git() helper trims that leading space away.\n */\nexport function uncommittedPaths(wtDir: string, timeoutMs?: number): string[] {\n const tracked = git(wtDir, [\"diff\", \"--name-only\", \"HEAD\"], timeoutMs).split(\"\\n\").filter(Boolean);\n const untracked = git(wtDir, [\"ls-files\", \"--others\", \"--exclude-standard\"], timeoutMs)\n .split(\"\\n\")\n .filter(Boolean);\n return [...tracked, ...untracked];\n}\n\n/**\n * True when `wtDir`/`relPath` is a symlink whose target resolves outside\n * `wtDir` — e.g. a worker's `node_modules -> <main checkout>/node_modules`\n * convenience link. A `.gitignore` using `dir/` (directories only) does not\n * ignore such a link, so it turns up as untracked; staging and merging it\n * lets git replace the real directory on the other side with a symlink to\n * itself. Never a candidate for salvage.\n */\nfunction symlinkEscapesWorktree(wtDir: string, relPath: string): boolean {\n const full = join(wtDir, relPath);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n return false; // vanished; nothing to skip\n }\n if (!stat.isSymbolicLink()) return false;\n const target = readlinkSync(full);\n const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(full), target);\n const realTarget = existsSync(resolvedTarget) ? realpathSync(resolvedTarget) : resolvedTarget;\n const realWtDir = realpathSync(wtDir);\n return realTarget !== realWtDir && !realTarget.startsWith(realWtDir + sep);\n}\n\nexport interface SalvageResult {\n /** Paths committed onto the branch. */\n committed: string[];\n /** Human-readable notes for paths left on disk, uncommitted, and why. */\n skipped: string[];\n}\n\n/**\n * Commit a worktree's uncommitted changes to its branch so the merge carries\n * them. `git merge` only moves committed work, so a worker that stopped\n * without committing would lose its edits to `worktree remove` — exactly\n * what happened live on 2026-09-17. Returns the salvaged paths, empty when\n * the worktree is clean. A symlink pointing outside the worktree (see\n * `symlinkEscapesWorktree`) is never staged; it is left on disk and reported\n * as skipped instead. A failed commit throws with the worktree untouched:\n * its edits are still on disk, so nothing is lost.\n */\nexport function salvageUncommitted(wtDir: string, label: string, timeoutMs?: number): SalvageResult {\n const paths = uncommittedPaths(wtDir, timeoutMs);\n if (!paths.length) return { committed: [], skipped: [] };\n const skipped: string[] = [];\n // tracked files gone from disk but not staged for removal: the worker never\n // ran `git rm`, so the absence is not its decision (see MAX_SALVAGE_DELETES)\n const missing = new Set(git(wtDir, [\"ls-files\", \"--deleted\"], timeoutMs).split(\"\\n\").filter(Boolean));\n const toStage: string[] = [];\n for (const p of paths) {\n if (missing.has(p)) {\n skipped.push(`${p} (missing, not committed)`);\n } else if (symlinkEscapesWorktree(wtDir, p)) {\n skipped.push(`symlink ${p} -> ${readlinkSync(join(wtDir, p))} (points outside the worktree)`);\n } else if (p === \"node_modules\" || p.startsWith(\"node_modules/\")) {\n skipped.push(`${p} (node_modules is never committed)`);\n } else {\n toStage.push(p);\n }\n }\n if (!toStage.length) return { committed: [], skipped };\n try {\n // --ignore-removal: additions and modifications only. Deletions the worker\n // staged itself (git rm) are already in the index and ride along.\n const excludes = paths.filter((p) => !toStage.includes(p)).map((p) => `:(exclude,literal)${p}`);\n git(wtDir, [\"add\", \"--ignore-removal\", \"--\", \".\", ...excludes], timeoutMs);\n const deleted = git(wtDir, [\"diff\", \"--cached\", \"--diff-filter=D\", \"--name-only\"], timeoutMs).split(\"\\n\").filter(Boolean);\n if (deleted.length > MAX_SALVAGE_DELETES) {\n git(wtDir, [\"reset\", \"-q\"], timeoutMs);\n throw new Error(`salvage would delete ${deleted.length} tracked files (${deleted.slice(0, 5).join(\", \")}, ...), refused`);\n }\n git(wtDir, [\"commit\", \"-m\", `salvaged: ${label}`], timeoutMs);\n } catch (e) {\n throw new Error(\n `cannot salvage the uncommitted changes in ${wtDir} — ${(e as Error).message}; ` +\n `nothing was lost: commit them there by hand, then re-run merge`\n );\n }\n return { committed: toStage, skipped };\n}\n\n/**\n * Refuse a merge whose branch adds or changes a top-level `node_modules` of\n * any type (a worker's own commit of its symlink included): merging it would\n * replace the checkout's real directory. Both merge paths call this.\n */\nexport function assertNoNodeModules(cwd: string, from: string, branch: string, id: string): void {\n if (git(cwd, [\"diff\", \"--name-only\", from, branch, \"--\", \"node_modules\"])) {\n throw new Error(\n `worker ${id}: branch ${branch} commits node_modules, which would replace the checkout's real directory. ` +\n `Remove it on the branch (git rm -r --cached node_modules, amend), then re-run merge`\n );\n }\n}\n\n/** A salvage commit that deletes more tracked files than this is refused. */\nconst MAX_SALVAGE_DELETES = 5;\n\n/**\n * The dirty paths of a checkout, parsed from `git status --porcelain -z`:\n * NUL-separated (a path with a newline in it cannot corrupt the parse),\n * rename entries contribute both sides, and any quoting is stripped.\n */\nfunction dirtyPaths(cwd: string): string[] {\n // raw execFileSync, not the shared git(): it trims stdout, which eats the\n // leading space of a worktree-only \" M path\" record and breaks the parse\n const raw = execFileSync(\"git\", [\"-C\", cwd, \"status\", \"--porcelain\", \"-z\"], {\n encoding: \"utf8\",\n timeout: 30_000,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n const fields = raw.split(\"\\0\");\n const strip = (p: string) => (p.startsWith('\"') && p.endsWith('\"') ? p.slice(1, -1) : p);\n const out: string[] = [];\n for (let i = 0; i < fields.length; i++) {\n const f = fields[i];\n if (!f || f.length < 4 || f.charAt(2) !== \" \") continue; // not an \"XY path\" record\n out.push(strip(f.slice(3)));\n const xy = f.slice(0, 2);\n if (xy.includes(\"R\") || xy.includes(\"C\")) {\n const orig = fields[i + 1]; // rename/copy records carry the source path next\n if (orig && orig.charAt(2) !== \" \") {\n out.push(strip(orig));\n i += 1;\n }\n }\n }\n return out;\n}\n\n/**\n * Refuse the merge when the original checkout is dirty in paths the branch\n * touches: the merge would overwrite those edits or fail on them, either way\n * leaving a half-state. No merge is made, the worktree stays.\n */\nfunction assertNoDirtyOverlap(\n cwd: string,\n incoming: string[],\n id: string,\n branch: string\n): void {\n const dirty = new Set(dirtyPaths(cwd));\n const overlap = [...new Set(incoming)].filter((p) => dirty.has(p)).sort();\n if (!overlap.length) return;\n throw new Error(\n `worker ${id}: the checkout ${cwd} has uncommitted changes in paths ${branch} touches: ` +\n `${overlap.join(\", \")}. Commit or stash them in the checkout, then re-run: pai worker merge ${id}. ` +\n `No merge was made; the worktree was kept.`\n );\n}\n\n/**\n * Never remove a worktree that still holds uncommitted changes — except a\n * symlink escaping the worktree (see `symlinkEscapesWorktree`): it was\n * deliberately left uncommitted by `salvageUncommitted`, and deleting the\n * link itself touches nothing outside the worktree.\n */\nexport function assertWorktreeClean(wtDir: string, id: string): void {\n const leftover = uncommittedPaths(wtDir).filter((p) => !symlinkEscapesWorktree(wtDir, p));\n if (leftover.length) {\n throw new Error(\n `worker ${id}: the worktree ${wtDir} still holds uncommitted changes ` +\n `(${leftover.join(\", \")}) — it was NOT removed; commit or copy them by hand, then re-run merge`\n );\n }\n}\n\n/** Blob sha of `path` at `rev` in `cwd`, or null when it does not exist there. */\nfunction blobAt(cwd: string, rev: string, path: string): string | null {\n try {\n return git(cwd, [\"rev-parse\", \"--verify\", \"-q\", `${rev}:${path}`]);\n } catch {\n return null;\n }\n}\n\n/** Blob sha of `path` as it sits on disk in `cwd`, or null when it is absent. */\nfunction workingBlob(cwd: string, path: string): string | null {\n if (!existsSync(join(cwd, path))) return null;\n try {\n return git(cwd, [\"hash-object\", \"--\", path]);\n } catch {\n return null;\n }\n}\n\n/**\n * Refuse a snapshot merge when the checkout differs from the snapshot the\n * worker branched from, for any path the branch touched: the operator edited\n * it, or it was created/deleted since — applying the branch's diff on top\n * would silently discard that. Absent-in-both counts as equal.\n */\nfunction assertNoSnapshotDrift(cwd: string, snapshot: string, paths: string[], id: string): void {\n const drifted = paths\n .filter((p) => blobAt(cwd, snapshot, p) !== workingBlob(cwd, p))\n .sort();\n if (!drifted.length) return;\n throw new Error(\n `worker ${id}: the checkout ${cwd} has changed since the worktree's snapshot in paths the branch touches: ` +\n `${drifted.join(\", \")}. Resolve by hand, then re-run: pai worker merge ${id}. ` +\n `No merge was made; the worktree was kept.`\n );\n}\n\n/**\n * `pai worker merge <id>` for a snapshot-based worktree (see `addWorktree`):\n * git's own merge only moves committed history, and the snapshot commit was\n * never on any branch reachable from `cwd`'s HEAD, so there is nothing to\n * `git merge` here. Instead the worker's own changes — `git diff --binary\n * <snapshot> <branch>` — are applied straight onto the checkout's working\n * tree as uncommitted edits, exactly like the uncommitted state the worker\n * started from. Works even if `cwd`'s HEAD moved since the snapshot: the\n * diff is applied to files, not merged into a tree.\n */\nfunction mergeSnapshotWorker(\n logDir: string,\n id: string,\n st: WorkerStatus & Required<Pick<WorkerStatus, \"branch\" | \"worktreeDir\">>,\n noCommit = false\n): string {\n const snapshot = st.worktreeBase!;\n const branch = st.branch!;\n const worktreeDir = st.worktreeDir!;\n const salvage = existsSync(worktreeDir)\n ? salvageUncommitted(worktreeDir, st.label || UNLABELED)\n : { committed: [], skipped: [] };\n const incoming = parseInt(git(st.cwd, [\"rev-list\", \"--count\", `${snapshot}..${branch}`]), 10) || 0;\n if (incoming <= 0) {\n throw new Error(\n `worker ${id}: branch ${branch} has no commits beyond its snapshot to merge. ` +\n `The worktree ${worktreeDir} was NOT removed — uncommitted work there would be destroyed. ` +\n `Commit it yourself, or drop everything with: pai worker discard ${id}`\n );\n }\n const changedPaths = git(st.cwd, [\"diff\", \"--name-only\", snapshot, branch]).split(\"\\n\").filter(Boolean);\n assertNoNodeModules(st.cwd, snapshot, branch, id);\n assertNoSnapshotDrift(st.cwd, snapshot, changedPaths, id);\n const diff = execFileSync(\"git\", [\"-C\", st.cwd, \"diff\", \"--binary\", snapshot, branch], {\n encoding: \"utf8\",\n timeout: 30_000,\n maxBuffer: 64 * 1024 * 1024,\n });\n if (diff.trim()) {\n try {\n execFileSync(\"git\", [\"-C\", st.cwd, \"apply\", \"--binary\"], {\n input: diff,\n encoding: \"utf8\",\n timeout: 30_000,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n } catch (e) {\n const err = e as { stderr?: Buffer | string; message?: string };\n const why =\n (typeof err.stderr === \"string\" ? err.stderr : err.stderr?.toString(\"utf8\")) ||\n err.message ||\n String(e);\n throw new Error(\n `worker ${id}: git refused to apply ${branch}'s changes to ${st.cwd} — ${why.trim()}. ` +\n `The worktree ${worktreeDir} was kept: resolve by hand, then re-run merge`\n );\n }\n }\n if (noCommit) return noCommitNextSteps(id, branch, worktreeDir, changedPaths);\n removeWorktree(st.cwd, worktreeDir, true);\n try {\n git(st.cwd, [\"branch\", \"-D\", branch]); // never merged by git — nothing for -d to see\n } catch {\n // already gone\n }\n const s = { ...st, merged: true };\n saveStatus(logDir, s);\n const base =\n `applied worker ${id}'s changes (branch ${branch}) to ${st.cwd} as uncommitted changes: ` +\n `${changedPaths.join(\", \")} (worktree removed, branch deleted)`;\n const notes: string[] = [];\n if (salvage.committed.length) {\n notes.push(\n `salvaged ${salvage.committed.length} uncommitted change(s) onto the branch first: ${salvage.committed.join(\", \")}`\n );\n }\n if (salvage.skipped.length) {\n notes.push(`skipped ${salvage.skipped.join(\", \")}`);\n }\n return notes.length ? `${base}; ${notes.join(\"; \")}` : base;\n}\n\nfunction noCommitNextSteps(id: string, branch: string, worktreeDir: string, paths: string[]): string {\n return (\n `applied worker ${id}'s changes (branch ${branch}) to the checkout as uncommitted changes: ${paths.join(\", \")}. ` +\n `Branch and worktree ${worktreeDir} were kept. Next: review (git diff), commit, then run: pai worker gc`\n );\n}\n\n/**\n * `merge --no-commit`: apply base..branch to the checkout with a 3-way\n * `git apply`, leaving the result uncommitted. On conflict the tree is left\n * as is (markers + unmerged index entries) and the conflicted paths are listed.\n */\nfunction applyNoCommit(cwd: string, id: string, branch: string, worktreeDir: string, base: string, paths: string[]): string {\n const diff = execFileSync(\"git\", [\"-C\", cwd, \"diff\", \"--binary\", `${base}..${branch}`], {\n encoding: \"buffer\",\n timeout: 30_000,\n maxBuffer: 64 * 1024 * 1024,\n env: { ...process.env, LC_ALL: \"C\" },\n });\n try {\n execFileSync(\"git\", [\"-C\", cwd, \"apply\", \"--3way\", \"--binary\"], {\n input: diff,\n timeout: 30_000,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env: { ...process.env, LC_ALL: \"C\" },\n });\n } catch (e) {\n const conflicted = git(cwd, [\"diff\", \"--name-only\", \"--diff-filter=U\"]).split(\"\\n\").filter(Boolean);\n const err = e as { stderr?: Buffer | string; message?: string };\n const why = ((typeof err.stderr === \"string\" ? err.stderr : err.stderr?.toString(\"utf8\")) || err.message || \"\").trim();\n throw new Error(\n conflicted.length\n ? `worker ${id}: applying ${branch} conflicted in ${conflicted.join(\", \")}. The tree was left for you: ` +\n `resolve those paths, review, commit; branch and worktree ${worktreeDir} were kept`\n : `worker ${id}: git refused to apply ${branch} to ${cwd} — ${why}. Nothing was changed; branch and worktree were kept`\n );\n }\n return noCommitNextSteps(id, branch, worktreeDir, paths);\n}\n\nexport type VerifyState = \"identical\" | \"differs\" | \"missing\" | \"deleted\";\nexport interface VerifyResult {\n id: string;\n ref: string;\n base: string;\n against: string;\n files: { path: string; state: VerifyState }[];\n ok: boolean;\n}\n\n/**\n * `pai worker verify <id>`: for every file the worker's branch (or its\n * refs/pai-archive/<id> archive once the branch is gone) changed since its\n * base, compare bytes with the working tree or `--against <ref>`.\n * identical = the target already holds the worker's version (a worker delete\n * counts as identical when the target lacks the file too); differs = other\n * bytes; missing = the worker's file is absent in the target; deleted = the\n * worker deleted it but the target still has it. ok = every file identical.\n */\nexport function verifyWorker(logDir: string, id: string, opts: { against?: string } = {}): VerifyResult {\n const st = loadStatus(logDir, id);\n if (!st) throw new Error(`no worker named \"${id}\"`);\n const env = { ...process.env, LC_ALL: \"C\" };\n const bytes = (args: string[]): Buffer | null => {\n try {\n return execFileSync(\"git\", [\"-C\", st.cwd, ...args], {\n env,\n timeout: 30_000,\n maxBuffer: 256 * 1024 * 1024,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n } catch {\n return null;\n }\n };\n const has = (ref: string): boolean => bytes([\"rev-parse\", \"--verify\", \"-q\", `${ref}^{commit}`]) !== null;\n const ref = [`refs/heads/${worktreeBranch(id)}`, `refs/pai-archive/${id}`].find(has);\n if (!ref) throw new Error(`worker ${id}: neither ${worktreeBranch(id)} nor refs/pai-archive/${id} exists in ${st.cwd}`);\n const base = st.worktreeSnapshot && st.worktreeBase ? st.worktreeBase : git(st.cwd, [\"merge-base\", \"HEAD\", ref]);\n const list = (bytes([\"diff\", \"--name-status\", \"--no-renames\", \"-z\", `${base}..${ref}`]) ?? Buffer.alloc(0))\n .toString(\"utf8\")\n .split(\"\\0\")\n .filter(Boolean);\n const files: VerifyResult[\"files\"] = [];\n for (let i = 0; i + 1 < list.length; i += 2) {\n const path = list[i + 1];\n const deleted = list[i] === \"D\";\n const target = opts.against\n ? bytes([\"show\", `${opts.against}:${path}`])\n : existsSync(join(st.cwd, path))\n ? readFileSync(join(st.cwd, path))\n : null;\n let state: VerifyState;\n if (deleted) state = target ? \"deleted\" : \"identical\";\n else {\n const mine = bytes([\"show\", `${ref}:${path}`]);\n state = !target ? \"missing\" : mine && mine.equals(target) ? \"identical\" : \"differs\";\n }\n files.push({ path, state });\n }\n return { id, ref, base, against: opts.against ?? \"working tree\", files, ok: files.every((f) => f.state === \"identical\") };\n}\n\n/**\n * `pai worker merge <id>`: salvage whatever the worker left uncommitted onto\n * its branch, refuse when the original checkout is dirty in paths the branch\n * touches, merge with --no-ff (the merge commit names the worker), then\n * remove the worktree and delete the branch; the status gains `merged: true`.\n * A branch with nothing to merge is refused loudly — after salvage that\n * genuinely means it holds nothing, and reporting success there would\n * destroy the worktree for no gain.\n */\nexport function mergeWorker(logDir: string, id: string, opts: { noCommit?: boolean } = {}): string {\n const st = mustHaveBranch(logDir, id);\n if (st.merged) return `worker ${id}: branch ${st.branch} already merged`;\n if (st.worktreeSnapshot) return mergeSnapshotWorker(logDir, id, st, opts.noCommit);\n // salvage first: only a commit can carry uncommitted work through the merge\n const salvage = existsSync(st.worktreeDir!)\n ? salvageUncommitted(st.worktreeDir!, st.label || UNLABELED)\n : { committed: [], skipped: [] };\n const incoming = parseInt(git(st.cwd, [\"rev-list\", \"--count\", `HEAD..${st.branch}`]), 10) || 0;\n if (incoming <= 0) {\n throw new Error(\n `worker ${id}: branch ${st.branch} has no commits to merge. ` +\n `The worktree ${st.worktreeDir} was NOT removed — uncommitted work there would be destroyed. ` +\n `Commit it yourself, or drop everything with: pai worker discard ${id}`\n );\n }\n const mergeBase = git(st.cwd, [\"merge-base\", \"HEAD\", st.branch!]);\n const incomingPaths = git(st.cwd, [\"diff\", \"--name-only\", mergeBase, st.branch!])\n .split(\"\\n\")\n .filter(Boolean);\n assertNoNodeModules(st.cwd, mergeBase, st.branch!, id);\n assertNoDirtyOverlap(st.cwd, incomingPaths, id, st.branch!);\n if (opts.noCommit) return applyNoCommit(st.cwd, id, st.branch!, st.worktreeDir!, mergeBase, incomingPaths);\n try {\n git(st.cwd, [\"merge\", \"--no-ff\", st.branch!, \"-m\", `merge worker ${id} (${st.label})`]);\n } catch (e) {\n throw new Error(\n `worker ${id}: git refused the merge of ${st.branch} — ${(e as Error).message}. ` +\n `The worktree ${st.worktreeDir} was kept: resolve the conflict, then re-run merge`\n );\n }\n if (existsSync(st.worktreeDir!)) assertWorktreeClean(st.worktreeDir!, id);\n // force: assertWorktreeClean already confirmed nothing but a skipped\n // outside-pointing symlink (if anything) remains uncommitted\n removeWorktree(st.cwd, st.worktreeDir!, true);\n let branchGone = true;\n try {\n git(st.cwd, [\"branch\", \"-d\", st.branch!]);\n } catch {\n branchGone = false; // -d refuses anything not fully merged; keep the branch, say so\n }\n const s = { ...st, merged: true };\n saveStatus(logDir, s);\n invalidatePendingCache(logDir, st.cwd);\n const base = `merged ${st.branch} into ${st.cwd} (worktree removed${branchGone ? \", branch deleted\" : \"; branch kept: git refused -d\"})`;\n const notes: string[] = [];\n if (salvage.committed.length) {\n notes.push(`salvaged ${salvage.committed.length} uncommitted change(s): ${salvage.committed.join(\", \")}`);\n }\n if (salvage.skipped.length) {\n notes.push(`skipped ${salvage.skipped.join(\", \")}`);\n }\n return notes.length ? `${base}; ${notes.join(\"; \")}` : base;\n}\n\n/** `pai worker discard <id>`: drop worktree and branch, keep nothing. */\nexport function discardWorker(logDir: string, id: string): string {\n const st = mustHaveBranch(logDir, id);\n removeWorktree(st.cwd, st.worktreeDir!, true);\n let branchGone = false;\n try {\n git(st.cwd, [\"branch\", \"-D\", st.branch!]);\n branchGone = true;\n } catch {\n branchGone = false;\n }\n const s = {\n ...st,\n branch: null,\n worktreeDir: null,\n worktreeBase: null,\n worktreeSnapshot: null,\n commits: null,\n merged: false,\n };\n saveStatus(logDir, s);\n invalidatePendingCache(logDir, st.cwd);\n return `discarded worker ${id}: worktree removed${branchGone ? `, branch ${st.branch} deleted` : \"\"}`;\n}\n\nfunction mustHaveBranch(logDir: string, id: string): WorkerStatus & Required<Pick<WorkerStatus, \"branch\" | \"worktreeDir\">> {\n const st = loadStatus(logDir, id);\n if (!st) throw new Error(`no worker named \"${id}\"`);\n if (!st.branch || !st.worktreeDir) {\n throw new Error(\n `worker ${id} has no worktree branch to ${st.branch ? \"clean up\" : \"merge\"} — ` +\n `it ran in place or its branch was already handled`\n );\n }\n return st as WorkerStatus & Required<Pick<WorkerStatus, \"branch\" | \"worktreeDir\">>;\n}\n\n/**\n * The paragraph a worktree run's system prompt gains: it may commit on its\n * own branch (the no-commit rule holds for the main branch only), it must not\n * merge or push itself, and the parent or operator merges.\n */\nexport function worktreeSystemPrompt(id: string, branch: string, dir: string, snapshot = false): string {\n return [\n \"You are running in your own git worktree:\",\n ` ${dir} on branch ${branch} (worker id ${id}).`,\n \"Commit your work on that branch as you go (git add / git commit) — committing here is expected;\",\n \"the no-commit rule applies to the main branch only, and this is not it.\",\n \"Do not merge, rebase or push; the operator merges your branch back with `pai worker merge`.\",\n \"Use ONLY relative paths inside the worktree, never absolute worktree paths — absolute paths break after merge and leak machine layout.\",\n ...(snapshot\n ? [\n \"This worktree was branched from a snapshot that includes files the parent checkout had uncommitted at spawn time — not just its last commit.\",\n ]\n : []),\n ].join(\"\\n\");\n}\n\n/**\n * For headless runs WITHOUT a worktree: the worker shares the operator's\n * checkout with other in-flight workers, so a hook blocks git stash (except\n * list/show), reset, clean, checkout/restore of paths, switch, rebase and\n * merge — take baselines from measurements before editing, not from stash.\n */\nexport function inPlaceSystemPrompt(): string {\n return [\n \"You are running in the operator's shared git checkout, alongside other workers editing it at the same time.\",\n \"A hook blocks git stash (except list/show), reset, clean, checkout/restore of files, switch, rebase and merge — take baselines from measurements taken before you edit, not from stashing.\",\n \"Read-only git (status, diff, log, show, stash list) is fine.\",\n ].join(\"\\n\");\n}\n\nexport interface GcEntry {\n id: string;\n /** archive ref, or the reason the worktree was skipped. */\n archive?: string;\n skipped?: string;\n}\n\nconst GC_STAMP = \"gc-last\";\n\n/**\n * Archive worktrees of finished workers older than `olderThanHours` and\n * status-less orphan directories: uncommitted state is salvaged onto the\n * branch, the branch is x, the worktree is\n * removed. Nothing is deleted without a salvage first; a failed salvage skips\n * that worktree. Running workers are never touched. `dryRun` reports only.\n */\nexport function gcWorktrees(\n logDir: string,\n olderThanHours = 24,\n dryRun = false,\n now = Date.now()\n): GcEntry[] {\n const cutoff = now - olderThanHours * 3_600_000;\n const out: GcEntry[] = [];\n const root = worktreesDir(logDir);\n const known = new Set<string>();\n const archive = (id: string, dir: string, st: WorkerStatus | null): void => {\n const target = `refs/pai-archive/${id}`;\n if (dryRun) return void out.push({ id, archive: target });\n try {\n // The shared object store and refs live in the main repo; the recorded cwd\n // may be another worker's worktree that gc already removed.\n let repo: string;\n try {\n const raw = git(dir, [\"rev-parse\", \"--git-common-dir\"]);\n repo = isAbsolute(raw) ? raw : resolve(dir, raw);\n } catch {\n throw new Error(\"not a git worktree\");\n }\n const label = `archive: leftover state of worker ${id}`;\n if (uncommittedPaths(dir).length) {\n // salvageUncommitted labels \"salvaged: <label>\"; keep the requested subject\n const res = salvageUncommitted(dir, label);\n if (res.committed.length) git(dir, [\"commit\", \"--amend\", \"-q\", \"-m\", label]);\n if (res.skipped.length && uncommittedPaths(dir).some((p) => !symlinkEscapesWorktree(dir, p))) {\n throw new Error(\"uncommitted paths remain\");\n }\n }\n const src = worktreeBranch(id);\n let hasBranch = true;\n try {\n git(repo, [\"rev-parse\", \"--verify\", \"-q\", `refs/heads/${src}`]);\n } catch {\n hasBranch = false;\n }\n const tip = git(dir, [\"rev-parse\", \"HEAD\"]);\n // never drop a branch whose tip the archive ref would not keep reachable\n if (hasBranch) git(repo, [\"merge-base\", \"--is-ancestor\", `refs/heads/${src}`, tip]);\n git(repo, [\"update-ref\", target, tip]);\n git(repo, [\"worktree\", \"remove\", \"--force\", dir]);\n if (hasBranch) git(repo, [\"branch\", \"-D\", src]);\n if (st) saveStatus(logDir, { ...st, branch: null, worktreeDir: null, archived: target });\n appendLedger(ledgerPath(logDir), \"WORKER-GC\", { id, archive: target });\n out.push({ id, archive: target });\n } catch (e) {\n out.push({ id, skipped: (e as Error).message.split(\"\\n\")[0] });\n }\n };\n\n for (const st of loadStatuses(logDir)) {\n known.add(st.id);\n if (!st.worktreeDir || !existsSync(st.worktreeDir) || isLive(st)) continue;\n let end = 0;\n try {\n end = statSync(statusPath(logDir, st.id)).mtimeMs;\n } catch {\n continue;\n }\n if (end > cutoff) continue;\n archive(st.id, st.worktreeDir, st);\n }\n\n if (existsSync(root)) {\n for (const ent of readdirSync(root, { withFileTypes: true })) {\n if (!ent.isDirectory() || known.has(ent.name) || existsSync(statusPath(logDir, ent.name))) continue;\n const dir = join(root, ent.name);\n try {\n if (statSync(dir).mtimeMs > cutoff) continue;\n } catch {\n continue;\n }\n archive(ent.name, dir, null);\n }\n }\n if (!dryRun) {\n for (const repo of new Set(loadStatuses(logDir).map((s) => s.cwd))) {\n try {\n git(repo, [\"worktree\", \"prune\"]);\n } catch {\n // repo gone\n }\n invalidatePendingCache(logDir, repo);\n }\n }\n return out;\n}\n\n/** Run `gcWorktrees` at most once per `everyMin` minutes (stamp file in the logDir). */\nexport function gcWorktreesThrottled(logDir: string, everyMin = 60, now = Date.now()): GcEntry[] | null {\n const stamp = join(logDir, GC_STAMP);\n try {\n if (now - statSync(stamp).mtimeMs < everyMin * 60_000) return null;\n } catch {\n // never ran\n }\n mkdirSync(logDir, { recursive: true });\n writeFileSync(stamp, String(now), \"utf8\");\n return gcWorktrees(logDir, 24, false, now);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import "./config-DjZELvoA.mjs";
|
|
2
|
-
import "./config-Dpe2JlbY.mjs";
|
|
3
|
-
import "./worktree-CwMRTa0H.mjs";
|
|
4
|
-
import "./pai-marker-BFOlchq2.mjs";
|
|
5
|
-
import { n as formatAutoRouteJson, t as autoRoute } from "./auto-route-DoFrncW7.mjs";
|
|
6
|
-
|
|
7
|
-
export { autoRoute, formatAutoRouteJson };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pai-marker-BFOlchq2.mjs","names":[],"sources":["../src/registry/registrable.ts","../src/registry/pai-marker.ts"],"sourcesContent":["/**\n * registrable.ts — directories that must never become registered projects.\n *\n * A project is a durable thing. The registry, however, has been accepting\n * whatever directory a session happened to start in, and some of those\n * directories are disposable by construction. Measured on the real registry,\n * 2026-08-04:\n *\n * 08 - Others/MDF/Infrastruktur/.claude/worktrees/cool-haibt 1 session\n * 08 - Others/MDF/Infrastruktur/.claude/worktrees/strange-haibt 7 sessions\n * /private/tmp/ops-webui dead\n * /private/tmp/claude-501/-Users-…-AIBroker/aae854c6-… dead\n *\n * The worktrees are agent isolation directories, created to be removed. The temp\n * paths are exactly what their name says. Registering them attributes session\n * history to a location with no future, and lets `pai <name>` route someone into\n * a directory a cleanup can delete underneath them.\n *\n * So this refuses at the point of registration rather than reclassifying\n * afterwards. It is a guard, not a policy about what health should call things —\n * it stops the set growing while the vocabulary question (dead vs duplicate vs\n * misnamed vs ephemeral) is decided separately.\n *\n * Found by the AIBroker session while we were dividing up the dead-path work.\n */\n\nimport { readFileSync, statSync } from \"node:fs\";\nimport { dirname, resolve, sep } from \"node:path\";\nimport { readWorkersSection } from \"../workers/config.js\";\nimport { workersLogDir } from \"../workers/paths.js\";\nimport { worktreesDir } from \"../workers/worktree.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n/**\n * The `gitdir:` target of a linked worktree, or undefined. A linked worktree's\n * `.git` is a FILE (\"gitdir: <main>/.git/worktrees/<name>\"); a normal repo's is\n * a directory and a submodule's points under `.git/modules/`.\n */\nfunction linkedWorktreeGitdir(root: string): string | undefined {\n const dotGit = resolve(root, \".git\");\n try {\n if (!statSync(dotGit).isFile()) return undefined;\n const m = readFileSync(dotGit, \"utf8\").match(/^gitdir:\\s*(.+?)\\s*$/m);\n return m && m[1].includes(`${sep}worktrees${sep}`) ? m[1] : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** True when `root` is a linked git worktree (its `.git` is a `gitdir:` file). */\nexport function isLinkedWorktree(root: string): boolean {\n return linkedWorktreeGitdir(root) !== undefined;\n}\n\n/** The main repo a linked worktree belongs to, when resolvable. */\nexport function mainRepoOf(root: string): string | undefined {\n const gitdir = linkedWorktreeGitdir(root);\n // <main>/.git/worktrees/<name> -> <main>\n return gitdir ? dirname(dirname(dirname(gitdir))) : undefined;\n}\n\n/** The worker worktrees root from the workers config; undefined if unreadable. */\nfunction workerWorktreesRoot(): string | undefined {\n try {\n return resolve(worktreesDir(workersLogDir(readWorkersSection().workers)));\n } catch {\n return undefined;\n }\n}\n\n/** Path fragments that mark a location as disposable, with why. */\nconst EPHEMERAL: ReadonlyArray<{ fragment: string; because: string }> = [\n {\n // Agent worktrees. `.claude/worktrees/<name>` is created for isolation during\n // one task and removed afterwards.\n fragment: `${sep}.claude${sep}worktrees${sep}`,\n because: \"a git worktree created for agent isolation — it is meant to be removed\",\n },\n {\n // Worker log dirs and their worktrees, current (`<pai home>/logs/workers`)\n // and legacy (`~/.claude/logs/workers`) layouts, including the dir itself.\n fragment: `${sep}logs${sep}workers${sep}`,\n because: \"a worker log directory or worktree — it is meant to be removed\",\n },\n {\n fragment: `${sep}private${sep}tmp${sep}`,\n because: \"a system temp directory\",\n },\n {\n fragment: `${sep}var${sep}folders${sep}`,\n because: \"a macOS per-user temp directory\",\n },\n];\n\n/** Why a path is a (linked or worker) git worktree, or undefined. */\nexport function worktreeReason(\n rootPath: string,\n worktreesRoot: string | undefined = workerWorktreesRoot()\n): string | undefined {\n if (isLinkedWorktree(rootPath)) {\n const main = mainRepoOf(rootPath);\n return `a linked git worktree${main ? ` of ${main}` : \"\"}`;\n }\n const abs = resolve(rootPath);\n if (worktreesRoot && (abs === worktreesRoot || abs.startsWith(worktreesRoot + sep))) {\n return \"a worker worktree\";\n }\n return undefined;\n}\n\n/**\n * Why this path cannot be a project, or undefined if it can.\n *\n * Returns the reason rather than a boolean so the caller can tell the user which\n * rule caught them — \"refused\" without a reason invites someone to work around it\n * rather than move their project somewhere durable.\n */\nexport function unregistrableReason(\n rootPath: string,\n worktreesRoot: string | undefined = workerWorktreesRoot()\n): string | undefined {\n const wt = worktreeReason(rootPath, worktreesRoot);\n if (wt) return wt;\n\n // Compare with trailing separators so a fragment cannot match a partial\n // directory name: `/tmp/` must not match `/tmpdir/`.\n const padded = rootPath.endsWith(sep) ? rootPath : rootPath + sep;\n\n for (const { fragment, because } of EPHEMERAL) {\n if (padded.includes(fragment)) return because;\n }\n\n // A bare `/tmp/...` too. Kept separate from the list above because on macOS\n // /tmp is a symlink to /private/tmp, so both spellings reach the same place and\n // only one of them contains \"private\".\n if (padded.startsWith(`${sep}tmp${sep}`)) return \"a system temp directory\";\n\n return undefined;\n}\n\n/**\n * Remove registry rows whose root is unregistrable (linked worktree, worker\n * worktree/log dir, temp dir; rows registered before the guard existed). Returns\n * the slugs removed. Sessions on those rows are worker/agent scratch history.\n */\nexport async function pruneWorktreeProjects(\n backend: RegistryBackend,\n worktreesRoot: string | undefined = workerWorktreesRoot()\n): Promise<string[]> {\n const removed: string[] = [];\n for (const p of await backend.listProjects({})) {\n if (unregistrableReason(p.root_path, worktreesRoot)) {\n await backend.deleteProjectCascade(p.id);\n removed.push(p.slug);\n }\n }\n return removed;\n}\n","/**\n * PAI.md marker file management.\n *\n * Each registered project gets a `Notes/PAI.md` file with a YAML frontmatter\n * `pai:` block that PAI manages. The rest of the file (body content, other\n * frontmatter keys) is user-owned and never modified by PAI.\n *\n * YAML parsing/updating is done with simple regex — no external dependency.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n readdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { isLinkedWorktree } from \"./registrable.js\";\n\n// ---------------------------------------------------------------------------\n// Container directories\n// ---------------------------------------------------------------------------\n\n/**\n * Directories that hold projects but are not themselves projects.\n *\n * Running a single Claude session from ~/Desktop is enough for the scanner to\n * register it as a project, after which every scan recreates a `Notes/PAI.md`\n * inside it. The user gets a Notes folder on their Desktop that reappears\n * however many times they delete it, containing a marker file that is\n * otherwise empty.\n *\n * The session is still worth tracking — it just belongs in the central notes\n * directory (~/.claude/projects/<encoded>/Notes) rather than as a folder\n * dropped into the user's Desktop.\n */\nconst CONTAINER_DIRS = [\n \"Desktop\",\n \"Documents\",\n \"Downloads\",\n \"Movies\",\n \"Music\",\n \"Pictures\",\n \"Public\",\n \"Library\",\n \"Applications\",\n];\n\n/**\n * True when a path is a place projects live rather than a project itself:\n * the home directory, a standard user container, or a system temp root.\n *\n * Only ever used to decline *creating* a marker. An existing marker is left\n * alone — the user may have put one there deliberately.\n */\nexport function isContainerDirectory(projectRoot: string): boolean {\n // Strip trailing slashes, but never strip the root itself down to \"\".\n const strip = (s: string): string => s.replace(/(?!^)\\/+$/, \"\");\n\n const p = strip(resolve(projectRoot));\n const home = strip(resolve(homedir()));\n\n if (p === home || p === \"/\") return true;\n if (p === \"/tmp\" || p === \"/private/tmp\" || p === \"/var/tmp\") return true;\n\n for (const dir of CONTAINER_DIRS) {\n if (p === join(home, dir)) return true;\n }\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PaiMarker {\n /** Absolute path to the PAI.md file */\n path: string;\n /** The `slug` value from the `pai:` frontmatter block */\n slug: string;\n /** Absolute path to the project root (parent of Notes/) */\n projectRoot: string;\n}\n\n// ---------------------------------------------------------------------------\n// Template content (mirrors templates/pai-project.template.md)\n// ---------------------------------------------------------------------------\n\nconst TEMPLATE = `---\npai:\n slug: \"\\${SLUG}\"\n registered: \"\\${DATE}\"\n last_indexed: null\n status: active\n---\n\n# \\${DISPLAY_NAME}\n\n<!-- Everything below the YAML frontmatter is yours — PAI never modifies content here. -->\n<!-- Use this file for project notes, decisions, preferences, or anything you want. -->\n<!-- PAI only reads and updates the \\`pai:\\` block in the frontmatter above. -->\n`;\n\nfunction isoDate(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction renderTemplate(slug: string, displayName: string): string {\n return TEMPLATE.replace(/\\$\\{SLUG\\}/g, slug)\n .replace(/\\$\\{DATE\\}/g, isoDate())\n .replace(/\\$\\{DISPLAY_NAME\\}/g, displayName);\n}\n\n// ---------------------------------------------------------------------------\n// YAML frontmatter helpers (regex-based, no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Split a markdown file with YAML frontmatter into its parts.\n *\n * Returns { frontmatter, body } where:\n * frontmatter — content between the opening and closing `---` delimiters\n * body — everything after the closing `---` line\n *\n * Returns null if the file does not begin with a `---` frontmatter block.\n */\nfunction parseFrontmatter(\n content: string\n): { frontmatter: string; body: string } | null {\n if (!content.startsWith(\"---\")) return null;\n\n // Skip past the opening \"---\" and its line ending\n const afterOpen = content.slice(3);\n const eolMatch = afterOpen.match(/^\\r?\\n/);\n if (!eolMatch) return null;\n\n const rest = afterOpen.slice(eolMatch[0].length);\n\n // Find closing \"---\" on its own line\n const closeMatch = rest.match(/^([\\s\\S]*?)\\n---[ \\t]*(\\r?\\n|$)/m);\n if (!closeMatch) return null;\n\n const frontmatter = closeMatch[1];\n const body = rest.slice(closeMatch[0].length);\n\n return { frontmatter, body };\n}\n\n/**\n * Extract a simple scalar YAML value from a block of YAML text.\n *\n * extractYamlValue(' slug: \"my-proj\"', \"slug\") → \"my-proj\"\n * extractYamlValue(' slug: my-proj', \"slug\") → \"my-proj\"\n * extractYamlValue(' last_indexed: null', \"last_indexed\") → \"null\"\n */\nfunction extractYamlValue(yamlBlock: string, key: string): string | null {\n const re = new RegExp(\n `^[ \\\\t]*${key}[ \\\\t]*:[ \\\\t]*\"?([^\"\\\\n\\\\r]*?)\"?[ \\\\t]*$`,\n \"m\"\n );\n const match = yamlBlock.match(re);\n if (!match) return null;\n return match[1].trim() || null;\n}\n\n/**\n * Replace the `pai:` mapping block inside a frontmatter string with\n * `newPaiBlock`. If no `pai:` block is found, appends it at the end.\n *\n * The regex captures the `pai:` key and all immediately-following indented\n * lines (the mapping values), then replaces the whole group.\n *\n * Edge case: the last indented line may not have a trailing newline when it\n * is the final line of the frontmatter string. We handle this by matching\n * lines that end with \\n OR with end-of-string.\n */\nfunction replacePaiBlock(frontmatter: string, newPaiBlock: string): string {\n // Normalise: ensure the frontmatter string ends with \\n so the regex\n // always finds a clean boundary after the last indented line.\n const fm = frontmatter.endsWith(\"\\n\") ? frontmatter : frontmatter + \"\\n\";\n\n // Match \"pai:\\n\" followed by any number of indented lines (each ending \\n).\n const paiRe = /^pai:[ \\t]*\\r?\\n(?:[ \\t]+[^\\r\\n]*\\r?\\n)*/m;\n if (paiRe.test(fm)) {\n // Replace and strip the extra trailing \\n we may have added.\n const replaced = fm.replace(paiRe, newPaiBlock);\n return frontmatter.endsWith(\"\\n\") ? replaced : replaced.replace(/\\n$/, \"\");\n }\n // pai: key not found — append it\n return fm + newPaiBlock;\n}\n\n/**\n * Build the canonical `pai:` YAML block (with a trailing newline).\n */\nfunction buildPaiBlock(\n slug: string,\n registered: string,\n lastIndexed: string | null,\n status: string\n): string {\n const lastIndexedStr =\n lastIndexed === null ? \"null\" : `\"${lastIndexed}\"`;\n return (\n `pai:\\n` +\n ` slug: \"${slug}\"\\n` +\n ` registered: \"${registered}\"\\n` +\n ` last_indexed: ${lastIndexedStr}\\n` +\n ` status: ${status}\\n`\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create or update `<projectRoot>/Notes/PAI.md`.\n *\n * - File absent: creates `Notes/` if needed, writes from template.\n * - File present: updates only the `pai:` frontmatter block; body and all\n * other frontmatter keys are preserved verbatim.\n *\n * @param projectRoot Absolute path to the project root directory.\n * @param slug PAI slug for this project.\n * @param displayName Human-readable name (defaults to slug if omitted).\n */\nexport function ensurePaiMarker(\n projectRoot: string,\n slug: string,\n displayName?: string\n): void {\n // A linked worktree shares its tracked files with the main repo: writing the\n // slug here lands in the main repo's PAI.md when the branch is merged.\n if (isLinkedWorktree(projectRoot)) return;\n\n const notesDir = join(projectRoot, \"Notes\");\n const markerPath = join(notesDir, \"PAI.md\");\n const name = displayName ?? slug;\n\n // --- File does not exist — create from template ---\n if (!existsSync(markerPath)) {\n // Never create a Notes/ folder inside a container directory. Doing so is\n // what puts a Notes folder on the user's Desktop that comes back every\n // time it is deleted.\n if (isContainerDirectory(projectRoot)) return;\n\n mkdirSync(notesDir, { recursive: true });\n writeFileSync(markerPath, renderTemplate(slug, name), \"utf8\");\n return;\n }\n\n // --- File exists — update only the `pai:` block ---\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n\n if (!parsed) {\n // No YAML frontmatter — prepend a fresh one, treat the whole file as body.\n const paiBlock = buildPaiBlock(slug, isoDate(), null, \"active\");\n const newContent = `---\\n${paiBlock}---\\n\\n${raw}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n return;\n }\n\n const { frontmatter, body } = parsed;\n\n // Preserve existing `registered` date so we don't reset it on re-scan.\n const existingRegistered =\n extractYamlValue(frontmatter, \"registered\") ?? isoDate();\n\n // Preserve existing `last_indexed` value (may be \"null\" string or a date).\n const rawLastIndexed = extractYamlValue(frontmatter, \"last_indexed\");\n const lastIndexed =\n rawLastIndexed === null || rawLastIndexed === \"null\"\n ? null\n : rawLastIndexed;\n\n // Preserve existing `status`.\n const existingStatus = extractYamlValue(frontmatter, \"status\") ?? \"active\";\n\n const newPaiBlock = buildPaiBlock(\n slug,\n existingRegistered,\n lastIndexed,\n existingStatus\n );\n\n const newFrontmatter = replacePaiBlock(frontmatter, newPaiBlock);\n\n // Ensure the frontmatter block ends with exactly one newline before the\n // closing --- delimiter.\n const fmWithNewline = newFrontmatter.endsWith(\"\\n\")\n ? newFrontmatter\n : newFrontmatter + \"\\n\";\n\n // Reconstruct the full file. Preserve whatever separator the body has.\n const newContent = `---\\n${fmWithNewline}---\\n${body}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n}\n\n/**\n * Read PAI marker data from `<projectRoot>/Notes/PAI.md`.\n * Returns null if the file does not exist or contains no `pai:` block.\n */\nexport function readPaiMarker(\n projectRoot: string\n): { slug: string; registered: string; status: string } | null {\n const markerPath = join(projectRoot, \"Notes\", \"PAI.md\");\n if (!existsSync(markerPath)) return null;\n\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n if (!parsed) return null;\n\n const slug = extractYamlValue(parsed.frontmatter, \"slug\");\n if (!slug) return null;\n\n const registered =\n extractYamlValue(parsed.frontmatter, \"registered\") ?? \"\";\n const status =\n extractYamlValue(parsed.frontmatter, \"status\") ?? \"active\";\n\n return { slug, registered, status };\n}\n\n/**\n * Scan a list of parent directories for `<child>/Notes/PAI.md` marker files.\n * Each directory in `searchDirs` is scanned one level deep — its immediate\n * child directories are checked for a `Notes/PAI.md` file.\n *\n * Returns an array of PaiMarker objects for every valid marker found.\n * Invalid or malformed markers are silently skipped.\n *\n * @param searchDirs Absolute paths to parent directories.\n */\nexport function discoverPaiMarkers(searchDirs: string[]): PaiMarker[] {\n const results: PaiMarker[] = [];\n\n for (const dir of searchDirs) {\n if (!existsSync(dir)) continue;\n\n let children: string[];\n try {\n children = readdirSync(dir);\n } catch {\n continue;\n }\n\n for (const child of children) {\n if (child.startsWith(\".\")) continue;\n const childPath = join(dir, child);\n try {\n if (!statSync(childPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const markerData = readPaiMarker(childPath);\n if (!markerData) continue;\n\n results.push({\n path: join(childPath, \"Notes\", \"PAI.md\"),\n slug: markerData.slug,\n projectRoot: childPath,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|