@tekmidian/pai 0.43.0 → 0.44.0

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.
Files changed (30) hide show
  1. package/dist/{chain-CEC1a5mX.mjs → chain-DAEC9AU7.mjs} +2 -2
  2. package/dist/{chain-CEC1a5mX.mjs.map → chain-DAEC9AU7.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +4 -4
  4. package/dist/cli/program.mjs +4 -4
  5. package/dist/daemon/index.mjs +2 -2
  6. package/dist/{daemon-QIvCXB5f.mjs → daemon-BVbvL8IA.mjs} +2 -2
  7. package/dist/{daemon-BhldpFvO.mjs → daemon-ebLDoKM3.mjs} +2 -2
  8. package/dist/{daemon-BhldpFvO.mjs.map → daemon-ebLDoKM3.mjs.map} +1 -1
  9. package/dist/daemon-mcp/index.mjs +3 -3
  10. package/dist/{fallback-BQJ8PbCK.mjs → fallback-CxGDTXWt.mjs} +2 -2
  11. package/dist/{fallback-BQJ8PbCK.mjs.map → fallback-CxGDTXWt.mjs.map} +1 -1
  12. package/dist/hooks/load-project-context.mjs.map +1 -1
  13. package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
  14. package/dist/{planner-BlKGgpWI.mjs → planner-B4DvNpXd.mjs} +3 -3
  15. package/dist/{planner-BlKGgpWI.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
  16. package/dist/{program-C5jRQ1kg.mjs → program-cr5U1-DK.mjs} +612 -54
  17. package/dist/program-cr5U1-DK.mjs.map +1 -0
  18. package/dist/{run-ByBKowN5.mjs → run-BgExF8Xg.mjs} +16 -4
  19. package/dist/{run-ByBKowN5.mjs.map → run-BgExF8Xg.mjs.map} +1 -1
  20. package/docs/commands/README.md +3 -1
  21. package/docs/commands/audit.md +25 -2
  22. package/package.json +1 -1
  23. package/src/hooks/ts/lib/context-fill.test.ts +40 -0
  24. package/src/hooks/ts/session-start/load-project-context.ts +3 -3
  25. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -2
  26. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +0 -1
  27. package/dist/hooks/session-commands.mjs +0 -26
  28. package/dist/hooks/session-commands.mjs.map +0 -7
  29. package/dist/program-C5jRQ1kg.mjs.map +0 -1
  30. package/src/hooks/ts/session-start/session-commands.ts +0 -30
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/hooks/ts/lib/mcp-deferred-gate.ts", "../../../src/hooks/ts/lib/mcp-deferred-reminder.ts", "../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/session-start/mcp-deferred-reminder.ts"],
4
- "sourcesContent": ["/**\n * mcp-deferred-gate.ts \u2014 pure pieces of the deferred MCP tool gate.\n *\n * Claude Code defers MCP tool schemas: an mcp__server__tool call fails input\n * validation unless the session surfaced the schema via ToolSearch first.\n * Sessions misread that error as \"MCP server disconnected\" and report an\n * outage that never happened (observed with a rename tool while every server\n * was connected).\n *\n * A second failure shape: when an MCP server re-registers mid-session, the\n * session's deferred tool handles are invalidated and calls fail with the\n * harness text \"N deferred tools are no longer available\" (observed 2026-09-18,\n * N=411, a rename tool). Sessions misread that as a disconnect too. The gate\n * denies that shape with the same ToolSearch remedy; a ToolSearch that\n * surfaces the tool after the last invalidation re-arms the pass-through.\n *\n * The decision is evidence-based: the session transcript either shows the tool\n * was surfaced (a ToolSearch mentioning it) or already ran successfully \u2014 in\n * which case the schema is loaded and the call passes through untouched \u2014 or\n * it shows neither, in which case the call is about to hit the gate (or is a\n * retry that just did) and gets the corrective instruction.\n *\n * Split out of the pre-tool-use entrypoint (which calls main() at import\n * time) so the decision is testable without spawning a hook process, per the\n * transcript-text.ts precedent.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GateHookInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n tool_name?: string;\n tool_input?: unknown;\n}\n\nexport interface GateEvidence {\n /** A ToolSearch call in the transcript mentions this tool by full name. */\n surfacedByToolSearch: boolean;\n /** This tool already ran successfully \u2014 its schema must be loaded. */\n succeededBefore: boolean;\n}\n\nexport interface GateObservation {\n type: \"decision\";\n title: string;\n narrative: string;\n tool_name: string;\n tool_input_summary: string;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n}\n\nexport interface GateDecision {\n action: \"pass\" | \"correct\";\n /** The JSON string to write to stdout, same shape as sibling PreToolUse hooks. */\n output: string;\n /** Observation payload for the daemon when a correction fired, else null. */\n observation: GateObservation | null;\n}\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\nexport const PASS_OUTPUT = JSON.stringify({\n hookSpecificOutput: { hookEventName: \"PreToolUse\", permissionDecision: \"allow\" },\n});\n\n/**\n * Harness text emitted when a mid-session MCP server re-registration\n * invalidates the session's deferred tool handles.\n */\nexport const STALE_HANDLE_MARKER = \"deferred tools are no longer available\";\n\n/** True for MCP tool names (`mcp__<server>__<tool>`), false for everything else. */\nexport function isMcpTool(name: string): boolean {\n return name.startsWith(\"mcp__\") && name.split(\"__\").length >= 3 && !!name.split(\"__\")[2];\n}\n\n/**\n * Scan transcript text (JSONL, one entry per line, tolerant of junk) for\n * evidence that `toolName` is loadable in this session.\n */\nexport function scanTranscriptEvidence(transcriptText: string, toolName: string): GateEvidence {\n const toolUseIds = new Set<string>();\n let surfacedByToolSearch = false;\n let succeededBefore = false;\n\n for (const line of transcriptText.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let entry: unknown;\n try {\n entry = JSON.parse(trimmed);\n } catch {\n continue;\n }\n const message = (entry as { message?: { content?: unknown } })?.message;\n const content = message?.content;\n if (!Array.isArray(content)) continue;\n\n for (const block of content as Array<Record<string, unknown>>) {\n if (!block || typeof block !== \"object\") continue;\n if (block.type === \"tool_use\" && typeof block.name === \"string\") {\n if (block.name === toolName && typeof block.id === \"string\") {\n toolUseIds.add(block.id);\n } else if (block.name === \"ToolSearch\") {\n // Liberal match: the query may be \"select:<tool>\" or a keyword list.\n if (JSON.stringify(block.input ?? \"\").includes(toolName)) {\n surfacedByToolSearch = true;\n }\n }\n } else if (\n block.type === \"tool_result\" &&\n typeof block.tool_use_id === \"string\" &&\n toolUseIds.has(block.tool_use_id) &&\n !block.is_error\n ) {\n succeededBefore = true;\n }\n }\n }\n\n return { surfacedByToolSearch, succeededBefore };\n}\n\n/**\n * Byte index of the last transcript line where a ToolSearch call surfaced\n * `toolName` (same liberal match as scanTranscriptEvidence), or -1 if never.\n * Used to order the stale-handle remedy against the last invalidation.\n */\nexport function lastToolSearchSurfacingIndex(\n transcriptText: string,\n toolName: string,\n): number {\n let last = -1;\n let offset = 0;\n for (const line of transcriptText.split(\"\\n\")) {\n const start = offset;\n offset += line.length + 1;\n const trimmed = line.trim();\n if (!trimmed) continue;\n let entry: unknown;\n try {\n entry = JSON.parse(trimmed);\n } catch {\n continue;\n }\n const content = (entry as { message?: { content?: unknown } })?.message?.content;\n if (!Array.isArray(content)) continue;\n for (const block of content as Array<Record<string, unknown>>) {\n if (\n block?.type === \"tool_use\" &&\n block.name === \"ToolSearch\" &&\n JSON.stringify(block.input ?? \"\").includes(toolName)\n ) {\n last = start;\n }\n }\n }\n return last;\n}\n\n/** The ToolSearch remedy shared by both correction shapes. */\nfunction buildRemedy(toolName: string): string {\n return (\n `Remedy: call ToolSearch with query \"select:${toolName}\" to load the fresh registry ` +\n `entry, then call ${toolName} again with the same arguments.`\n );\n}\n\n/** The corrective instruction fed back to the session on a deny. */\nexport function buildCorrection(toolName: string): string {\n return (\n `${toolName} is a deferred MCP tool: its schema is not loaded in this session yet, so this ` +\n `call would fail input validation. The MCP server is NOT disconnected \u2014 do not report an ` +\n `outage and do not retry the call unchanged. ${buildRemedy(toolName)}`\n );\n}\n\n/** Same remedy for the stale-handle shape: a re-registration invalidated a loaded schema. */\nexport function buildStaleHandleCorrection(toolName: string): string {\n return (\n `${toolName}'s deferred tool handle is stale: the MCP server re-registered mid-session, ` +\n `invalidating the schema this session had loaded (\"${STALE_HANDLE_MARKER}\"). The MCP ` +\n `server is NOT disconnected \u2014 do not report an outage and do not retry the call ` +\n `unchanged. ${buildRemedy(toolName)}`\n );\n}\n\n/** The deny decision shared by both failure shapes. */\nfunction correctDecision(toolName: string, reason: string, narrative: string): GateDecision {\n return {\n action: \"correct\",\n output: JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n }),\n observation: {\n type: \"decision\",\n title: `MCP deferred-tool gate corrected: ${toolName}`,\n narrative,\n tool_name: toolName,\n tool_input_summary: toolName,\n files_read: [],\n files_modified: [],\n concepts: [\"mcp\", \"deferred\", \"toolsearch\"],\n },\n };\n}\n\n/**\n * The whole gate: given hook input and the session transcript, decide whether\n * this mcp__ call passes through or gets the deferred-schema correction.\n */\nexport function decideMcpGate(input: GateHookInput, transcriptText: string): GateDecision {\n const toolName = typeof input.tool_name === \"string\" ? input.tool_name : \"\";\n if (!isMcpTool(toolName)) {\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n // Stale-handle shape: a mid-session re-registration invalidated loaded\n // schemas. Overrides the normal evidence pass-through, because a tool that\n // was surfaced (or even ran) earlier can still have a dead handle now. The\n // gate re-arms to pass-through only once a ToolSearch re-surfaced the tool\n // after the last invalidation.\n const toolInputText = JSON.stringify(input.tool_input ?? \"\");\n const lastStale = transcriptText.lastIndexOf(STALE_HANDLE_MARKER);\n if (toolInputText.includes(STALE_HANDLE_MARKER) || lastStale !== -1) {\n const remedyAt = lastToolSearchSurfacingIndex(transcriptText, toolName);\n if (remedyAt === -1 || remedyAt <= lastStale) {\n return correctDecision(\n toolName,\n buildStaleHandleCorrection(toolName),\n `Blocked a retry against a stale deferred-tool handle for ${toolName} and issued ` +\n \"the ToolSearch remedy (server not disconnected)\",\n );\n }\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n const evidence = scanTranscriptEvidence(transcriptText, toolName);\n if (evidence.surfacedByToolSearch || evidence.succeededBefore) {\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n return correctDecision(\n toolName,\n buildCorrection(toolName),\n `Blocked a call to deferred MCP tool ${toolName} and issued the ToolSearch remedy ` +\n \"(server not disconnected)\",\n );\n}\n", "/**\n * mcp-deferred-reminder.ts \u2014 pure pieces of the session-start deferred-handle reminder.\n *\n * A resumed session inherits its transcript, and a transcript can contain a\n * mid-session MCP re-registration: the deferred tool handles the session held\n * are stale from then on (\"deferred tools are no longer available\"). The\n * call-time gate (../pre-tool-use/mcp-deferred-gate.ts) can only correct a call\n * that is still ATTEMPTED \u2014 but a resumed session that already concluded once\n * that \"the MCP server is disconnected\" never retries, so the gate never fires\n * (observed 2026-09-18: 37s of thinking, then a skip on \"aibroker is down\n * right now\"). The cure has to arrive before the first tool call, at session\n * start: a fresh claim that stale history is not evidence, so the model\n * attempts the call and lands in the gate if the handle really is stale.\n *\n * Fired for sources \"resume\" and \"compact\" only \u2014 fresh sessions have no\n * stale handles and no stale conclusions to unlearn.\n *\n * Split out of the session-start entrypoint so the decision is testable\n * without spawning a hook process, per the mcp-deferred-gate.ts precedent.\n */\n\n// Reused so reminder and gate corrections read as one instrument.\nimport { STALE_HANDLE_MARKER } from \"./mcp-deferred-gate.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ReminderHookInput {\n source?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\n/**\n * SessionStart sources the reminder applies to. \"startup\" and \"clear\" begin\n * with an empty session state \u2014 nothing to heal there.\n */\nexport const REMINDER_SOURCES = new Set([\"resume\", \"compact\"]);\n\n/** The exact reminder text; exported so tests can match against the real thing. */\nexport function buildReminder(): string {\n return (\n `<system-reminder>\\n` +\n `Resumed sessions keep stale deferred MCP tool handles: a mid-session MCP server ` +\n `re-registration invalidates every loaded mcp__ schema (\"${STALE_HANDLE_MARKER}\"). ` +\n `If any mcp__ tool call fails or a tool seems unavailable: run ToolSearch with query ` +\n `\"select:<full tool name>\" and retry once. A prior \"the MCP server is disconnected\" ` +\n `conclusion in this history is stale evidence \u2014 do not skip the retry; only a call ` +\n `failing after a fresh ToolSearch proves an outage.\\n` +\n `</system-reminder>`\n );\n}\n\n/**\n * The whole reminder: given hook input, return the stdout payload for sources\n * resume/compact, or null for every other (silent exit 0).\n */\nexport function decideMcpDeferredReminder(input: ReminderHookInput): string | null {\n if (typeof input.source !== \"string\" || !REMINDER_SOURCES.has(input.source)) {\n return null;\n }\n return buildReminder();\n}\n", "/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n\n/**\n * mcp-deferred-reminder.ts \u2014 SessionStart hook (matchers: resume, compact)\n *\n * Resumed sessions can inherit stale deferred MCP tool handles (a mid-session\n * server re-registration) together with a stale \"the MCP server is\n * disconnected\" conclusion from before the restart \u2014 a combination that makes\n * the session skip the retry the call-time gate would need in order to fire.\n * This hook injects the counter-instruction at session start, before any tool\n * call: see ../lib/mcp-deferred-reminder.ts for the rationale and text.\n *\n * stdout is captured by Claude Code and injected into the session context,\n * same channel as session-commands/inject-observations. Never blocks session\n * start: always exits 0.\n */\n\nimport { decideMcpDeferredReminder, type ReminderHookInput } from \"../lib/mcp-deferred-reminder.js\";\nimport { isWorkerSession } from \"../lib/worker-session.js\";\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n let input: ReminderHookInput = {};\n\n try {\n const decoder = new TextDecoder();\n let raw = '';\n const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 500); });\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n raw += decoder.decode(chunk, { stream: true });\n }\n })();\n await Promise.race([readPromise, timeoutPromise]);\n if (raw.trim()) {\n input = JSON.parse(raw) as ReminderHookInput;\n }\n } catch {\n // Junk input: stay silent, exit 0\n process.exit(0);\n }\n\n const reminder = decideMcpDeferredReminder(input);\n if (reminder) {\n console.log(reminder);\n console.error(`mcp-deferred-reminder: injected reminder for source=${input.source}`);\n }\n\n process.exit(0);\n}\n\nmain().catch(() => {\n process.exit(0);\n});\n"],
4
+ "sourcesContent": ["/**\n * mcp-deferred-gate.ts \u2014 pure pieces of the deferred MCP tool gate.\n *\n * Claude Code defers MCP tool schemas: an mcp__server__tool call fails input\n * validation unless the session surfaced the schema via ToolSearch first.\n * Sessions misread that error as \"MCP server disconnected\" and report an\n * outage that never happened (observed with a rename tool while every server\n * was connected).\n *\n * A second failure shape: when an MCP server re-registers mid-session, the\n * session's deferred tool handles are invalidated and calls fail with the\n * harness text \"N deferred tools are no longer available\" (observed 2026-09-18,\n * N=411, a rename tool). Sessions misread that as a disconnect too. The gate\n * denies that shape with the same ToolSearch remedy; a ToolSearch that\n * surfaces the tool after the last invalidation re-arms the pass-through.\n *\n * The decision is evidence-based: the session transcript either shows the tool\n * was surfaced (a ToolSearch mentioning it) or already ran successfully \u2014 in\n * which case the schema is loaded and the call passes through untouched \u2014 or\n * it shows neither, in which case the call is about to hit the gate (or is a\n * retry that just did) and gets the corrective instruction.\n *\n * Split out of the pre-tool-use entrypoint (which calls main() at import\n * time) so the decision is testable without spawning a hook process, per the\n * transcript-text.ts precedent.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GateHookInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n tool_name?: string;\n tool_input?: unknown;\n}\n\nexport interface GateEvidence {\n /** A ToolSearch call in the transcript mentions this tool by full name. */\n surfacedByToolSearch: boolean;\n /** This tool already ran successfully \u2014 its schema must be loaded. */\n succeededBefore: boolean;\n}\n\nexport interface GateObservation {\n type: \"decision\";\n title: string;\n narrative: string;\n tool_name: string;\n tool_input_summary: string;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n}\n\nexport interface GateDecision {\n action: \"pass\" | \"correct\";\n /** The JSON string to write to stdout, same shape as sibling PreToolUse hooks. */\n output: string;\n /** Observation payload for the daemon when a correction fired, else null. */\n observation: GateObservation | null;\n}\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\nexport const PASS_OUTPUT = JSON.stringify({\n hookSpecificOutput: { hookEventName: \"PreToolUse\", permissionDecision: \"allow\" },\n});\n\n/**\n * Harness text emitted when a mid-session MCP server re-registration\n * invalidates the session's deferred tool handles.\n */\nexport const STALE_HANDLE_MARKER = \"deferred tools are no longer available\";\n\n/** True for MCP tool names (`mcp__<server>__<tool>`), false for everything else. */\nexport function isMcpTool(name: string): boolean {\n return name.startsWith(\"mcp__\") && name.split(\"__\").length >= 3 && !!name.split(\"__\")[2];\n}\n\n/**\n * Scan transcript text (JSONL, one entry per line, tolerant of junk) for\n * evidence that `toolName` is loadable in this session.\n */\nexport function scanTranscriptEvidence(transcriptText: string, toolName: string): GateEvidence {\n const toolUseIds = new Set<string>();\n let surfacedByToolSearch = false;\n let succeededBefore = false;\n\n for (const line of transcriptText.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let entry: unknown;\n try {\n entry = JSON.parse(trimmed);\n } catch {\n continue;\n }\n const message = (entry as { message?: { content?: unknown } })?.message;\n const content = message?.content;\n if (!Array.isArray(content)) continue;\n\n for (const block of content as Array<Record<string, unknown>>) {\n if (!block || typeof block !== \"object\") continue;\n if (block.type === \"tool_use\" && typeof block.name === \"string\") {\n if (block.name === toolName && typeof block.id === \"string\") {\n toolUseIds.add(block.id);\n } else if (block.name === \"ToolSearch\") {\n // Liberal match: the query may be \"select:<tool>\" or a keyword list.\n if (JSON.stringify(block.input ?? \"\").includes(toolName)) {\n surfacedByToolSearch = true;\n }\n }\n } else if (\n block.type === \"tool_result\" &&\n typeof block.tool_use_id === \"string\" &&\n toolUseIds.has(block.tool_use_id) &&\n !block.is_error\n ) {\n succeededBefore = true;\n }\n }\n }\n\n return { surfacedByToolSearch, succeededBefore };\n}\n\n/**\n * Byte index of the last transcript line where a ToolSearch call surfaced\n * `toolName` (same liberal match as scanTranscriptEvidence), or -1 if never.\n * Used to order the stale-handle remedy against the last invalidation.\n */\nexport function lastToolSearchSurfacingIndex(\n transcriptText: string,\n toolName: string,\n): number {\n let last = -1;\n let offset = 0;\n for (const line of transcriptText.split(\"\\n\")) {\n const start = offset;\n offset += line.length + 1;\n const trimmed = line.trim();\n if (!trimmed) continue;\n let entry: unknown;\n try {\n entry = JSON.parse(trimmed);\n } catch {\n continue;\n }\n const content = (entry as { message?: { content?: unknown } })?.message?.content;\n if (!Array.isArray(content)) continue;\n for (const block of content as Array<Record<string, unknown>>) {\n if (\n block?.type === \"tool_use\" &&\n block.name === \"ToolSearch\" &&\n JSON.stringify(block.input ?? \"\").includes(toolName)\n ) {\n last = start;\n }\n }\n }\n return last;\n}\n\n/** The ToolSearch remedy shared by both correction shapes. */\nfunction buildRemedy(toolName: string): string {\n return (\n `Remedy: call ToolSearch with query \"select:${toolName}\" to load the fresh registry ` +\n `entry, then call ${toolName} again with the same arguments.`\n );\n}\n\n/** The corrective instruction fed back to the session on a deny. */\nexport function buildCorrection(toolName: string): string {\n return (\n `${toolName} is a deferred MCP tool: its schema is not loaded in this session yet, so this ` +\n `call would fail input validation. The MCP server is NOT disconnected \u2014 do not report an ` +\n `outage and do not retry the call unchanged. ${buildRemedy(toolName)}`\n );\n}\n\n/** Same remedy for the stale-handle shape: a re-registration invalidated a loaded schema. */\nexport function buildStaleHandleCorrection(toolName: string): string {\n return (\n `${toolName}'s deferred tool handle is stale: the MCP server re-registered mid-session, ` +\n `invalidating the schema this session had loaded (\"${STALE_HANDLE_MARKER}\"). The MCP ` +\n `server is NOT disconnected \u2014 do not report an outage and do not retry the call ` +\n `unchanged. ${buildRemedy(toolName)}`\n );\n}\n\n/** The deny decision shared by both failure shapes. */\nfunction correctDecision(toolName: string, reason: string, narrative: string): GateDecision {\n return {\n action: \"correct\",\n output: JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n }),\n observation: {\n type: \"decision\",\n title: `MCP deferred-tool gate corrected: ${toolName}`,\n narrative,\n tool_name: toolName,\n tool_input_summary: toolName,\n files_read: [],\n files_modified: [],\n concepts: [\"mcp\", \"deferred\", \"toolsearch\"],\n },\n };\n}\n\n/**\n * The whole gate: given hook input and the session transcript, decide whether\n * this mcp__ call passes through or gets the deferred-schema correction.\n */\nexport function decideMcpGate(input: GateHookInput, transcriptText: string): GateDecision {\n const toolName = typeof input.tool_name === \"string\" ? input.tool_name : \"\";\n if (!isMcpTool(toolName)) {\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n // Stale-handle shape: a mid-session re-registration invalidated loaded\n // schemas. Overrides the normal evidence pass-through, because a tool that\n // was surfaced (or even ran) earlier can still have a dead handle now. The\n // gate re-arms to pass-through only once a ToolSearch re-surfaced the tool\n // after the last invalidation.\n const toolInputText = JSON.stringify(input.tool_input ?? \"\");\n const lastStale = transcriptText.lastIndexOf(STALE_HANDLE_MARKER);\n if (toolInputText.includes(STALE_HANDLE_MARKER) || lastStale !== -1) {\n const remedyAt = lastToolSearchSurfacingIndex(transcriptText, toolName);\n if (remedyAt === -1 || remedyAt <= lastStale) {\n return correctDecision(\n toolName,\n buildStaleHandleCorrection(toolName),\n `Blocked a retry against a stale deferred-tool handle for ${toolName} and issued ` +\n \"the ToolSearch remedy (server not disconnected)\",\n );\n }\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n const evidence = scanTranscriptEvidence(transcriptText, toolName);\n if (evidence.surfacedByToolSearch || evidence.succeededBefore) {\n return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n }\n\n return correctDecision(\n toolName,\n buildCorrection(toolName),\n `Blocked a call to deferred MCP tool ${toolName} and issued the ToolSearch remedy ` +\n \"(server not disconnected)\",\n );\n}\n", "/**\n * mcp-deferred-reminder.ts \u2014 pure pieces of the session-start deferred-handle reminder.\n *\n * A resumed session inherits its transcript, and a transcript can contain a\n * mid-session MCP re-registration: the deferred tool handles the session held\n * are stale from then on (\"deferred tools are no longer available\"). The\n * call-time gate (../pre-tool-use/mcp-deferred-gate.ts) can only correct a call\n * that is still ATTEMPTED \u2014 but a resumed session that already concluded once\n * that \"the MCP server is disconnected\" never retries, so the gate never fires\n * (observed 2026-09-18: 37s of thinking, then a skip on \"aibroker is down\n * right now\"). The cure has to arrive before the first tool call, at session\n * start: a fresh claim that stale history is not evidence, so the model\n * attempts the call and lands in the gate if the handle really is stale.\n *\n * Fired for sources \"resume\" and \"compact\" only \u2014 fresh sessions have no\n * stale handles and no stale conclusions to unlearn.\n *\n * Split out of the session-start entrypoint so the decision is testable\n * without spawning a hook process, per the mcp-deferred-gate.ts precedent.\n */\n\n// Reused so reminder and gate corrections read as one instrument.\nimport { STALE_HANDLE_MARKER } from \"./mcp-deferred-gate.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ReminderHookInput {\n source?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\n/**\n * SessionStart sources the reminder applies to. \"startup\" and \"clear\" begin\n * with an empty session state \u2014 nothing to heal there.\n */\nexport const REMINDER_SOURCES = new Set([\"resume\", \"compact\"]);\n\n/** The exact reminder text; exported so tests can match against the real thing. */\nexport function buildReminder(): string {\n return (\n `<system-reminder>\\n` +\n `Resumed sessions keep stale deferred MCP tool handles: a mid-session MCP server ` +\n `re-registration invalidates every loaded mcp__ schema (\"${STALE_HANDLE_MARKER}\"). ` +\n `If any mcp__ tool call fails or a tool seems unavailable: run ToolSearch with query ` +\n `\"select:<full tool name>\" and retry once. A prior \"the MCP server is disconnected\" ` +\n `conclusion in this history is stale evidence \u2014 do not skip the retry; only a call ` +\n `failing after a fresh ToolSearch proves an outage.\\n` +\n `</system-reminder>`\n );\n}\n\n/**\n * The whole reminder: given hook input, return the stdout payload for sources\n * resume/compact, or null for every other (silent exit 0).\n */\nexport function decideMcpDeferredReminder(input: ReminderHookInput): string | null {\n if (typeof input.source !== \"string\" || !REMINDER_SOURCES.has(input.source)) {\n return null;\n }\n return buildReminder();\n}\n", "/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n\n/**\n * mcp-deferred-reminder.ts \u2014 SessionStart hook (matchers: resume, compact)\n *\n * Resumed sessions can inherit stale deferred MCP tool handles (a mid-session\n * server re-registration) together with a stale \"the MCP server is\n * disconnected\" conclusion from before the restart \u2014 a combination that makes\n * the session skip the retry the call-time gate would need in order to fire.\n * This hook injects the counter-instruction at session start, before any tool\n * call: see ../lib/mcp-deferred-reminder.ts for the rationale and text.\n *\n * stdout is captured by Claude Code and injected into the session context,\n * same channel as inject-observations. Never blocks session start: always\n * exits 0.\n */\n\nimport { decideMcpDeferredReminder, type ReminderHookInput } from \"../lib/mcp-deferred-reminder.js\";\nimport { isWorkerSession } from \"../lib/worker-session.js\";\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n let input: ReminderHookInput = {};\n\n try {\n const decoder = new TextDecoder();\n let raw = '';\n const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 500); });\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n raw += decoder.decode(chunk, { stream: true });\n }\n })();\n await Promise.race([readPromise, timeoutPromise]);\n if (raw.trim()) {\n input = JSON.parse(raw) as ReminderHookInput;\n }\n } catch {\n // Junk input: stay silent, exit 0\n process.exit(0);\n }\n\n const reminder = decideMcpDeferredReminder(input);\n if (reminder) {\n console.log(reminder);\n console.error(`mcp-deferred-reminder: injected reminder for source=${input.source}`);\n }\n\n process.exit(0);\n}\n\nmain().catch(() => {\n process.exit(0);\n});\n"],
5
5
  "mappings": ";;;;;;AAqEO,IAAM,cAAc,KAAK,UAAU;AAAA,EACxC,oBAAoB,EAAE,eAAe,cAAc,oBAAoB,QAAQ;AACjF,CAAC;AAMM,IAAM,sBAAsB;;;ACrC5B,IAAM,mBAAmB,oBAAI,IAAI,CAAC,UAAU,SAAS,CAAC;AAGtD,SAAS,gBAAwB;AACtC,SACE;AAAA,0IAE2D,mBAAmB;AAAA;AAOlF;AAMO,SAAS,0BAA0B,OAAyC;AACjF,MAAI,OAAO,MAAM,WAAW,YAAY,CAAC,iBAAiB,IAAI,MAAM,MAAM,GAAG;AAC3E,WAAO;AAAA,EACT;AACA,SAAO,cAAc;AACvB;;;AC7CO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACFA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI,QAA2B,CAAC;AAEhC,MAAI;AACF,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,MAAM;AACV,UAAM,iBAAiB,IAAI,QAAc,CAAC,YAAY;AAAE,iBAAW,SAAS,GAAG;AAAA,IAAG,CAAC;AACnF,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,QAAQ,OAAO;AACvC,eAAO,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAAA,MAC/C;AAAA,IACF,GAAG;AACH,UAAM,QAAQ,KAAK,CAAC,aAAa,cAAc,CAAC;AAChD,QAAI,IAAI,KAAK,GAAG;AACd,cAAQ,KAAK,MAAM,GAAG;AAAA,IACxB;AAAA,EACF,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,WAAW,0BAA0B,KAAK;AAChD,MAAI,UAAU;AACZ,YAAQ,IAAI,QAAQ;AACpB,YAAQ,MAAM,uDAAuD,MAAM,MAAM,EAAE;AAAA,EACrF;AAEA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,MAAM;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,9 @@
1
1
  import "./db-Ddns80yf.mjs";
2
2
  import "./config-DCu8THKi.mjs";
3
3
  import { v as readWorkersSection } from "./config-DxMZjpvY.mjs";
4
- import { A as loadStatuses, L as parseRunnerArgs, R as shortText, j as newWorkerId, n as runWorker, t as printResult, x as appendLedger } from "./run-ByBKowN5.mjs";
4
+ import { M as newWorkerId, R as parseRunnerArgs, S as appendLedger, j as loadStatuses, n as runWorker, t as printResult, z as shortText } from "./run-BgExF8Xg.mjs";
5
5
  import { _ as workersLogDir, m as ledgerPath } from "./server-Ch99xt5K.mjs";
6
- import { n as swapPromptArg } from "./chain-CEC1a5mX.mjs";
6
+ import { n as swapPromptArg } from "./chain-DAEC9AU7.mjs";
7
7
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
 
@@ -244,4 +244,4 @@ function statusesOfChildren(logDir, plannerId) {
244
244
 
245
245
  //#endregion
246
246
  export { runPlanner };
247
- //# sourceMappingURL=planner-BlKGgpWI.mjs.map
247
+ //# sourceMappingURL=planner-B4DvNpXd.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"planner-BlKGgpWI.mjs","names":[],"sources":["../src/workers/planner.ts"],"sourcesContent":["/**\n * planner.ts — the plan class: plan, spawn, integrate.\n *\n * `pai worker run --class plan -p \"<goal>\"` does not run one worker but a\n * small orchestration:\n *\n * 1. a planner worker (the plan class's provider) reads the repository and\n * writes `<logDir>/plans/<planner id>.json` — 5–50 sub-tasks, fewer only\n * when the goal itself names a smaller count, each with title, brief,\n * class, files and acceptance;\n * 2. the runner validates the plan and spawns the sub-tasks as children of\n * the planner worker (`parent` = the planner id, so `ps` shows the\n * tree), at most `workers.tree.maxChildren` at a time;\n * 3. each child that finishes delivers its structured report to the\n * planner's inbox as a `kind: \"result\"` handoff (see handoff.ts);\n * 4. the run finishes with a summary report and the branches to merge.\n *\n * The planner's prompt carries the prompt rules from the self-driving\n * codebases write-up: domain-specific instructions only, constraints over\n * step lists, explicit quantity ranges, no checkbox style.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { loadStatuses, newWorkerId, type WorkerStatus } from \"./status.js\";\nimport { parseRunnerArgs, shortText } from \"./args.js\";\nimport { swapPromptArg } from \"./chain.js\";\nimport { runWorker, printResult, type RunOptions, type StreamEvent } from \"./run.js\";\nimport type { WorkerReport } from \"./report.js\";\n\n/** Where a planner run's plan file lives: <logDir>/plans/<planner id>.json. */\nexport function planPathFor(logDir: string, plannerId: string): string {\n return join(logDir, \"plans\", `${plannerId}.json`);\n}\n\nexport interface PlannerTask {\n title: string;\n brief: string;\n /** Task class of the child (implement, research, …); default implement. */\n class?: string;\n /** Files the sub-task likely touches. */\n files?: string[];\n /** How to tell the sub-task is done. */\n acceptance?: string[];\n}\n\nexport const MIN_TASKS = 5;\nexport const MAX_TASKS = 50;\n\n/**\n * Validate a raw plan file into its tasks. Accepts 1–MAX_TASKS tasks: the\n * prompt asks for MIN_TASKS–MAX_TASKS, but a goal may itself name a smaller\n * explicit count (\"one sub-task per file\" with three files), and that\n * explicit range wins. Everything else — missing file, damaged JSON, tasks\n * without a title or brief — throws with a message the operator can act on.\n */\nexport function validatePlan(raw: unknown): PlannerTask[] {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new Error(\"plan file must be a JSON object\");\n }\n const tasksRaw = (raw as Record<string, unknown>).tasks;\n if (!Array.isArray(tasksRaw)) throw new Error('plan file needs a \"tasks\" array');\n if (tasksRaw.length < 1) throw new Error(\"plan file has no tasks\");\n if (tasksRaw.length > MAX_TASKS) {\n throw new Error(`plan file has ${tasksRaw.length} tasks; the maximum is ${MAX_TASKS}`);\n }\n const tasks: PlannerTask[] = [];\n for (let i = 0; i < tasksRaw.length; i++) {\n const t = tasksRaw[i];\n if (typeof t !== \"object\" || t === null || Array.isArray(t)) {\n throw new Error(`task ${i + 1} must be an object`);\n }\n const o = t as Record<string, unknown>;\n const title = typeof o.title === \"string\" ? o.title.trim() : \"\";\n const brief = typeof o.brief === \"string\" ? o.brief.trim() : \"\";\n if (!title) throw new Error(`task ${i + 1} needs a non-empty \"title\"`);\n if (!brief) throw new Error(`task ${i + 1} needs a non-empty \"brief\"`);\n const strArr = (v: unknown): string[] | undefined =>\n Array.isArray(v) && v.every((x) => typeof x === \"string\") ? (v as string[]) : undefined;\n const files = strArr(o.files);\n const acceptance = strArr(o.acceptance);\n tasks.push({\n title,\n brief,\n ...(typeof o.class === \"string\" && o.class.trim() ? { class: o.class.trim() } : {}),\n ...(files?.length ? { files } : {}),\n ...(acceptance?.length ? { acceptance } : {}),\n });\n }\n return tasks;\n}\n\n/** The planner worker's instructions, goal and prompt rules included. */\nexport function plannerPrompt(goal: string, planFile: string, maxChildren: number): string {\n return [\n \"You are a PLANNER worker. Turn the goal below into a plan file and stop —\",\n \"you do not implement anything, and you do not spawn workers yourself:\",\n \"the runner executes your plan on your behalf (the sub-tasks run as your\",\n \"children and report back to you).\",\n \"\",\n `Write the plan with the Write tool to ${planFile} as one JSON object:`,\n '{\"tasks\":[{\"title\":\"…\",\"brief\":\"…\",\"class\":\"implement\",\"files\":[\"…\"],\"acceptance\":[\"…\"]}]}',\n \"\",\n \"Prompt rules for the plan itself (they are yours too):\",\n \"- Domain-specific instructions only: name real files, commands and\",\n \" constraints of this repository, never generic advice.\",\n \"- Constraints over step lists: state what must hold (and what must not\",\n \" change), not a numbered procedure to follow.\",\n \"- Explicit quantity ranges: how many, how much, how long — never\",\n ' \"several\", \"some\", \"as needed\".',\n \"- No checkbox style: no `[ ]` items, no step numbering theatre.\",\n \"\",\n `Emit between ${MIN_TASKS} and ${MAX_TASKS} sub-tasks — unless the goal itself`,\n 'names a smaller explicit count (\"one sub-task per file\" with three files',\n \"means three); an explicit count in the goal always wins.\",\n `At most ${maxChildren} of them run at a time, so independent tasks are better`,\n \"than long chains. Each task's class picks its provider: implement, draft,\",\n \"review, research, spotcheck, simple, complex, image.\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so tasks name real files.\",\n \"Every sub-task runs in its own worktree of the same repository: name files\",\n \"relative to the repository root and never an absolute path or a worktree\",\n \"directory in a title, brief, files or acceptance entry.\",\n \"\",\n \"## Goal\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\n/** The child's prompt for one planned sub-task. */\nexport function taskPrompt(goal: string, task: PlannerTask, index: number, of: number): string {\n return [\n `You are sub-task ${index + 1} of ${of} of a planned goal. Do exactly this sub-task;`,\n \"the other sub-tasks are other workers' business.\",\n \"\",\n `# ${task.title}`,\n \"\",\n task.brief,\n ...(task.files?.length ? [\"\", \"Files likely touched: \" + task.files.join(\", \")] : []),\n ...(task.acceptance?.length\n ? [\"\", \"Done means:\", ...task.acceptance.map((a) => `- ${a}`)]\n : []),\n \"\",\n \"## The overall goal (context only — your scope is the sub-task above)\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\nexport interface PlannerDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests. */\n logDir?: string;\n /** maxChildren override for tests. */\n maxChildren?: number;\n}\n\n/**\n * The planner orchestration; returns the process exit code. The planner id is\n * minted here and preset on the phase-1 run, so the plan file's name is known\n * before the worker starts and the prompt can name its exact path.\n */\nexport async function runPlanner(opts: RunOptions, deps: PlannerDeps = {}): Promise<number> {\n const runStage = deps.runStage ?? runWorker;\n const { workers: config } = readWorkersSection();\n const logDir = deps.logDir ?? workersLogDir(config);\n const maxChildren = deps.maxChildren ?? config.tree.maxChildren;\n const parsed = parseRunnerArgs(opts.claudeArgs);\n const goal = parsed.prompt ?? \"\";\n const plannerId = newWorkerId();\n const planFile = planPathFor(logDir, plannerId);\n mkdirSync(join(logDir, \"plans\"), { recursive: true });\n\n // --- phase 1: the planner worker writes the plan file\n process.stderr.write(`planner ${plannerId}: writing plan (${planFile})\\n`);\n const rc1 = await runStage({\n ...opts,\n id: plannerId,\n claudeArgs: swapPromptArg(opts.claudeArgs, plannerPrompt(goal, planFile, maxChildren)),\n quiet: true,\n _planner: true,\n });\n if (rc1 !== 0) return rc1;\n if (!existsSync(planFile)) {\n process.stderr.write(\n `planner ${plannerId}: no plan file at ${planFile} — the planner worker did not write one. ` +\n `Re-run, or write the plan yourself and run the tasks with pai worker run.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n let tasks: PlannerTask[];\n try {\n tasks = validatePlan(JSON.parse(readFileSync(planFile, \"utf8\")));\n } catch (e) {\n process.stderr.write(`planner ${plannerId}: invalid plan file ${planFile}: ${(e as Error).message}\\n`);\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN\", {\n planner: plannerId,\n tasks: tasks.length,\n plan: planFile,\n });\n\n // --- phase 2: run the sub-tasks as children, maxChildren at a time\n const results: { task: PlannerTask; rc: number }[] = [];\n for (let i = 0; i < tasks.length; i += maxChildren) {\n const wave = tasks.slice(i, i + maxChildren);\n process.stderr.write(\n `planner ${plannerId}: sub-tasks ${i + 1}–${i + wave.length} of ${tasks.length}\\n`\n );\n const rcs = await Promise.all(\n wave.map((task, w) =>\n runStage({\n className: task.class ?? \"implement\",\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: shortText(task.title, 40),\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n claudeArgs: swapPromptArg(opts.claudeArgs, taskPrompt(goal, task, i + w, tasks.length)),\n cwd: opts.cwd,\n worktreeFlag: opts.worktreeFlag,\n parent: plannerId,\n quiet: true,\n })\n )\n );\n for (let w = 0; w < wave.length; w++) results.push({ task: wave[w], rc: rcs[w] });\n }\n\n // --- phase 3: summary from the children's statuses and inbox handoffs\n const failed = results.filter((r) => r.rc !== 0).length;\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", {\n planner: plannerId,\n rc: failed ? 1 : 0,\n failed,\n });\n printPlannerSummary(logDir, plannerId, results, parsed.outputFormat, opts.quiet === true);\n return failed ? 1 : 0;\n}\n\n/** Compose and print the planner's summary report (text or json). */\nfunction printPlannerSummary(\n logDir: string,\n plannerId: string,\n results: { task: PlannerTask; rc: number }[],\n fmt: \"text\" | \"json\" | \"stream-json\",\n quiet: boolean\n): void {\n const children = statusesOfChildren(logDir, plannerId);\n const ok = results.filter((r) => r.rc === 0).length;\n const unmerged = children.filter((c) => c.branch && !c.merged);\n const report: WorkerReport = {\n checks: results.map((r, i) => ({\n name: r.task.title,\n ok: r.rc === 0,\n detail: r.rc === 0 ? shortText(children[i]?.last, 80) : `rc=${r.rc}`,\n })),\n ...(unmerged.length\n ? { open: unmerged.map((c) => `branch to merge: pai worker merge ${c.id} (${c.branch})`) }\n : {}),\n notes: `${ok}/${results.length} sub-tasks ok${unmerged.length ? `; ${unmerged.length} branch(es) to merge` : \"\"}`,\n };\n if (quiet || fmt === \"stream-json\") return;\n const resultEvent: StreamEvent = {\n type: \"result\",\n result: [\n `planner ${plannerId}: ${report.notes}`,\n ...(unmerged.length ? [\"branches to merge:\"] : []),\n ...unmerged.map((c) => ` pai worker merge ${c.id} # ${c.branch}`),\n ].join(\"\\n\"),\n is_error: ok !== results.length,\n };\n printResult(fmt, resultEvent, ok === results.length ? 0 : 1, logDir, plannerId, report, {\n plan: results.length,\n ...(unmerged.length ? { branches: unmerged.map((c) => c.branch!) } : {}),\n });\n}\n\n/** Children of the planner, oldest first, from the status files. */\nfunction statusesOfChildren(logDir: string, plannerId: string): WorkerStatus[] {\n return loadStatuses(logDir).filter((s) => s.parent === plannerId);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"planner-B4DvNpXd.mjs","names":[],"sources":["../src/workers/planner.ts"],"sourcesContent":["/**\n * planner.ts — the plan class: plan, spawn, integrate.\n *\n * `pai worker run --class plan -p \"<goal>\"` does not run one worker but a\n * small orchestration:\n *\n * 1. a planner worker (the plan class's provider) reads the repository and\n * writes `<logDir>/plans/<planner id>.json` — 5–50 sub-tasks, fewer only\n * when the goal itself names a smaller count, each with title, brief,\n * class, files and acceptance;\n * 2. the runner validates the plan and spawns the sub-tasks as children of\n * the planner worker (`parent` = the planner id, so `ps` shows the\n * tree), at most `workers.tree.maxChildren` at a time;\n * 3. each child that finishes delivers its structured report to the\n * planner's inbox as a `kind: \"result\"` handoff (see handoff.ts);\n * 4. the run finishes with a summary report and the branches to merge.\n *\n * The planner's prompt carries the prompt rules from the self-driving\n * codebases write-up: domain-specific instructions only, constraints over\n * step lists, explicit quantity ranges, no checkbox style.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { loadStatuses, newWorkerId, type WorkerStatus } from \"./status.js\";\nimport { parseRunnerArgs, shortText } from \"./args.js\";\nimport { swapPromptArg } from \"./chain.js\";\nimport { runWorker, printResult, type RunOptions, type StreamEvent } from \"./run.js\";\nimport type { WorkerReport } from \"./report.js\";\n\n/** Where a planner run's plan file lives: <logDir>/plans/<planner id>.json. */\nexport function planPathFor(logDir: string, plannerId: string): string {\n return join(logDir, \"plans\", `${plannerId}.json`);\n}\n\nexport interface PlannerTask {\n title: string;\n brief: string;\n /** Task class of the child (implement, research, …); default implement. */\n class?: string;\n /** Files the sub-task likely touches. */\n files?: string[];\n /** How to tell the sub-task is done. */\n acceptance?: string[];\n}\n\nexport const MIN_TASKS = 5;\nexport const MAX_TASKS = 50;\n\n/**\n * Validate a raw plan file into its tasks. Accepts 1–MAX_TASKS tasks: the\n * prompt asks for MIN_TASKS–MAX_TASKS, but a goal may itself name a smaller\n * explicit count (\"one sub-task per file\" with three files), and that\n * explicit range wins. Everything else — missing file, damaged JSON, tasks\n * without a title or brief — throws with a message the operator can act on.\n */\nexport function validatePlan(raw: unknown): PlannerTask[] {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new Error(\"plan file must be a JSON object\");\n }\n const tasksRaw = (raw as Record<string, unknown>).tasks;\n if (!Array.isArray(tasksRaw)) throw new Error('plan file needs a \"tasks\" array');\n if (tasksRaw.length < 1) throw new Error(\"plan file has no tasks\");\n if (tasksRaw.length > MAX_TASKS) {\n throw new Error(`plan file has ${tasksRaw.length} tasks; the maximum is ${MAX_TASKS}`);\n }\n const tasks: PlannerTask[] = [];\n for (let i = 0; i < tasksRaw.length; i++) {\n const t = tasksRaw[i];\n if (typeof t !== \"object\" || t === null || Array.isArray(t)) {\n throw new Error(`task ${i + 1} must be an object`);\n }\n const o = t as Record<string, unknown>;\n const title = typeof o.title === \"string\" ? o.title.trim() : \"\";\n const brief = typeof o.brief === \"string\" ? o.brief.trim() : \"\";\n if (!title) throw new Error(`task ${i + 1} needs a non-empty \"title\"`);\n if (!brief) throw new Error(`task ${i + 1} needs a non-empty \"brief\"`);\n const strArr = (v: unknown): string[] | undefined =>\n Array.isArray(v) && v.every((x) => typeof x === \"string\") ? (v as string[]) : undefined;\n const files = strArr(o.files);\n const acceptance = strArr(o.acceptance);\n tasks.push({\n title,\n brief,\n ...(typeof o.class === \"string\" && o.class.trim() ? { class: o.class.trim() } : {}),\n ...(files?.length ? { files } : {}),\n ...(acceptance?.length ? { acceptance } : {}),\n });\n }\n return tasks;\n}\n\n/** The planner worker's instructions, goal and prompt rules included. */\nexport function plannerPrompt(goal: string, planFile: string, maxChildren: number): string {\n return [\n \"You are a PLANNER worker. Turn the goal below into a plan file and stop —\",\n \"you do not implement anything, and you do not spawn workers yourself:\",\n \"the runner executes your plan on your behalf (the sub-tasks run as your\",\n \"children and report back to you).\",\n \"\",\n `Write the plan with the Write tool to ${planFile} as one JSON object:`,\n '{\"tasks\":[{\"title\":\"…\",\"brief\":\"…\",\"class\":\"implement\",\"files\":[\"…\"],\"acceptance\":[\"…\"]}]}',\n \"\",\n \"Prompt rules for the plan itself (they are yours too):\",\n \"- Domain-specific instructions only: name real files, commands and\",\n \" constraints of this repository, never generic advice.\",\n \"- Constraints over step lists: state what must hold (and what must not\",\n \" change), not a numbered procedure to follow.\",\n \"- Explicit quantity ranges: how many, how much, how long — never\",\n ' \"several\", \"some\", \"as needed\".',\n \"- No checkbox style: no `[ ]` items, no step numbering theatre.\",\n \"\",\n `Emit between ${MIN_TASKS} and ${MAX_TASKS} sub-tasks — unless the goal itself`,\n 'names a smaller explicit count (\"one sub-task per file\" with three files',\n \"means three); an explicit count in the goal always wins.\",\n `At most ${maxChildren} of them run at a time, so independent tasks are better`,\n \"than long chains. Each task's class picks its provider: implement, draft,\",\n \"review, research, spotcheck, simple, complex, image.\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so tasks name real files.\",\n \"Every sub-task runs in its own worktree of the same repository: name files\",\n \"relative to the repository root and never an absolute path or a worktree\",\n \"directory in a title, brief, files or acceptance entry.\",\n \"\",\n \"## Goal\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\n/** The child's prompt for one planned sub-task. */\nexport function taskPrompt(goal: string, task: PlannerTask, index: number, of: number): string {\n return [\n `You are sub-task ${index + 1} of ${of} of a planned goal. Do exactly this sub-task;`,\n \"the other sub-tasks are other workers' business.\",\n \"\",\n `# ${task.title}`,\n \"\",\n task.brief,\n ...(task.files?.length ? [\"\", \"Files likely touched: \" + task.files.join(\", \")] : []),\n ...(task.acceptance?.length\n ? [\"\", \"Done means:\", ...task.acceptance.map((a) => `- ${a}`)]\n : []),\n \"\",\n \"## The overall goal (context only — your scope is the sub-task above)\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\nexport interface PlannerDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests. */\n logDir?: string;\n /** maxChildren override for tests. */\n maxChildren?: number;\n}\n\n/**\n * The planner orchestration; returns the process exit code. The planner id is\n * minted here and preset on the phase-1 run, so the plan file's name is known\n * before the worker starts and the prompt can name its exact path.\n */\nexport async function runPlanner(opts: RunOptions, deps: PlannerDeps = {}): Promise<number> {\n const runStage = deps.runStage ?? runWorker;\n const { workers: config } = readWorkersSection();\n const logDir = deps.logDir ?? workersLogDir(config);\n const maxChildren = deps.maxChildren ?? config.tree.maxChildren;\n const parsed = parseRunnerArgs(opts.claudeArgs);\n const goal = parsed.prompt ?? \"\";\n const plannerId = newWorkerId();\n const planFile = planPathFor(logDir, plannerId);\n mkdirSync(join(logDir, \"plans\"), { recursive: true });\n\n // --- phase 1: the planner worker writes the plan file\n process.stderr.write(`planner ${plannerId}: writing plan (${planFile})\\n`);\n const rc1 = await runStage({\n ...opts,\n id: plannerId,\n claudeArgs: swapPromptArg(opts.claudeArgs, plannerPrompt(goal, planFile, maxChildren)),\n quiet: true,\n _planner: true,\n });\n if (rc1 !== 0) return rc1;\n if (!existsSync(planFile)) {\n process.stderr.write(\n `planner ${plannerId}: no plan file at ${planFile} — the planner worker did not write one. ` +\n `Re-run, or write the plan yourself and run the tasks with pai worker run.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n let tasks: PlannerTask[];\n try {\n tasks = validatePlan(JSON.parse(readFileSync(planFile, \"utf8\")));\n } catch (e) {\n process.stderr.write(`planner ${plannerId}: invalid plan file ${planFile}: ${(e as Error).message}\\n`);\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN\", {\n planner: plannerId,\n tasks: tasks.length,\n plan: planFile,\n });\n\n // --- phase 2: run the sub-tasks as children, maxChildren at a time\n const results: { task: PlannerTask; rc: number }[] = [];\n for (let i = 0; i < tasks.length; i += maxChildren) {\n const wave = tasks.slice(i, i + maxChildren);\n process.stderr.write(\n `planner ${plannerId}: sub-tasks ${i + 1}–${i + wave.length} of ${tasks.length}\\n`\n );\n const rcs = await Promise.all(\n wave.map((task, w) =>\n runStage({\n className: task.class ?? \"implement\",\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: shortText(task.title, 40),\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n claudeArgs: swapPromptArg(opts.claudeArgs, taskPrompt(goal, task, i + w, tasks.length)),\n cwd: opts.cwd,\n worktreeFlag: opts.worktreeFlag,\n parent: plannerId,\n quiet: true,\n })\n )\n );\n for (let w = 0; w < wave.length; w++) results.push({ task: wave[w], rc: rcs[w] });\n }\n\n // --- phase 3: summary from the children's statuses and inbox handoffs\n const failed = results.filter((r) => r.rc !== 0).length;\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", {\n planner: plannerId,\n rc: failed ? 1 : 0,\n failed,\n });\n printPlannerSummary(logDir, plannerId, results, parsed.outputFormat, opts.quiet === true);\n return failed ? 1 : 0;\n}\n\n/** Compose and print the planner's summary report (text or json). */\nfunction printPlannerSummary(\n logDir: string,\n plannerId: string,\n results: { task: PlannerTask; rc: number }[],\n fmt: \"text\" | \"json\" | \"stream-json\",\n quiet: boolean\n): void {\n const children = statusesOfChildren(logDir, plannerId);\n const ok = results.filter((r) => r.rc === 0).length;\n const unmerged = children.filter((c) => c.branch && !c.merged);\n const report: WorkerReport = {\n checks: results.map((r, i) => ({\n name: r.task.title,\n ok: r.rc === 0,\n detail: r.rc === 0 ? shortText(children[i]?.last, 80) : `rc=${r.rc}`,\n })),\n ...(unmerged.length\n ? { open: unmerged.map((c) => `branch to merge: pai worker merge ${c.id} (${c.branch})`) }\n : {}),\n notes: `${ok}/${results.length} sub-tasks ok${unmerged.length ? `; ${unmerged.length} branch(es) to merge` : \"\"}`,\n };\n if (quiet || fmt === \"stream-json\") return;\n const resultEvent: StreamEvent = {\n type: \"result\",\n result: [\n `planner ${plannerId}: ${report.notes}`,\n ...(unmerged.length ? [\"branches to merge:\"] : []),\n ...unmerged.map((c) => ` pai worker merge ${c.id} # ${c.branch}`),\n ].join(\"\\n\"),\n is_error: ok !== results.length,\n };\n printResult(fmt, resultEvent, ok === results.length ? 0 : 1, logDir, plannerId, report, {\n plan: results.length,\n ...(unmerged.length ? { branches: unmerged.map((c) => c.branch!) } : {}),\n });\n}\n\n/** Children of the planner, oldest first, from the status files. */\nfunction statusesOfChildren(logDir: string, plannerId: string): WorkerStatus[] {\n return loadStatuses(logDir).filter((s) => s.parent === plannerId);\n}\n"],"mappings":"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"}