@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4

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 (342) hide show
  1. package/CHANGELOG.md +1645 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1529 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +194 -0
  43. package/dist/history/ingest.js +447 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +461 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/broker/broker.js +426 -0
  53. package/dist/pi-intercom/broker/client.js +492 -0
  54. package/dist/pi-intercom/broker/framing.js +65 -0
  55. package/dist/pi-intercom/broker/paths.js +42 -0
  56. package/dist/pi-intercom/broker/pid.js +66 -0
  57. package/dist/pi-intercom/broker/spawn.js +221 -0
  58. package/dist/pi-intercom/config.js +102 -0
  59. package/dist/pi-intercom/index.js +2942 -0
  60. package/dist/pi-intercom/reply-tracker.js +182 -0
  61. package/dist/pi-intercom/session-targets.js +128 -0
  62. package/dist/pi-intercom/subagent-live-events.js +132 -0
  63. package/dist/pi-intercom/topics.js +157 -0
  64. package/dist/pi-intercom/types.js +163 -0
  65. package/dist/pi-intercom/ui/compose.js +269 -0
  66. package/dist/pi-intercom/ui/inline-message.js +80 -0
  67. package/dist/pi-intercom/ui/session-list.js +124 -0
  68. package/dist/runs/background/async-control.js +95 -0
  69. package/dist/runs/background/async-execution.js +676 -0
  70. package/dist/runs/background/async-job-tracker.js +383 -0
  71. package/dist/runs/background/async-result-file.js +58 -0
  72. package/dist/runs/background/async-resume.js +305 -0
  73. package/dist/runs/background/async-status.js +442 -0
  74. package/dist/runs/background/completion-dedupe.js +69 -0
  75. package/dist/runs/background/completion-delivery.js +245 -0
  76. package/dist/runs/background/notify.js +71 -0
  77. package/dist/runs/background/parallel-groups.js +55 -0
  78. package/dist/runs/background/result-watcher.js +426 -0
  79. package/dist/runs/background/run-id-resolver.js +64 -0
  80. package/dist/runs/background/run-status.js +382 -0
  81. package/dist/runs/background/stale-run-reconciler.js +335 -0
  82. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  83. package/dist/runs/background/subagent-runner.js +1872 -0
  84. package/dist/runs/background/top-level-async.js +5 -0
  85. package/dist/runs/foreground/chain-clarify.js +1152 -0
  86. package/dist/runs/foreground/clarify-invocation.js +72 -0
  87. package/dist/runs/foreground/execution-input.js +423 -0
  88. package/dist/runs/foreground/foreground-control.js +817 -0
  89. package/dist/runs/foreground/question-control.js +77 -0
  90. package/dist/runs/foreground/run-async-path.js +205 -0
  91. package/dist/runs/foreground/subagent-executor.js +442 -0
  92. package/dist/runs/foreground/subagent-params.js +165 -0
  93. package/dist/runs/foreground/wait-run.js +139 -0
  94. package/dist/runs/shared/acceptance-contract.js +253 -0
  95. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  96. package/dist/runs/shared/acceptance-finalization.js +280 -0
  97. package/dist/runs/shared/acceptance-reports.js +147 -0
  98. package/dist/runs/shared/acceptance.js +4 -0
  99. package/dist/runs/shared/chain-outputs.js +110 -0
  100. package/dist/runs/shared/child-attempt.js +693 -0
  101. package/dist/runs/shared/child-execution-cwd.js +96 -0
  102. package/dist/runs/shared/child-observations.js +114 -0
  103. package/dist/runs/shared/claude-code.js +199 -0
  104. package/dist/runs/shared/completion-guard.js +31 -0
  105. package/dist/runs/shared/dynamic-fanout.js +274 -0
  106. package/dist/runs/shared/history-index.js +72 -0
  107. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  108. package/dist/runs/shared/model-fallback.js +185 -0
  109. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  110. package/dist/runs/shared/native-finalization.js +101 -0
  111. package/dist/runs/shared/native-usage.js +153 -0
  112. package/dist/runs/shared/nested-events.js +700 -0
  113. package/dist/runs/shared/nested-path.js +48 -0
  114. package/dist/runs/shared/nested-render.js +108 -0
  115. package/dist/runs/shared/parallel-utils.js +61 -0
  116. package/dist/runs/shared/parent-receipts.js +68 -0
  117. package/dist/runs/shared/parent-usage.js +121 -0
  118. package/dist/runs/shared/pi-args.js +310 -0
  119. package/dist/runs/shared/pi-spawn.js +52 -0
  120. package/dist/runs/shared/run-history-queries.js +45 -0
  121. package/dist/runs/shared/run-history.js +103 -0
  122. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  123. package/dist/runs/shared/run-records.js +679 -0
  124. package/dist/runs/shared/session-cwd-preload.js +33 -0
  125. package/dist/runs/shared/single-output.js +222 -0
  126. package/dist/runs/shared/streaming-text.js +15 -0
  127. package/dist/runs/shared/structured-output.js +55 -0
  128. package/dist/runs/shared/subagent-control.js +179 -0
  129. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  130. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  131. package/dist/runs/shared/supervisor-questions.js +320 -0
  132. package/dist/runs/shared/workflow-graph.js +214 -0
  133. package/dist/runs/shared/workflow-policy.js +84 -0
  134. package/dist/runs/shared/worktree.js +525 -0
  135. package/dist/shared/agent-context-policy.js +126 -0
  136. package/dist/shared/artifacts.js +96 -0
  137. package/dist/shared/atomic-json.js +13 -0
  138. package/dist/shared/cooperative.js +20 -0
  139. package/dist/shared/execution-cwd.js +14 -0
  140. package/dist/shared/file-coalescer.js +21 -0
  141. package/dist/shared/fork-context.js +49 -0
  142. package/dist/shared/formatters.js +85 -0
  143. package/dist/shared/journal-reader.js +407 -0
  144. package/dist/shared/lazy-tools.js +32 -0
  145. package/dist/shared/model-info.js +68 -0
  146. package/dist/shared/native-session.js +15 -0
  147. package/dist/shared/native-tui.js +18 -0
  148. package/dist/shared/native-typebox.js +20 -0
  149. package/dist/shared/post-exit-stdio-guard.js +197 -0
  150. package/dist/shared/prompt-sections.js +8 -0
  151. package/dist/shared/session-entries.js +52 -0
  152. package/dist/shared/session-identity.js +14 -0
  153. package/dist/shared/settings.js +254 -0
  154. package/dist/shared/status-format.js +111 -0
  155. package/dist/shared/temp-root.js +114 -0
  156. package/dist/shared/types.js +207 -0
  157. package/dist/shared/ui-mode.js +3 -0
  158. package/dist/shared/utils.js +334 -0
  159. package/dist/slash/prompt-template-bridge.js +298 -0
  160. package/dist/slash/slash-bridge.js +116 -0
  161. package/dist/slash/slash-commands.js +607 -0
  162. package/dist/slash/slash-live-state.js +246 -0
  163. package/dist/tui/action-hints.js +75 -0
  164. package/dist/tui/agent-history.js +197 -0
  165. package/dist/tui/agent-view.js +1656 -0
  166. package/dist/tui/render.js +1515 -0
  167. package/docs/intercom.md +579 -0
  168. package/docs/performance.md +121 -0
  169. package/package.json +101 -4
  170. package/prompts/gather-context-and-clarify.md +14 -0
  171. package/prompts/parallel-cleanup.md +62 -0
  172. package/prompts/parallel-context-build.md +56 -0
  173. package/prompts/parallel-handoff-plan.md +62 -0
  174. package/prompts/parallel-research.md +51 -0
  175. package/prompts/parallel-review.md +59 -0
  176. package/prompts/review-loop.md +42 -0
  177. package/scripts/build.mjs +134 -0
  178. package/scripts/check-compat.mjs +48 -0
  179. package/scripts/compat-host.mjs +19 -0
  180. package/scripts/linux-smoke.sh +51 -0
  181. package/scripts/local-install-smoke.mjs +66 -0
  182. package/scripts/native-package-smoke.mjs +82 -0
  183. package/scripts/package-smoke.mjs +205 -0
  184. package/scripts/prepare.mjs +67 -0
  185. package/scripts/real-pi-smoke.mjs +415 -0
  186. package/scripts/run-tests.mjs +149 -0
  187. package/skills/pi-intercom/SKILL.md +113 -0
  188. package/skills/pi-intercom/evals/evals.json +29 -0
  189. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  190. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  191. package/skills/pi-subagents/SKILL.md +76 -0
  192. package/skills/pi-subagents/evals/evals.json +29 -0
  193. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  194. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  195. package/src/agents/agent-management.ts +813 -0
  196. package/src/agents/agent-scope.ts +7 -0
  197. package/src/agents/agent-selection.ts +23 -0
  198. package/src/agents/agent-serializer.ts +99 -0
  199. package/src/agents/agents.ts +916 -0
  200. package/src/agents/chain-serializer.ts +329 -0
  201. package/src/agents/frontmatter.ts +21 -0
  202. package/src/agents/identity.ts +30 -0
  203. package/src/agents/skills.ts +637 -0
  204. package/src/extension/build-info.ts +2 -0
  205. package/src/extension/compact-tools.ts +114 -0
  206. package/src/extension/config.ts +23 -0
  207. package/src/extension/control-notices.ts +47 -0
  208. package/src/extension/doctor.ts +171 -0
  209. package/src/extension/fanout-child.ts +312 -0
  210. package/src/extension/index.ts +649 -0
  211. package/src/extension/schemas.ts +444 -0
  212. package/src/extension/tool-input.ts +19 -0
  213. package/src/extension/tool-result.ts +16 -0
  214. package/src/history/canonical-result.ts +62 -0
  215. package/src/history/database.ts +59 -0
  216. package/src/history/index.ts +152 -0
  217. package/src/history/ingest.ts +332 -0
  218. package/src/history/queries.ts +241 -0
  219. package/src/history/store.ts +203 -0
  220. package/src/history/types.ts +166 -0
  221. package/src/history/worker.ts +317 -0
  222. package/src/intercom/intercom-bridge.ts +123 -0
  223. package/src/intercom/live-intercom.ts +104 -0
  224. package/src/intercom/result-intercom.ts +429 -0
  225. package/src/pi-intercom/agent-dir.ts +9 -0
  226. package/src/pi-intercom/broker/broker.ts +463 -0
  227. package/src/pi-intercom/broker/client.ts +558 -0
  228. package/src/pi-intercom/broker/framing.ts +77 -0
  229. package/src/pi-intercom/broker/paths.ts +47 -0
  230. package/src/pi-intercom/broker/pid.ts +59 -0
  231. package/src/pi-intercom/broker/spawn.ts +248 -0
  232. package/src/pi-intercom/config.ts +142 -0
  233. package/src/pi-intercom/index.ts +2929 -0
  234. package/src/pi-intercom/reply-tracker.ts +211 -0
  235. package/src/pi-intercom/session-targets.ts +172 -0
  236. package/src/pi-intercom/subagent-live-events.ts +133 -0
  237. package/src/pi-intercom/topics.ts +125 -0
  238. package/src/pi-intercom/types.ts +271 -0
  239. package/src/pi-intercom/ui/compose.ts +289 -0
  240. package/src/pi-intercom/ui/inline-message.ts +92 -0
  241. package/src/pi-intercom/ui/session-list.ts +139 -0
  242. package/src/runs/background/async-control.ts +89 -0
  243. package/src/runs/background/async-execution.ts +850 -0
  244. package/src/runs/background/async-job-tracker.ts +385 -0
  245. package/src/runs/background/async-result-file.ts +62 -0
  246. package/src/runs/background/async-resume.ts +333 -0
  247. package/src/runs/background/async-status.ts +481 -0
  248. package/src/runs/background/completion-dedupe.ts +79 -0
  249. package/src/runs/background/completion-delivery.ts +195 -0
  250. package/src/runs/background/notify.ts +123 -0
  251. package/src/runs/background/parallel-groups.ts +53 -0
  252. package/src/runs/background/result-watcher.ts +423 -0
  253. package/src/runs/background/run-id-resolver.ts +69 -0
  254. package/src/runs/background/run-status.ts +384 -0
  255. package/src/runs/background/stale-run-reconciler.ts +369 -0
  256. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  257. package/src/runs/background/subagent-runner.ts +2053 -0
  258. package/src/runs/background/top-level-async.ts +13 -0
  259. package/src/runs/foreground/chain-clarify.ts +1340 -0
  260. package/src/runs/foreground/clarify-invocation.ts +77 -0
  261. package/src/runs/foreground/execution-input.ts +467 -0
  262. package/src/runs/foreground/foreground-control.ts +821 -0
  263. package/src/runs/foreground/question-control.ts +70 -0
  264. package/src/runs/foreground/run-async-path.ts +243 -0
  265. package/src/runs/foreground/subagent-executor.ts +501 -0
  266. package/src/runs/foreground/subagent-params.ts +320 -0
  267. package/src/runs/foreground/wait-run.ts +121 -0
  268. package/src/runs/shared/acceptance-contract.ts +257 -0
  269. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  270. package/src/runs/shared/acceptance-finalization.ts +353 -0
  271. package/src/runs/shared/acceptance-reports.ts +152 -0
  272. package/src/runs/shared/acceptance.ts +33 -0
  273. package/src/runs/shared/chain-outputs.ts +114 -0
  274. package/src/runs/shared/child-attempt.ts +623 -0
  275. package/src/runs/shared/child-execution-cwd.ts +91 -0
  276. package/src/runs/shared/child-observations.ts +99 -0
  277. package/src/runs/shared/claude-code.ts +243 -0
  278. package/src/runs/shared/completion-guard.ts +35 -0
  279. package/src/runs/shared/dynamic-fanout.ts +295 -0
  280. package/src/runs/shared/history-index.ts +69 -0
  281. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  282. package/src/runs/shared/model-fallback.ts +219 -0
  283. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  284. package/src/runs/shared/native-finalization.ts +128 -0
  285. package/src/runs/shared/native-usage.ts +113 -0
  286. package/src/runs/shared/nested-events.ts +752 -0
  287. package/src/runs/shared/nested-path.ts +52 -0
  288. package/src/runs/shared/nested-render.ts +119 -0
  289. package/src/runs/shared/parallel-utils.ts +153 -0
  290. package/src/runs/shared/parent-receipts.ts +59 -0
  291. package/src/runs/shared/parent-usage.ts +121 -0
  292. package/src/runs/shared/pi-args.ts +350 -0
  293. package/src/runs/shared/pi-spawn.ts +52 -0
  294. package/src/runs/shared/run-history-queries.ts +44 -0
  295. package/src/runs/shared/run-history.ts +93 -0
  296. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  297. package/src/runs/shared/run-records.ts +607 -0
  298. package/src/runs/shared/session-cwd-preload.ts +27 -0
  299. package/src/runs/shared/single-output.ts +267 -0
  300. package/src/runs/shared/streaming-text.ts +18 -0
  301. package/src/runs/shared/structured-output.ts +68 -0
  302. package/src/runs/shared/subagent-control.ts +219 -0
  303. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  304. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  305. package/src/runs/shared/supervisor-questions.ts +372 -0
  306. package/src/runs/shared/workflow-graph.ts +220 -0
  307. package/src/runs/shared/workflow-policy.ts +125 -0
  308. package/src/runs/shared/worktree.ts +635 -0
  309. package/src/shared/agent-context-policy.ts +184 -0
  310. package/src/shared/artifacts.ts +95 -0
  311. package/src/shared/atomic-json.ts +16 -0
  312. package/src/shared/cooperative.ts +21 -0
  313. package/src/shared/execution-cwd.ts +14 -0
  314. package/src/shared/file-coalescer.ts +29 -0
  315. package/src/shared/fork-context.ts +68 -0
  316. package/src/shared/formatters.ts +84 -0
  317. package/src/shared/journal-reader.ts +333 -0
  318. package/src/shared/lazy-tools.ts +36 -0
  319. package/src/shared/model-info.ts +81 -0
  320. package/src/shared/native-session.ts +8 -0
  321. package/src/shared/native-tui.ts +10 -0
  322. package/src/shared/native-typebox.ts +14 -0
  323. package/src/shared/post-exit-stdio-guard.ts +204 -0
  324. package/src/shared/prompt-sections.ts +10 -0
  325. package/src/shared/session-entries.ts +49 -0
  326. package/src/shared/session-identity.ts +18 -0
  327. package/src/shared/settings.ts +384 -0
  328. package/src/shared/status-format.ts +123 -0
  329. package/src/shared/temp-root.ts +107 -0
  330. package/src/shared/types.ts +1259 -0
  331. package/src/shared/ui-mode.ts +5 -0
  332. package/src/shared/utils.ts +342 -0
  333. package/src/slash/prompt-template-bridge.ts +408 -0
  334. package/src/slash/slash-bridge.ts +178 -0
  335. package/src/slash/slash-commands.ts +617 -0
  336. package/src/slash/slash-live-state.ts +292 -0
  337. package/src/tui/action-hints.ts +69 -0
  338. package/src/tui/agent-history.ts +210 -0
  339. package/src/tui/agent-view.ts +1323 -0
  340. package/src/tui/render.ts +1550 -0
  341. package/tsconfig.build.json +12 -0
  342. package/tsconfig.json +19 -0
@@ -0,0 +1,201 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "../../shared/native-typebox.ts";
5
+ import { SUBAGENT_FANOUT_CHILD_ENV } from "./pi-args.ts";
6
+ import { setPromptSection } from "../../shared/prompt-sections.ts";
7
+ import { loadConfig } from "../../extension/config.ts";
8
+ import { registerChildExecutionCwd } from "./child-execution-cwd.ts";
9
+ import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
10
+ import type { JsonSchemaObject } from "../../shared/types.ts";
11
+ import { SessionEntryCursor } from "../../shared/session-entries.ts";
12
+
13
+ const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
14
+ const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
15
+ export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
16
+
17
+ const STRUCTURED_OUTPUT_INSTRUCTIONS = [
18
+ "This subagent step has a strict structured output contract.",
19
+ "Your final action must be to call the `structured_output` tool with JSON matching the provided schema.",
20
+ "Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
21
+ ].join("\n");
22
+
23
+ export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
24
+ "You are a child subagent, not the parent orchestrator.",
25
+ "The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
26
+ "Ignore prior parent-only orchestration instructions in inherited conversation history.",
27
+ "Do not propose or run subagents. Complete only your assigned role-specific task with the tools available to you.",
28
+ "If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
29
+ ].join("\n");
30
+
31
+ export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
32
+ "You are a child subagent with delegation enabled for your assigned task.",
33
+ "You may delegate useful helper work within that task when it saves time or improves quality, using the available delegation tools.",
34
+ "You remain responsible for your assigned result. The original parent owns integration, review synthesis, and final delivery.",
35
+ "Do not broaden the assigned scope or repeat approval requests for already-authorized work.",
36
+ "The native allowSubagents and maxSubagentDepth settings still apply.",
37
+ "If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
38
+ ].join("\n");
39
+
40
+ const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
41
+ "subagent-orchestration-instructions",
42
+ "subagent-slash-result",
43
+ "subagent-notify",
44
+ "subagent_control_notice",
45
+ "subagent-control",
46
+ "subagent-control-notice",
47
+ ]);
48
+ const ORCHESTRATION_TOOLS = new Set(["subagent", "delegate", "agent_runs", "load_subagent"]);
49
+
50
+ function readBooleanEnv(name: string): boolean | undefined {
51
+ const value = process.env[name];
52
+ if (value === undefined) return undefined;
53
+ return value !== "0";
54
+ }
55
+
56
+ function isParentOnlySubagentMessage(message: unknown): boolean {
57
+ const m = message as { role?: string; customType?: string };
58
+ return m?.role === "custom"
59
+ && typeof m.customType === "string"
60
+ && PARENT_ONLY_CUSTOM_MESSAGE_TYPES.has(m.customType);
61
+ }
62
+
63
+ function isSubagentExecutionResultMessage(message: unknown): boolean {
64
+ const m = message as { role?: string; toolName?: string };
65
+ return m?.role === "toolResult" && typeof m.toolName === "string" && ORCHESTRATION_TOOLS.has(m.toolName);
66
+ }
67
+
68
+ function isSubagentToolCallBlock(block: unknown): boolean {
69
+ const b = block as { type?: string; name?: string };
70
+ return b?.type === "toolCall" && typeof b.name === "string" && ORCHESTRATION_TOOLS.has(b.name);
71
+ }
72
+
73
+ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undefined {
74
+ const m = message as { role?: string; content?: unknown };
75
+ if (m?.role !== "assistant" || !Array.isArray(m.content)) return message;
76
+ const filteredContent = m.content.filter((block) => !isSubagentToolCallBlock(block));
77
+ if (filteredContent.length === m.content.length) return message;
78
+ if (filteredContent.length === 0) return undefined;
79
+ return { ...m, content: filteredContent };
80
+ }
81
+
82
+ function stripParentOnlySubagentMessages<T>(messages: T[], fanoutChild = false): T[] {
83
+ let changed = false;
84
+ const filtered: T[] = [];
85
+ for (const message of messages) {
86
+ if (isParentOnlySubagentMessage(message) || (!fanoutChild && isSubagentExecutionResultMessage(message))) {
87
+ changed = true;
88
+ continue;
89
+ }
90
+ const stripped = fanoutChild ? message : stripAssistantSubagentToolCallBlocks(message);
91
+ if (stripped === undefined) {
92
+ changed = true;
93
+ continue;
94
+ }
95
+ if (stripped !== message) changed = true;
96
+ filtered.push(stripped as T);
97
+ }
98
+ return changed ? filtered : messages;
99
+ }
100
+
101
+ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
102
+ registerChildExecutionCwd(pi);
103
+ // Print-mode observation uses the existing child runtime. turn_end follows native
104
+ // message append; message_end itself is deliberately never called a commit receipt.
105
+ let observedMessages = 0;
106
+ const cursor = new SessionEntryCursor();
107
+ let previousIds = new Set<string>();
108
+ pi.on("session_start", (_event, ctx) => {
109
+ cursor.reset();
110
+ observedMessages = 0;
111
+ const { entries } = cursor.read(ctx.sessionManager);
112
+ const baseline = process.env.PI_SUBAGENT_NATIVE_BASELINE_COUNT;
113
+ if (baseline && /^\d+$/.test(baseline)) process.stdout.write(`${JSON.stringify({ type: "subagent.native_baseline", sessionId: ctx.sessionManager.getSessionId(), entryIds: entries.slice(0, Number(baseline)).map((entry) => entry.id) })}\n`);
114
+ const file = ctx.sessionManager.getSessionFile();
115
+ previousIds = file && fs.existsSync(file) ? new Set(entries.map((entry) => entry.id)) : new Set();
116
+ if (!file || !fs.existsSync(file)) cursor.reset();
117
+ });
118
+ pi.on("message_end", (event) => { if (["assistant", "user", "toolResult"].includes(event.message.role)) observedMessages++; });
119
+ const observe = (ctx: import("@earendil-works/pi-coding-agent").ExtensionContext, boundary: string) => {
120
+ if (process.env.PI_SUBAGENT_CHILD !== "1") return;
121
+ const entries = [];
122
+ for (const entry of cursor.read(ctx.sessionManager).entries) if (!previousIds.has(entry.id)) {
123
+ previousIds.add(entry.id);
124
+ const message = entry.type === "message" ? entry.message : undefined;
125
+ entries.push({ id: entry.id, type: entry.type, parentId: entry.parentId,
126
+ ...("checkpoint" in entry ? { checkpoint: entry.checkpoint } : {}),
127
+ ...(entry.type === "usage" || entry.type === "compaction" || entry.type === "branch_summary" ? { usage: entry.usage } : {}),
128
+ ...(entry.type === "usage" ? { provider: entry.provider, model: entry.model } : {}),
129
+ ...(message ? { message: { role: message.role, timestamp: message.timestamp,
130
+ ...("usage" in message ? { usage: message.usage } : {}),
131
+ ...(message.role === "assistant" ? { provider: message.provider, model: message.model, responseModel: message.responseModel } : {}),
132
+ ...("toolCallId" in message ? { toolCallId: message.toolCallId } : {}) } } : {}) });
133
+ }
134
+ const sessionFile = ctx.sessionManager.getSessionFile();
135
+ process.stdout.write(`${JSON.stringify({ type: "subagent.native", boundary, entries,
136
+ sessionId: ctx.sessionManager.getSessionId(), sessionFile, leafId: ctx.sessionManager.getLeafId(),
137
+ persisted: Boolean(sessionFile && fs.existsSync(sessionFile)), messageCount: observedMessages,
138
+ configuration: { ...(ctx.model ? { model: `${ctx.model.provider}/${ctx.model.id}` } : {}), thinking: pi.getThinkingLevel() } })}\n`);
139
+ };
140
+ pi.on("turn_end", (_event, ctx) => observe(ctx, "turn"));
141
+ pi.on("agent_settled", (_event, ctx) => observe(ctx, "settled"));
142
+ const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
143
+ const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
144
+ if (structuredOutputPath && structuredSchemaPath) {
145
+ const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
146
+ const parameters = Type.Object({ value: Type.Unsafe(schema) }, { additionalProperties: false });
147
+ pi.registerTool({
148
+ name: "structured_output",
149
+ label: "Structured Output",
150
+ description: "Submit the required final structured output for this subagent step. This terminates the step.",
151
+ parameters,
152
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
153
+ async execute(_id: string, params: { value: unknown }) {
154
+ const validation = validateStructuredOutputValue(schema, params.value);
155
+ if (validation.status === "invalid") {
156
+ throw new Error(`Structured output validation failed: ${validation.message}`);
157
+ }
158
+ fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
159
+ fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
160
+ return {
161
+ content: [{ type: "text", text: "Structured output captured." }],
162
+ details: { path: structuredOutputPath },
163
+ terminate: true,
164
+ };
165
+ },
166
+ });
167
+ pi.on("session_start", () => {
168
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), "structured_output"])]);
169
+ });
170
+ }
171
+
172
+ pi.on("context", (event) => {
173
+ // Filtering changes the provider prefix, never the saved journal. Fanout children
174
+ // need their own nested calls/results on later turns and resumes, so retain tool history.
175
+ const messages = stripParentOnlySubagentMessages(event.messages, readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV) === true);
176
+ if (messages === event.messages) return;
177
+ return { messages };
178
+ });
179
+
180
+ pi.on("before_agent_start", (event) => {
181
+ const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
182
+ if (intercomSessionName && typeof pi.setSessionName === "function") {
183
+ pi.setSessionName(intercomSessionName);
184
+ }
185
+
186
+ const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
187
+ const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
188
+ const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
189
+ if (inheritProjectContext === undefined && inheritSkills === undefined && fanoutChild === undefined) return;
190
+ const options = event.systemPromptOptions;
191
+ // --no-skills/--no-context-files govern discovery. Keep explicitly selected
192
+ // skill bodies and context intact; never parse the rendered prompt to remove resources.
193
+ options.skills = options.skills.filter((skill) => skill.name !== "pi-subagents");
194
+ setPromptSection(options, "subagent_role", fanoutChild === true
195
+ ? `${CHILD_FANOUT_BOUNDARY_INSTRUCTIONS}\n${loadConfig().compactChildTools === false
196
+ ? "Use subagent({action:'list'}) to discover agents before delegation."
197
+ : "Use load_subagent({advanced:false}), then agent_runs({action:'profiles'}) to discover agents, delegate for ordinary work, and load_subagent for advanced workflows."}`
198
+ : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS);
199
+ if (structuredOutputPath) setPromptSection(options, "subagent_output", STRUCTURED_OUTPUT_INSTRUCTIONS);
200
+ });
201
+ }
@@ -0,0 +1,89 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const REPEATED_SUBAGENT_CALL_LIMIT = 5;
4
+ const SUBAGENT_CALL_WINDOW_SIZE = REPEATED_SUBAGENT_CALL_LIMIT * 2 - 1;
5
+
6
+ interface SubagentCallStart {
7
+ callKey: string;
8
+ toolName: unknown;
9
+ failed?: boolean;
10
+ toolCallId?: string;
11
+ }
12
+
13
+ export interface RepeatedSubagentCallGuardState {
14
+ recentStarts: boolean[];
15
+ recentSubagentCalls: SubagentCallStart[];
16
+ }
17
+
18
+ export function createRepeatedSubagentCallGuardState(): RepeatedSubagentCallGuardState {
19
+ return { recentStarts: [], recentSubagentCalls: [] };
20
+ }
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ function stableStringify(value: unknown): string {
27
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
28
+ if (!isRecord(value)) return JSON.stringify(value) ?? "undefined";
29
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
30
+ }
31
+
32
+ function subagentCallKey(toolName: unknown, args: unknown): string | undefined {
33
+ if ((toolName !== "subagent" && toolName !== "delegate" && toolName !== "agent_runs") || !isRecord(args)) return undefined;
34
+ try {
35
+ return createHash("sha256").update(`${toolName}:${stableStringify(args)}`).digest("hex");
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ export function recordToolStartForSubagentLoopGuard(input: {
42
+ state: RepeatedSubagentCallGuardState;
43
+ toolCallId?: unknown;
44
+ toolName: unknown;
45
+ args: unknown;
46
+ }): string | undefined {
47
+ const args = input.args;
48
+ const isList = isRecord(args) && (
49
+ input.toolName === "subagent" && args.action === "list"
50
+ || input.toolName === "agent_runs" && args.action === "profiles"
51
+ );
52
+ input.state.recentStarts.push(isList);
53
+ if (input.state.recentStarts.length > SUBAGENT_CALL_WINDOW_SIZE) input.state.recentStarts.shift();
54
+ const callKey = subagentCallKey(input.toolName, input.args);
55
+ if (callKey) {
56
+ input.state.recentSubagentCalls.push({
57
+ callKey,
58
+ toolName: input.toolName,
59
+ toolCallId: typeof input.toolCallId === "string" ? input.toolCallId : undefined,
60
+ });
61
+ if (input.state.recentSubagentCalls.length > SUBAGENT_CALL_WINDOW_SIZE) input.state.recentSubagentCalls.shift();
62
+ }
63
+ if (!isList) return undefined;
64
+ const listCount = input.state.recentStarts.filter(Boolean).length;
65
+ if (listCount >= REPEATED_SUBAGENT_CALL_LIMIT) {
66
+ return `Child appears stuck repeating ${input.toolName}({ action: "${args.action}" }) ${listCount} times. Stopping to avoid a tool loop.`;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ export function recordToolEndForSubagentLoopGuard(input: {
72
+ state: RepeatedSubagentCallGuardState;
73
+ toolCallId?: unknown;
74
+ toolName: unknown;
75
+ isError: unknown;
76
+ }): string | undefined {
77
+ if (input.isError !== true) return undefined;
78
+ const toolCallId = typeof input.toolCallId === "string" ? input.toolCallId : undefined;
79
+ const entry = [...input.state.recentSubagentCalls].reverse().find((candidate) =>
80
+ candidate.toolName === input.toolName && candidate.failed === undefined
81
+ && (!toolCallId || !candidate.toolCallId || candidate.toolCallId === toolCallId));
82
+ if (!entry) return undefined;
83
+ entry.failed = true;
84
+ const failedCount = input.state.recentSubagentCalls.filter((candidate) => candidate.failed && candidate.callKey === entry.callKey).length;
85
+ if (failedCount >= REPEATED_SUBAGENT_CALL_LIMIT) {
86
+ return `Child appears stuck repeating the same failed ${input.toolName} call ${failedCount} times. Stopping to avoid a tool loop.`;
87
+ }
88
+ return undefined;
89
+ }
@@ -0,0 +1,372 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
+ import { formatRunAction } from "../../shared/status-format.ts";
6
+ import { NativeJournal, ownerProjection, readJsonProjection } from "../../shared/journal-reader.ts";
7
+ import { getAgentDir } from "../../shared/utils.ts";
8
+ import { runCooperatively, runSynchronously } from "../../shared/cooperative.ts";
9
+ import { ASYNC_DIR, TEMP_ROOT_DIR, type AsyncStatus, type AsyncResultFile, type ResolvedAcceptanceConfig, type JsonSchemaObject, type OutputMode, type SavedLaunchConfig, type SingleResult, type AgentProgress } from "../../shared/types.ts";
10
+
11
+ export const LEGACY_QUESTIONS_DIR = path.join(TEMP_ROOT_DIR, "supervisor-questions");
12
+ export const QUESTIONS_DIR = path.join(getAgentDir(), "sessions", "subagent-runs");
13
+
14
+ export interface SupervisorRunContract {
15
+ legacySource?: string;
16
+ recordVersion?: 3;
17
+ nativeSessionId?: string;
18
+ terminalLeafId?: string | null;
19
+ terminalEntryId?: string;
20
+ attemptBaseline?: string[];
21
+ baselineSource?: "native-migration";
22
+ effectiveConfiguration?: { model?: string; thinking?: string; modelRecordedAt?: number };
23
+ accounting?: { state: "complete" | "incomplete"; error?: string };
24
+ auditPath?: string;
25
+ task?: string;
26
+ label?: string;
27
+ result?: SingleResult;
28
+ effectiveAcceptance?: ResolvedAcceptanceConfig;
29
+ output?: string | false;
30
+ outputMode?: OutputMode;
31
+ outputSchema?: JsonSchemaObject;
32
+ launch?: SavedLaunchConfig;
33
+ /** Kept independently of progress, which is compacted away or stops streaming after detachment. */
34
+ modelSelection?: Pick<AgentProgress, "model" | "thinking" | "modelStartedAt">;
35
+ sessionFile?: string;
36
+ pid?: number;
37
+ processIdentity?: string;
38
+ updatedAt?: number;
39
+ }
40
+
41
+ export interface SupervisorQuestion extends SupervisorRunContract {
42
+ questionId: string;
43
+ runId: string;
44
+ ownerSessionId: string;
45
+ ownerTarget: string;
46
+ agent: string;
47
+ index: number;
48
+ childSessionId: string;
49
+ childTarget: string;
50
+ sessionFile: string;
51
+ cwd: string;
52
+ pid: number;
53
+ createdAt: number;
54
+ reason: "need_decision" | "interview_request";
55
+ message: string;
56
+ interview?: unknown;
57
+ }
58
+
59
+ export interface QuestionAnswer {
60
+ message: string;
61
+ answeredAt: number;
62
+ origin?: "human";
63
+ }
64
+
65
+ export interface QuestionDelivery {
66
+ kind: "live" | "revive";
67
+ runId: string;
68
+ deliveredAt: number;
69
+ }
70
+
71
+ export interface SupervisorQuestionView extends SupervisorQuestion {
72
+ state: "awaiting_input" | "answer_pending" | "answered" | "cancelled";
73
+ answer?: QuestionAnswer;
74
+ delivery?: QuestionDelivery;
75
+ revival?: { runId: string; pid: number; startedAt: number };
76
+ }
77
+
78
+ function safeId(value: string): string {
79
+ if (typeof value !== "string" || !/^[a-zA-Z0-9_-]+$/.test(value)) throw new Error("Invalid run or question ID.");
80
+ return value;
81
+ }
82
+
83
+ export function getRunMetadataDir(runId: string, root = QUESTIONS_DIR): string {
84
+ return path.join(root, safeId(runId));
85
+ }
86
+
87
+ export function saveAsyncRunResult(runId: string, result: AsyncResultFile): AsyncResultFile {
88
+ const file = path.join(getRunMetadataDir(runId), "result.json");
89
+ const previous = readRunJson<AsyncResultFile>(file);
90
+ if (previous?.completionId && result.completionId && previous.completionId !== result.completionId) throw new Error("Conflicting completion identity");
91
+ const saved = { ...result, recordVersion: 3 as const, completionId: previous?.completionId ?? result.completionId ?? randomUUID(),
92
+ ...(preserveLegacyOwner(file, previous) ? { legacySource: `${file}.legacy` } : {}),
93
+ results: result.results?.map((child, index) => compactOwnerResult(runId, index, child)) };
94
+ writeAtomicJson(file, saved);
95
+ return saved;
96
+ }
97
+
98
+ function preserveLegacyOwner(file: string, previous: { recordVersion?: number } | undefined): boolean {
99
+ if (!previous || previous.recordVersion === 3) return false;
100
+ try { fs.copyFileSync(file, `${file}.legacy`, fs.constants.COPYFILE_EXCL); }
101
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
102
+ return true;
103
+ }
104
+
105
+ export function compactOwnerResult<T extends { finalOutput?: string; output?: string; initialOutput?: string; initialOutputPath?: string; messages?: unknown; artifactPaths?: { outputPath?: string }; fullOutputPath?: string }>(runId: string, index: number, result: T, root = QUESTIONS_DIR): T {
106
+ const output = result.finalOutput ?? result.output;
107
+ let fullOutputPath = result.fullOutputPath;
108
+ if (output && output.length > 8192) {
109
+ fullOutputPath ??= result.artifactPaths?.outputPath;
110
+ if (!fullOutputPath) {
111
+ fullOutputPath = path.join(getRunMetadataDir(runId, root), "outputs", `${index}.txt`);
112
+ fs.mkdirSync(path.dirname(fullOutputPath), { recursive: true });
113
+ fs.writeFileSync(fullOutputPath, output, { mode: 0o600 });
114
+ }
115
+ }
116
+ let initialOutputPath = result.initialOutputPath;
117
+ if (result.initialOutput && result.initialOutput.length > 8192 && !initialOutputPath) {
118
+ initialOutputPath = path.join(getRunMetadataDir(runId, root), "outputs", `${index}.initial.txt`);
119
+ fs.mkdirSync(path.dirname(initialOutputPath), { recursive: true });
120
+ fs.writeFileSync(initialOutputPath, result.initialOutput, { mode: 0o600 });
121
+ }
122
+ return { ...result, messages: undefined, ...(fullOutputPath ? { fullOutputPath } : {}),
123
+ ...(initialOutputPath ? { initialOutputPath } : {}),
124
+ ...(result.finalOutput !== undefined ? { finalOutput: result.finalOutput.slice(-8192) } : {}),
125
+ ...(result.output !== undefined ? { output: result.output.slice(-8192) } : {}),
126
+ ...(result.initialOutput !== undefined ? { initialOutput: result.initialOutput.slice(-8192) } : {}) };
127
+ }
128
+
129
+ export function saveRunStatus(runId: string, status: AsyncStatus): void {
130
+ writeAtomicJson(path.join(getRunMetadataDir(runId), "status.json"), status);
131
+ }
132
+
133
+ export function readRunJson<T>(file: string): T | undefined {
134
+ try {
135
+ return readJsonProjection(file, ownerProjection) as T;
136
+ } catch (error) {
137
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ function questionDir(question: Pick<SupervisorQuestion, "runId" | "questionId">, root = QUESTIONS_DIR): string {
143
+ return path.join(root, safeId(question.runId), "questions", safeId(question.questionId));
144
+ }
145
+
146
+ // Atomic publication without replacement: concurrent answers/launches have exactly one winner.
147
+ function writeOnce(file: string, value: object): boolean {
148
+ const temporary = `${file}.${randomUUID()}.tmp`;
149
+ writeAtomicJson(temporary, value);
150
+ try {
151
+ fs.linkSync(temporary, file);
152
+ return true;
153
+ } catch (error) {
154
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
155
+ throw error;
156
+ } finally {
157
+ fs.rmSync(temporary, { force: true });
158
+ }
159
+ }
160
+
161
+ export function saveQuestionOwner(runId: string, sessionId: string, root = QUESTIONS_DIR): void {
162
+ writeAtomicJson(path.join(root, safeId(runId), "question-owner.json"), { sessionId });
163
+ }
164
+
165
+ export function saveQuestionContract(runId: string, index: number, contract: SupervisorRunContract, root = QUESTIONS_DIR): void {
166
+ if (!Number.isSafeInteger(index) || index < 0) throw new Error("Child index must be a non-negative integer.");
167
+ const file = path.join(root, safeId(runId), "contracts", `${index}.json`);
168
+ const previous = readRunJson<SupervisorRunContract>(file);
169
+ writeAtomicJson(file, { ...previous, ...contract, recordVersion: 3,
170
+ ...(preserveLegacyOwner(file, previous) ? { legacySource: `${file}.legacy` } : {}),
171
+ ...(previous?.attemptBaseline && contract.baselineSource !== "native-migration" ? { attemptBaseline: previous.attemptBaseline } : {}),
172
+ ...(contract.result ? { result: compactOwnerResult(runId, index, contract.result, root) } : {}),
173
+ ...(previous?.launch ? { launch: previous.launch } : {}) });
174
+ }
175
+
176
+ export function readQuestionContract(runId: string, index: number, root = QUESTIONS_DIR, projection: { sessionFile?: string; endedAt?: number; readConfiguration?: false } = {}): SupervisorRunContract | undefined {
177
+ const contract = readRunJson<SupervisorRunContract>(path.join(root, safeId(runId), "contracts", `${index}.json`));
178
+ if (!contract?.launch || projection.readConfiguration === false) return contract;
179
+ const sessionFile = projection.sessionFile ?? contract.sessionFile;
180
+ if (contract.recordVersion !== 3 && !contract.effectiveConfiguration && sessionFile && fs.existsSync(sessionFile) && !contract.launch.model?.startsWith("claude-code/")) {
181
+ // One-time read-only native recovery. Subsequent controls use the compact
182
+ // selection, and the frozen requested profile is never rewritten.
183
+ const journal = new NativeJournal(sessionFile, "inspect", true);
184
+ contract.effectiveConfiguration = journal.configuration(projection.endedAt, contract.terminalLeafId);
185
+ saveQuestionContract(runId, index, { effectiveConfiguration: contract.effectiveConfiguration }, root);
186
+ }
187
+ const native = contract.effectiveConfiguration ?? {};
188
+ return { ...contract, launch: { ...contract.launch, ...native } };
189
+ }
190
+
191
+ export function migrateSupervisorQuestions(ownerSessionId: string, runId?: string): void {
192
+ runSynchronously(migrateSupervisorQuestionSteps(ownerSessionId, runId));
193
+ }
194
+
195
+ export function* migrateSupervisorQuestionSteps(ownerSessionId: string, runId?: string): Generator<void> {
196
+ if (!fs.existsSync(LEGACY_QUESTIONS_DIR)) return;
197
+ const runs = runId === undefined ? fs.readdirSync(LEGACY_QUESTIONS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [safeId(runId)];
198
+ for (const id of runs) {
199
+ yield;
200
+ try {
201
+ const source = getRunMetadataDir(id, LEGACY_QUESTIONS_DIR);
202
+ const owner = readRunJson<{ sessionId?: string }>(path.join(source, "question-owner.json"));
203
+ if (owner?.sessionId !== ownerSessionId) continue;
204
+ fs.cpSync(source, getRunMetadataDir(id), { recursive: true, force: false });
205
+ } catch (error) {
206
+ if (runId !== undefined) throw error;
207
+ console.error(`Could not recover legacy questions for ${id}: ${String(error)}`);
208
+ }
209
+ }
210
+ }
211
+
212
+ export function createSupervisorQuestion(input: Omit<SupervisorQuestion, "questionId" | "createdAt" | "ownerSessionId">, root = QUESTIONS_DIR): SupervisorQuestion {
213
+ const runDir = path.join(root, safeId(input.runId));
214
+ const status = readRunJson<AsyncStatus>(path.join(runDir, "status.json"))
215
+ ?? (root === QUESTIONS_DIR ? readRunJson<AsyncStatus>(path.join(ASYNC_DIR, safeId(input.runId), "status.json")) : undefined);
216
+ const owner = readRunJson<{ sessionId: string }>(path.join(runDir, "question-owner.json")) ?? status;
217
+ if (!owner?.sessionId) throw new Error(`Run ${input.runId} has no saved question owner; cannot create a recoverable supervisor question.`);
218
+ if (!input.sessionFile || path.extname(input.sessionFile) !== ".jsonl") throw new Error("Supervisor questions require a saved child session.");
219
+ if (!Number.isSafeInteger(input.index) || input.index < 0) throw new Error("Child index must be a non-negative integer.");
220
+ if (!input.message.trim()) throw new Error("Supervisor question must not be empty.");
221
+ const contract = readQuestionContract(input.runId, input.index, root, { sessionFile: input.sessionFile })
222
+ ?? { effectiveAcceptance: status?.steps?.[input.index]?.acceptance?.effectiveAcceptance };
223
+ const question = { ...input, ...contract, sessionFile: input.sessionFile, pid: input.pid, ownerSessionId: owner.sessionId, questionId: randomUUID(), createdAt: Date.now() };
224
+ writeAtomicJson(path.join(questionDir(question, root), "question.json"), question);
225
+ return question;
226
+ }
227
+
228
+ function questionStatePaths(question: SupervisorQuestion, file: string, root: string): string[] {
229
+ const current = path.join(questionDir(question, root), file);
230
+ const legacy = questionDir(question, LEGACY_QUESTIONS_DIR);
231
+ // Old waiters still claim the legacy file; both runtimes must compete on that same inode.
232
+ return root === QUESTIONS_DIR && fs.existsSync(path.join(legacy, "question.json")) ? [path.join(legacy, file), current] : [current];
233
+ }
234
+
235
+ function writeQuestionStateOnce(question: SupervisorQuestion, file: string, value: object, root: string): boolean {
236
+ const [primary, ...mirrors] = questionStatePaths(question, file, root);
237
+ const written = writeOnce(primary!, value);
238
+ const winner = written ? value : readRunJson<object>(primary!)!;
239
+ for (const target of mirrors) writeOnce(target, winner);
240
+ return written;
241
+ }
242
+
243
+ export function readQuestionState(question: SupervisorQuestion, root = QUESTIONS_DIR): SupervisorQuestionView {
244
+ const read = <T>(file: string): T | undefined => questionStatePaths(question, file, root).map((entry) => readRunJson<T>(entry)).find((entry) => entry !== undefined);
245
+ const answer = read<QuestionAnswer>("answer.json");
246
+ const delivery = read<QuestionDelivery>("delivery.json");
247
+ const cancelled = read("cancelled.json") !== undefined;
248
+ const revival = read<SupervisorQuestionView["revival"]>("revival.json");
249
+ return { ...question, state: delivery ? "answered" : cancelled ? "cancelled" : answer ? "answer_pending" : "awaiting_input", ...(answer ? { answer } : {}), ...(delivery ? { delivery } : {}), ...(revival ? { revival } : {}) };
250
+ }
251
+
252
+ export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
253
+ let entries: fs.Dirent[];
254
+ try {
255
+ entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
256
+ } catch (error) {
257
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
258
+ throw error;
259
+ }
260
+ return entries.filter((entry) => entry.isDirectory()).flatMap((entry) => {
261
+ const question = readRunJson<SupervisorQuestion>(path.join(runDir, "questions", entry.name, "question.json"));
262
+ return question ? [readQuestionState(question, path.dirname(runDir))] : [];
263
+ });
264
+ }
265
+
266
+ /** Known run IDs need neither a global directory walk nor prefix resolution. */
267
+ export function listOwnedRunQuestions(ownerSessionId: string, runId: string): SupervisorQuestionView[] {
268
+ migrateSupervisorQuestions(ownerSessionId, runId);
269
+ return listRunQuestions(getRunMetadataDir(runId)).filter((question) => question.ownerSessionId === ownerSessionId);
270
+ }
271
+
272
+ export function pendingSupervisorQuestion(input: { runId: string; agent: string; index: number; sessionFile?: string; pid?: number }): import("../../shared/types.ts").ControlEvent["supervisorQuestion"] {
273
+ if (!input.sessionFile) return undefined;
274
+ try {
275
+ const pid = input.pid ?? readQuestionContract(input.runId, input.index)?.pid;
276
+ const question = listRunQuestions(getRunMetadataDir(input.runId)).find((question) =>
277
+ question.runId === input.runId && question.agent === input.agent && question.index === input.index
278
+ && question.sessionFile === input.sessionFile && question.pid === pid
279
+ && (question.state === "awaiting_input" || question.state === "answer_pending"));
280
+ if (question && (question.state === "awaiting_input" || question.state === "answer_pending")) return {
281
+ questionId: question.questionId, state: question.state, ...(question.answer ? { answer: question.answer.message } : {}),
282
+ };
283
+ } catch (error) {
284
+ console.error(`Could not read supervisor wait for ${input.runId}:${input.index}:`, error);
285
+ }
286
+ return undefined;
287
+ }
288
+
289
+ export function listSupervisorQuestions(ownerSessionId: string, runId?: string, root = QUESTIONS_DIR): SupervisorQuestionView[] {
290
+ return runSynchronously(supervisorQuestionSteps(ownerSessionId, runId, root));
291
+ }
292
+
293
+ export function listSupervisorQuestionsAsync(ownerSessionId: string): Promise<SupervisorQuestionView[]> {
294
+ return runCooperatively(supervisorQuestionSteps(ownerSessionId));
295
+ }
296
+
297
+ function* supervisorQuestionSteps(ownerSessionId: string, runId?: string, root = QUESTIONS_DIR): Generator<void, SupervisorQuestionView[]> {
298
+ if (runId !== undefined) safeId(runId);
299
+ if (root === QUESTIONS_DIR) yield* migrateSupervisorQuestionSteps(ownerSessionId);
300
+ if (!fs.existsSync(root)) return [];
301
+ const runs = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (!runId || entry.name.startsWith(runId)));
302
+ const questions: SupervisorQuestionView[] = [];
303
+ for (const entry of runs) {
304
+ yield;
305
+ try { questions.push(...listRunQuestions(path.join(root, entry.name)).filter((question) => question.ownerSessionId === ownerSessionId)); }
306
+ catch (error) {
307
+ if (runId !== undefined) throw error;
308
+ console.error(`Could not list questions for ${entry.name}: ${String(error)}`);
309
+ }
310
+ }
311
+ if (runId && new Set(questions.map((question) => question.runId)).size > 1) throw new Error(`Ambiguous run ID prefix '${runId}'.`);
312
+ return questions.sort((a, b) => a.createdAt - b.createdAt);
313
+ }
314
+
315
+ export function saveQuestionAnswer(question: SupervisorQuestion, message: string, root = QUESTIONS_DIR, origin?: "human"): QuestionAnswer {
316
+ if (typeof message !== "string" || !message.trim()) throw new Error("action='answer' requires a non-empty message.");
317
+ const state = readQuestionState(question, root);
318
+ if (state.state === "cancelled") throw new Error(`Question ${question.questionId} was cancelled. Use continue for a new follow-up.`);
319
+ const answer: QuestionAnswer = { message: message.trim(), answeredAt: state.answer?.answeredAt ?? Date.now(), ...(state.answer?.origin ?? origin ? { origin: state.answer?.origin ?? origin } : {}) };
320
+ if (state.answer && state.answer.message !== answer.message) throw new Error(`Question ${question.questionId} already has a different saved answer. The original answer was retained.`);
321
+ if (writeQuestionStateOnce(question, "answer.json", answer, root)) return answer;
322
+ const existing = readQuestionState(question, root).answer!;
323
+ if (existing.message !== answer.message) throw new Error(`Question ${question.questionId} already has a different saved answer. The original answer was retained.`);
324
+ return existing;
325
+ }
326
+
327
+ export function recordQuestionDelivery(question: SupervisorQuestion, delivery: QuestionDelivery, root = QUESTIONS_DIR): void {
328
+ writeQuestionStateOnce(question, "delivery.json", delivery, root);
329
+ }
330
+
331
+ export function cancelSupervisorQuestion(question: SupervisorQuestion, root = QUESTIONS_DIR): void {
332
+ writeQuestionStateOnce(question, "cancelled.json", { cancelledAt: Date.now() }, root);
333
+ }
334
+
335
+ export function questionProcessAlive(question: Pick<SupervisorQuestion, "pid">): boolean {
336
+ if (!Number.isSafeInteger(question.pid) || question.pid <= 0) throw new Error("Invalid child process ID.");
337
+ try {
338
+ process.kill(question.pid, 0);
339
+ return true;
340
+ } catch (error) {
341
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
342
+ }
343
+ }
344
+
345
+ export function claimQuestionRevival(question: SupervisorQuestion, root = QUESTIONS_DIR): { claimed: boolean; runId: string } {
346
+ const runId = `answer-${question.questionId}`;
347
+ if (readQuestionState(question, root).revival) return { claimed: false, runId };
348
+ return { claimed: writeQuestionStateOnce(question, "revival.json", { runId, pid: process.pid, startedAt: Date.now() }, root), runId };
349
+ }
350
+
351
+ export function releaseQuestionRevival(question: SupervisorQuestion, root = QUESTIONS_DIR): void {
352
+ for (const file of questionStatePaths(question, "revival.json", root)) fs.rmSync(file, { force: true });
353
+ }
354
+
355
+ export function questionRecoveryHint(question: SupervisorQuestion, childSafe = false): string {
356
+ return `Recover an unlaunched continuation: ${formatRunAction("resume", question.runId, { index: question.index, message: "Continue with the saved supervisor answer." }, childSafe)}`;
357
+ }
358
+
359
+ export function formatSupervisorQuestions(questions: SupervisorQuestionView[], childSafe = false): string {
360
+ if (!questions.length) return "No supervisor questions owned by this session.";
361
+ return questions.map((question) => [
362
+ `Question: ${question.questionId} | ${question.state}`,
363
+ `Run: ${question.runId} | Child: ${question.index} (${question.agent}) | Owner: ${question.ownerSessionId}`,
364
+ `Session: ${question.sessionFile} | Child target: ${question.childTarget}`,
365
+ question.message,
366
+ ...(question.answer ? [`Saved answer: ${question.answer.message}`] : []),
367
+ ...(question.revival && !question.delivery ? [`Continuation requested: ${question.revival.runId}; delivery unconfirmed. Answer retained.`, questionRecoveryHint(question, childSafe)] : []),
368
+ question.delivery ? `Answer delivered via ${question.delivery.kind}; run: ${question.delivery.runId}`
369
+ : question.state === "cancelled" ? "Question cancelled; use continue for a new follow-up."
370
+ : `Answer: ${formatRunAction("answer", question.runId, { questionId: question.questionId, message: question.answer?.message ?? "..." }, childSafe)}`,
371
+ ].join("\n")).join("\n\n");
372
+ }