@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -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 +199 -0
  43. package/dist/history/ingest.js +468 -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 +466 -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/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,353 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { Message } from "@earendil-works/pi-ai";
3
+ import { createStructuredOutputRuntime, readStructuredOutput, validateStructuredOutputValue, type StructuredOutputRuntime } from "./structured-output.ts";
4
+ import type {
5
+ AcceptanceFinalizationTurn,
6
+ AcceptanceLedger,
7
+ AcceptanceReport,
8
+ JsonSchemaObject,
9
+ ResolvedAcceptanceConfig,
10
+ } from "../../shared/types.ts";
11
+ import { acceptanceFailureMessage, evaluateAcceptance } from "./acceptance-evaluation.ts";
12
+ import { acceptanceSelfReviewConfig, formatEvidenceReportFieldMapping, shouldRunAcceptanceFinalization } from "./acceptance-contract.ts";
13
+ import type { AttemptOutcome } from "./model-fallback.ts";
14
+ import { isFailFastAbort } from "./parallel-utils.ts";
15
+ import { ACCEPTANCE_REPORT_SCHEMA, formatAcceptanceReportExample, parseAcceptanceReport, stripAcceptanceReport, validateAcceptanceReportShape } from "./acceptance-reports.ts";
16
+
17
+ type FinalizationReportRuntime = StructuredOutputRuntime & { publicOutputSchema?: JsonSchemaObject };
18
+
19
+ export function createFinalizationReportRuntime(publicOutputSchema?: JsonSchemaObject): FinalizationReportRuntime {
20
+ const runtime = createStructuredOutputRuntime({
21
+ type: "object", properties: {
22
+ // A schema resource keeps root-local references (including recursive "#") scoped to the public answer.
23
+ answer: publicOutputSchema ? { $id: "urn:pi-subagents:public-output", ...publicOutputSchema }
24
+ : { type: "string", pattern: "\\S", description: "Complete standalone final answer, including every requested handoff detail." },
25
+ report: ACCEPTANCE_REPORT_SCHEMA,
26
+ }, required: ["answer", "report"], additionalProperties: false,
27
+ });
28
+ return { ...runtime, publicOutputSchema };
29
+ }
30
+
31
+ export interface FinalizationReportSubmission {
32
+ output: string;
33
+ structuredOutput?: unknown;
34
+ report?: AcceptanceReport;
35
+ reportSubmissionError?: string;
36
+ unconfirmedOutput?: string;
37
+ }
38
+
39
+ function readReportValue(value: unknown, publicOutputSchema?: JsonSchemaObject): (FinalizationReportSubmission & { report: AcceptanceReport }) | undefined {
40
+ if (!value || typeof value !== "object") return;
41
+ // Stored legacy runtimes retain their original string schema.
42
+ if (!publicOutputSchema && "report" in value && typeof value.report === "string") {
43
+ const parsed = parseAcceptanceReport(value.report);
44
+ if (parsed.report) return { output: value.report, report: parsed.report };
45
+ }
46
+ if (!("answer" in value) || !("report" in value)) return;
47
+ if (validateAcceptanceReportShape(value.report)) return;
48
+ if (publicOutputSchema) {
49
+ if (validateStructuredOutputValue(publicOutputSchema, value.answer).status === "invalid") return;
50
+ return { output: JSON.stringify(value.answer), structuredOutput: value.answer, report: value.report as AcceptanceReport };
51
+ }
52
+ if (typeof value.answer !== "string" || !value.answer.trim()) return;
53
+ return { output: value.answer, report: value.report as AcceptanceReport };
54
+ }
55
+
56
+ function reportAuditOutput(submission: { output: string; report?: AcceptanceReport }): string {
57
+ return submission.report && !parseAcceptanceReport(submission.output).report
58
+ ? `${submission.output}\n\n\`\`\`acceptance-report\n${JSON.stringify(submission.report)}\n\`\`\`` : submission.output;
59
+ }
60
+
61
+ /** Pass the current attempt's messages, or its starting offset in a shared native session. */
62
+ export function readFinalizationReport(messages: Message[], runtime: FinalizationReportRuntime, options: { messageOffset?: number; structuredResult?: boolean } = {}): FinalizationReportSubmission {
63
+ messages = messages.slice(options.messageOffset ?? 0);
64
+ const captured = readStructuredOutput(runtime);
65
+ const submitted = readReportValue(captured.value, runtime.publicOutputSchema);
66
+ // Older submissions are audit evidence only, never the current output below.
67
+ const successfulIds = new Set(messages.flatMap((message) => message.role === "toolResult" && message.toolName === "structured_output" && message.isError === false ? [message.toolCallId] : []));
68
+ let unconfirmedOutput = submitted && reportAuditOutput(submitted);
69
+ for (const message of messages) {
70
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
71
+ for (const call of message.content) {
72
+ if (call.type !== "toolCall" || call.name !== "structured_output" || !successfulIds.has(call.id)) continue;
73
+ const value = call.arguments.value;
74
+ const previous = readReportValue(value, runtime.publicOutputSchema);
75
+ if (previous && validateStructuredOutputValue(runtime.schema, value).status === "valid") unconfirmedOutput = reportAuditOutput(previous);
76
+ }
77
+ }
78
+ const rejected = (reason: string): FinalizationReportSubmission => ({
79
+ output: "", reportSubmissionError: `No current finalization report: ${reason}`, unconfirmedOutput,
80
+ });
81
+ const index = messages.findLastIndex((message) => message.role === "assistant");
82
+ const last = messages[index];
83
+ if (last?.role !== "assistant" || last.errorMessage || !["stop", "toolUse"].includes(last.stopReason) || !Array.isArray(last.content)) {
84
+ return rejected("the latest assistant turn did not finish successfully.");
85
+ }
86
+ // Claude Code returns its schema-constrained payload in the terminal result event, not a Pi tool call.
87
+ if (options.structuredResult) {
88
+ if (captured.error) return rejected(captured.error);
89
+ return submitted ? { ...submitted, unconfirmedOutput } : rejected("the submission must contain a complete answer and a valid acceptance report.");
90
+ }
91
+ const calls = last.content.filter((part) => part.type === "toolCall");
92
+ if (calls.length !== 1 || calls[0]!.name !== "structured_output") return rejected("the latest assistant turn must submit structured_output as its only tool call.");
93
+ const call = calls[0]!;
94
+ const result = messages.slice(index + 1).findLast((message) => message.role === "toolResult" && message.toolCallId === call.id);
95
+ if (result?.role !== "toolResult" || result.toolName !== "structured_output" || result.isError !== false) return rejected("the latest structured_output call has no matching successful result.");
96
+ if (captured.error) return rejected(captured.error);
97
+ if (!isDeepStrictEqual(captured.value, call.arguments.value)) return rejected("the capture does not match the latest submission.");
98
+ if (!submitted) return rejected("the submission must contain a complete answer and a valid acceptance report.");
99
+ return { ...submitted, unconfirmedOutput };
100
+ }
101
+
102
+ export function formatUnconfirmedFinalizationOutput(output: string): string {
103
+ return `UNCONFIRMED task report — retained for audit only; finalization did not deliver a current complete report.\n\n${stripAcceptanceReport(output)}`;
104
+ }
105
+
106
+ export function resolveFinalizationOutput(rawOutput: string, previousOutput: string): string {
107
+ const prose = stripAcceptanceReport(rawOutput);
108
+ if (prose.trim()) return prose;
109
+ const report = parseAcceptanceReport(rawOutput).report;
110
+ return report?.diffSummary?.trim() || report?.notes?.trim() || previousOutput;
111
+ }
112
+
113
+ type ExecutionOutcome = Pick<AttemptOutcome, "exitCode" | "error" | "interrupted" | "timedOut" | "resourceLimitExceeded">;
114
+
115
+ export function resolveExecutionOutcome(input: {
116
+ result: ExecutionOutcome;
117
+ acceptance?: AcceptanceLedger;
118
+ signal?: AbortSignal;
119
+ interruptSignal?: AbortSignal;
120
+ }): ExecutionOutcome {
121
+ const { result } = input;
122
+ if (input.signal?.aborted && input.signal.reason instanceof Error && input.signal.reason.name === "TimeoutError") {
123
+ return { ...result, exitCode: 124, timedOut: true, interrupted: false, error: input.signal.reason.message || "Subagent timed out." };
124
+ }
125
+ if (input.signal?.aborted) return { ...result, exitCode: 1, interrupted: false, error: "Subagent cancelled." };
126
+ if (isFailFastAbort(input.interruptSignal)) return { ...result, exitCode: -1, interrupted: false, error: "Interrupted due to fail-fast" };
127
+ if (result.timedOut || result.resourceLimitExceeded) return { ...result, exitCode: result.timedOut ? 124 : 1, interrupted: result.interrupted === undefined ? undefined : false };
128
+ if (input.interruptSignal?.aborted || result.interrupted) return { ...result, exitCode: 0, interrupted: true, error: undefined };
129
+ const failure = input.acceptance?.explicit ? acceptanceFailureMessage(input.acceptance) : undefined;
130
+ if (failure && result.exitCode === 0) return { ...result, exitCode: 1, error: result.error ? `${result.error}\n${failure}` : failure };
131
+ return result;
132
+ }
133
+
134
+ export async function evaluateRunAcceptance(input: {
135
+ acceptance: ResolvedAcceptanceConfig;
136
+ initial: ExecutionOutcome;
137
+ initialOutput: string;
138
+ initialReport?: AcceptanceReport;
139
+ initialAcceptance?: AcceptanceLedger;
140
+ sessionFile?: string;
141
+ cwd: string;
142
+ signal?: AbortSignal;
143
+ nativeReport?: boolean;
144
+ outputSchema?: JsonSchemaObject;
145
+ recordedTurns?: number;
146
+ runTurn: (prompt: string, turn: number, sessionFile: string) => Promise<FinalizationReportSubmission & { error?: string; acceptance?: AcceptanceLedger }>;
147
+ }): Promise<AcceptanceLedger> {
148
+ const review = shouldRunAcceptanceFinalization(input.acceptance);
149
+ const selfReview = review ? acceptanceSelfReviewConfig(input.acceptance) : input.acceptance;
150
+ const initialLedger = input.initialAcceptance ?? await evaluateAcceptance({ acceptance: selfReview, governing: input.acceptance, output: input.initialOutput, report: input.initialReport, cwd: input.cwd, signal: input.signal });
151
+ if (initialLedger.status === "blocked" || !review || input.initial.exitCode !== 0 || input.initial.error || input.initial.interrupted || (input.signal?.aborted && !input.recordedTurns)) return initialLedger;
152
+
153
+ const maxTurns = input.acceptance.finalization.maxTurns;
154
+ const turns: AcceptanceFinalizationTurn[] = [];
155
+ if (!input.sessionFile) {
156
+ const message = "Acceptance finalization requires a session file for same-session continuation.";
157
+ turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
158
+ return buildFinalizationProcessFailureLedger({ initialLedger, turns, maxTurns, message });
159
+ }
160
+ let previousFailure = acceptanceFailureMessage(initialLedger);
161
+ let authoritativeLedger = initialLedger;
162
+ let auditOutput = reportAuditOutput({ output: input.initialOutput, report: input.initialReport });
163
+ for (let turn = 1; turn <= maxTurns; turn++) {
164
+ const prompt = formatAcceptanceFinalizationPrompt({ acceptance: input.acceptance, initialOutput: input.initialOutput, initialLedger, turn, maxTurns, previousFailure, nativeReport: input.nativeReport, outputSchema: input.outputSchema });
165
+ const result = input.signal?.aborted && turn > (input.recordedTurns ?? 0)
166
+ ? { output: "", error: "Acceptance finalization cancelled." }
167
+ : await input.runTurn(prompt, turn, input.sessionFile);
168
+ const retained = result.unconfirmedOutput ?? reportAuditOutput(result);
169
+ if (input.nativeReport && parseAcceptanceReport(retained).report) auditOutput = retained;
170
+ if (result.error) {
171
+ turns.push({ ...createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: result.output, message: result.error }),
172
+ ...(input.nativeReport ? { unconfirmedOutput: auditOutput } : {}) });
173
+ const ledger = buildFinalizationProcessFailureLedger({ initialLedger, turns, maxTurns, message: result.error });
174
+ return input.nativeReport ? { ...ledger, childReport: undefined, childReportParseError: result.reportSubmissionError, unconfirmedOutput: auditOutput } : ledger;
175
+ }
176
+ // Replay each native boundary's checks, not the workspace left by a later repair.
177
+ authoritativeLedger = !result.reportSubmissionError && result.acceptance
178
+ ? result.acceptance
179
+ : await evaluateAcceptance({ acceptance: selfReview, governing: input.acceptance, output: result.reportSubmissionError ? "" : result.output, report: result.reportSubmissionError ? undefined : result.report, cwd: input.cwd, signal: input.signal });
180
+ if (result.reportSubmissionError) {
181
+ authoritativeLedger.childReportParseError = result.reportSubmissionError;
182
+ authoritativeLedger.runtimeChecks = [{ id: "finalization-report", status: "failed", message: result.reportSubmissionError }];
183
+ }
184
+ if (input.nativeReport && authoritativeLedger.childReportParseError) authoritativeLedger.unconfirmedOutput = auditOutput;
185
+ turns.push(createFinalizationTurn({ turn, prompt, rawOutput: result.output, ledger: authoritativeLedger }));
186
+ if (authoritativeLedger.status === "blocked") return attachFinalizationToLedger({ initialLedger, authoritativeLedger, turns, status: "blocked", maxTurns });
187
+ const failure = acceptanceFailureMessage(authoritativeLedger);
188
+ if (!failure && !input.signal?.aborted) {
189
+ if (result.acceptance || selfReview !== input.acceptance) authoritativeLedger = await evaluateAcceptance({ acceptance: input.acceptance, output: result.output, report: result.report, cwd: input.cwd, signal: input.signal });
190
+ return attachFinalizationToLedger({ initialLedger, authoritativeLedger, turns, status: input.signal?.aborted ? "failed" : "completed", maxTurns });
191
+ }
192
+ if (input.signal?.aborted) break;
193
+ previousFailure = failure;
194
+ }
195
+ return attachFinalizationToLedger({ initialLedger, authoritativeLedger, turns, status: "failed", maxTurns });
196
+ }
197
+
198
+ const INITIAL_OUTPUT_LIMIT = 8_000;
199
+
200
+ function truncateForPrompt(value: string): string {
201
+ const trimmed = stripAcceptanceReport(value).trim();
202
+ if (trimmed.length <= INITIAL_OUTPUT_LIMIT) return trimmed || "(initial output was empty after removing acceptance-report)";
203
+ return `${trimmed.slice(0, INITIAL_OUTPUT_LIMIT)}\n...[truncated]`;
204
+ }
205
+
206
+ function formatReportForPrompt(ledger: AcceptanceLedger): string {
207
+ if (ledger.childReport) return JSON.stringify(ledger.childReport, null, 2);
208
+ return `Missing or malformed acceptance report: ${ledger.childReportParseError ?? "no parse detail"}`;
209
+ }
210
+
211
+ export function formatAcceptanceFinalizationPrompt(input: {
212
+ acceptance: ResolvedAcceptanceConfig;
213
+ initialOutput: string;
214
+ initialLedger: AcceptanceLedger;
215
+ turn: number;
216
+ maxTurns: number;
217
+ previousFailure?: string;
218
+ nativeReport?: boolean;
219
+ outputSchema?: JsonSchemaObject;
220
+ }): string {
221
+ const evidence = [...new Set([...input.acceptance.evidence, ...input.acceptance.criteria.flatMap((criterion) => criterion.evidence)])];
222
+ const lines = [
223
+ "## Acceptance Finalization",
224
+ "You are continuing the same subagent session. Before this run can be accepted, compare the current work to the acceptance contract and the evidence below.",
225
+ `This is finalization turn ${input.turn} of ${input.maxTurns}. The run will be rejected if the contract is still not satisfied after turn ${input.maxTurns}.`,
226
+ "",
227
+ "If a criterion is incomplete and fixable in this session, keep working now before returning the final report.",
228
+ "Only an observed human-only boundary, such as Touch ID or an unavailable MFA code, may use criterion status blocked with concrete evidence and a nonempty humanAction describing the exact action needed. Preserve completed criteria/evidence; blocked acceptance is incomplete and stops further review and verification until explicit Continue. Ordinary errors, missing evidence, or fixable work remain not-satisfied, not blocked; explain the blocker in residualRisks.",
229
+ "Do not claim a criterion is satisfied unless the current work has concrete evidence from files, commands, validation output, or other inspectable artifacts.",
230
+ "Report cumulative evidence for the whole delegated task, not just this finalization turn. Retain still-valid changed files, tests, commands, and other evidence from the initial report; correct or remove evidence only when the final state invalidates it. No new edits during review does not mean changedFiles is empty.",
231
+ "",
232
+ "## Acceptance Contract",
233
+ "Criteria:",
234
+ ...(input.acceptance.criteria.length ? input.acceptance.criteria.map((criterion) => `- ${criterion.id}: ${criterion.must}${criterion.evidence.length ? ` (evidence: ${criterion.evidence.join(", ")})` : ""}`) : ["- No explicit criteria were configured; satisfy the requested task and required evidence/checks."]),
235
+ "",
236
+ `Required evidence: ${evidence.join(", ") || "none explicitly requested"}`,
237
+ ];
238
+ if (evidence.length > 0) {
239
+ lines.push(
240
+ "",
241
+ `Structured evidence must be present in the ${input.nativeReport || input.outputSchema ? "typed report object" : "final `acceptance-report` JSON fields"}. Markdown sections in the visible answer do not satisfy required evidence by themselves. If the previous visible output already included the evidence, copy or summarize it into the matching JSON field.`,
242
+ "Evidence field mapping:",
243
+ ...formatEvidenceReportFieldMapping(evidence),
244
+ );
245
+ }
246
+ if (input.acceptance.verify.length > 0) {
247
+ lines.push("", "Runtime verification commands that must pass:", ...input.acceptance.verify.map((command) => `- ${command.id}: ${command.command}`));
248
+ }
249
+ if (input.acceptance.stopRules.length > 0) {
250
+ lines.push("", "Stop rules are hard constraints while deciding whether to continue, stop as blocked, or report success:", ...input.acceptance.stopRules.map((rule) => `- ${rule}`));
251
+ }
252
+ lines.push(
253
+ "",
254
+ "Initial visible output:",
255
+ truncateForPrompt(input.initialOutput),
256
+ "",
257
+ "Initial acceptance report:",
258
+ formatReportForPrompt(input.initialLedger),
259
+ );
260
+ if (input.previousFailure) {
261
+ lines.push("", "Previous finalization failure to address:", input.previousFailure);
262
+ }
263
+ lines.push(
264
+ "",
265
+ input.outputSchema
266
+ ? `${input.nativeReport ? "Your final action must be a sole `structured_output` tool call with {value:{answer,report}}." : "Return the schema-constrained JSON object {answer,report}."} answer must be the complete current public payload matching outputSchema below, including all repairs; do not embed it in a prose string. report is the private typed acceptance object. This submission replaces the initial payload. If more activity follows submission, resubmit the complete current answer and report.\noutputSchema: ${JSON.stringify(input.outputSchema)}`
267
+ : input.nativeReport
268
+ ? "Now do the self-check. Your final action must be a sole `structured_output` tool call with {value:{answer,report}}. answer must contain the complete standalone final answer with the current result and every requested handoff detail (including paths, identifiers, findings, and evidence), repairs or remaining blockers. report is a typed object matching the schema, never JSON embedded in a string. This answer replaces the initial answer; do not replace useful details with only a statement that you rechecked them. If additional messages prompt more activity after submission, resubmit the complete current answer and report as your final action; a prose-only reply does not finalize the task."
269
+ : "Now do the self-check. Return a standalone final answer with the current result and every requested handoff detail (including paths, identifiers, findings, and evidence). This answer replaces the initial answer; do not replace useful details with only a statement that you rechecked them. Include repairs or remaining blockers, then finish with exactly one fenced JSON block tagged `acceptance-report`.",
270
+ ...(input.outputSchema ? [] : [formatAcceptanceReportExample(input.nativeReport)]),
271
+ );
272
+ return lines.join("\n");
273
+ }
274
+
275
+ export function createFinalizationTurn(input: {
276
+ turn: number;
277
+ prompt: string;
278
+ rawOutput: string;
279
+ ledger: AcceptanceLedger;
280
+ }): AcceptanceFinalizationTurn {
281
+ const failureMessage = acceptanceFailureMessage(input.ledger);
282
+ return {
283
+ turn: input.turn,
284
+ prompt: input.prompt,
285
+ status: input.ledger.status,
286
+ rawOutput: input.rawOutput,
287
+ ...(input.ledger.unconfirmedOutput !== undefined ? { unconfirmedOutput: input.ledger.unconfirmedOutput } : {}),
288
+ ...(input.ledger.childReport ? { report: input.ledger.childReport } : {}),
289
+ ...(input.ledger.childReportParseError ? { parseError: input.ledger.childReportParseError } : {}),
290
+ runtimeChecks: input.ledger.runtimeChecks,
291
+ verifyRuns: input.ledger.verifyRuns,
292
+ ...(failureMessage ? { failureMessage } : {}),
293
+ };
294
+ }
295
+
296
+ export function createFinalizationProcessFailureTurn(input: {
297
+ turn: number;
298
+ prompt: string;
299
+ rawOutput?: string;
300
+ message: string;
301
+ }): AcceptanceFinalizationTurn {
302
+ return {
303
+ turn: input.turn,
304
+ prompt: input.prompt,
305
+ status: "rejected",
306
+ ...(input.rawOutput ? { rawOutput: input.rawOutput } : {}),
307
+ runtimeChecks: [{ id: "finalization-process", status: "failed", message: input.message }],
308
+ verifyRuns: [],
309
+ failureMessage: `Acceptance rejected: ${input.message}`,
310
+ };
311
+ }
312
+
313
+ export function attachFinalizationToLedger(input: {
314
+ initialLedger: AcceptanceLedger;
315
+ authoritativeLedger: AcceptanceLedger;
316
+ turns: AcceptanceFinalizationTurn[];
317
+ status: "completed" | "blocked" | "failed";
318
+ maxTurns: number;
319
+ }): AcceptanceLedger {
320
+ return {
321
+ ...input.authoritativeLedger,
322
+ ...(input.initialLedger.childReport ? { initialChildReport: input.initialLedger.childReport } : {}),
323
+ ...(input.initialLedger.childReportParseError ? { initialChildReportParseError: input.initialLedger.childReportParseError } : {}),
324
+ finalization: {
325
+ mode: "self-review-loop",
326
+ status: input.status,
327
+ maxTurns: input.maxTurns,
328
+ turns: input.turns,
329
+ },
330
+ };
331
+ }
332
+
333
+ export function buildFinalizationProcessFailureLedger(input: {
334
+ initialLedger: AcceptanceLedger;
335
+ turns: AcceptanceFinalizationTurn[];
336
+ maxTurns: number;
337
+ message: string;
338
+ }): AcceptanceLedger {
339
+ return attachFinalizationToLedger({
340
+ initialLedger: input.initialLedger,
341
+ authoritativeLedger: {
342
+ ...input.initialLedger,
343
+ status: "rejected",
344
+ runtimeChecks: [
345
+ ...input.initialLedger.runtimeChecks,
346
+ { id: "finalization-process", status: "failed", message: input.message },
347
+ ],
348
+ },
349
+ turns: input.turns,
350
+ status: "failed",
351
+ maxTurns: input.maxTurns,
352
+ });
353
+ }
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AcceptanceReport,
3
+ JsonSchemaObject,
4
+ } from "../../shared/types.ts";
5
+
6
+ // Open finding objects are intentional: native strict conversion must fall back,
7
+ // rather than discarding caller-defined review evidence.
8
+ export const ACCEPTANCE_REPORT_SCHEMA: JsonSchemaObject = {
9
+ type: "object",
10
+ properties: {
11
+ criteriaSatisfied: { type: "array", items: {
12
+ type: "object", properties: {
13
+ id: { type: "string" },
14
+ status: { type: "string", enum: ["satisfied", "not-satisfied", "not-applicable", "blocked"] },
15
+ evidence: { type: "string", pattern: "\\S" },
16
+ humanAction: { type: "string" },
17
+ }, required: ["status", "evidence"],
18
+ if: { properties: { status: { const: "blocked" } } }, then: { required: ["humanAction"], properties: { humanAction: { pattern: "\\S" } } },
19
+ } },
20
+ changedFiles: { type: "array", items: { type: "string" } },
21
+ testsAddedOrUpdated: { type: "array", items: { type: "string" } },
22
+ commandsRun: { type: "array", items: {
23
+ type: "object", properties: {
24
+ command: { type: "string" }, result: { type: "string", enum: ["passed", "failed", "not-run"] }, summary: { type: "string" },
25
+ }, required: ["command", "result", "summary"],
26
+ } },
27
+ validationOutput: { type: "array", items: { type: "string" } },
28
+ residualRisks: { type: "array", items: { type: "string" } },
29
+ noStagedFiles: { type: "boolean" },
30
+ diffSummary: { type: "string" },
31
+ reviewFindings: { type: "array", items: { anyOf: [{ type: "string" }, { type: "object", minProperties: 1, additionalProperties: true }] } },
32
+ manualNotes: { type: "string" },
33
+ notes: { type: "string" },
34
+ },
35
+ };
36
+
37
+ export function formatAcceptanceReportExample(nativeReport = false): string {
38
+ const report: AcceptanceReport = {
39
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof from the final state" }],
40
+ changedFiles: [],
41
+ testsAddedOrUpdated: [],
42
+ commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
43
+ validationOutput: [],
44
+ residualRisks: [],
45
+ noStagedFiles: true,
46
+ diffSummary: "concise summary of changed behavior and important files",
47
+ reviewFindings: [],
48
+ manualNotes: "manual notes or external evidence, if any",
49
+ notes: "self-review summary and remaining work",
50
+ };
51
+ return `\`\`\`${nativeReport ? "json" : "acceptance-report"}\n${JSON.stringify(nativeReport ? { value: { answer: "Complete standalone final answer with all requested handoff details", report } } : report, null, 2)}\n\`\`\``;
52
+ }
53
+
54
+ export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
55
+ const fenced = [...output.matchAll(/```acceptance-report\s*\n([\s\S]*?)```/gi)]
56
+ .map((match) => match[1]?.trim())
57
+ .filter((value): value is string => Boolean(value));
58
+ const parseErrors: string[] = [];
59
+ for (const body of fenced) {
60
+ try {
61
+ const parsed = JSON.parse(body) as unknown;
62
+ const report = (parsed && typeof parsed === "object" && "acceptance" in parsed)
63
+ ? (parsed as { acceptance?: unknown }).acceptance
64
+ : parsed;
65
+ const shapeError = validateAcceptanceReportShape(report);
66
+ if (!shapeError) return { report: report as AcceptanceReport };
67
+ parseErrors.push(`acceptance-report block is invalid: ${shapeError}`);
68
+ } catch (error) {
69
+ parseErrors.push(error instanceof Error ? error.message : String(error));
70
+ }
71
+ }
72
+ if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
73
+ return { error: "Structured acceptance report not found." };
74
+ }
75
+
76
+ export function stripAcceptanceReport(output: string): string {
77
+ return output
78
+ .replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
79
+ .trimEnd();
80
+ }
81
+
82
+ function isStringArray(value: unknown): value is string[] {
83
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
84
+ }
85
+
86
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
87
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
88
+ }
89
+
90
+ function isCriterionReport(value: unknown): value is NonNullable<AcceptanceReport["criteriaSatisfied"]>[number] {
91
+ if (!isPlainObject(value)) return false;
92
+ const criterion = value as { id?: unknown; status?: unknown; evidence?: unknown; humanAction?: unknown };
93
+ if (criterion.id !== undefined && typeof criterion.id !== "string") return false;
94
+ if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable" && criterion.status !== "blocked") return false;
95
+ if (criterion.status === "blocked" && (typeof criterion.humanAction !== "string" || !criterion.humanAction.trim())) return false;
96
+ return typeof criterion.evidence === "string" && criterion.evidence.trim().length > 0;
97
+ }
98
+
99
+ function isCommandReport(value: unknown): value is NonNullable<AcceptanceReport["commandsRun"]>[number] {
100
+ if (!isPlainObject(value)) return false;
101
+ const command = value as { command?: unknown; result?: unknown; summary?: unknown };
102
+ return typeof command.command === "string"
103
+ && (command.result === "passed" || command.result === "failed" || command.result === "not-run")
104
+ && typeof command.summary === "string";
105
+ }
106
+
107
+ function isReviewFinding(value: unknown): value is NonNullable<AcceptanceReport["reviewFindings"]>[number] {
108
+ if (typeof value === "string") return true;
109
+ if (!isPlainObject(value)) return false;
110
+ const values = Object.values(value);
111
+ return values.length > 0 && values.some((item) => typeof item === "string" && item.trim().length > 0);
112
+ }
113
+
114
+ export function validateAcceptanceReportShape(value: unknown): string | undefined {
115
+ if (!isPlainObject(value)) return "acceptance report must be a JSON object";
116
+ const report = value as {
117
+ criteriaSatisfied?: unknown;
118
+ changedFiles?: unknown;
119
+ testsAddedOrUpdated?: unknown;
120
+ commandsRun?: unknown;
121
+ validationOutput?: unknown;
122
+ residualRisks?: unknown;
123
+ noStagedFiles?: unknown;
124
+ diffSummary?: unknown;
125
+ reviewFindings?: unknown;
126
+ manualNotes?: unknown;
127
+ notes?: unknown;
128
+ };
129
+ if (report.criteriaSatisfied !== undefined && (!Array.isArray(report.criteriaSatisfied) || !report.criteriaSatisfied.every(isCriterionReport))) return "criteriaSatisfied must be an array of {id?, status, evidence} objects";
130
+ if (report.changedFiles !== undefined && !isStringArray(report.changedFiles)) return "changedFiles must be an array of strings";
131
+ if (report.testsAddedOrUpdated !== undefined && !isStringArray(report.testsAddedOrUpdated)) return "testsAddedOrUpdated must be an array of strings";
132
+ if (report.commandsRun !== undefined && (!Array.isArray(report.commandsRun) || !report.commandsRun.every(isCommandReport))) return "commandsRun must be an array of {command, result, summary} objects with result passed, failed, or not-run";
133
+ if (report.validationOutput !== undefined && !isStringArray(report.validationOutput)) return "validationOutput must be an array of strings";
134
+ if (report.residualRisks !== undefined && !isStringArray(report.residualRisks)) return "residualRisks must be an array of strings";
135
+ if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") return "noStagedFiles must be a boolean";
136
+ if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") return "diffSummary must be a string";
137
+ if (report.reviewFindings !== undefined && (!Array.isArray(report.reviewFindings) || !report.reviewFindings.every(isReviewFinding))) return "reviewFindings must be an array of strings or non-empty objects with at least one string value";
138
+ if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") return "manualNotes must be a string";
139
+ if (report.notes !== undefined && typeof report.notes !== "string") return "notes must be a string";
140
+ const hasReportField = report.criteriaSatisfied !== undefined
141
+ || report.changedFiles !== undefined
142
+ || report.testsAddedOrUpdated !== undefined
143
+ || report.commandsRun !== undefined
144
+ || report.validationOutput !== undefined
145
+ || report.residualRisks !== undefined
146
+ || report.noStagedFiles !== undefined
147
+ || report.diffSummary !== undefined
148
+ || report.manualNotes !== undefined
149
+ || report.reviewFindings !== undefined;
150
+ return hasReportField ? undefined : "acceptance report must include at least one report field";
151
+ }
152
+
@@ -0,0 +1,33 @@
1
+ export {
2
+ acceptanceInputFromResolved,
3
+ acceptanceSelfReviewConfig,
4
+ formatAcceptancePrompt,
5
+ resolveEffectiveAcceptance,
6
+ shouldRunAcceptanceFinalization,
7
+ validateAcceptanceInput,
8
+ } from "./acceptance-contract.ts";
9
+ export {
10
+ parseAcceptanceReport,
11
+ validateAcceptanceReportShape,
12
+ stripAcceptanceReport,
13
+ } from "./acceptance-reports.ts";
14
+ export {
15
+ acceptanceFailureMessage,
16
+ acceptanceHumanAction,
17
+ evaluateAcceptance,
18
+ evaluateAcceptanceReport,
19
+ } from "./acceptance-evaluation.ts";
20
+ export {
21
+ attachFinalizationToLedger,
22
+ buildFinalizationProcessFailureLedger,
23
+ createFinalizationProcessFailureTurn,
24
+ createFinalizationTurn,
25
+ createFinalizationReportRuntime,
26
+ type FinalizationReportSubmission,
27
+ readFinalizationReport,
28
+ formatUnconfirmedFinalizationOutput,
29
+ formatAcceptanceFinalizationPrompt,
30
+ evaluateRunAcceptance,
31
+ resolveExecutionOutcome,
32
+ resolveFinalizationOutput,
33
+ } from "./acceptance-finalization.ts";
@@ -0,0 +1,114 @@
1
+ import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
2
+ import type { ChainOutputMap, ChainOutputMapEntry } from "../../shared/types.ts";
3
+ import { DynamicFanoutError, hasDynamicFanoutFields, isSafeOutputName, resolveItemTemplate, type DynamicFanoutConfig, validateDynamicStepShape } from "./dynamic-fanout.ts";
4
+
5
+ const OUTPUT_REF_PATTERN = /\{outputs\.([^}]*)\}/g;
6
+
7
+ export class ChainOutputValidationError extends Error {}
8
+
9
+ function outputNamesForStep(step: ChainStep): string[] {
10
+ if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
11
+ if (isDynamicParallelStep(step)) return [step.collect.as];
12
+ const name = (step as SequentialStep).as;
13
+ return name ? [name] : [];
14
+ }
15
+
16
+ function taskTemplatesForStep(step: ChainStep): string[] {
17
+ if (isParallelStep(step)) return step.parallel.map((task) => task.task ?? "{previous}");
18
+ if (isDynamicParallelStep(step)) return [step.parallel.task ?? "{previous}", step.parallel.label ?? ""].filter(Boolean);
19
+ return [(step as SequentialStep).task ?? "{previous}"];
20
+ }
21
+
22
+ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
23
+ const available = new Set<string>();
24
+ const seen = new Set<string>();
25
+ for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
26
+ const step = steps[stepIndex]!;
27
+ if (hasDynamicFanoutFields(step)) {
28
+ if (!isDynamicParallelStep(step)) {
29
+ throw new ChainOutputValidationError(`Dynamic chain step ${stepIndex + 1} requires expand, a single parallel template object, and collect; dynamic expand/collect cannot be mixed with static parallel arrays.`);
30
+ }
31
+ try {
32
+ validateDynamicStepShape(step, stepIndex, dynamicFanoutConfig);
33
+ } catch (error) {
34
+ if (error instanceof DynamicFanoutError) throw new ChainOutputValidationError(error.message);
35
+ throw error;
36
+ }
37
+ if (!available.has(step.expand.from.output)) {
38
+ throw new ChainOutputValidationError(`Dynamic chain step ${stepIndex + 1} references unknown output '${step.expand.from.output}'. Named outputs are only available after producing step/group completes.`);
39
+ }
40
+ }
41
+ for (const name of outputNamesForStep(step)) {
42
+ if (!isSafeOutputName(name)) {
43
+ throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${stepIndex + 1}. Use /^[A-Za-z_][A-Za-z0-9_]*$/.`);
44
+ }
45
+ if (seen.has(name)) {
46
+ throw new ChainOutputValidationError(`Duplicate chain output name '${name}'. Each as name must be unique.`);
47
+ }
48
+ seen.add(name);
49
+ }
50
+ for (const template of taskTemplatesForStep(step)) {
51
+ for (const match of template.matchAll(OUTPUT_REF_PATTERN)) {
52
+ const rawReference = match[0];
53
+ const name = match[1]!;
54
+ if (!isSafeOutputName(name)) {
55
+ throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${stepIndex + 1}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
56
+ }
57
+ if (!available.has(name)) {
58
+ throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${stepIndex + 1}. Named outputs are only available after producing step/group completes.`);
59
+ }
60
+ }
61
+ }
62
+ for (const name of outputNamesForStep(step)) {
63
+ available.add(name);
64
+ }
65
+ }
66
+ }
67
+
68
+ export function resolveOutputReferences(template: string, outputs: ChainOutputMap): string {
69
+ return template.replace(OUTPUT_REF_PATTERN, (rawReference, name: string) => {
70
+ if (!isSafeOutputName(name)) {
71
+ throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}'. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
72
+ }
73
+ const entry = outputs[name];
74
+ if (!entry) throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}'.`);
75
+ return entry.text;
76
+ });
77
+ }
78
+
79
+ export function renderChainTask(template: string, values: {
80
+ originalTask?: string;
81
+ previousOutput?: string;
82
+ chainDir?: string;
83
+ outputs?: ChainOutputMap;
84
+ item?: { name: string; value: unknown };
85
+ }, placeholder = "{previous}"): string {
86
+ const pattern = new RegExp(`${placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|\\{([^{}]*)\\}`, "g");
87
+ const previousOutput = values.previousOutput ?? "";
88
+ let usedPrevious = false;
89
+ const task = template.replace(pattern, (raw, reference: string | undefined) => {
90
+ if (values.item && (raw === `{${values.item.name}}` || raw.startsWith(`{${values.item.name}.`))) {
91
+ return resolveItemTemplate(raw, values.item.name, values.item.value);
92
+ }
93
+ if (raw === placeholder) {
94
+ usedPrevious = true;
95
+ return previousOutput;
96
+ }
97
+ if (reference === "task") return values.originalTask ?? "";
98
+ if (reference === "chain_dir") return values.chainDir ?? "";
99
+ if (reference?.startsWith("outputs.")) return resolveOutputReferences(raw, values.outputs ?? {});
100
+ return raw;
101
+ });
102
+ return !usedPrevious && previousOutput.trim()
103
+ ? `${task}\n\n---\nPrevious step output:\n${previousOutput.trim()}`
104
+ : task;
105
+ }
106
+
107
+ export function outputEntryFromResult(result: { agent: string; output: string; structuredOutput?: unknown }, stepIndex: number): ChainOutputMapEntry {
108
+ return {
109
+ text: result.structuredOutput !== undefined ? JSON.stringify(result.structuredOutput) : result.output,
110
+ ...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
111
+ agent: result.agent,
112
+ stepIndex,
113
+ };
114
+ }