@duckmind/dm-windows-x64 0.61.4 → 0.61.9

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 (279) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +211 -67
  3. package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
  4. package/extensions/dm-subagents/agents/claude-code.md +15 -0
  5. package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
  6. package/extensions/dm-subagents/agents/codex-exec.md +15 -0
  7. package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
  8. package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
  9. package/extensions/dm-subagents/agents/delegate.md +3 -2
  10. package/extensions/dm-subagents/agents/oracle.md +10 -5
  11. package/extensions/dm-subagents/agents/researcher.md +2 -2
  12. package/extensions/dm-subagents/agents/reviewer.md +17 -7
  13. package/extensions/dm-subagents/agents/scout.md +5 -5
  14. package/extensions/dm-subagents/agents/worker.md +6 -2
  15. package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
  16. package/extensions/dm-subagents/index.js +4 -0
  17. package/extensions/dm-subagents/inspector-runner.mjs +10 -0
  18. package/extensions/dm-subagents/install.mjs +3 -2
  19. package/extensions/dm-subagents/package.json +2 -2
  20. package/extensions/dm-subagents/prompts/council.md +60 -0
  21. package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
  22. package/extensions/dm-subagents/prompts/review-loop.md +13 -7
  23. package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
  24. package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  25. package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
  26. package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
  27. package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
  28. package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
  29. package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
  30. package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
  31. package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
  32. package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
  33. package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
  34. package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
  35. package/extensions/dm-subagents/src/agents/agents.js +1447 -299
  36. package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
  37. package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
  38. package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
  39. package/extensions/dm-subagents/src/agents/identity.js +1 -1
  40. package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
  41. package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
  42. package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
  43. package/extensions/dm-subagents/src/agents/skills.js +52 -35
  44. package/extensions/dm-subagents/src/api/agents.js +6 -0
  45. package/extensions/dm-subagents/src/api/background-work.js +151 -0
  46. package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
  47. package/extensions/dm-subagents/src/api/control-channel.js +3 -0
  48. package/extensions/dm-subagents/src/api/delegation.js +5 -0
  49. package/extensions/dm-subagents/src/api/dm-args.js +3 -0
  50. package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
  51. package/extensions/dm-subagents/src/api/external-runs.js +233 -0
  52. package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
  53. package/extensions/dm-subagents/src/api/preflight.js +322 -0
  54. package/extensions/dm-subagents/src/api/project-panes.js +11 -0
  55. package/extensions/dm-subagents/src/api/shared-types.js +4 -0
  56. package/extensions/dm-subagents/src/extension/config.js +175 -0
  57. package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
  58. package/extensions/dm-subagents/src/extension/doctor.js +71 -17
  59. package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
  60. package/extensions/dm-subagents/src/extension/index.js +711 -265
  61. package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
  62. package/extensions/dm-subagents/src/extension/rpc.js +427 -22
  63. package/extensions/dm-subagents/src/extension/schemas.js +158 -65
  64. package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
  65. package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
  66. package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
  67. package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
  68. package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
  69. package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
  70. package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
  71. package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
  72. package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
  73. package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
  74. package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
  75. package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
  76. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
  77. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
  78. package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
  79. package/extensions/dm-subagents/src/missions/actions.js +394 -0
  80. package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
  81. package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
  82. package/extensions/dm-subagents/src/missions/store.js +548 -0
  83. package/extensions/dm-subagents/src/missions/types.js +9 -0
  84. package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
  85. package/extensions/dm-subagents/src/policy/authority.js +37 -0
  86. package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
  87. package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
  88. package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
  89. package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
  90. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
  91. package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
  92. package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
  93. package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
  94. package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
  95. package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
  96. package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
  97. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
  98. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
  99. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
  100. package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
  101. package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
  102. package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
  103. package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
  104. package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
  105. package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
  106. package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
  107. package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
  108. package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
  109. package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
  110. package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
  111. package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
  112. package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
  113. package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
  114. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
  115. package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
  116. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
  117. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
  118. package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
  119. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
  120. package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
  121. package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
  122. package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
  123. package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
  124. package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
  125. package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
  126. package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
  127. package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
  128. package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
  129. package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
  130. package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
  131. package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
  132. package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
  133. package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
  134. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
  135. package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
  136. package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
  137. package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
  138. package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
  139. package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
  140. package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
  141. package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
  142. package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
  143. package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
  144. package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
  145. package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
  146. package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
  147. package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
  148. package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
  149. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
  150. package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
  151. package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
  152. package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
  153. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
  154. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
  155. package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
  156. package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
  157. package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
  158. package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
  159. package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
  160. package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
  161. package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
  162. package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
  163. package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
  164. package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
  165. package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
  166. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
  167. package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
  168. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
  169. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
  170. package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
  171. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
  172. package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
  173. package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
  174. package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
  175. package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
  176. package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
  177. package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
  178. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
  179. package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
  180. package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
  181. package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
  182. package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
  183. package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
  184. package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
  185. package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
  186. package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
  187. package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
  188. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
  189. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
  190. package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
  191. package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
  192. package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
  193. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
  194. package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
  195. package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
  196. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
  197. package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
  198. package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
  199. package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
  200. package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
  201. package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
  202. package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
  203. package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
  204. package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
  205. package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
  206. package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
  207. package/extensions/dm-subagents/src/shared/display-text.js +142 -0
  208. package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
  209. package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
  210. package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
  211. package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
  212. package/extensions/dm-subagents/src/shared/formatters.js +21 -7
  213. package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
  214. package/extensions/dm-subagents/src/shared/model-info.js +10 -5
  215. package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
  216. package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
  217. package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
  218. package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
  219. package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
  220. package/extensions/dm-subagents/src/shared/settings.js +39 -47
  221. package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
  222. package/extensions/dm-subagents/src/shared/status-format.js +9 -2
  223. package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
  224. package/extensions/dm-subagents/src/shared/types.js +47 -7
  225. package/extensions/dm-subagents/src/shared/utf8.js +12 -0
  226. package/extensions/dm-subagents/src/shared/utils.js +151 -131
  227. package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
  228. package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
  229. package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
  230. package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
  231. package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
  232. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
  233. package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
  234. package/extensions/dm-subagents/src/slash/selector.js +101 -0
  235. package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
  236. package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
  237. package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
  238. package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
  239. package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
  240. package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
  241. package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
  242. package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
  243. package/extensions/dm-subagents/src/tui/render.js +1511 -261
  244. package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
  245. package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
  246. package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
  247. package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
  248. package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
  249. package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
  250. package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
  251. package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
  252. package/extensions/dm-subagents/src/watchdog/render.js +54 -0
  253. package/extensions/dm-subagents/src/watchdog/review.js +251 -0
  254. package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
  255. package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
  256. package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
  257. package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
  258. package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
  259. package/extensions/dm-subagents/src/watchdog/types.js +29 -0
  260. package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
  261. package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
  262. package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
  263. package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
  264. package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
  265. package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
  266. package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
  267. package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
  268. package/package.json +4 -3
  269. package/extensions/dm-fff/package.json +0 -21
  270. package/extensions/dm-fff/src/index.js +0 -691
  271. package/extensions/dm-fff/src/query.js +0 -60
  272. package/extensions/dm-subagents/agents/context-builder.md +0 -46
  273. package/extensions/dm-subagents/agents/planner.md +0 -55
  274. package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
  275. package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
  276. package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
  277. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
  278. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
  279. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
@@ -1,15 +1,18 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import * as fs from "node:fs";
3
5
  import * as path from "node:path";
6
+ import { isAgentContractV1 } from "./agent-contract.js";
7
+ import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.js";
4
8
  const LEVEL_RANK = {
5
9
  none: 0,
6
10
  attested: 1,
7
11
  checked: 2,
8
- verified: 3,
9
- reviewed: 4
12
+ verified: 3
10
13
  };
11
- const VALID_LEVELS = new Set(["auto", "none", "attested", "checked", "verified", "reviewed"]);
12
- const VALID_EVIDENCE = new Set([
14
+ const VALID_LEVELS = new Set(["auto", "none", "attested", "checked", "verified"]);
15
+ const VALID_EVIDENCE_KINDS = [
13
16
  "changed-files",
14
17
  "tests-added",
15
18
  "commands-run",
@@ -19,11 +22,15 @@ const VALID_EVIDENCE = new Set([
19
22
  "diff-summary",
20
23
  "review-findings",
21
24
  "manual-notes"
22
- ]);
25
+ ];
26
+ const VALID_EVIDENCE = new Set(VALID_EVIDENCE_KINDS);
27
+ const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
28
+ const ACCEPTANCE_OBJECT_EXAMPLE = 'Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.';
23
29
  const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
24
30
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
25
31
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
26
32
  const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
33
+ const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
27
34
  function normalizeLevel(level) {
28
35
  return level ?? "auto";
29
36
  }
@@ -39,7 +46,6 @@ function requiredEvidenceForLevel(level) {
39
46
  case "checked":
40
47
  return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
41
48
  case "verified":
42
- case "reviewed":
43
49
  return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
44
50
  }
45
51
  }
@@ -47,24 +53,30 @@ function inferLevel(input) {
47
53
  const agent = input.agentName.toLowerCase();
48
54
  const task = input.task?.toLowerCase() ?? "";
49
55
  const reasons = [];
50
- const readOnlyAgent = /\b(?:reviewer|scout|context-builder|researcher|analyst)\b/.test(agent);
51
- const readOnlyTask = /\b(?:read[- ]only|review[- ]only|do not edit|don't edit|no edits|without edits|inspect|summari[sz]e)\b/.test(task);
52
- const writeTask = /\b(?:fix|implement|update|write|edit|modify|migrate|release|security|delete|remove|refactor|commit)\b/.test(task) || /\bworker\b/.test(agent);
53
- const risky = Boolean(input.async && writeTask) || Boolean(input.dynamic) || Boolean(input.dynamicGroup) || /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
56
+ const intent = classifyTaskMutationIntent(input.acceptanceRole ? "worker" : input.agentName, input.task ?? "");
57
+ const readOnlyTask = intent.kind === "read-only" || intent.kind === "unknown" && /\b(?:read[- ]only|review[- ]only|no edits|without edits|inspect|summari[sz]e)\b/.test(task);
58
+ const rolePatchTask = input.acceptanceRole !== undefined && intent.kind !== "read-only" && !/\b(?:do not|don't|must not)\s+patch\b/.test(task) && /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(stripSeverityCompounds(task));
59
+ const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
60
+ const readOnlyAgent = input.acceptanceRole === "read-only" || input.acceptanceRole === undefined && /\b(?:reviewer|oracle|scout|researcher|analyst)\b/.test(agent);
61
+ const writeTask = taskMayWrite || input.acceptanceRole === "writer" && !readOnlyTask || input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask;
62
+ const inferredReadOnly = readOnlyTask || input.acceptanceRole === "read-only" && !taskMayWrite;
63
+ const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
64
+ const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
65
+ const risky = Boolean(input.async && writeTask) || Boolean(input.dynamic) && !roleResolvesReadOnly || Boolean(input.dynamicGroup) && !roleResolvesReadOnly || !keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
54
66
  if (risky) {
55
67
  reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
56
68
  if (input.dynamic || input.dynamicGroup)
57
69
  reasons.push("dynamic fanout context");
58
70
  return {
59
- level: "reviewed",
71
+ level: "checked",
60
72
  reasons,
61
73
  criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
62
- evidence: requiredEvidenceForLevel("reviewed"),
74
+ evidence: requiredEvidenceForLevel("checked"),
63
75
  review: { agent: "reviewer", required: true }
64
76
  };
65
77
  }
66
78
  if (writeTask && !readOnlyTask) {
67
- reasons.push("write-capable worker/task");
79
+ reasons.push(input.acceptanceRole === "writer" && !taskMayWrite ? "declared writer acceptance role" : "write-capable worker/task");
68
80
  return {
69
81
  level: "checked",
70
82
  reasons,
@@ -73,7 +85,7 @@ function inferLevel(input) {
73
85
  };
74
86
  }
75
87
  if (readOnlyAgent || readOnlyTask) {
76
- reasons.push(readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
88
+ reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
77
89
  return {
78
90
  level: "attested",
79
91
  reasons,
@@ -98,9 +110,22 @@ export function normalizeAcceptanceInput(input) {
98
110
  return { level: input };
99
111
  return { ...input };
100
112
  }
113
+ export function normalizeGateAcceptance(gate, acceptance) {
114
+ if (gate === undefined)
115
+ return acceptance === undefined ? { ok: true } : { ok: true, acceptance };
116
+ if (typeof gate !== "string" || !gate.trim())
117
+ return { ok: false, error: "gate must be a non-empty command string." };
118
+ if (acceptance !== undefined)
119
+ return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
120
+ return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
121
+ }
101
122
  function explicitAcceptanceCanDisable(explicit) {
102
123
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
103
124
  }
125
+ function unsupportedEvidenceKindMessage(pathLabel, item) {
126
+ const value = typeof item === "string" ? ` "${item}"` : "";
127
+ return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
128
+ }
104
129
  export function validateAcceptanceInput(input, pathLabel = "acceptance") {
105
130
  const errors = [];
106
131
  if (input === undefined)
@@ -108,12 +133,18 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
108
133
  if (input === false)
109
134
  return errors;
110
135
  if (typeof input === "string") {
111
- if (!VALID_LEVELS.has(input))
136
+ if (input === "reviewed")
137
+ errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
138
+ else if (!VALID_LEVELS.has(input))
112
139
  errors.push(`${pathLabel} has invalid level '${input}'.`);
140
+ else if (input === "none")
141
+ errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
142
+ else if (input === "verified")
143
+ errors.push(`${pathLabel} level "verified" requires object form with at least one runtime verify command. Use level "checked" or provide a non-empty acceptance.verify array.`);
113
144
  return errors;
114
145
  }
115
146
  if (!input || typeof input !== "object" || Array.isArray(input)) {
116
- errors.push(`${pathLabel} must be a string level, false, or an object.`);
147
+ errors.push(`${pathLabel} must be a string level, false, or an object. ${ACCEPTANCE_OBJECT_EXAMPLE}`);
117
148
  return errors;
118
149
  }
119
150
  const value = input;
@@ -121,8 +152,10 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
121
152
  if (!ACCEPTANCE_CONFIG_KEYS.has(key))
122
153
  errors.push(`${pathLabel}.${key} is not supported.`);
123
154
  }
124
- if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level))) {
125
- errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified, reviewed.`);
155
+ if (value.level === "reviewed") {
156
+ errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
157
+ } else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level))) {
158
+ errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
126
159
  }
127
160
  if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
128
161
  errors.push(`${pathLabel}.reason is required when level is none.`);
@@ -132,6 +165,7 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
132
165
  if (value.criteria !== undefined && !Array.isArray(value.criteria))
133
166
  errors.push(`${pathLabel}.criteria must be an array.`);
134
167
  if (Array.isArray(value.criteria)) {
168
+ const criterionIds = new Set;
135
169
  for (const [index, criterion] of value.criteria.entries()) {
136
170
  if (typeof criterion === "string")
137
171
  continue;
@@ -145,16 +179,22 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
145
179
  if (!ACCEPTANCE_GATE_KEYS.has(key))
146
180
  errors.push(`${criterionPath}.${key} is not supported.`);
147
181
  }
148
- if (typeof gate.id !== "string" || !gate.id.trim())
182
+ if (typeof gate.id !== "string" || !gate.id.trim()) {
149
183
  errors.push(`${criterionPath}.id is required.`);
184
+ } else {
185
+ const normalizedId = normalizedToken(gate.id);
186
+ if (criterionIds.has(normalizedId))
187
+ errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalizedId}'.`);
188
+ criterionIds.add(normalizedId);
189
+ }
150
190
  if (typeof gate.must !== "string" || !gate.must.trim())
151
191
  errors.push(`${criterionPath}.must is required.`);
152
192
  if (gate.evidence !== undefined && !Array.isArray(gate.evidence))
153
- errors.push(`${criterionPath}.evidence must be an array.`);
193
+ errors.push(`${criterionPath}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
154
194
  if (Array.isArray(gate.evidence)) {
155
195
  for (const [evidenceIndex, item] of gate.evidence.entries()) {
156
196
  if (typeof item !== "string" || !VALID_EVIDENCE.has(item)) {
157
- errors.push(`${criterionPath}.evidence[${evidenceIndex}] is not a supported evidence kind.`);
197
+ errors.push(unsupportedEvidenceKindMessage(`${criterionPath}.evidence[${evidenceIndex}]`, item));
158
198
  }
159
199
  }
160
200
  }
@@ -166,14 +206,17 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
166
206
  if (Array.isArray(value.evidence)) {
167
207
  for (const [index, item] of value.evidence.entries()) {
168
208
  if (typeof item !== "string" || !VALID_EVIDENCE.has(item)) {
169
- errors.push(`${pathLabel}.evidence[${index}] is not a supported evidence kind.`);
209
+ errors.push(unsupportedEvidenceKindMessage(`${pathLabel}.evidence[${index}]`, item));
170
210
  }
171
211
  }
172
212
  } else if (value.evidence !== undefined) {
173
- errors.push(`${pathLabel}.evidence must be an array.`);
213
+ errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
174
214
  }
175
- if (value.verify !== undefined && !Array.isArray(value.verify))
215
+ if (value.level === "verified" && (!Array.isArray(value.verify) || value.verify.length === 0)) {
216
+ errors.push(`${pathLabel}.verify must contain at least one runtime command when level is verified. Use level "checked" or provide a non-empty acceptance.verify array.`);
217
+ } else if (value.verify !== undefined && !Array.isArray(value.verify)) {
176
218
  errors.push(`${pathLabel}.verify must be an array.`);
219
+ }
177
220
  if (Array.isArray(value.verify)) {
178
221
  for (const [index, command] of value.verify.entries()) {
179
222
  if (!command || typeof command !== "object" || Array.isArray(command)) {
@@ -236,6 +279,23 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
236
279
  }
237
280
  return errors;
238
281
  }
282
+ export function validateExecutionAcceptance(input) {
283
+ const errors = validateAcceptanceInput(input.acceptance, "acceptance");
284
+ for (const [index, task] of (input.tasks ?? []).entries()) {
285
+ errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
286
+ }
287
+ for (const [stepIndex, step] of (input.chain ?? []).entries()) {
288
+ errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
289
+ if (Array.isArray(step.parallel)) {
290
+ for (const [taskIndex, task] of step.parallel.entries()) {
291
+ errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
292
+ }
293
+ } else if (step.parallel) {
294
+ errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
295
+ }
296
+ }
297
+ return errors;
298
+ }
239
299
  function normalizeCriteria(criteria, evidence) {
240
300
  return (criteria ?? []).map((criterion, index) => {
241
301
  if (typeof criterion === "string") {
@@ -251,15 +311,28 @@ function normalizeCriteria(criteria, evidence) {
251
311
  }
252
312
  export function resolveEffectiveAcceptance(input) {
253
313
  const explicit = normalizeAcceptanceInput(input.explicit);
254
- const inferred = inferLevel(input);
255
314
  const explicitLevel = normalizeLevel(explicit.level);
315
+ if (isAgentContractV1(input.agentContract)) {
316
+ const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto" ? "none" : explicitLevel;
317
+ const evidence = unique(explicit.evidence ?? []);
318
+ const criteria = normalizeCriteria(explicit.criteria, evidence);
319
+ return {
320
+ level,
321
+ explicit: input.explicit !== undefined,
322
+ inferredReason: [],
323
+ criteria,
324
+ evidence,
325
+ verify: explicit.verify ?? [],
326
+ review: explicit.review,
327
+ stopRules: explicit.stopRules ?? [],
328
+ reason: explicit.reason
329
+ };
330
+ }
331
+ const inferred = inferLevel(input);
256
332
  const level = explicitAcceptanceCanDisable(explicit) ? "none" : explicitLevel === "auto" ? inferred.level : LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferred.level] ? explicitLevel : inferred.level;
257
333
  const evidence = unique([...level === inferred.level ? inferred.evidence : requiredEvidenceForLevel(level), ...explicit.evidence ?? []]);
258
334
  const criteria = normalizeCriteria(explicit.criteria?.length ? explicit.criteria : inferred.criteria, evidence);
259
- let review = explicit.review !== undefined ? explicit.review : inferred.review;
260
- if (level === "reviewed" && explicitLevel !== "auto" && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
261
- review = { ...review, required: false };
262
- }
335
+ const review = explicit.review !== undefined ? explicit.review : inferred.review;
263
336
  return {
264
337
  level,
265
338
  explicit: input.explicit !== undefined,
@@ -272,9 +345,14 @@ export function resolveEffectiveAcceptance(input) {
272
345
  reason: explicit.reason
273
346
  };
274
347
  }
275
- export function formatAcceptancePrompt(acceptance) {
348
+ function acceptanceRequiresChildReport(acceptance) {
349
+ return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
350
+ }
351
+ export function formatAcceptancePrompt(acceptance, options = {}) {
276
352
  if (acceptance.level === "none")
277
353
  return "";
354
+ if (options.reportOptional && !acceptanceRequiresChildReport(acceptance))
355
+ return "";
278
356
  const lines = [
279
357
  "",
280
358
  "## Acceptance Contract",
@@ -291,7 +369,7 @@ export function formatAcceptancePrompt(acceptance) {
291
369
  for (const command of acceptance.verify)
292
370
  lines.push(`- ${command.id}: ${command.command}`);
293
371
  }
294
- if (acceptance.review && acceptance.review !== false) {
372
+ if (acceptance.review) {
295
373
  lines.push("", `Review gate: ${acceptance.review.required === false ? "optional" : "required"}${acceptance.review.agent ? ` by ${acceptance.review.agent}` : ""}.`);
296
374
  if (acceptance.review.focus)
297
375
  lines.push(`Review focus: ${acceptance.review.focus}`);
@@ -299,8 +377,8 @@ export function formatAcceptancePrompt(acceptance) {
299
377
  if (acceptance.stopRules.length > 0) {
300
378
  lines.push("", "Stop rules:", ...acceptance.stopRules.map((rule) => `- ${rule}`));
301
379
  }
302
- lines.push("", "Finish with a fenced JSON block tagged `acceptance-report` in this shape:", "Use empty arrays when no items apply; array fields contain strings unless object entries are shown.", "```acceptance-report", JSON.stringify({
303
- criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof" }],
380
+ lines.push("", options.structuredOutput ? "Include an `acceptanceReport` object in your final `structured_output` tool call in this shape:" : "Finish with a fenced JSON block tagged `acceptance-report` in this shape:", "Use empty arrays when no items apply; array fields contain strings unless object entries are shown.", 'Empty-string entries (`[""]`) are ignored; use `[]` when nothing applies.', "`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.", "`commandsRun[].result` must be exactly one of: passed, failed, not-run.", "`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.", ...options.structuredOutput ? [] : ["```acceptance-report"], JSON.stringify({
381
+ criteriaSatisfied: acceptance.criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => ({ id: criterion.id, status: "satisfied", evidence: "specific proof" })),
304
382
  changedFiles: ["src/file.ts"],
305
383
  testsAddedOrUpdated: ["test/file.test.ts"],
306
384
  commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
@@ -310,7 +388,7 @@ export function formatAcceptancePrompt(acceptance) {
310
388
  diffSummary: "short description of the diff",
311
389
  reviewFindings: ["blocker: file.ts:12 - issue found, or no blockers"],
312
390
  manualNotes: "anything else the parent should know"
313
- }, null, 2), "```");
391
+ }, null, 2), ...options.structuredOutput ? [] : ["```"]);
314
392
  return lines.join(`
315
393
  `);
316
394
  }
@@ -343,29 +421,164 @@ function extractBalancedJson(text, start) {
343
421
  }
344
422
  return;
345
423
  }
346
- function unwrapAcceptanceReport(value) {
347
- if (!value || typeof value !== "object" || Array.isArray(value))
424
+ const ACCEPTANCE_REPORT_WRAPPERS = new Set(["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]);
425
+ const ACCEPTANCE_REPORT_FIELDS = {
426
+ criteriaSatisfied: "criteriaSatisfied",
427
+ criteria_satisfied: "criteriaSatisfied",
428
+ changedFiles: "changedFiles",
429
+ changed_files: "changedFiles",
430
+ testsAddedOrUpdated: "testsAddedOrUpdated",
431
+ tests_added_or_updated: "testsAddedOrUpdated",
432
+ commandsRun: "commandsRun",
433
+ commands_run: "commandsRun",
434
+ validationOutput: "validationOutput",
435
+ validation_output: "validationOutput",
436
+ residualRisks: "residualRisks",
437
+ residual_risks: "residualRisks",
438
+ noStagedFiles: "noStagedFiles",
439
+ no_staged_files: "noStagedFiles",
440
+ diffSummary: "diffSummary",
441
+ diff_summary: "diffSummary",
442
+ reviewFindings: "reviewFindings",
443
+ review_findings: "reviewFindings",
444
+ manualNotes: "manualNotes",
445
+ manual_notes: "manualNotes",
446
+ notes: "notes"
447
+ };
448
+ const CRITERION_REPORT_FIELDS = new Set(["id", "status", "evidence"]);
449
+ const COMMAND_REPORT_FIELDS = new Set(["command", "result", "summary"]);
450
+ function normalizedToken(value) {
451
+ return value.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
452
+ }
453
+ function normalizeCriterionStatus(value) {
454
+ if (typeof value !== "string")
348
455
  return value;
349
- const record = value;
350
- if ("acceptance" in record)
351
- return record.acceptance;
352
- if ("acceptance-report" in record)
353
- return record["acceptance-report"];
456
+ const token = normalizedToken(value);
457
+ if (["satisfied", "met", "complete", "completed", "done", "pass", "passed", "success", "succeeded"].includes(token))
458
+ return "satisfied";
459
+ if (["not-satisfied", "not-met", "unmet", "incomplete", "fail", "failed"].includes(token))
460
+ return "not-satisfied";
461
+ if (["not-applicable", "n-a", "na", "skip", "skipped"].includes(token))
462
+ return "not-applicable";
354
463
  return value;
355
464
  }
356
- function isCommandsRunArray(value) {
357
- return Array.isArray(value) && value.every((item) => {
358
- if (!item || typeof item !== "object" || Array.isArray(item))
359
- return false;
360
- const command = item;
361
- return typeof command.command === "string" && (command.result === "passed" || command.result === "failed" || command.result === "not-run") && typeof command.summary === "string";
362
- });
465
+ function normalizeCommandResult(value) {
466
+ if (typeof value !== "string")
467
+ return value;
468
+ const token = normalizedToken(value);
469
+ if (["passed", "pass", "success", "successful", "succeeded", "ok"].includes(token))
470
+ return "passed";
471
+ if (["failed", "fail", "failure", "error"].includes(token))
472
+ return "failed";
473
+ if (["not-run", "not-executed", "skip", "skipped"].includes(token))
474
+ return "not-run";
475
+ return value;
476
+ }
477
+ function normalizeCriterionReport(value, pathLabel, errors) {
478
+ if (!value || typeof value !== "object" || Array.isArray(value))
479
+ return value;
480
+ const normalized = {};
481
+ for (const [key, fieldValue] of Object.entries(value)) {
482
+ if (!CRITERION_REPORT_FIELDS.has(key)) {
483
+ errors.push(`${pathLabel}.${key}: unsupported acceptance criterion field`);
484
+ continue;
485
+ }
486
+ normalized[key] = key === "id" && typeof fieldValue === "string" ? normalizedToken(fieldValue) : key === "status" ? normalizeCriterionStatus(fieldValue) : fieldValue;
487
+ }
488
+ return normalized;
489
+ }
490
+ function normalizeCommandReport(value, pathLabel, errors) {
491
+ if (!value || typeof value !== "object" || Array.isArray(value))
492
+ return value;
493
+ const normalized = {};
494
+ for (const [key, fieldValue] of Object.entries(value)) {
495
+ if (!COMMAND_REPORT_FIELDS.has(key)) {
496
+ errors.push(`${pathLabel}.${key}: unsupported acceptance command field`);
497
+ continue;
498
+ }
499
+ normalized[key] = key === "result" ? normalizeCommandResult(fieldValue) : fieldValue;
500
+ }
501
+ return normalized;
502
+ }
503
+ function normalizeAcceptanceReportValue(value, pathLabel = "") {
504
+ const errors = [];
505
+ let reportValue = value;
506
+ let reportPath = pathLabel;
507
+ if (reportValue && typeof reportValue === "object" && !Array.isArray(reportValue)) {
508
+ const record = reportValue;
509
+ const wrapperKeys = Object.keys(record).filter((key) => ACCEPTANCE_REPORT_WRAPPERS.has(key));
510
+ if (wrapperKeys.length > 0) {
511
+ const wrapperKey = wrapperKeys[0];
512
+ if (wrapperKeys.length > 1)
513
+ errors.push(`${pathLabel || "acceptance-report"}: multiple acceptance report wrappers are ambiguous`);
514
+ for (const key of Object.keys(record)) {
515
+ if (key !== wrapperKey)
516
+ errors.push(`${pathFor(pathLabel, key)}: unsupported alongside acceptance report wrapper '${wrapperKey}'`);
517
+ }
518
+ reportValue = record[wrapperKey];
519
+ reportPath = pathFor(pathLabel, wrapperKey);
520
+ }
521
+ }
522
+ if (!reportValue || typeof reportValue !== "object" || Array.isArray(reportValue))
523
+ return { value: reportValue, pathLabel: reportPath, errors };
524
+ const normalized = {};
525
+ for (const [key, fieldValue] of Object.entries(reportValue)) {
526
+ const canonical = ACCEPTANCE_REPORT_FIELDS[key];
527
+ if (!canonical) {
528
+ errors.push(`${pathFor(reportPath, key)}: unsupported acceptance report field`);
529
+ continue;
530
+ }
531
+ if (Object.hasOwn(normalized, canonical)) {
532
+ errors.push(`${pathFor(reportPath, key)}: duplicates normalized field '${canonical}'`);
533
+ continue;
534
+ }
535
+ const fieldPath = pathFor(reportPath, canonical);
536
+ switch (canonical) {
537
+ case "criteriaSatisfied": {
538
+ const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
539
+ normalized[canonical] = Array.isArray(items) ? items.map((item, index) => normalizeCriterionReport(item, `${fieldPath}[${index}]`, errors)) : items;
540
+ break;
541
+ }
542
+ case "commandsRun": {
543
+ const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
544
+ normalized[canonical] = Array.isArray(items) ? items.map((item, index) => normalizeCommandReport(item, `${fieldPath}[${index}]`, errors)) : items;
545
+ break;
546
+ }
547
+ case "changedFiles":
548
+ case "testsAddedOrUpdated":
549
+ case "validationOutput":
550
+ case "residualRisks":
551
+ case "reviewFindings": {
552
+ const items = typeof fieldValue === "string" ? [fieldValue] : fieldValue;
553
+ normalized[canonical] = Array.isArray(items) ? items.filter((item) => typeof item !== "string" || item.trim().length > 0) : items;
554
+ break;
555
+ }
556
+ case "noStagedFiles": {
557
+ const token = typeof fieldValue === "string" ? fieldValue.trim().toLowerCase() : undefined;
558
+ normalized[canonical] = token === "true" ? true : token === "false" ? false : fieldValue;
559
+ break;
560
+ }
561
+ default:
562
+ normalized[canonical] = fieldValue;
563
+ }
564
+ }
565
+ return { value: normalized, pathLabel: reportPath, errors };
363
566
  }
364
567
  function hasGenericAcceptanceReportSignal(value) {
365
568
  if (!value || typeof value !== "object" || Array.isArray(value))
366
569
  return false;
367
570
  const record = value;
368
- return "criteriaSatisfied" in record && (isStringArray(record.changedFiles) || isStringArray(record.testsAddedOrUpdated) || isCommandsRunArray(record.commandsRun) || isStringArray(record.validationOutput) || isStringArray(record.residualRisks) || typeof record.noStagedFiles === "boolean" || typeof record.diffSummary === "string" || isStringArray(record.reviewFindings) || typeof record.manualNotes === "string");
571
+ return "criteriaSatisfied" in record && [
572
+ "changedFiles",
573
+ "testsAddedOrUpdated",
574
+ "commandsRun",
575
+ "validationOutput",
576
+ "residualRisks",
577
+ "noStagedFiles",
578
+ "diffSummary",
579
+ "reviewFindings",
580
+ "manualNotes"
581
+ ].some((key) => (key in record));
369
582
  }
370
583
  function parseReportJson(body) {
371
584
  const trimmed = body.trim();
@@ -384,31 +597,36 @@ function parseReportJson(body) {
384
597
  function fencedBlocks(output, tag) {
385
598
  return [...output.matchAll(new RegExp(`\`\`\`${tag}\\s*\\n([\\s\\S]*?)\`\`\``, "gi"))].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
386
599
  }
387
- function validationPathLabelForWrapper(value) {
388
- if (!value || typeof value !== "object" || Array.isArray(value))
389
- return "";
390
- const record = value;
391
- if ("acceptance" in record)
392
- return "acceptance";
393
- if ("acceptance-report" in record)
394
- return "acceptance-report";
395
- return "";
396
- }
397
600
  function parseAcceptanceReportBody(body) {
398
- const parsed = parseReportJson(body);
399
- const report = unwrapAcceptanceReport(parsed);
400
- return validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
601
+ return validateAcceptanceReport(parseReportJson(body));
602
+ }
603
+ function parseUnterminatedAcceptanceReportFence(output) {
604
+ const opener = /```acceptance[-_]report\b[^\n]*\n/gi.exec(output);
605
+ if (!opener)
606
+ return {};
607
+ const bodyStart = opener.index + opener[0].length;
608
+ if (output.indexOf("```", bodyStart) !== -1)
609
+ return {};
610
+ try {
611
+ const validation = validateAcceptanceReport(JSON.parse(output.slice(bodyStart).trim()));
612
+ return validation.report ? { report: validation.report } : { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
613
+ } catch (error) {
614
+ return { error: `Failed to parse acceptance-report: ${error instanceof Error ? error.message : String(error)}`, malformed: true };
615
+ }
401
616
  }
402
617
  function parseGenericJsonAcceptanceReportBody(body) {
403
618
  const parsed = parseReportJson(body);
404
- const report = unwrapAcceptanceReport(parsed);
405
- const validation = validateAcceptanceReport(report);
406
- if (!validation.report)
407
- return;
408
- return hasGenericAcceptanceReportSignal(validation.report) ? validation.report : undefined;
619
+ const normalized = normalizeAcceptanceReportValue(parsed);
620
+ const hasCriteriaMarker = normalized.value !== null && typeof normalized.value === "object" && !Array.isArray(normalized.value) && "criteriaSatisfied" in normalized.value;
621
+ if (!hasGenericAcceptanceReportSignal(normalized.value) && !(hasCriteriaMarker && normalized.errors.length > 0))
622
+ return {};
623
+ const validation = validateAcceptanceReport(parsed);
624
+ return validation.report ? { report: validation.report } : { error: `Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
409
625
  }
626
+ export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
410
627
  export function parseAcceptanceReport(output) {
411
- const fenced = fencedBlocks(output, "acceptance-report");
628
+ const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
629
+ const fenced = fencedBlocks(output, "acceptance[-_]report");
412
630
  const parseErrors = [];
413
631
  for (const body of fenced) {
414
632
  try {
@@ -421,37 +639,65 @@ export function parseAcceptanceReport(output) {
421
639
  }
422
640
  }
423
641
  if (parseErrors.length > 0)
424
- return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
642
+ return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}`, malformed: true };
643
+ if (explicitFencePresent) {
644
+ const recovered = parseUnterminatedAcceptanceReportFence(output);
645
+ if (recovered.report || recovered.error)
646
+ return recovered;
647
+ return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence.", malformed: true };
648
+ }
425
649
  for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
426
650
  try {
427
- const report = parseGenericJsonAcceptanceReportBody(body);
428
- if (report)
429
- return { report };
651
+ const parsed = parseGenericJsonAcceptanceReportBody(body);
652
+ if (parsed.report)
653
+ return { report: parsed.report };
654
+ if (parsed.error)
655
+ return { error: `Failed to parse acceptance-report: ${parsed.error}`, malformed: parsed.malformed };
430
656
  } catch {}
431
657
  }
432
658
  const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
433
659
  if (markerIndex !== -1) {
434
660
  const jsonStart = output.indexOf("{", markerIndex);
435
- if (jsonStart !== -1) {
436
- const json = extractBalancedJson(output, jsonStart);
437
- if (json) {
438
- try {
439
- const parsed = JSON.parse(json);
440
- const report = unwrapAcceptanceReport(parsed);
441
- const validation = validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
442
- if (validation.report)
443
- return { report: validation.report };
444
- return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
445
- } catch (error) {
446
- return { error: error instanceof Error ? error.message : String(error) };
447
- }
448
- }
661
+ if (jsonStart === -1) {
662
+ return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:.", malformed: true };
663
+ }
664
+ const json = extractBalancedJson(output, jsonStart);
665
+ if (!json) {
666
+ return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:.", malformed: true };
667
+ }
668
+ try {
669
+ const parsed = JSON.parse(json);
670
+ const validation = validateAcceptanceReport(parsed);
671
+ if (validation.report)
672
+ return { report: validation.report };
673
+ return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
674
+ } catch (error) {
675
+ const message = error instanceof Error ? error.message : String(error);
676
+ return { error: `Failed to parse acceptance-report: ${message}`, malformed: true };
449
677
  }
450
678
  }
451
- return { error: "Structured acceptance report not found." };
679
+ return { error: ACCEPTANCE_REPORT_NOT_FOUND };
680
+ }
681
+ function parseAcceptanceReportSources(output, fileOutput) {
682
+ const fromText = () => parseAcceptanceReport(output);
683
+ const fromFile = () => {
684
+ if (!fileOutput)
685
+ return { error: ACCEPTANCE_REPORT_NOT_FOUND };
686
+ const parsed = parseAcceptanceReport(fileOutput.content);
687
+ return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND ? parsed : {
688
+ ...parsed,
689
+ error: `${parsed.error} (in configured output ${fileOutput.path})`,
690
+ sourcePath: fileOutput.path
691
+ };
692
+ };
693
+ const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
694
+ const first = primary();
695
+ if (first.report || first.error !== ACCEPTANCE_REPORT_NOT_FOUND)
696
+ return first;
697
+ return secondary();
452
698
  }
453
699
  export function stripAcceptanceReport(output) {
454
- const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
700
+ const trailingFencePattern = /\n?```(acceptance[-_]report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
455
701
  let trailingFence;
456
702
  for (const match of output.matchAll(trailingFencePattern)) {
457
703
  const end = (match.index ?? 0) + match[0].length;
@@ -460,14 +706,14 @@ export function stripAcceptanceReport(output) {
460
706
  }
461
707
  }
462
708
  if (trailingFence) {
463
- if (trailingFence.tag === "acceptance-report")
709
+ if (trailingFence.tag === "acceptance-report" || trailingFence.tag === "acceptance_report")
464
710
  return output.slice(0, trailingFence.index).trimEnd();
465
711
  try {
466
- if (parseGenericJsonAcceptanceReportBody(trailingFence.body))
712
+ if (parseGenericJsonAcceptanceReportBody(trailingFence.body).report)
467
713
  return output.slice(0, trailingFence.index).trimEnd();
468
714
  } catch {}
469
715
  }
470
- return output.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "").replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "").trimEnd();
716
+ return output.replace(/\n?```acceptance[-_]report\s*\n[\s\S]*?```\s*$/i, "").replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "").trimEnd();
471
717
  }
472
718
  function isStringArray(value) {
473
719
  return Array.isArray(value) && value.every((item) => typeof item === "string");
@@ -499,12 +745,15 @@ function validateStringArrayField(errors, value, pathLabel) {
499
745
  return;
500
746
  }
501
747
  for (const [index, item] of value.entries()) {
502
- if (typeof item !== "string")
503
- pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
748
+ if (typeof item !== "string" || !item.trim())
749
+ pushTypeError(errors, `${pathLabel}[${index}]`, "non-empty string", item);
504
750
  }
505
751
  }
506
752
  function validateAcceptanceReport(value, pathLabel = "") {
507
- const errors = [];
753
+ const normalized = normalizeAcceptanceReportValue(value, pathLabel);
754
+ value = normalized.value;
755
+ pathLabel = normalized.pathLabel;
756
+ const errors = normalized.errors;
508
757
  if (!value || typeof value !== "object" || Array.isArray(value)) {
509
758
  pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
510
759
  return { errors };
@@ -514,6 +763,7 @@ function validateAcceptanceReport(value, pathLabel = "") {
514
763
  if (!Array.isArray(report.criteriaSatisfied)) {
515
764
  pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
516
765
  } else {
766
+ const criterionIds = new Set;
517
767
  for (const [index, item] of report.criteriaSatisfied.entries()) {
518
768
  const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
519
769
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -521,8 +771,13 @@ function validateAcceptanceReport(value, pathLabel = "") {
521
771
  continue;
522
772
  }
523
773
  const criterion = item;
524
- if (criterion.id !== undefined && typeof criterion.id !== "string")
774
+ if (criterion.id !== undefined && typeof criterion.id !== "string") {
525
775
  pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
776
+ } else if (typeof criterion.id === "string" && criterion.id) {
777
+ if (criterionIds.has(criterion.id))
778
+ errors.push(`${itemPath}.id: duplicate normalized criterion id '${criterion.id}'`);
779
+ criterionIds.add(criterion.id);
780
+ }
526
781
  if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
527
782
  pushTypeError(errors, `${itemPath}.status`, 'one of "satisfied", "not-satisfied", "not-applicable"', criterion.status);
528
783
  }
@@ -551,8 +806,8 @@ function validateAcceptanceReport(value, pathLabel = "") {
551
806
  if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
552
807
  pushTypeError(errors, `${itemPath}.result`, 'one of "passed", "failed", "not-run"', command.result);
553
808
  }
554
- if (typeof command.summary !== "string")
555
- pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
809
+ if (typeof command.summary !== "string" || !command.summary.trim())
810
+ pushTypeError(errors, `${itemPath}.summary`, "non-empty string", command.summary);
556
811
  }
557
812
  }
558
813
  }
@@ -562,8 +817,8 @@ function validateAcceptanceReport(value, pathLabel = "") {
562
817
  validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
563
818
  if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean")
564
819
  pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
565
- if (report.diffSummary !== undefined && typeof report.diffSummary !== "string")
566
- pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
820
+ if (report.diffSummary !== undefined && (typeof report.diffSummary !== "string" || !report.diffSummary.trim()))
821
+ pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "non-empty string", report.diffSummary);
567
822
  if (report.reviewFindings !== undefined)
568
823
  validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
569
824
  if (report.manualNotes !== undefined && typeof report.manualNotes !== "string")
@@ -576,9 +831,9 @@ function validateAcceptanceReport(value, pathLabel = "") {
576
831
  return hasReportField ? { report, errors } : { errors: [`${pathLabel || "acceptance-report"}: expected at least one acceptance report field`] };
577
832
  }
578
833
  function checkCriteriaSatisfied(criteria, report) {
579
- const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id, item]));
834
+ const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [normalizedToken(item.id), item]));
580
835
  return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
581
- const item = reports.get(criterion.id);
836
+ const item = reports.get(normalizedToken(criterion.id));
582
837
  if (!item)
583
838
  return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
584
839
  if (item.status !== "satisfied")
@@ -586,30 +841,34 @@ function checkCriteriaSatisfied(criteria, report) {
586
841
  return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
587
842
  });
588
843
  }
589
- function reportEvidencePresent(report, kind) {
844
+ function reportEvidenceStatus(report, kind) {
590
845
  switch (kind) {
591
846
  case "changed-files":
592
- return isStringArray(report.changedFiles) && report.changedFiles.length > 0;
847
+ if (!isStringArray(report.changedFiles))
848
+ return "failed";
849
+ return report.changedFiles.length === 0 ? "not-applicable" : "passed";
593
850
  case "tests-added":
594
- return isStringArray(report.testsAddedOrUpdated) && report.testsAddedOrUpdated.length > 0;
851
+ if (!isStringArray(report.testsAddedOrUpdated))
852
+ return "failed";
853
+ return report.testsAddedOrUpdated.length === 0 ? "not-applicable" : "passed";
595
854
  case "commands-run":
596
- return Array.isArray(report.commandsRun) && report.commandsRun.length > 0;
855
+ return Array.isArray(report.commandsRun) && report.commandsRun.length > 0 ? "passed" : "failed";
597
856
  case "validation-output":
598
- return isStringArray(report.validationOutput) && report.validationOutput.length > 0;
857
+ return isStringArray(report.validationOutput) && report.validationOutput.length > 0 ? "passed" : "failed";
599
858
  case "residual-risks":
600
- return isStringArray(report.residualRisks);
859
+ return isStringArray(report.residualRisks) ? "passed" : "failed";
601
860
  case "no-staged-files":
602
- return report.noStagedFiles === true;
861
+ return report.noStagedFiles === true ? "passed" : "failed";
603
862
  case "diff-summary":
604
- return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
863
+ return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0 ? "passed" : "failed";
605
864
  case "review-findings":
606
- return isStringArray(report.reviewFindings);
865
+ return isStringArray(report.reviewFindings) ? "passed" : "failed";
607
866
  case "manual-notes":
608
- return Boolean((report.manualNotes ?? report.notes)?.trim());
867
+ return Boolean((report.manualNotes ?? report.notes)?.trim()) ? "passed" : "failed";
609
868
  }
610
869
  }
611
870
  function checkNoStagedFiles(cwd) {
612
- const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
871
+ const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8", windowsHide: true });
613
872
  if (result.status !== 0) {
614
873
  return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
615
874
  }
@@ -619,11 +878,13 @@ function checkNoStagedFiles(cwd) {
619
878
  function runStructuralChecks(acceptance, report, cwd) {
620
879
  const checks = [];
621
880
  for (const kind of acceptance.evidence) {
622
- const present = reportEvidencePresent(report, kind);
881
+ if (kind === "no-staged-files" && report.noStagedFiles === undefined)
882
+ continue;
883
+ const status = reportEvidenceStatus(report, kind);
623
884
  checks.push({
624
885
  id: `evidence:${kind}`,
625
- status: present ? "passed" : "failed",
626
- message: present ? `${kind} evidence present.` : `${kind} evidence missing from child report.`
886
+ status,
887
+ message: status === "passed" ? `${kind} evidence present.` : status === "not-applicable" ? `${kind} evidence explicitly reported as not applicable.` : `${kind} evidence missing from child report.`
627
888
  });
628
889
  }
629
890
  if (acceptance.evidence.includes("no-staged-files"))
@@ -637,6 +898,25 @@ function trimOutput(value) {
637
898
  return trimmed.length > 12000 ? `${trimmed.slice(0, 12000)}
638
899
  ...[truncated]` : trimmed;
639
900
  }
901
+ const SENSITIVE_ENV_KEY_PATTERN = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASS|AUTH|CREDENTIAL|COOKIE|SESSION|PRIVATE|API_KEY|ACCESS_KEY)(?:_|$)/i;
902
+ function effectiveVerifyEnv(env) {
903
+ const inherited = Object.fromEntries(Object.entries(process.env).flatMap(([key, value]) => {
904
+ return typeof value === "string" ? [[key, value]] : [];
905
+ }));
906
+ return { ...inherited, ...env ?? {} };
907
+ }
908
+ function verifyRedactionEnv(env) {
909
+ return Object.fromEntries(Object.entries(effectiveVerifyEnv(env)).filter(([key, value]) => {
910
+ return value.length >= 4 && SENSITIVE_ENV_KEY_PATTERN.test(key);
911
+ }));
912
+ }
913
+ function redactVerifyEnv(value, env) {
914
+ let redacted = value;
915
+ const secrets = [...new Set(Object.values(verifyRedactionEnv(env)).filter(Boolean))].sort((left, right) => right.length - left.length);
916
+ for (const secret of secrets)
917
+ redacted = redacted.replaceAll(secret, "[REDACTED]");
918
+ return redacted;
919
+ }
640
920
  function uniqueStrings(items) {
641
921
  return unique(items.map((item) => item?.trim()).filter((item) => Boolean(item)));
642
922
  }
@@ -668,6 +948,119 @@ export function aggregateAcceptanceReport(input) {
668
948
  notes: input.notes
669
949
  };
670
950
  }
951
+ const DEFAULT_VERIFY_TIMEOUT_MS = 120000;
952
+ function hash(value) {
953
+ return createHash("sha256").update(value).digest("hex");
954
+ }
955
+ function readVerifyWorkspaceState(cwd) {
956
+ const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", windowsHide: true });
957
+ if (repo.status !== 0 || !repo.stdout.trim())
958
+ return;
959
+ const repoRoot = fs.realpathSync(repo.stdout.trim());
960
+ const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8", windowsHide: true });
961
+ const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, windowsHide: true });
962
+ if (head.status !== 0 || diff.status !== 0 || !head.stdout.trim())
963
+ return;
964
+ return {
965
+ kind: "git-tracked",
966
+ repoRoot,
967
+ cwdRelative: path.relative(repoRoot, fs.realpathSync(cwd)) || ".",
968
+ head: head.stdout.trim(),
969
+ diffHash: hash(diff.stdout)
970
+ };
971
+ }
972
+ function isCachedVerifyResult(value) {
973
+ if (!value || typeof value !== "object" || Array.isArray(value))
974
+ return false;
975
+ const result = value;
976
+ return typeof result.id === "string" && typeof result.command === "string" && (typeof result.exitCode === "number" || result.exitCode === null) && (result.status === "passed" || result.status === "failed" || result.status === "timed-out" || result.status === "allowed-failure") && typeof result.durationMs === "number";
977
+ }
978
+ async function runMemoizedVerifyCommand(command, defaultCwd, options = {}) {
979
+ const cwd = command.cwd ? path.resolve(defaultCwd, command.cwd) : defaultCwd;
980
+ let workspaceState;
981
+ try {
982
+ workspaceState = readVerifyWorkspaceState(cwd);
983
+ } catch {
984
+ workspaceState = undefined;
985
+ }
986
+ if (!workspaceState || !options.artifactsDir || !options.runId) {
987
+ return runVerifyCommand(command, defaultCwd, options);
988
+ }
989
+ const envKeys = Object.keys(command.env ?? {}).sort();
990
+ const envHash = hash(JSON.stringify(Object.fromEntries(Object.entries(effectiveVerifyEnv(command.env)).sort(([left], [right]) => left.localeCompare(right)))));
991
+ const timeoutMs = command.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS;
992
+ const cacheKey = hash(JSON.stringify({
993
+ version: 1,
994
+ command: command.command,
995
+ cwdRelative: workspaceState.cwdRelative,
996
+ envKeys,
997
+ envHash,
998
+ timeoutMs,
999
+ allowFailure: command.allowFailure === true,
1000
+ head: workspaceState.head,
1001
+ diffHash: workspaceState.diffHash
1002
+ }));
1003
+ const artifactPath = path.join(options.artifactsDir, "acceptance", "verify", options.runId, `${cacheKey}.json`);
1004
+ try {
1005
+ const cached = JSON.parse(fs.readFileSync(artifactPath, "utf-8"));
1006
+ if (cached.cacheKey === cacheKey && isCachedVerifyResult(cached.result)) {
1007
+ return { ...cached.result, id: command.id, command: command.command, cwd, artifactPath, cacheKey, memoized: true, envKeys, envHash, workspaceState };
1008
+ }
1009
+ } catch {}
1010
+ const result = await runVerifyCommand(command, defaultCwd, options);
1011
+ const evidenced = { ...result, artifactPath, cacheKey, memoized: false, envKeys, envHash, workspaceState };
1012
+ try {
1013
+ fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
1014
+ fs.writeFileSync(artifactPath, JSON.stringify({
1015
+ version: 1,
1016
+ cacheKey,
1017
+ command: command.command,
1018
+ cwdRelative: workspaceState.cwdRelative,
1019
+ envKeys,
1020
+ envHash,
1021
+ timeoutMs,
1022
+ allowFailure: command.allowFailure === true,
1023
+ workspaceState,
1024
+ result: evidenced
1025
+ }, null, 2), "utf-8");
1026
+ } catch (error) {
1027
+ evidenced.artifactError = error instanceof Error ? error.message : String(error);
1028
+ delete evidenced.artifactPath;
1029
+ }
1030
+ return evidenced;
1031
+ }
1032
+ export function quoteExecutableForShell(command, platform = process.platform) {
1033
+ if (platform !== "win32")
1034
+ return command;
1035
+ const trimmed = command.trimStart();
1036
+ if (trimmed.startsWith('"'))
1037
+ return command;
1038
+ const firstToken = trimmed.match(/^\S+/)?.[0];
1039
+ if (/\.(?:exe|bat|cmd|com|ps1)$/i.test(firstToken ?? ""))
1040
+ return command;
1041
+ const match = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?\\[^"<>|&*?\r\n]*?\.(?:exe|bat|cmd|com|ps1))(?=\s|$)/i);
1042
+ const executable = match?.[1];
1043
+ if (executable && /\s/.test(executable) && !/[A-Za-z]:\\/.test(executable.slice(3))) {
1044
+ const rest = trimmed.slice(executable.length);
1045
+ const leading = command.slice(0, command.length - trimmed.length);
1046
+ return `${leading}"${executable}"${rest}`;
1047
+ }
1048
+ const extensionlessSpacedFilenameMatch = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?)(?=\s+--|$)/i);
1049
+ const extensionlessSpacedFilename = extensionlessSpacedFilenameMatch?.[1];
1050
+ if (extensionlessSpacedFilename && /\s/.test(extensionlessSpacedFilename.slice(0, extensionlessSpacedFilename.lastIndexOf("\\"))) && !/[A-Za-z]:\\/.test(extensionlessSpacedFilename.slice(3))) {
1051
+ const rest = trimmed.slice(extensionlessSpacedFilename.length);
1052
+ const leading = command.slice(0, command.length - trimmed.length);
1053
+ return `${leading}"${extensionlessSpacedFilename}"${rest}`;
1054
+ }
1055
+ const extensionlessMatch = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?\\[^"<>|&*?\s\r\n]+)(?=\s|$)/i);
1056
+ const extensionlessExecutable = extensionlessMatch?.[1];
1057
+ if (extensionlessExecutable && /\s/.test(extensionlessExecutable) && !/[A-Za-z]:\\/.test(extensionlessExecutable.slice(3)) && !/\s/.test(extensionlessExecutable.slice(extensionlessExecutable.lastIndexOf("\\") + 1))) {
1058
+ const rest = trimmed.slice(extensionlessExecutable.length);
1059
+ const leading = command.slice(0, command.length - trimmed.length);
1060
+ return `${leading}"${extensionlessExecutable}"${rest}`;
1061
+ }
1062
+ return command;
1063
+ }
671
1064
  function runVerifyCommand(command, defaultCwd, options = {}) {
672
1065
  return new Promise((resolve) => {
673
1066
  const startedAt = Date.now();
@@ -677,9 +1070,9 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
677
1070
  let timedOut = false;
678
1071
  let settled = false;
679
1072
  let hardKill;
680
- const child = spawn(command.command, {
1073
+ const child = spawn(quoteExecutableForShell(command.command), {
681
1074
  cwd,
682
- env: { ...process.env, ...command.env ?? {} },
1075
+ env: effectiveVerifyEnv(command.env),
683
1076
  shell: true,
684
1077
  stdio: ["ignore", "pipe", "pipe"],
685
1078
  windowsHide: true
@@ -710,13 +1103,13 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
710
1103
  finish({
711
1104
  exitCode: null,
712
1105
  status: "timed-out",
713
- stdout: trimOutput(stdout),
714
- stderr: trimOutput(stderr || options.abortMessage || "Acceptance verification timed out.")
1106
+ stdout: trimOutput(redactVerifyEnv(stdout, command.env)),
1107
+ stderr: trimOutput(redactVerifyEnv(stderr || options.abortMessage || "Acceptance verification timed out.", command.env))
715
1108
  });
716
1109
  }, 1000);
717
1110
  hardKill.unref?.();
718
1111
  };
719
- const timeout = setTimeout(abortVerification, command.timeoutMs ?? 120000);
1112
+ const timeout = setTimeout(abortVerification, command.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS);
720
1113
  timeout.unref?.();
721
1114
  if (options.signal?.aborted)
722
1115
  abortVerification();
@@ -733,23 +1126,25 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
733
1126
  finish({
734
1127
  exitCode,
735
1128
  status: timedOut ? "timed-out" : passed ? "passed" : command.allowFailure ? "allowed-failure" : "failed",
736
- stdout: trimOutput(stdout),
737
- stderr: trimOutput(stderr || (timedOut ? options.abortMessage ?? "" : ""))
1129
+ stdout: trimOutput(redactVerifyEnv(stdout, command.env)),
1130
+ stderr: trimOutput(redactVerifyEnv(stderr || (timedOut ? options.abortMessage ?? "" : ""), command.env))
738
1131
  });
739
1132
  });
740
1133
  child.on("error", (error) => {
741
1134
  finish({
742
1135
  exitCode: timedOut ? null : 1,
743
1136
  status: timedOut ? "timed-out" : command.allowFailure ? "allowed-failure" : "failed",
744
- stderr: timedOut ? trimOutput(stderr || options.abortMessage || "Acceptance verification timed out.") : error instanceof Error ? error.message : String(error)
1137
+ stderr: timedOut ? trimOutput(redactVerifyEnv(stderr || options.abortMessage || "Acceptance verification timed out.", command.env)) : redactVerifyEnv(error instanceof Error ? error.message : String(error), command.env)
745
1138
  });
746
1139
  });
747
1140
  });
748
1141
  }
749
1142
  export async function evaluateAcceptance(input) {
750
1143
  const acceptance = input.acceptance;
1144
+ const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
751
1145
  const ledger = {
752
- status: acceptance.level === "none" ? "not-required" : "claimed",
1146
+ status: initialStatus,
1147
+ evidenceStatus: initialStatus,
753
1148
  explicit: acceptance.explicit,
754
1149
  effectiveAcceptance: acceptance,
755
1150
  inferredReason: acceptance.inferredReason,
@@ -759,65 +1154,121 @@ export async function evaluateAcceptance(input) {
759
1154
  };
760
1155
  if (acceptance.level === "none")
761
1156
  return ledger;
762
- const parsed = input.report ? { report: input.report } : parseAcceptanceReport(input.output);
1157
+ const parsed = input.reportError ? { error: input.reportError } : input.report ? (() => {
1158
+ const validation = validateAcceptanceReport(input.report);
1159
+ return validation.report ? { report: validation.report } : { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
1160
+ })() : parseAcceptanceReportSources(input.output, input.fileOutput);
1161
+ const durableFileOutput = input.fileOutput?.authoritative && input.fileOutput.durable ? input.fileOutput : undefined;
1162
+ if (parsed.malformed && parsed.sourcePath && durableFileOutput) {
1163
+ ledger.recovery = {
1164
+ status: "available-for-review",
1165
+ reason: "acceptance-metadata-rejected",
1166
+ reportPath: parsed.sourcePath,
1167
+ reportHash: hash(durableFileOutput.content)
1168
+ };
1169
+ }
1170
+ const needsReport = acceptanceRequiresChildReport(acceptance);
763
1171
  if (parsed.report) {
764
1172
  ledger.childReport = parsed.report;
765
1173
  ledger.status = "attested";
766
- } else {
1174
+ ledger.evidenceStatus = "attested";
1175
+ } else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
767
1176
  ledger.childReportParseError = parsed.error;
768
1177
  ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
769
- ledger.status = "rejected";
770
- return ledger;
1178
+ if (ledger.recovery) {
1179
+ ledger.status = "rejected";
1180
+ ledger.evidenceStatus = "rejected";
1181
+ }
1182
+ if (!input.reportOptional) {
1183
+ ledger.status = "rejected";
1184
+ ledger.evidenceStatus = "rejected";
1185
+ return ledger;
1186
+ }
1187
+ } else {
1188
+ ledger.childReportParseError = parsed.error;
771
1189
  }
772
- if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
1190
+ if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
773
1191
  ledger.runtimeChecks = [
1192
+ ...ledger.runtimeChecks,
774
1193
  ...checkCriteriaSatisfied(acceptance.criteria, parsed.report),
775
1194
  ...runStructuralChecks(acceptance, parsed.report, input.cwd)
776
1195
  ];
777
- if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
778
- ledger.status = "rejected";
779
- return ledger;
1196
+ if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1197
+ ledger.status = "checked";
1198
+ ledger.evidenceStatus = "checked";
780
1199
  }
781
- ledger.status = "checked";
782
1200
  }
783
1201
  if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || acceptance.verify.length > 0)) {
784
1202
  if (acceptance.level === "verified" && acceptance.verify.length === 0) {
785
1203
  ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
786
1204
  ledger.status = "rejected";
1205
+ ledger.evidenceStatus = "rejected";
787
1206
  return ledger;
788
1207
  }
789
1208
  ledger.verifyRuns = [];
790
1209
  for (const command of acceptance.verify) {
791
- ledger.verifyRuns.push(await runVerifyCommand(command, input.cwd, { signal: input.signal, abortMessage: input.abortMessage }));
1210
+ ledger.verifyRuns.push(await runMemoizedVerifyCommand(command, input.cwd, {
1211
+ signal: input.signal,
1212
+ abortMessage: input.abortMessage,
1213
+ artifactsDir: input.artifactsDir,
1214
+ runId: input.runId
1215
+ }));
792
1216
  if (input.signal?.aborted)
793
1217
  break;
794
1218
  }
795
1219
  if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
796
1220
  ledger.status = "rejected";
1221
+ ledger.evidenceStatus = "rejected";
797
1222
  return ledger;
798
1223
  }
799
- ledger.status = "verified";
1224
+ if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1225
+ ledger.status = "verified";
1226
+ ledger.evidenceStatus = "verified";
1227
+ }
1228
+ }
1229
+ if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
1230
+ ledger.status = "rejected";
1231
+ ledger.evidenceStatus = "rejected";
1232
+ return ledger;
800
1233
  }
801
- if (acceptance.level === "reviewed") {
802
- if (input.reviewResult) {
1234
+ if (ledger.status === "claimed") {
1235
+ ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
1236
+ ledger.evidenceStatus = ledger.status;
1237
+ }
1238
+ if (acceptance.review) {
1239
+ if (input.reviewResult?.status === "reviewed") {
803
1240
  ledger.reviewResult = input.reviewResult;
804
- ledger.status = input.reviewResult.status === "no-blockers" ? "reviewed" : "rejected";
805
- } else {
806
- const optionalReview = acceptance.review && acceptance.review !== false && acceptance.review.required === false;
807
- ledger.reviewResult = {
808
- status: "needs-parent-decision",
1241
+ ledger.status = "reviewed";
1242
+ } else if (input.reviewResult?.status === "blockers") {
1243
+ ledger.reviewResult = input.reviewResult;
1244
+ ledger.status = "rejected";
1245
+ } else if (acceptance.review.required !== false) {
1246
+ ledger.reviewResult = input.reviewResult ?? {
1247
+ status: "review-required",
809
1248
  findings: [{
810
- severity: acceptance.explicit && !optionalReview ? "blocker" : "non-blocking",
811
- issue: "Reviewed acceptance requires an independent reviewer result.",
1249
+ severity: "non-blocking",
1250
+ issue: "Independent review has not been supplied.",
812
1251
  rationale: "The run cannot be marked reviewed from child evidence alone."
813
1252
  }]
814
1253
  };
815
- if (acceptance.review === false || acceptance.explicit && !optionalReview)
816
- ledger.status = "rejected";
1254
+ ledger.status = "review-required";
817
1255
  }
818
1256
  }
819
1257
  return ledger;
820
1258
  }
1259
+ export function buildSkippedAcceptanceLedger(acceptance, input) {
1260
+ const status = acceptance.level === "none" ? "not-required" : "rejected";
1261
+ return {
1262
+ status,
1263
+ evidenceStatus: status,
1264
+ explicit: acceptance.explicit,
1265
+ effectiveAcceptance: acceptance,
1266
+ inferredReason: acceptance.inferredReason,
1267
+ criteria: acceptance.criteria,
1268
+ runtimeChecks: acceptance.level === "none" ? [] : [{ id: input.id, status: "failed", message: input.message }],
1269
+ verifyRuns: []
1270
+ };
1271
+ }
821
1272
  export function acceptanceFailureMessage(ledger) {
822
1273
  if (ledger.status !== "rejected")
823
1274
  return;
@@ -827,8 +1278,6 @@ export function acceptanceFailureMessage(ledger) {
827
1278
  const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
828
1279
  if (failedVerify)
829
1280
  return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
830
- if (ledger.reviewResult?.status === "needs-parent-decision")
831
- return "Acceptance review required but no automatic reviewer result is available.";
832
1281
  if (ledger.reviewResult?.status === "blockers")
833
1282
  return "Acceptance review found blockers.";
834
1283
  return "Acceptance rejected.";