@selesai/code 0.5.19 → 0.5.21

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 (265) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -6,10 +6,13 @@ import type {
6
6
  AcceptanceConfig,
7
7
  AcceptanceEvidenceKind,
8
8
  AcceptanceInput,
9
+ AgentContract,
9
10
  AcceptanceLedger,
10
11
  AcceptanceLevel,
11
12
  AcceptanceReport,
13
+ AcceptanceRole,
12
14
  AcceptanceRuntimeCheck,
15
+ AcceptanceRuntimeCheckStatus,
13
16
  AcceptanceReviewResult,
14
17
  AcceptanceVerifyCommand,
15
18
  AcceptanceVerifyResult,
@@ -18,16 +21,17 @@ import type {
18
21
  SingleResult,
19
22
  SubagentRunMode,
20
23
  } from "../../shared/types.ts";
24
+ import { isAgentContractV1 } from "./agent-contract.ts";
25
+ import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
21
26
 
22
27
  const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
23
28
  none: 0,
24
29
  attested: 1,
25
30
  checked: 2,
26
31
  verified: 3,
27
- reviewed: 4,
28
32
  };
29
33
 
30
- const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified", "reviewed"]);
34
+ const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
31
35
  const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
32
36
  "changed-files",
33
37
  "tests-added",
@@ -43,6 +47,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
43
47
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
44
48
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
45
49
  const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
50
+ 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.";
46
51
 
47
52
  function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
48
53
  return level ?? "auto";
@@ -61,13 +66,13 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
61
66
  case "checked":
62
67
  return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
63
68
  case "verified":
64
- case "reviewed":
65
69
  return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
66
70
  }
67
71
  }
68
72
 
69
73
  function inferLevel(input: {
70
74
  agentName: string;
75
+ acceptanceRole?: AcceptanceRole;
71
76
  task?: string;
72
77
  mode?: SubagentRunMode;
73
78
  async?: boolean;
@@ -77,28 +82,41 @@ function inferLevel(input: {
77
82
  const agent = input.agentName.toLowerCase();
78
83
  const task = input.task?.toLowerCase() ?? "";
79
84
  const reasons: string[] = [];
80
- const readOnlyAgent = /\b(?:reviewer|scout|context-builder|researcher|analyst)\b/.test(agent);
81
- const readOnlyTask = /\b(?:read[- ]only|review[- ]only|do not edit|don't edit|no edits|without edits|inspect|summari[sz]e)\b/.test(task);
82
- const writeTask = /\b(?:fix|implement|update|write|edit|modify|migrate|release|security|delete|remove|refactor|commit)\b/.test(task)
83
- || /\bworker\b/.test(agent);
85
+ // Declared roles replace name heuristics, so use the full writer grammar to detect explicit mutation independently of the actual agent name.
86
+ const intent = classifyTaskMutationIntent(input.acceptanceRole ? "builder" : input.agentName, input.task ?? "");
87
+ const readOnlyTask = intent.kind === "read-only"
88
+ || (intent.kind === "unknown" && /\b(?:read[- ]only|review[- ]only|no edits|without edits|inspect|summari[sz]e)\b/.test(task));
89
+ const rolePatchTask = input.acceptanceRole !== undefined
90
+ && intent.kind !== "read-only"
91
+ && !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
92
+ && /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
93
+ const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
94
+ const readOnlyAgent = input.acceptanceRole === "read-only"
95
+ || (input.acceptanceRole === undefined && /\b(?:architect|commentator|explorer|recapper|researcher|analyst)\b/.test(agent));
96
+ const writeTask = taskMayWrite
97
+ || (input.acceptanceRole === "writer" && !readOnlyTask)
98
+ || (input.acceptanceRole === undefined && /\bbuilder\b/.test(agent) && !readOnlyTask);
99
+ const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
100
+ const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
101
+ const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
84
102
  const risky = Boolean(input.async && writeTask)
85
- || Boolean(input.dynamic)
86
- || Boolean(input.dynamicGroup)
87
- || /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
103
+ || (Boolean(input.dynamic) && !roleResolvesReadOnly)
104
+ || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
105
+ || (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
88
106
 
89
107
  if (risky) {
90
108
  reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
91
109
  if (input.dynamic || input.dynamicGroup) reasons.push("dynamic fanout context");
92
110
  return {
93
- level: "reviewed",
111
+ level: "checked",
94
112
  reasons,
95
113
  criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
96
- evidence: requiredEvidenceForLevel("reviewed"),
97
- review: { agent: "reviewer", required: true },
114
+ evidence: requiredEvidenceForLevel("checked"),
115
+ review: { agent: "commentator", required: true },
98
116
  };
99
117
  }
100
118
  if (writeTask && !readOnlyTask) {
101
- reasons.push("write-capable worker/task");
119
+ reasons.push(input.acceptanceRole === "writer" && !taskMayWrite ? "declared writer acceptance role" : "write-capable worker/task");
102
120
  return {
103
121
  level: "checked",
104
122
  reasons,
@@ -107,7 +125,7 @@ function inferLevel(input: {
107
125
  };
108
126
  }
109
127
  if (readOnlyAgent || readOnlyTask) {
110
- reasons.push(readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
128
+ reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
111
129
  return {
112
130
  level: "attested",
113
131
  reasons,
@@ -140,7 +158,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
140
158
  if (input === undefined) return errors;
141
159
  if (input === false) return errors;
142
160
  if (typeof input === "string") {
143
- if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
161
+ if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
162
+ else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
163
+ else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
144
164
  return errors;
145
165
  }
146
166
  if (!input || typeof input !== "object" || Array.isArray(input)) {
@@ -151,8 +171,10 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
151
171
  for (const key of Object.keys(value)) {
152
172
  if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
153
173
  }
154
- if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
155
- errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified, reviewed.`);
174
+ if (value.level === "reviewed") {
175
+ errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
176
+ } else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
177
+ errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
156
178
  }
157
179
  if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
158
180
  errors.push(`${pathLabel}.reason is required when level is none.`);
@@ -160,6 +182,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
160
182
  if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
161
183
  if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
162
184
  if (Array.isArray(value.criteria)) {
185
+ const criterionIds = new Set<string>();
163
186
  for (const [index, criterion] of value.criteria.entries()) {
164
187
  if (typeof criterion === "string") continue;
165
188
  const criterionPath = `${pathLabel}.criteria[${index}]`;
@@ -171,7 +194,13 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
171
194
  for (const key of Object.keys(gate)) {
172
195
  if (!ACCEPTANCE_GATE_KEYS.has(key)) errors.push(`${criterionPath}.${key} is not supported.`);
173
196
  }
174
- if (typeof gate.id !== "string" || !gate.id.trim()) errors.push(`${criterionPath}.id is required.`);
197
+ if (typeof gate.id !== "string" || !gate.id.trim()) {
198
+ errors.push(`${criterionPath}.id is required.`);
199
+ } else {
200
+ const normalizedId = normalizedToken(gate.id);
201
+ if (criterionIds.has(normalizedId)) errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalizedId}'.`);
202
+ criterionIds.add(normalizedId);
203
+ }
175
204
  if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
176
205
  if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array.`);
177
206
  if (Array.isArray(gate.evidence)) {
@@ -248,6 +277,31 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
248
277
  return errors;
249
278
  }
250
279
 
280
+ export function validateExecutionAcceptance(input: {
281
+ acceptance?: unknown;
282
+ tasks?: Array<{ acceptance?: unknown }>;
283
+ chain?: Array<{
284
+ acceptance?: unknown;
285
+ parallel?: Array<{ acceptance?: unknown }> | { acceptance?: unknown };
286
+ }>;
287
+ }): string[] {
288
+ const errors = validateAcceptanceInput(input.acceptance, "acceptance");
289
+ for (const [index, task] of (input.tasks ?? []).entries()) {
290
+ errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
291
+ }
292
+ for (const [stepIndex, step] of (input.chain ?? []).entries()) {
293
+ errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
294
+ if (Array.isArray(step.parallel)) {
295
+ for (const [taskIndex, task] of step.parallel.entries()) {
296
+ errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
297
+ }
298
+ } else if (step.parallel) {
299
+ errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
300
+ }
301
+ }
302
+ return errors;
303
+ }
304
+
251
305
  function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
252
306
  return (criteria ?? []).map((criterion, index) => {
253
307
  if (typeof criterion === "string") {
@@ -265,15 +319,38 @@ function normalizeCriteria(criteria: Array<string | { id?: string; must?: string
265
319
  export function resolveEffectiveAcceptance(input: {
266
320
  explicit?: AcceptanceInput;
267
321
  agentName: string;
322
+ acceptanceRole?: AcceptanceRole;
268
323
  task?: string;
269
324
  mode?: SubagentRunMode;
270
325
  async?: boolean;
271
326
  dynamic?: boolean;
272
327
  dynamicGroup?: boolean;
328
+ agentContract?: AgentContract;
273
329
  }): ResolvedAcceptanceConfig {
274
330
  const explicit = normalizeAcceptanceInput(input.explicit);
275
- const inferred = inferLevel(input);
276
331
  const explicitLevel = normalizeLevel(explicit.level);
332
+ if (isAgentContractV1(input.agentContract)) {
333
+ const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
334
+ ? "none"
335
+ : explicitLevel;
336
+ const evidence = unique(explicit.evidence ?? []);
337
+ const criteria = normalizeCriteria(
338
+ explicit.criteria as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined,
339
+ evidence,
340
+ );
341
+ return {
342
+ level,
343
+ explicit: input.explicit !== undefined,
344
+ inferredReason: [],
345
+ criteria,
346
+ evidence,
347
+ verify: explicit.verify ?? [],
348
+ review: explicit.review,
349
+ stopRules: explicit.stopRules ?? [],
350
+ reason: explicit.reason,
351
+ };
352
+ }
353
+ const inferred = inferLevel(input);
277
354
  const level = explicitAcceptanceCanDisable(explicit)
278
355
  ? "none"
279
356
  : explicitLevel === "auto"
@@ -284,10 +361,7 @@ export function resolveEffectiveAcceptance(input: {
284
361
  (explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
285
362
  evidence,
286
363
  );
287
- let review = explicit.review !== undefined ? explicit.review : inferred.review;
288
- if (level === "reviewed" && explicitLevel !== "auto" && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
289
- review = { ...review, required: false };
290
- }
364
+ const review = explicit.review !== undefined ? explicit.review : inferred.review;
291
365
  return {
292
366
  level,
293
367
  explicit: input.explicit !== undefined,
@@ -301,8 +375,13 @@ export function resolveEffectiveAcceptance(input: {
301
375
  };
302
376
  }
303
377
 
304
- export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): string {
378
+ function acceptanceRequiresChildReport(acceptance: ResolvedAcceptanceConfig): boolean {
379
+ return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
380
+ }
381
+
382
+ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { reportOptional?: boolean } = {}): string {
305
383
  if (acceptance.level === "none") return "";
384
+ if (options.reportOptional && !acceptanceRequiresChildReport(acceptance)) return "";
306
385
  const lines = [
307
386
  "",
308
387
  "## Acceptance Contract",
@@ -329,9 +408,14 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
329
408
  "",
330
409
  "Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
331
410
  "Use empty arrays when no items apply; array fields contain strings unless object entries are shown.",
411
+ "`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.",
412
+ "`commandsRun[].result` must be exactly one of: passed, failed, not-run.",
413
+ "`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.",
332
414
  "```acceptance-report",
333
415
  JSON.stringify({
334
- criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof" }],
416
+ criteriaSatisfied: acceptance.criteria
417
+ .filter((criterion) => criterion.severity !== "recommended")
418
+ .map((criterion) => ({ id: criterion.id, status: "satisfied", evidence: "specific proof" })),
335
419
  changedFiles: ["src/file.ts"],
336
420
  testsAddedOrUpdated: ["test/file.test.ts"],
337
421
  commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
@@ -372,38 +456,166 @@ function extractBalancedJson(text: string, start: number): string | undefined {
372
456
  return undefined;
373
457
  }
374
458
 
375
- function unwrapAcceptanceReport(value: unknown): unknown {
376
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
377
- const record = value as { acceptance?: unknown; "acceptance-report"?: unknown };
378
- if ("acceptance" in record) return record.acceptance;
379
- if ("acceptance-report" in record) return record["acceptance-report"];
459
+ const ACCEPTANCE_REPORT_WRAPPERS = new Set(["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]);
460
+
461
+ const ACCEPTANCE_REPORT_FIELDS: Record<string, keyof AcceptanceReport> = {
462
+ criteriaSatisfied: "criteriaSatisfied",
463
+ criteria_satisfied: "criteriaSatisfied",
464
+ changedFiles: "changedFiles",
465
+ changed_files: "changedFiles",
466
+ testsAddedOrUpdated: "testsAddedOrUpdated",
467
+ tests_added_or_updated: "testsAddedOrUpdated",
468
+ commandsRun: "commandsRun",
469
+ commands_run: "commandsRun",
470
+ validationOutput: "validationOutput",
471
+ validation_output: "validationOutput",
472
+ residualRisks: "residualRisks",
473
+ residual_risks: "residualRisks",
474
+ noStagedFiles: "noStagedFiles",
475
+ no_staged_files: "noStagedFiles",
476
+ diffSummary: "diffSummary",
477
+ diff_summary: "diffSummary",
478
+ reviewFindings: "reviewFindings",
479
+ review_findings: "reviewFindings",
480
+ manualNotes: "manualNotes",
481
+ manual_notes: "manualNotes",
482
+ notes: "notes",
483
+ };
484
+
485
+ const CRITERION_REPORT_FIELDS = new Set(["id", "status", "evidence"]);
486
+ const COMMAND_REPORT_FIELDS = new Set(["command", "result", "summary"]);
487
+
488
+ function normalizedToken(value: string): string {
489
+ return value.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
490
+ }
491
+
492
+ function normalizeCriterionStatus(value: unknown): unknown {
493
+ if (typeof value !== "string") return value;
494
+ const token = normalizedToken(value);
495
+ if (["satisfied", "met", "complete", "completed", "done", "pass", "passed", "success", "succeeded"].includes(token)) return "satisfied";
496
+ if (["not-satisfied", "not-met", "unmet", "incomplete", "fail", "failed"].includes(token)) return "not-satisfied";
497
+ if (["not-applicable", "n-a", "na", "skip", "skipped"].includes(token)) return "not-applicable";
380
498
  return value;
381
499
  }
382
500
 
383
- function isCommandsRunArray(value: unknown): value is NonNullable<AcceptanceReport["commandsRun"]> {
384
- return Array.isArray(value) && value.every((item) => {
385
- if (!item || typeof item !== "object" || Array.isArray(item)) return false;
386
- const command = item as { command?: unknown; result?: unknown; summary?: unknown };
387
- return typeof command.command === "string"
388
- && (command.result === "passed" || command.result === "failed" || command.result === "not-run")
389
- && typeof command.summary === "string";
390
- });
501
+ function normalizeCommandResult(value: unknown): unknown {
502
+ if (typeof value !== "string") return value;
503
+ const token = normalizedToken(value);
504
+ if (["passed", "pass", "success", "successful", "succeeded", "ok"].includes(token)) return "passed";
505
+ if (["failed", "fail", "failure", "error"].includes(token)) return "failed";
506
+ if (["not-run", "not-executed", "skip", "skipped"].includes(token)) return "not-run";
507
+ return value;
508
+ }
509
+
510
+ function normalizeCriterionReport(value: unknown, pathLabel: string, errors: string[]): unknown {
511
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
512
+ const normalized: Record<string, unknown> = {};
513
+ for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
514
+ if (!CRITERION_REPORT_FIELDS.has(key)) {
515
+ errors.push(`${pathLabel}.${key}: unsupported acceptance criterion field`);
516
+ continue;
517
+ }
518
+ normalized[key] = key === "id" && typeof fieldValue === "string"
519
+ ? normalizedToken(fieldValue)
520
+ : key === "status"
521
+ ? normalizeCriterionStatus(fieldValue)
522
+ : fieldValue;
523
+ }
524
+ return normalized;
525
+ }
526
+
527
+ function normalizeCommandReport(value: unknown, pathLabel: string, errors: string[]): unknown {
528
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
529
+ const normalized: Record<string, unknown> = {};
530
+ for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
531
+ if (!COMMAND_REPORT_FIELDS.has(key)) {
532
+ errors.push(`${pathLabel}.${key}: unsupported acceptance command field`);
533
+ continue;
534
+ }
535
+ normalized[key] = key === "result" ? normalizeCommandResult(fieldValue) : fieldValue;
536
+ }
537
+ return normalized;
538
+ }
539
+
540
+ function normalizeAcceptanceReportValue(value: unknown, pathLabel = ""): { value: unknown; pathLabel: string; errors: string[] } {
541
+ const errors: string[] = [];
542
+ let reportValue = value;
543
+ let reportPath = pathLabel;
544
+ if (reportValue && typeof reportValue === "object" && !Array.isArray(reportValue)) {
545
+ const record = reportValue as Record<string, unknown>;
546
+ const wrapperKeys = Object.keys(record).filter((key) => ACCEPTANCE_REPORT_WRAPPERS.has(key));
547
+ if (wrapperKeys.length > 0) {
548
+ const wrapperKey = wrapperKeys[0]!;
549
+ if (wrapperKeys.length > 1) errors.push(`${pathLabel || "acceptance-report"}: multiple acceptance report wrappers are ambiguous`);
550
+ for (const key of Object.keys(record)) {
551
+ if (key !== wrapperKey) errors.push(`${pathFor(pathLabel, key)}: unsupported alongside acceptance report wrapper '${wrapperKey}'`);
552
+ }
553
+ reportValue = record[wrapperKey];
554
+ reportPath = pathFor(pathLabel, wrapperKey);
555
+ }
556
+ }
557
+ if (!reportValue || typeof reportValue !== "object" || Array.isArray(reportValue)) return { value: reportValue, pathLabel: reportPath, errors };
558
+
559
+ const normalized: Record<string, unknown> = {};
560
+ for (const [key, fieldValue] of Object.entries(reportValue as Record<string, unknown>)) {
561
+ const canonical = ACCEPTANCE_REPORT_FIELDS[key];
562
+ if (!canonical) {
563
+ errors.push(`${pathFor(reportPath, key)}: unsupported acceptance report field`);
564
+ continue;
565
+ }
566
+ if (Object.hasOwn(normalized, canonical)) {
567
+ errors.push(`${pathFor(reportPath, key)}: duplicates normalized field '${canonical}'`);
568
+ continue;
569
+ }
570
+ const fieldPath = pathFor(reportPath, canonical);
571
+ switch (canonical) {
572
+ case "criteriaSatisfied": {
573
+ const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
574
+ normalized[canonical] = Array.isArray(items)
575
+ ? items.map((item, index) => normalizeCriterionReport(item, `${fieldPath}[${index}]`, errors))
576
+ : items;
577
+ break;
578
+ }
579
+ case "commandsRun": {
580
+ const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
581
+ normalized[canonical] = Array.isArray(items)
582
+ ? items.map((item, index) => normalizeCommandReport(item, `${fieldPath}[${index}]`, errors))
583
+ : items;
584
+ break;
585
+ }
586
+ case "changedFiles":
587
+ case "testsAddedOrUpdated":
588
+ case "validationOutput":
589
+ case "residualRisks":
590
+ case "reviewFindings":
591
+ normalized[canonical] = typeof fieldValue === "string" ? [fieldValue] : fieldValue;
592
+ break;
593
+ case "noStagedFiles": {
594
+ const token = typeof fieldValue === "string" ? fieldValue.trim().toLowerCase() : undefined;
595
+ normalized[canonical] = token === "true" ? true : token === "false" ? false : fieldValue;
596
+ break;
597
+ }
598
+ default:
599
+ normalized[canonical] = fieldValue;
600
+ }
601
+ }
602
+ return { value: normalized, pathLabel: reportPath, errors };
391
603
  }
392
604
 
393
605
  function hasGenericAcceptanceReportSignal(value: unknown): boolean {
394
606
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
395
607
  const record = value as Record<string, unknown>;
396
- return "criteriaSatisfied" in record && (
397
- isStringArray(record.changedFiles)
398
- || isStringArray(record.testsAddedOrUpdated)
399
- || isCommandsRunArray(record.commandsRun)
400
- || isStringArray(record.validationOutput)
401
- || isStringArray(record.residualRisks)
402
- || typeof record.noStagedFiles === "boolean"
403
- || typeof record.diffSummary === "string"
404
- || isStringArray(record.reviewFindings)
405
- || typeof record.manualNotes === "string"
406
- );
608
+ return "criteriaSatisfied" in record && [
609
+ "changedFiles",
610
+ "testsAddedOrUpdated",
611
+ "commandsRun",
612
+ "validationOutput",
613
+ "residualRisks",
614
+ "noStagedFiles",
615
+ "diffSummary",
616
+ "reviewFindings",
617
+ "manualNotes",
618
+ ].some((key) => key in record);
407
619
  }
408
620
 
409
621
  function parseReportJson(body: string): unknown {
@@ -426,30 +638,29 @@ function fencedBlocks(output: string, tag: string): string[] {
426
638
  .filter((value): value is string => Boolean(value));
427
639
  }
428
640
 
429
- function validationPathLabelForWrapper(value: unknown): string {
430
- if (!value || typeof value !== "object" || Array.isArray(value)) return "";
431
- const record = value as Record<string, unknown>;
432
- if ("acceptance" in record) return "acceptance";
433
- if ("acceptance-report" in record) return "acceptance-report";
434
- return "";
435
- }
436
-
437
641
  function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; errors: string[] } {
438
- const parsed = parseReportJson(body);
439
- const report = unwrapAcceptanceReport(parsed);
440
- return validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
642
+ return validateAcceptanceReport(parseReportJson(body));
441
643
  }
442
644
 
443
- function parseGenericJsonAcceptanceReportBody(body: string): AcceptanceReport | undefined {
645
+ function parseGenericJsonAcceptanceReportBody(body: string): { report?: AcceptanceReport; error?: string } {
444
646
  const parsed = parseReportJson(body);
445
- const report = unwrapAcceptanceReport(parsed);
446
- const validation = validateAcceptanceReport(report);
447
- if (!validation.report) return undefined;
448
- return hasGenericAcceptanceReportSignal(validation.report) ? validation.report : undefined;
647
+ const normalized = normalizeAcceptanceReportValue(parsed);
648
+ const hasCriteriaMarker = normalized.value !== null
649
+ && typeof normalized.value === "object"
650
+ && !Array.isArray(normalized.value)
651
+ && "criteriaSatisfied" in normalized.value;
652
+ if (!hasGenericAcceptanceReportSignal(normalized.value) && !(hasCriteriaMarker && normalized.errors.length > 0)) return {};
653
+ const validation = validateAcceptanceReport(parsed);
654
+ return validation.report
655
+ ? { report: validation.report }
656
+ : { error: `Invalid acceptance-report: ${validation.errors.join("; ")}` };
449
657
  }
450
658
 
659
+ export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
660
+
451
661
  export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
452
- const fenced = fencedBlocks(output, "acceptance-report");
662
+ const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
663
+ const fenced = fencedBlocks(output, "acceptance[-_]report");
453
664
  const parseErrors: string[] = [];
454
665
  for (const body of fenced) {
455
666
  try {
@@ -461,38 +672,65 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
461
672
  }
462
673
  }
463
674
  if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
675
+ if (explicitFencePresent) {
676
+ return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence." };
677
+ }
464
678
  for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
465
679
  try {
466
- const report = parseGenericJsonAcceptanceReportBody(body);
467
- if (report) return { report };
680
+ const parsed = parseGenericJsonAcceptanceReportBody(body);
681
+ if (parsed.report) return { report: parsed.report };
682
+ if (parsed.error) return { error: `Failed to parse acceptance-report: ${parsed.error}` };
468
683
  } catch {
469
- // Ignore unrelated or malformed generic JSON fences; only explicit
470
- // acceptance-report fences should turn parse failures into blockers.
684
+ // Ignore unrelated malformed generic JSON. A recognizable report shape
685
+ // returns exact validation errors above instead of being mistaken for prose.
471
686
  }
472
687
  }
473
688
  const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
474
689
  if (markerIndex !== -1) {
475
690
  const jsonStart = output.indexOf("{", markerIndex);
476
- if (jsonStart !== -1) {
477
- const json = extractBalancedJson(output, jsonStart);
478
- if (json) {
479
- try {
480
- const parsed = JSON.parse(json) as unknown;
481
- const report = unwrapAcceptanceReport(parsed);
482
- const validation = validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
483
- if (validation.report) return { report: validation.report };
484
- return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
485
- } catch (error) {
486
- return { error: error instanceof Error ? error.message : String(error) };
487
- }
488
- }
489
- }
691
+ if (jsonStart === -1) {
692
+ return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:." };
693
+ }
694
+ const json = extractBalancedJson(output, jsonStart);
695
+ if (!json) {
696
+ return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:." };
490
697
  }
491
- return { error: "Structured acceptance report not found." };
698
+ try {
699
+ const parsed = JSON.parse(json) as unknown;
700
+ const validation = validateAcceptanceReport(parsed);
701
+ if (validation.report) return { report: validation.report };
702
+ return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
703
+ } catch (error) {
704
+ const message = error instanceof Error ? error.message : String(error);
705
+ return { error: `Failed to parse acceptance-report: ${message}` };
706
+ }
707
+ }
708
+ return { error: ACCEPTANCE_REPORT_NOT_FOUND };
709
+ }
710
+
711
+ function parseAcceptanceReportSources(
712
+ output: string,
713
+ fileOutput: { content: string; path: string; authoritative?: boolean } | undefined,
714
+ ): { report?: AcceptanceReport; error?: string } {
715
+ const fromText = () => parseAcceptanceReport(output);
716
+ const fromFile = () => {
717
+ if (!fileOutput) return { error: ACCEPTANCE_REPORT_NOT_FOUND };
718
+ const parsed = parseAcceptanceReport(fileOutput.content);
719
+ return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND
720
+ ? parsed
721
+ : { error: `${parsed.error} (in configured output ${fileOutput.path})` };
722
+ };
723
+ const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
724
+ const first = primary();
725
+ // A malformed report in the primary source is a defect to surface, not a
726
+ // miss to paper over with the secondary source; only a genuinely absent
727
+ // report falls through.
728
+ if (first.report || first.error !== ACCEPTANCE_REPORT_NOT_FOUND) return first;
729
+ return secondary();
492
730
  }
493
731
 
494
732
  export function stripAcceptanceReport(output: string): string {
495
- const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
733
+ const trailingFencePattern = /\n?```(acceptance[-_]report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
496
734
  let trailingFence: { index: number; tag: string; body: string } | undefined;
497
735
  for (const match of output.matchAll(trailingFencePattern)) {
498
736
  const end = (match.index ?? 0) + match[0].length;
@@ -501,15 +739,15 @@ export function stripAcceptanceReport(output: string): string {
501
739
  }
502
740
  }
503
741
  if (trailingFence) {
504
- if (trailingFence.tag === "acceptance-report") return output.slice(0, trailingFence.index).trimEnd();
742
+ if (trailingFence.tag === "acceptance-report" || trailingFence.tag === "acceptance_report") return output.slice(0, trailingFence.index).trimEnd();
505
743
  try {
506
- if (parseGenericJsonAcceptanceReportBody(trailingFence.body)) return output.slice(0, trailingFence.index).trimEnd();
744
+ if (parseGenericJsonAcceptanceReportBody(trailingFence.body).report) return output.slice(0, trailingFence.index).trimEnd();
507
745
  } catch {
508
746
  // Leave unrelated or malformed generic JSON fences visible.
509
747
  }
510
748
  }
511
749
  return output
512
- .replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
750
+ .replace(/\n?```acceptance[-_]report\s*\n[\s\S]*?```\s*$/i, "")
513
751
  .replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
514
752
  .trimEnd();
515
753
  }
@@ -544,12 +782,15 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
544
782
  return;
545
783
  }
546
784
  for (const [index, item] of value.entries()) {
547
- if (typeof item !== "string") pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
785
+ if (typeof item !== "string" || !item.trim()) pushTypeError(errors, `${pathLabel}[${index}]`, "non-empty string", item);
548
786
  }
549
787
  }
550
788
 
551
789
  function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
552
- const errors: string[] = [];
790
+ const normalized = normalizeAcceptanceReportValue(value, pathLabel);
791
+ value = normalized.value;
792
+ pathLabel = normalized.pathLabel;
793
+ const errors = normalized.errors;
553
794
  if (!value || typeof value !== "object" || Array.isArray(value)) {
554
795
  pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
555
796
  return { errors };
@@ -559,6 +800,7 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
559
800
  if (!Array.isArray(report.criteriaSatisfied)) {
560
801
  pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
561
802
  } else {
803
+ const criterionIds = new Set<string>();
562
804
  for (const [index, item] of report.criteriaSatisfied.entries()) {
563
805
  const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
564
806
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -566,7 +808,12 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
566
808
  continue;
567
809
  }
568
810
  const criterion = item as { id?: unknown; status?: unknown; evidence?: unknown };
569
- if (criterion.id !== undefined && typeof criterion.id !== "string") pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
811
+ if (criterion.id !== undefined && typeof criterion.id !== "string") {
812
+ pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
813
+ } else if (typeof criterion.id === "string" && criterion.id) {
814
+ if (criterionIds.has(criterion.id)) errors.push(`${itemPath}.id: duplicate normalized criterion id '${criterion.id}'`);
815
+ criterionIds.add(criterion.id);
816
+ }
570
817
  if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
571
818
  pushTypeError(errors, `${itemPath}.status`, "one of \"satisfied\", \"not-satisfied\", \"not-applicable\"", criterion.status);
572
819
  }
@@ -591,14 +838,14 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
591
838
  if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
592
839
  pushTypeError(errors, `${itemPath}.result`, "one of \"passed\", \"failed\", \"not-run\"", command.result);
593
840
  }
594
- if (typeof command.summary !== "string") pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
841
+ if (typeof command.summary !== "string" || !command.summary.trim()) pushTypeError(errors, `${itemPath}.summary`, "non-empty string", command.summary);
595
842
  }
596
843
  }
597
844
  }
598
845
  if (report.validationOutput !== undefined) validateStringArrayField(errors, report.validationOutput, pathFor(pathLabel, "validationOutput"));
599
846
  if (report.residualRisks !== undefined) validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
600
847
  if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
601
- if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
848
+ if (report.diffSummary !== undefined && (typeof report.diffSummary !== "string" || !report.diffSummary.trim())) pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "non-empty string", report.diffSummary);
602
849
  if (report.reviewFindings !== undefined) validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
603
850
  if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") pushTypeError(errors, pathFor(pathLabel, "manualNotes"), "string", report.manualNotes);
604
851
  if (report.notes !== undefined && typeof report.notes !== "string") pushTypeError(errors, pathFor(pathLabel, "notes"), "string", report.notes);
@@ -620,26 +867,30 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
620
867
  }
621
868
 
622
869
  function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: AcceptanceReport): AcceptanceRuntimeCheck[] {
623
- const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id!, item]));
870
+ const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [normalizedToken(item.id!), item]));
624
871
  return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
625
- const item = reports.get(criterion.id);
872
+ const item = reports.get(normalizedToken(criterion.id));
626
873
  if (!item) return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
627
874
  if (item.status !== "satisfied") return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was reported as ${item.status}.` };
628
875
  return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
629
876
  });
630
877
  }
631
878
 
632
- function reportEvidencePresent(report: AcceptanceReport, kind: AcceptanceEvidenceKind): boolean {
879
+ function reportEvidenceStatus(report: AcceptanceReport, kind: AcceptanceEvidenceKind): AcceptanceRuntimeCheckStatus {
633
880
  switch (kind) {
634
- case "changed-files": return isStringArray(report.changedFiles) && report.changedFiles.length > 0;
635
- case "tests-added": return isStringArray(report.testsAddedOrUpdated) && report.testsAddedOrUpdated.length > 0;
636
- case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.length > 0;
637
- case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0;
638
- case "residual-risks": return isStringArray(report.residualRisks);
639
- case "no-staged-files": return report.noStagedFiles === true;
640
- case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
641
- case "review-findings": return isStringArray(report.reviewFindings);
642
- case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim());
881
+ case "changed-files":
882
+ if (!isStringArray(report.changedFiles)) return "failed";
883
+ return report.changedFiles.length === 0 ? "not-applicable" : "passed";
884
+ case "tests-added":
885
+ if (!isStringArray(report.testsAddedOrUpdated)) return "failed";
886
+ return report.testsAddedOrUpdated.length === 0 ? "not-applicable" : "passed";
887
+ case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.length > 0 ? "passed" : "failed";
888
+ case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0 ? "passed" : "failed";
889
+ case "residual-risks": return isStringArray(report.residualRisks) ? "passed" : "failed";
890
+ case "no-staged-files": return report.noStagedFiles === true ? "passed" : "failed";
891
+ case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0 ? "passed" : "failed";
892
+ case "review-findings": return isStringArray(report.reviewFindings) ? "passed" : "failed";
893
+ case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim()) ? "passed" : "failed";
643
894
  }
644
895
  }
645
896
 
@@ -657,11 +908,15 @@ function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
657
908
  function runStructuralChecks(acceptance: ResolvedAcceptanceConfig, report: AcceptanceReport, cwd: string): AcceptanceRuntimeCheck[] {
658
909
  const checks: AcceptanceRuntimeCheck[] = [];
659
910
  for (const kind of acceptance.evidence) {
660
- const present = reportEvidencePresent(report, kind);
911
+ const status = reportEvidenceStatus(report, kind);
661
912
  checks.push({
662
913
  id: `evidence:${kind}`,
663
- status: present ? "passed" : "failed",
664
- message: present ? `${kind} evidence present.` : `${kind} evidence missing from child report.`,
914
+ status,
915
+ message: status === "passed"
916
+ ? `${kind} evidence present.`
917
+ : status === "not-applicable"
918
+ ? `${kind} evidence explicitly reported as not applicable.`
919
+ : `${kind} evidence missing from child report.`,
665
920
  });
666
921
  }
667
922
  if (acceptance.evidence.includes("no-staged-files")) checks.push(checkNoStagedFiles(cwd));
@@ -788,14 +1043,24 @@ export async function evaluateAcceptance(input: {
788
1043
  acceptance: ResolvedAcceptanceConfig;
789
1044
  output: string;
790
1045
  cwd: string;
1046
+ /**
1047
+ * Content the child sent to its configured output file (from its own write
1048
+ * tool calls, not from disk, so a concurrent writer to the same path cannot
1049
+ * be misattributed). Searched for the acceptance report; searched before
1050
+ * the assistant output when `authoritative` (outputMode "file-only").
1051
+ */
1052
+ fileOutput?: { content: string; path: string; authoritative?: boolean };
791
1053
  report?: AcceptanceReport;
792
1054
  reviewResult?: AcceptanceReviewResult;
793
1055
  signal?: AbortSignal;
794
1056
  abortMessage?: string;
1057
+ reportOptional?: boolean;
795
1058
  }): Promise<AcceptanceLedger> {
796
1059
  const acceptance = input.acceptance;
1060
+ const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
797
1061
  const ledger: AcceptanceLedger = {
798
- status: acceptance.level === "none" ? "not-required" : "claimed",
1062
+ status: initialStatus,
1063
+ evidenceStatus: initialStatus,
799
1064
  explicit: acceptance.explicit,
800
1065
  effectiveAcceptance: acceptance,
801
1066
  inferredReason: acceptance.inferredReason,
@@ -805,33 +1070,48 @@ export async function evaluateAcceptance(input: {
805
1070
  };
806
1071
  if (acceptance.level === "none") return ledger;
807
1072
 
808
- const parsed = input.report ? { report: input.report } : parseAcceptanceReport(input.output);
1073
+ const parsed = input.report
1074
+ ? (() => {
1075
+ const validation = validateAcceptanceReport(input.report);
1076
+ return validation.report
1077
+ ? { report: validation.report }
1078
+ : { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
1079
+ })()
1080
+ : parseAcceptanceReportSources(input.output, input.fileOutput);
1081
+ const needsReport = acceptanceRequiresChildReport(acceptance);
809
1082
  if (parsed.report) {
810
1083
  ledger.childReport = parsed.report;
811
1084
  ledger.status = "attested";
812
- } else {
1085
+ ledger.evidenceStatus = "attested";
1086
+ } else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
813
1087
  ledger.childReportParseError = parsed.error;
814
1088
  ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
815
- ledger.status = "rejected";
816
- return ledger;
1089
+ if (!input.reportOptional) {
1090
+ ledger.status = "rejected";
1091
+ ledger.evidenceStatus = "rejected";
1092
+ return ledger;
1093
+ }
1094
+ } else {
1095
+ ledger.childReportParseError = parsed.error;
817
1096
  }
818
1097
 
819
- if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
1098
+ if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
820
1099
  ledger.runtimeChecks = [
1100
+ ...ledger.runtimeChecks,
821
1101
  ...checkCriteriaSatisfied(acceptance.criteria, parsed.report),
822
1102
  ...runStructuralChecks(acceptance, parsed.report, input.cwd),
823
1103
  ];
824
- if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
825
- ledger.status = "rejected";
826
- return ledger;
1104
+ if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1105
+ ledger.status = "checked";
1106
+ ledger.evidenceStatus = "checked";
827
1107
  }
828
- ledger.status = "checked";
829
1108
  }
830
1109
 
831
1110
  if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || acceptance.verify.length > 0)) {
832
1111
  if (acceptance.level === "verified" && acceptance.verify.length === 0) {
833
1112
  ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
834
1113
  ledger.status = "rejected";
1114
+ ledger.evidenceStatus = "rejected";
835
1115
  return ledger;
836
1116
  }
837
1117
  ledger.verifyRuns = [];
@@ -841,39 +1121,70 @@ export async function evaluateAcceptance(input: {
841
1121
  }
842
1122
  if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
843
1123
  ledger.status = "rejected";
1124
+ ledger.evidenceStatus = "rejected";
844
1125
  return ledger;
845
1126
  }
846
- ledger.status = "verified";
1127
+ if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1128
+ ledger.status = "verified";
1129
+ ledger.evidenceStatus = "verified";
1130
+ }
1131
+ }
1132
+
1133
+ if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
1134
+ ledger.status = "rejected";
1135
+ ledger.evidenceStatus = "rejected";
1136
+ return ledger;
1137
+ }
1138
+ if (ledger.status === "claimed") {
1139
+ ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
1140
+ ledger.evidenceStatus = ledger.status;
847
1141
  }
848
1142
 
849
- if (acceptance.level === "reviewed") {
850
- if (input.reviewResult) {
1143
+ if (acceptance.review && acceptance.review !== false) {
1144
+ if (input.reviewResult?.status === "reviewed") {
851
1145
  ledger.reviewResult = input.reviewResult;
852
- ledger.status = input.reviewResult.status === "no-blockers" ? "reviewed" : "rejected";
853
- } else {
854
- const optionalReview = acceptance.review && acceptance.review !== false && acceptance.review.required === false;
855
- ledger.reviewResult = {
856
- status: "needs-parent-decision",
1146
+ ledger.status = "reviewed";
1147
+ } else if (input.reviewResult?.status === "blockers") {
1148
+ ledger.reviewResult = input.reviewResult;
1149
+ ledger.status = "rejected";
1150
+ } else if (acceptance.review.required !== false) {
1151
+ ledger.reviewResult = input.reviewResult ?? {
1152
+ status: "review-required",
857
1153
  findings: [{
858
- severity: acceptance.explicit && !optionalReview ? "blocker" : "non-blocking",
859
- issue: "Reviewed acceptance requires an independent reviewer result.",
1154
+ severity: "non-blocking",
1155
+ issue: "Independent review has not been supplied.",
860
1156
  rationale: "The run cannot be marked reviewed from child evidence alone.",
861
1157
  }],
862
1158
  };
863
- if (acceptance.review === false || (acceptance.explicit && !optionalReview)) ledger.status = "rejected";
1159
+ ledger.status = "review-required";
864
1160
  }
865
1161
  }
866
1162
 
867
1163
  return ledger;
868
1164
  }
869
1165
 
1166
+ export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
1167
+ const status = acceptance.level === "none" ? "not-required" : "rejected";
1168
+ return {
1169
+ status,
1170
+ evidenceStatus: status,
1171
+ explicit: acceptance.explicit,
1172
+ effectiveAcceptance: acceptance,
1173
+ inferredReason: acceptance.inferredReason,
1174
+ criteria: acceptance.criteria,
1175
+ runtimeChecks: acceptance.level === "none"
1176
+ ? []
1177
+ : [{ id: input.id, status: "failed", message: input.message }],
1178
+ verifyRuns: [],
1179
+ };
1180
+ }
1181
+
870
1182
  export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | undefined {
871
1183
  if (ledger.status !== "rejected") return undefined;
872
1184
  const failedCheck = ledger.runtimeChecks.find((check) => check.status === "failed");
873
1185
  if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
874
1186
  const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
875
1187
  if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
876
- if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
877
1188
  if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
878
1189
  return "Acceptance rejected.";
879
1190
  }