@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
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
+ import type { Message } from "@earendil-works/pi-ai";
6
7
  import {
7
8
  acceptanceFailureMessage,
8
9
  aggregateAcceptanceReport,
@@ -12,7 +13,9 @@ import {
12
13
  resolveEffectiveAcceptance,
13
14
  stripAcceptanceReport,
14
15
  validateAcceptanceInput,
16
+ validateExecutionAcceptance,
15
17
  } from "../../src/runs/shared/acceptance.ts";
18
+ import { extractChildWrittenOutput } from "../../src/runs/shared/single-output.ts";
16
19
 
17
20
  function reportData(overrides: Record<string, unknown> = {}): Record<string, unknown> {
18
21
  return {
@@ -44,16 +47,125 @@ function tempRepo(): string {
44
47
  }
45
48
 
46
49
  describe("acceptance gates", () => {
47
- it("infers different policies for reviewer, writer, async writer, and dynamic contexts", () => {
48
- assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "attested");
49
- assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single" }).level, "checked");
50
- assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true }).level, "reviewed");
51
- assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Fix each item", mode: "chain", dynamic: true }).level, "reviewed");
50
+ it("infers evidence levels and review requirements independently", () => {
51
+ assert.equal(resolveEffectiveAcceptance({ agentName: "commentator", task: "Review-only. Do not edit.", mode: "single" }).level, "attested");
52
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single" }).level, "checked");
53
+ for (const resolved of [
54
+ resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single", async: true }),
55
+ resolveEffectiveAcceptance({ agentName: "builder", task: "Fix each item", mode: "chain", dynamic: true }),
56
+ ]) {
57
+ assert.equal(resolved.level, "checked");
58
+ assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true);
59
+ }
60
+ });
61
+
62
+ it("keeps async commentator review tasks on read-only acceptance despite implementation vocabulary", () => {
63
+ const resolved = resolveEffectiveAcceptance({
64
+ agentName: "commentator",
65
+ task: "Review prep findings and determine what to implement with playbooks instead of before.",
66
+ mode: "single",
67
+ async: true,
68
+ });
69
+
70
+ assert.equal(resolved.level, "attested");
71
+ assert.deepEqual(resolved.inferredReason, ["read-only/reviewer-style agent"]);
72
+ assert.deepEqual(resolved.criteria.map((criterion) => criterion.must), ["Return concrete findings with file paths and severity when applicable"]);
73
+ });
74
+
75
+ it("uses explicit agent roles for ambiguous tasks while preserving task-intent precedence", () => {
76
+ assert.equal(resolveEffectiveAcceptance({
77
+ agentName: "explorer",
78
+ acceptanceRole: "read-only",
79
+ task: "Explore the authentication flow",
80
+ mode: "single",
81
+ }).level, "attested");
82
+ assert.equal(resolveEffectiveAcceptance({
83
+ agentName: "commentator",
84
+ acceptanceRole: "writer",
85
+ task: "Handle the authentication flow",
86
+ mode: "single",
87
+ }).level, "checked");
88
+ for (const task of ["Implement the authentication fix", "Create a fixture", "Add coverage", "Replace the dependency", "Patch src/auth.ts"]) {
89
+ assert.equal(resolveEffectiveAcceptance({
90
+ agentName: "builder",
91
+ acceptanceRole: "read-only",
92
+ task,
93
+ mode: "single",
94
+ }).level, "checked", task);
95
+ }
96
+ assert.equal(resolveEffectiveAcceptance({
97
+ agentName: "builder",
98
+ acceptanceRole: "read-only",
99
+ task: "Patch src/auth.ts",
100
+ mode: "single",
101
+ async: true,
102
+ }).level, "checked");
103
+ assert.equal(resolveEffectiveAcceptance({
104
+ agentName: "builder",
105
+ acceptanceRole: "read-only",
106
+ task: "Create a report",
107
+ mode: "single",
108
+ }).level, "attested");
109
+ assert.equal(resolveEffectiveAcceptance({
110
+ agentName: "builder",
111
+ acceptanceRole: "writer",
112
+ task: "Review only; do not edit files",
113
+ mode: "single",
114
+ }).level, "attested");
115
+ assert.equal(resolveEffectiveAcceptance({
116
+ agentName: "commentator",
117
+ acceptanceRole: "writer",
118
+ task: "Handle the authentication flow",
119
+ mode: "single",
120
+ async: true,
121
+ }).level, "checked");
122
+ assert.equal(resolveEffectiveAcceptance({
123
+ agentName: "builder",
124
+ acceptanceRole: "read-only",
125
+ task: "Explore the authentication flow",
126
+ mode: "single",
127
+ }).level, "attested");
128
+ assert.equal(resolveEffectiveAcceptance({
129
+ agentName: "explorer",
130
+ acceptanceRole: "read-only",
131
+ task: "Audit the security posture",
132
+ mode: "single",
133
+ }).level, "attested");
134
+ assert.equal(resolveEffectiveAcceptance({
135
+ agentName: "explorer",
136
+ acceptanceRole: "read-only",
137
+ task: "Explore each target",
138
+ mode: "chain",
139
+ dynamic: true,
140
+ }).level, "attested");
141
+ assert.equal(resolveEffectiveAcceptance({
142
+ agentName: "builder",
143
+ acceptanceRole: "writer",
144
+ task: "Review only; do not edit files",
145
+ mode: "chain",
146
+ dynamicGroup: true,
147
+ }).level, "attested");
148
+ const dynamicReviewer = resolveEffectiveAcceptance({
149
+ agentName: "commentator",
150
+ task: "Review each target",
151
+ mode: "chain",
152
+ dynamic: true,
153
+ });
154
+ assert.equal(dynamicReviewer.level, "checked");
155
+ assert.equal(dynamicReviewer.review && dynamicReviewer.review !== false ? dynamicReviewer.review.required : undefined, true);
156
+ });
157
+
158
+ it("preserves risky keyword review inference when acceptance role metadata is omitted", () => {
159
+ for (const task of ["Inspect the security posture", "Read-only security audit"]) {
160
+ const resolved = resolveEffectiveAcceptance({ agentName: "builder", task });
161
+ assert.equal(resolved.level, "checked", task);
162
+ assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true, task);
163
+ }
52
164
  });
53
165
 
54
166
  it("explicit acceptance can strengthen inferred policy", () => {
55
167
  const resolved = resolveEffectiveAcceptance({
56
- agentName: "reviewer",
168
+ agentName: "commentator",
57
169
  task: "Review-only.",
58
170
  explicit: { level: "verified", verify: [{ id: "ok", command: "node --version" }] },
59
171
  });
@@ -62,9 +174,55 @@ describe("acceptance gates", () => {
62
174
  assert.equal(resolved.verify[0]?.id, "ok");
63
175
  });
64
176
 
177
+ it("agent contract v1 disables inferred acceptance without changing current defaults", () => {
178
+ const current = resolveEffectiveAcceptance({ agentName: "builder", acceptanceRole: "writer", task: "Implement the fix", mode: "single", async: true });
179
+ assert.equal(current.level, "checked");
180
+ assert.equal(current.review && current.review !== false ? current.review.required : undefined, true);
181
+ assert.deepEqual(current.inferredReason, ["async write-capable or risky run"]);
182
+
183
+ for (const explicit of [undefined, "auto" as const, false] as const) {
184
+ const resolved = resolveEffectiveAcceptance({ agentName: "builder", acceptanceRole: "writer", task: "Implement the fix", mode: "single", async: true, explicit, agentContract: { version: 1 } });
185
+ assert.equal(resolved.level, "none");
186
+ assert.deepEqual(resolved.inferredReason, []);
187
+ assert.equal(resolved.explicit, explicit !== undefined);
188
+ }
189
+ });
190
+
191
+ it("agent contract v1 keeps explicit acceptance report-optional and verify-only", async () => {
192
+ const checked = resolveEffectiveAcceptance({
193
+ agentName: "builder",
194
+ task: "Implement the fix",
195
+ explicit: "checked",
196
+ agentContract: { version: 1 },
197
+ });
198
+ assert.equal(formatAcceptancePrompt(checked, { reportOptional: true }), "");
199
+ const checkedLedger = await evaluateAcceptance({ acceptance: checked, output: "done", cwd: process.cwd(), reportOptional: true });
200
+ assert.equal(checkedLedger.status, "checked");
201
+ assert.equal(acceptanceFailureMessage(checkedLedger), undefined);
202
+
203
+ const verified = resolveEffectiveAcceptance({
204
+ agentName: "builder",
205
+ task: "Implement the fix",
206
+ explicit: { level: "verified", verify: [{ id: "ok", command: `${process.execPath} -e "process.exit(0)"` }] },
207
+ agentContract: { version: 1 },
208
+ });
209
+ assert.equal(formatAcceptancePrompt(verified, { reportOptional: true }), "");
210
+ const verifiedLedger = await evaluateAcceptance({ acceptance: verified, output: "done", cwd: process.cwd(), reportOptional: true });
211
+ assert.equal(verifiedLedger.status, "verified");
212
+ assert.equal(verifiedLedger.verifyRuns[0]?.status, "passed");
213
+ });
214
+
215
+ it("current/default acceptance still rejects missing required reports", async () => {
216
+ const current = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", explicit: "checked" });
217
+ const ledger = await evaluateAcceptance({ acceptance: current, output: "done", cwd: process.cwd() });
218
+
219
+ assert.equal(ledger.status, "rejected");
220
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /Structured acceptance report not found/);
221
+ });
222
+
65
223
  it("formats a standardized child prompt section", () => {
66
224
  const resolved = resolveEffectiveAcceptance({
67
- agentName: "worker",
225
+ agentName: "builder",
68
226
  task: "Implement a fix",
69
227
  explicit: { level: "checked", criteria: ["Patch the bug"], stopRules: ["Do not stop after analysis"] },
70
228
  });
@@ -75,9 +233,37 @@ describe("acceptance gates", () => {
75
233
  assert.match(prompt, /Patch the bug/);
76
234
  assert.match(prompt, /```acceptance-report/);
77
235
  assert.match(prompt, /array fields contain strings/);
236
+ assert.match(prompt, /criteriaSatisfied\[\]\.status.*satisfied, not-satisfied, not-applicable/);
237
+ assert.match(prompt, /commandsRun\[\]\.result.*passed, failed, not-run/);
238
+ assert.match(prompt, /manualNotes.*optional strings.*empty string.*does not satisfy.*manual-notes/);
78
239
  assert.match(prompt, /"reviewFindings": \[\n "blocker:/);
79
240
  });
80
241
 
242
+ it("includes every required resolved criterion in report examples", () => {
243
+ const inferred = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single", async: true });
244
+ const inferredExample = formatAcceptancePrompt(inferred).match(/```acceptance-report\n([\s\S]*?)\n```/);
245
+ assert.ok(inferredExample?.[1]);
246
+ assert.deepEqual(
247
+ (JSON.parse(inferredExample[1]!) as { criteriaSatisfied: Array<{ id: string }> }).criteriaSatisfied.map((criterion) => criterion.id),
248
+ ["criterion-1", "criterion-2"],
249
+ );
250
+
251
+ const custom = resolveEffectiveAcceptance({
252
+ agentName: "builder",
253
+ task: "Implement the fix",
254
+ explicit: { level: "checked", criteria: [
255
+ { id: "required-check", must: "Required", severity: "required" },
256
+ { id: "recommended-check", must: "Recommended", severity: "recommended" },
257
+ ] },
258
+ });
259
+ const customExample = formatAcceptancePrompt(custom).match(/```acceptance-report\n([\s\S]*?)\n```/);
260
+ assert.ok(customExample?.[1]);
261
+ assert.deepEqual(
262
+ (JSON.parse(customExample[1]!) as { criteriaSatisfied: Array<{ id: string }> }).criteriaSatisfied.map((criterion) => criterion.id),
263
+ ["required-check"],
264
+ );
265
+ });
266
+
81
267
  it("parses acceptance-report fences and ignores unrelated json fences", () => {
82
268
  const parsed = parseAcceptanceReport(report());
83
269
 
@@ -97,12 +283,18 @@ describe("acceptance gates", () => {
97
283
  assert.equal(criteriaOnlyJson.report, undefined);
98
284
  assert.match(criteriaOnlyJson.error ?? "", /Structured acceptance report not found/);
99
285
 
286
+ const criteriaWithUnknownJson = parseAcceptanceReport(`done\n\
287
+ \
288
+ \`\`\`json\n{\"criteriaSatisfied\":[],\"unexpected\":true}\n\`\`\``);
289
+ assert.equal(criteriaWithUnknownJson.report, undefined);
290
+ assert.match(criteriaWithUnknownJson.error ?? "", /unexpected: unsupported acceptance report field/);
291
+
100
292
  const invalidSignalJson = `done\n\
101
293
  \
102
294
  \`\`\`json\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"satisfied\",\"evidence\":\"example\"}],\"changedFiles\":false}\n\`\`\``;
103
295
  const genericJsonWithInvalidSignal = parseAcceptanceReport(invalidSignalJson);
104
296
  assert.equal(genericJsonWithInvalidSignal.report, undefined);
105
- assert.match(genericJsonWithInvalidSignal.error ?? "", /Structured acceptance report not found/);
297
+ assert.match(genericJsonWithInvalidSignal.error ?? "", /changedFiles: expected string\[\]; got boolean false/);
106
298
  assert.equal(stripAcceptanceReport(invalidSignalJson), invalidSignalJson);
107
299
 
108
300
  const partialWrapperJson = `done\n\
@@ -121,9 +313,17 @@ describe("acceptance gates", () => {
121
313
  assert.match(genericReportShapedJson.error ?? "", /Structured acceptance report not found/);
122
314
  assert.equal(stripAcceptanceReport(reportShapedJson), reportShapedJson);
123
315
 
124
- const malformed = parseAcceptanceReport("```acceptance-report\n{bad-json\n```");
125
- assert.equal(malformed.report, undefined);
126
- assert.match(malformed.error ?? "", /Failed to parse acceptance-report/);
316
+ for (const malformedOutput of [
317
+ "```acceptance-report\n{bad-json\n```",
318
+ "```acceptance-report\n```",
319
+ "```acceptance-report\n{\"criteriaSatisfied\": []}",
320
+ "ACCEPTANCE_REPORT: { not json",
321
+ "ACCEPTANCE_REPORT: no object",
322
+ ]) {
323
+ const malformed = parseAcceptanceReport(malformedOutput);
324
+ assert.equal(malformed.report, undefined);
325
+ assert.match(malformed.error ?? "", /Failed to parse acceptance-report/, malformedOutput);
326
+ }
127
327
  });
128
328
 
129
329
  it("parses acceptance reports from json-family fences", () => {
@@ -161,46 +361,162 @@ describe("acceptance gates", () => {
161
361
  ].join("\n"));
162
362
  });
163
363
 
164
- it("unwraps acceptance-report wrapper objects", () => {
364
+ it("unwraps every accepted report wrapper", () => {
365
+ for (const wrapper of ["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]) {
366
+ const output = [
367
+ "done",
368
+ "```json",
369
+ JSON.stringify({ [wrapper]: reportData() }),
370
+ "```",
371
+ ].join("\n");
372
+ const parsed = parseAcceptanceReport(output);
373
+
374
+ assert.ok(parsed.report, wrapper);
375
+ assert.deepEqual(parsed.report.testsAddedOrUpdated, ["test/file.test.ts"]);
376
+ assert.equal(stripAcceptanceReport(output), "done");
377
+ }
378
+ });
379
+
380
+ it("normalizes known report wire variants to the canonical shape", () => {
165
381
  const output = [
166
382
  "done",
167
- "```json",
168
- JSON.stringify({ "acceptance-report": reportData() }),
383
+ "```acceptance_report",
384
+ JSON.stringify({ acceptance_report: {
385
+ criteria_satisfied: { id: "Criterion_1", status: "DONE", evidence: "verified" },
386
+ changed_files: "src/file.ts",
387
+ tests_added_or_updated: "test/file.test.ts",
388
+ commands_run: { command: "npm test", result: "success", summary: "passed" },
389
+ validation_output: "tests passed",
390
+ residual_risks: "none",
391
+ no_staged_files: " TRUE ",
392
+ diff_summary: "patched",
393
+ review_findings: "no blockers",
394
+ manual_notes: "complete",
395
+ } }),
169
396
  "```",
170
397
  ].join("\n");
171
398
  const parsed = parseAcceptanceReport(output);
172
399
 
173
- assert.ok(parsed.report);
174
- assert.deepEqual(parsed.report.testsAddedOrUpdated, ["test/file.test.ts"]);
400
+ assert.equal(parsed.error, undefined);
401
+ assert.deepEqual(parsed.report, {
402
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "verified" }],
403
+ changedFiles: ["src/file.ts"],
404
+ testsAddedOrUpdated: ["test/file.test.ts"],
405
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
406
+ validationOutput: ["tests passed"],
407
+ residualRisks: ["none"],
408
+ noStagedFiles: true,
409
+ diffSummary: "patched",
410
+ reviewFindings: ["no blockers"],
411
+ manualNotes: "complete",
412
+ });
175
413
  assert.equal(stripAcceptanceReport(output), "done");
176
414
  });
177
415
 
416
+ it("normalizes only known enum synonyms and criterion id separators", () => {
417
+ for (const [value, expected] of [
418
+ ["met", "satisfied"],
419
+ ["not met", "not-satisfied"],
420
+ ["not_applicable", "not-applicable"],
421
+ ] as const) {
422
+ const parsed = parseAcceptanceReport(report({ criteriaSatisfied: [{ id: "Criterion 1", status: value, evidence: "proof" }] }));
423
+ assert.equal(parsed.report?.criteriaSatisfied?.[0]?.id, "criterion-1");
424
+ assert.equal(parsed.report?.criteriaSatisfied?.[0]?.status, expected);
425
+ }
426
+ for (const [value, expected] of [
427
+ ["ok", "passed"],
428
+ ["failure", "failed"],
429
+ ["not run", "not-run"],
430
+ ] as const) {
431
+ const parsed = parseAcceptanceReport(report({ commandsRun: [{ command: "check", result: value, summary: "complete" }] }));
432
+ assert.equal(parsed.report?.commandsRun?.[0]?.result, expected);
433
+ }
434
+ });
435
+
436
+ it("rejects unknown and ambiguous report fields with exact diagnostics", () => {
437
+ for (const [value, expected] of [
438
+ [{ ...reportData(), unexpected: true }, /unexpected: unsupported acceptance report field/],
439
+ [{ ...reportData(), changed_files: ["src/other.ts"] }, /changed_files: duplicates normalized field 'changedFiles'/],
440
+ [{ acceptance: reportData(), changedFiles: ["src/file.ts"] }, /changedFiles: unsupported alongside acceptance report wrapper 'acceptance'/],
441
+ [{ acceptance: reportData(), acceptance_report: reportData() }, /multiple acceptance report wrappers are ambiguous/],
442
+ ] as const) {
443
+ const parsed = parseAcceptanceReport(`\`\`\`acceptance-report\n${JSON.stringify(value)}\n\`\`\``);
444
+ assert.equal(parsed.report, undefined);
445
+ assert.match(parsed.error ?? "", expected);
446
+ }
447
+ });
448
+
449
+ it("rejects unknown enums, duplicate criterion ids, and blank evidence", () => {
450
+ for (const [overrides, expected] of [
451
+ [{ commandsRun: [{ command: "npm test", result: "maybe", summary: "passed" }] }, /commandsRun\[0\]\.result.*got "maybe"/],
452
+ [{ commandsRun: [{ command: "npm test", result: "passed", summary: "", exitCode: 0 }] }, /commandsRun\[0\]\.exitCode: unsupported acceptance command field/],
453
+ [{ commandsRun: [{ command: "npm test", result: "passed", summary: "" }] }, /commandsRun\[0\]\.summary: expected non-empty string; got ""/],
454
+ [{ criteriaSatisfied: [{ id: "criterion-1", status: "maybe", evidence: "proof" }] }, /criteriaSatisfied\[0\]\.status.*got "maybe"/],
455
+ [{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof", confidence: 1 }] }, /criteriaSatisfied\[0\]\.confidence: unsupported acceptance criterion field/],
456
+ [{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof" }, { id: "Criterion_1", status: "satisfied", evidence: "proof" }] }, /duplicate normalized criterion id 'criterion-1'/],
457
+ [{ reviewFindings: [""] }, /reviewFindings\[0\]: expected non-empty string; got ""/],
458
+ [{ noStagedFiles: "yes" }, /noStagedFiles: expected boolean; got "yes"/],
459
+ ] as const) {
460
+ const parsed = parseAcceptanceReport(report(overrides));
461
+ assert.equal(parsed.report, undefined);
462
+ assert.match(parsed.error ?? "", expected);
463
+ }
464
+ });
465
+
466
+ it("accepts empty optional note strings without treating them as evidence", async () => {
467
+ for (const field of ["manualNotes", "notes"] as const) {
468
+ const parsed = parseAcceptanceReport(report({ notes: undefined, [field]: "" }));
469
+ assert.equal(parsed.error, undefined);
470
+ assert.equal(parsed.report?.[field], "");
471
+ }
472
+
473
+ const cwd = tempRepo();
474
+ try {
475
+ const acceptance = resolveEffectiveAcceptance({
476
+ agentName: "builder",
477
+ task: "Return a concise result",
478
+ explicit: { level: "attested", evidence: ["manual-notes"] },
479
+ });
480
+ const ledger = await evaluateAcceptance({
481
+ acceptance,
482
+ output: report({ notes: undefined, manualNotes: "" }),
483
+ cwd,
484
+ });
485
+ assert.equal(ledger.childReportParseError, undefined);
486
+ assert.equal(ledger.childReport?.manualNotes, "");
487
+ assert.equal(ledger.status, "rejected");
488
+ assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:manual-notes")?.status, "failed");
489
+ } finally {
490
+ fs.rmSync(cwd, { recursive: true, force: true });
491
+ }
492
+ });
493
+
178
494
  it("reports field-level validation errors for malformed acceptance-report fields", () => {
179
495
  const invalidReviewerReport = parseAcceptanceReport(report({
180
496
  reviewFindings: [{ id: "B-1", severity: "blocker", finding: "Missing evidence" }],
181
497
  }));
182
498
  assert.equal(invalidReviewerReport.report, undefined);
183
- assert.match(invalidReviewerReport.error ?? "", /reviewFindings\[0\]: expected string; got object/);
499
+ assert.match(invalidReviewerReport.error ?? "", /reviewFindings\[0\]: expected non-empty string; got object/);
184
500
 
185
501
  const invalidCommandReport = parseAcceptanceReport(report({
186
502
  commandsRun: [{ command: "npm test", exitCode: 0 }],
187
503
  }));
188
504
  assert.equal(invalidCommandReport.report, undefined);
189
505
  assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.result: expected one of "passed", "failed", "not-run"; got missing/);
190
- assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.summary: expected string; got missing/);
506
+ assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.summary: expected non-empty string; got missing/);
191
507
 
192
508
  const invalidCriteriaReport = parseAcceptanceReport(report({
193
- criteriaSatisfied: [{ id: 7, status: "done", evidence: "" }],
509
+ criteriaSatisfied: [{ id: 7, status: "maybe", evidence: "" }],
194
510
  }));
195
511
  assert.equal(invalidCriteriaReport.report, undefined);
196
512
  assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.id: expected string; got number 7/);
197
- assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.status: expected one of "satisfied", "not-satisfied", "not-applicable"; got "done"/);
513
+ assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.status: expected one of "satisfied", "not-satisfied", "not-applicable"; got "maybe"/);
198
514
  assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.evidence: expected non-empty string; got ""/);
199
515
  });
200
516
 
201
517
  it("explicit none disables inferred gates when a reason is present", () => {
202
518
  const acceptance = resolveEffectiveAcceptance({
203
- agentName: "worker",
519
+ agentName: "builder",
204
520
  task: "Implement a fix",
205
521
  explicit: { level: "none", reason: "parent is doing manual acceptance" },
206
522
  });
@@ -209,22 +525,53 @@ describe("acceptance gates", () => {
209
525
  assert.deepEqual(acceptance.evidence, []);
210
526
  });
211
527
 
212
- it("checked mode rejects missing required evidence", async () => {
528
+ it("checked mode accepts explicit empty changed and test arrays as not applicable", async () => {
213
529
  const cwd = tempRepo();
214
530
  try {
215
531
  const acceptance = resolveEffectiveAcceptance({
216
- agentName: "worker",
532
+ agentName: "builder",
217
533
  task: "Implement a fix",
218
534
  explicit: { level: "checked" },
219
535
  });
220
536
  const ledger = await evaluateAcceptance({
221
537
  acceptance,
222
- output: report({ testsAddedOrUpdated: [] }),
538
+ output: report({ changedFiles: [], testsAddedOrUpdated: [] }),
223
539
  cwd,
224
540
  });
225
541
 
226
- assert.equal(ledger.status, "rejected");
227
- assert.match(acceptanceFailureMessage(ledger) ?? "", /tests-added evidence missing/);
542
+ assert.equal(ledger.status, "checked");
543
+ assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:changed-files")?.status, "not-applicable");
544
+ assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:tests-added")?.status, "not-applicable");
545
+ } finally {
546
+ fs.rmSync(cwd, { recursive: true, force: true });
547
+ }
548
+ });
549
+
550
+ it("still rejects missing changed and test evidence and empty required commands", async () => {
551
+ const cwd = tempRepo();
552
+ try {
553
+ const acceptance = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement a fix", explicit: { level: "checked" } });
554
+ const missing = await evaluateAcceptance({ acceptance, output: report({
555
+ changedFiles: undefined,
556
+ testsAddedOrUpdated: undefined,
557
+ }), cwd });
558
+ assert.equal(missing.status, "rejected");
559
+ assert.match(acceptanceFailureMessage(missing) ?? "", /changed-files evidence missing/);
560
+
561
+ const missingTests = await evaluateAcceptance({ acceptance, output: report({
562
+ changedFiles: [],
563
+ testsAddedOrUpdated: undefined,
564
+ }), cwd });
565
+ assert.equal(missingTests.status, "rejected");
566
+ assert.match(acceptanceFailureMessage(missingTests) ?? "", /tests-added evidence missing/);
567
+
568
+ const emptyCommands = await evaluateAcceptance({
569
+ acceptance,
570
+ output: report({ changedFiles: [], testsAddedOrUpdated: [], commandsRun: [] }),
571
+ cwd,
572
+ });
573
+ assert.equal(emptyCommands.status, "rejected");
574
+ assert.match(acceptanceFailureMessage(emptyCommands) ?? "", /commands-run evidence missing/);
228
575
  } finally {
229
576
  fs.rmSync(cwd, { recursive: true, force: true });
230
577
  }
@@ -234,7 +581,7 @@ describe("acceptance gates", () => {
234
581
  const cwd = tempRepo();
235
582
  try {
236
583
  const acceptance = resolveEffectiveAcceptance({
237
- agentName: "reviewer",
584
+ agentName: "commentator",
238
585
  task: "Review-only. Do not edit.",
239
586
  explicit: { level: "attested", evidence: ["review-findings"] },
240
587
  });
@@ -246,7 +593,40 @@ describe("acceptance gates", () => {
246
593
 
247
594
  assert.equal(ledger.status, "rejected");
248
595
  assert.match(acceptanceFailureMessage(ledger) ?? "", /Failed to parse acceptance-report/);
249
- assert.match(acceptanceFailureMessage(ledger) ?? "", /reviewFindings\[0\]: expected string; got object/);
596
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /reviewFindings\[0\]: expected non-empty string; got object/);
597
+ } finally {
598
+ fs.rmSync(cwd, { recursive: true, force: true });
599
+ }
600
+ });
601
+
602
+ it("normalizes configured criterion ids and validates direct report objects", async () => {
603
+ const cwd = tempRepo();
604
+ try {
605
+ const acceptance = resolveEffectiveAcceptance({
606
+ agentName: "builder",
607
+ task: "Implement a fix",
608
+ explicit: { level: "checked", criteria: [{ id: "Release_Check", must: "Release check passes" }] },
609
+ });
610
+ const normalized = await evaluateAcceptance({
611
+ acceptance,
612
+ output: report({
613
+ criteriaSatisfied: [{ id: "release check", status: "met", evidence: "verified" }],
614
+ changedFiles: [],
615
+ testsAddedOrUpdated: [],
616
+ }),
617
+ cwd,
618
+ });
619
+ assert.equal(normalized.status, "checked");
620
+ assert.equal(normalized.childReport?.criteriaSatisfied?.[0]?.id, "release-check");
621
+
622
+ const malformedDirect = await evaluateAcceptance({
623
+ acceptance,
624
+ output: "",
625
+ report: { ...reportData(), unexpected: true } as never,
626
+ cwd,
627
+ });
628
+ assert.equal(malformedDirect.status, "rejected");
629
+ assert.match(malformedDirect.childReportParseError ?? "", /unexpected: unsupported acceptance report field/);
250
630
  } finally {
251
631
  fs.rmSync(cwd, { recursive: true, force: true });
252
632
  }
@@ -256,7 +636,7 @@ describe("acceptance gates", () => {
256
636
  const cwd = tempRepo();
257
637
  try {
258
638
  const acceptance = resolveEffectiveAcceptance({
259
- agentName: "worker",
639
+ agentName: "builder",
260
640
  task: "Implement a fix",
261
641
  explicit: { level: "checked", criteria: [{ id: "regression", must: "Regression is covered" }] },
262
642
  });
@@ -277,7 +657,7 @@ describe("acceptance gates", () => {
277
657
  const cwd = tempRepo();
278
658
  try {
279
659
  const passing = resolveEffectiveAcceptance({
280
- agentName: "worker",
660
+ agentName: "builder",
281
661
  task: "Implement a fix",
282
662
  explicit: { level: "verified", verify: [{ id: "pass", command: "node -e \"process.exit(0)\"", timeoutMs: 10_000 }] },
283
663
  });
@@ -286,7 +666,7 @@ describe("acceptance gates", () => {
286
666
  assert.equal(passLedger.verifyRuns[0]?.status, "passed");
287
667
 
288
668
  const failing = resolveEffectiveAcceptance({
289
- agentName: "worker",
669
+ agentName: "builder",
290
670
  task: "Implement a fix",
291
671
  explicit: { level: "verified", verify: [{ id: "fail", command: "node -e \"process.exit(7)\"", timeoutMs: 10_000 }] },
292
672
  });
@@ -299,22 +679,23 @@ describe("acceptance gates", () => {
299
679
  }
300
680
  });
301
681
 
302
- it("reviewed mode records no-blocker and blocker reviewer outcomes", async () => {
682
+ it("records achieved, blocked, and pending independent review separately from evidence", async () => {
303
683
  const cwd = tempRepo();
304
684
  try {
305
685
  const acceptance = resolveEffectiveAcceptance({
306
- agentName: "worker",
686
+ agentName: "builder",
307
687
  task: "Implement a risky fix",
308
- explicit: { level: "reviewed", review: { agent: "reviewer", required: true } },
688
+ explicit: { level: "checked", review: { agent: "commentator", required: true } },
309
689
  });
310
- const noBlockers = await evaluateAcceptance({
690
+ const reviewed = await evaluateAcceptance({
311
691
  acceptance,
312
692
  output: report(),
313
693
  cwd,
314
- reviewResult: { status: "no-blockers", findings: [] },
694
+ reviewResult: { status: "reviewed", findings: [] },
315
695
  });
316
- assert.equal(noBlockers.status, "reviewed");
317
- assert.equal(noBlockers.reviewResult?.status, "no-blockers");
696
+ assert.equal(reviewed.status, "reviewed");
697
+ assert.equal(reviewed.evidenceStatus, "checked");
698
+ assert.equal(reviewed.reviewResult?.status, "reviewed");
318
699
 
319
700
  const blockers = await evaluateAcceptance({
320
701
  acceptance,
@@ -326,91 +707,344 @@ describe("acceptance gates", () => {
326
707
  },
327
708
  });
328
709
  assert.equal(blockers.status, "rejected");
710
+ assert.equal(blockers.evidenceStatus, "checked");
329
711
  assert.equal(blockers.reviewResult?.status, "blockers");
330
712
 
331
- const unavailable = await evaluateAcceptance({ acceptance, output: report(), cwd });
332
- assert.equal(unavailable.status, "rejected");
333
- assert.equal(unavailable.reviewResult?.status, "needs-parent-decision");
713
+ const pending = await evaluateAcceptance({ acceptance, output: report(), cwd });
714
+ assert.equal(pending.status, "review-required");
715
+ assert.equal(pending.evidenceStatus, "checked");
716
+ assert.equal(pending.reviewResult?.status, "review-required");
717
+ } finally {
718
+ fs.rmSync(cwd, { recursive: true, force: true });
719
+ }
720
+ });
721
+
722
+ it("keeps inferred review required when callers explicitly select checked or auto evidence", async () => {
723
+ const cwd = tempRepo();
724
+ try {
725
+ for (const explicit of [{ level: "checked" }, "auto", { level: "auto" }] as const) {
726
+ const acceptance = resolveEffectiveAcceptance({
727
+ agentName: "builder",
728
+ task: "Implement each dynamic item",
729
+ dynamic: true,
730
+ explicit,
731
+ });
732
+
733
+ assert.equal(acceptance.level, "checked");
734
+ assert.equal(acceptance.review && acceptance.review !== false ? acceptance.review.required : undefined, true);
735
+ const ledger = await evaluateAcceptance({ acceptance, output: report({ criteriaSatisfied: [
736
+ { id: "criterion-1", status: "satisfied", evidence: "implemented" },
737
+ { id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
738
+ ] }), cwd });
739
+ assert.equal(ledger.status, "review-required");
740
+ assert.equal(ledger.evidenceStatus, "checked");
741
+ assert.equal(ledger.reviewResult?.status, "review-required");
742
+ }
743
+ } finally {
744
+ fs.rmSync(cwd, { recursive: true, force: true });
745
+ }
746
+ });
747
+
748
+ it("allows an explicit review opt-out without claiming reviewed status", async () => {
749
+ const cwd = tempRepo();
750
+ try {
751
+ for (const review of [false, { agent: "commentator", required: false }] as const) {
752
+ const acceptance = resolveEffectiveAcceptance({
753
+ agentName: "builder",
754
+ task: "Implement an async fix",
755
+ async: true,
756
+ explicit: { level: "checked", review },
757
+ });
758
+ const ledger = await evaluateAcceptance({
759
+ acceptance,
760
+ output: report({ criteriaSatisfied: [
761
+ { id: "criterion-1", status: "satisfied", evidence: "implemented" },
762
+ { id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
763
+ ] }),
764
+ cwd,
765
+ reviewResult: { status: "review-required", findings: [] },
766
+ });
767
+ assert.equal(ledger.status, "checked");
768
+ assert.equal(ledger.evidenceStatus, "checked");
769
+ assert.equal(ledger.reviewResult, undefined);
770
+ }
771
+ } finally {
772
+ fs.rmSync(cwd, { recursive: true, force: true });
773
+ }
774
+ });
775
+
776
+ it("zero-child aggregate reports do not fabricate required evidence", async () => {
777
+ const cwd = tempRepo();
778
+ try {
779
+ const acceptance = resolveEffectiveAcceptance({
780
+ agentName: "builder",
781
+ task: "Implement dynamic fanout fixes",
782
+ explicit: { level: "checked" },
783
+ });
784
+ const ledger = await evaluateAcceptance({
785
+ acceptance,
786
+ output: "",
787
+ report: aggregateAcceptanceReport({ results: [] }),
788
+ cwd,
789
+ });
790
+
791
+ assert.equal(ledger.status, "rejected");
792
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /criterion|changed-files|tests-added|commands-run|validation-output|no-staged-files/);
334
793
  } finally {
335
794
  fs.rmSync(cwd, { recursive: true, force: true });
336
795
  }
337
796
  });
338
797
 
339
- it("does not make explicit checked acceptance an explicit reviewed blocker when inference recommends review", async () => {
798
+ it("recovers the acceptance report from child-written configured output", async () => {
340
799
  const cwd = tempRepo();
341
800
  try {
342
801
  const acceptance = resolveEffectiveAcceptance({
343
- agentName: "worker",
344
- task: "Implement each dynamic item",
345
- dynamic: true,
802
+ agentName: "builder",
803
+ task: "Implement a fix",
346
804
  explicit: { level: "checked" },
347
805
  });
806
+ const receipt = "Report written to the configured output file.";
807
+
808
+ const withoutFile = await evaluateAcceptance({ acceptance, output: receipt, cwd });
809
+ assert.equal(withoutFile.status, "rejected");
348
810
 
349
- assert.equal(acceptance.level, "reviewed");
350
- assert.equal(acceptance.review && acceptance.review !== false ? acceptance.review.required : undefined, false);
351
- const ledger = await evaluateAcceptance({ acceptance, output: report({ criteriaSatisfied: [
352
- { id: "criterion-1", status: "satisfied", evidence: "implemented" },
353
- { id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
354
- ] }), cwd });
811
+ const ledger = await evaluateAcceptance({
812
+ acceptance,
813
+ output: receipt,
814
+ fileOutput: { content: report(), path: "/tmp/report.md", authoritative: true },
815
+ cwd,
816
+ });
355
817
  assert.equal(ledger.status, "checked");
356
- assert.equal(ledger.reviewResult?.status, "needs-parent-decision");
818
+ assert.ok(ledger.childReport);
357
819
  } finally {
358
820
  fs.rmSync(cwd, { recursive: true, force: true });
359
821
  }
360
822
  });
361
823
 
362
- it("does not mark reviewed without an independent reviewer result", async () => {
824
+ it("report source order follows output authority", async () => {
363
825
  const cwd = tempRepo();
364
826
  try {
365
827
  const acceptance = resolveEffectiveAcceptance({
366
- agentName: "worker",
828
+ agentName: "builder",
367
829
  task: "Implement a fix",
368
- explicit: {
369
- level: "reviewed",
370
- review: false,
371
- },
830
+ explicit: { level: "checked" },
372
831
  });
373
- assert.equal(acceptance.level, "reviewed");
832
+ const inline = await evaluateAcceptance({
833
+ acceptance,
834
+ output: report({ notes: "from text" }),
835
+ fileOutput: { content: report({ notes: "from file" }), path: "/tmp/report.md" },
836
+ cwd,
837
+ });
838
+ assert.equal(inline.childReport?.notes, "from text");
374
839
 
375
- const ledger = await evaluateAcceptance({ acceptance, output: report(), cwd });
376
- assert.equal(ledger.status, "rejected");
377
- assert.equal(ledger.reviewResult?.status, "needs-parent-decision");
378
- assert.match(acceptanceFailureMessage(ledger) ?? "", /review required/i);
840
+ const fileOnly = await evaluateAcceptance({
841
+ acceptance,
842
+ output: report({ notes: "from text" }),
843
+ fileOutput: { content: report({ notes: "from file" }), path: "/tmp/report.md", authoritative: true },
844
+ cwd,
845
+ });
846
+ assert.equal(fileOnly.childReport?.notes, "from file");
379
847
  } finally {
380
848
  fs.rmSync(cwd, { recursive: true, force: true });
381
849
  }
382
850
  });
383
851
 
384
- it("zero-child aggregate reports do not fabricate required evidence", async () => {
852
+ it("sibling overwrites of a shared output path cannot pollute this child's acceptance", async () => {
385
853
  const cwd = tempRepo();
854
+ const sharedPath = path.join(cwd, "context.md");
386
855
  try {
856
+ // Sibling child B wrote the shared path last (issue #420); the disk
857
+ // content is B's. Child A's acceptance input comes from A's own
858
+ // successful write-tool call, so B's report must not be attributed to A.
859
+ fs.writeFileSync(sharedPath, report({ notes: "sibling B report" }), "utf-8");
860
+ const childAMessages: Message[] = [
861
+ {
862
+ role: "assistant",
863
+ content: [{ type: "toolCall", id: "w1", name: "write", arguments: { path: sharedPath, content: report({ notes: "child A report" }) } }],
864
+ api: "test",
865
+ provider: "test",
866
+ model: "mock/test-model",
867
+ usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
868
+ stopReason: "toolUse",
869
+ timestamp: 0,
870
+ },
871
+ { role: "toolResult", toolCallId: "w1", toolName: "write", content: [{ type: "text", text: "ok" }], isError: false, timestamp: 0 },
872
+ ];
873
+ const childAContent = extractChildWrittenOutput(childAMessages, sharedPath, cwd);
874
+ assert.equal(typeof childAContent, "string");
875
+
387
876
  const acceptance = resolveEffectiveAcceptance({
388
- agentName: "worker",
389
- task: "Implement dynamic fanout fixes",
877
+ agentName: "builder",
878
+ task: "Implement a fix",
390
879
  explicit: { level: "checked" },
391
880
  });
392
881
  const ledger = await evaluateAcceptance({
393
882
  acceptance,
394
- output: "",
395
- report: aggregateAcceptanceReport({ results: [] }),
883
+ output: "Report written to the configured output file.",
884
+ fileOutput: { content: childAContent!, path: sharedPath, authoritative: true },
885
+ cwd,
886
+ });
887
+ assert.equal(ledger.childReport?.notes, "child A report");
888
+ } finally {
889
+ fs.rmSync(cwd, { recursive: true, force: true });
890
+ }
891
+ });
892
+
893
+ it("rejects a malformed primary report instead of falling back to the secondary source", async () => {
894
+ const cwd = tempRepo();
895
+ try {
896
+ const acceptance = resolveEffectiveAcceptance({
897
+ agentName: "builder",
898
+ task: "Implement a fix",
899
+ explicit: { level: "checked" },
900
+ });
901
+ const malformedReports = [
902
+ "```acceptance-report\n{ not json\n```",
903
+ "```acceptance-report\n```",
904
+ "```acceptance-report\n{\"criteriaSatisfied\": []}",
905
+ "ACCEPTANCE_REPORT: { not json",
906
+ ];
907
+
908
+ for (const malformed of malformedReports) {
909
+ const fileOnly = await evaluateAcceptance({
910
+ acceptance,
911
+ output: report({ notes: "valid text report" }),
912
+ fileOutput: { content: malformed, path: "/tmp/report.md", authoritative: true },
913
+ cwd,
914
+ });
915
+ assert.equal(fileOnly.status, "rejected", malformed);
916
+ assert.match(fileOnly.childReportParseError ?? "", /configured output/, malformed);
917
+ }
918
+
919
+ const inline = await evaluateAcceptance({
920
+ acceptance,
921
+ output: malformedReports[0]!,
922
+ fileOutput: { content: report({ notes: "valid file report" }), path: "/tmp/report.md" },
396
923
  cwd,
397
924
  });
925
+ assert.equal(inline.status, "rejected");
926
+ assert.match(inline.childReportParseError ?? "", /Failed to parse acceptance-report/);
927
+ } finally {
928
+ fs.rmSync(cwd, { recursive: true, force: true });
929
+ }
930
+ });
398
931
 
932
+ it("surfaces field-level errors from an invalid configured-output report", async () => {
933
+ const cwd = tempRepo();
934
+ try {
935
+ const acceptance = resolveEffectiveAcceptance({
936
+ agentName: "builder",
937
+ task: "Implement a fix",
938
+ explicit: { level: "checked" },
939
+ });
940
+ const ledger = await evaluateAcceptance({
941
+ acceptance,
942
+ output: "wrote the report to the file",
943
+ fileOutput: {
944
+ content: report({ criteriaSatisfied: [{ id: "criterion-1", status: "partially_done", evidence: "x" }] }),
945
+ path: "/tmp/report.md",
946
+ },
947
+ cwd,
948
+ });
399
949
  assert.equal(ledger.status, "rejected");
400
- assert.match(acceptanceFailureMessage(ledger) ?? "", /criterion|changed-files|tests-added|commands-run|validation-output|no-staged-files/);
950
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /partially_done/);
951
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /configured output/);
401
952
  } finally {
402
953
  fs.rmSync(cwd, { recursive: true, force: true });
403
954
  }
404
955
  });
405
956
 
957
+ it("validates explicit reviewed acceptance at every execution nesting level", () => {
958
+ const errors = validateExecutionAcceptance({
959
+ acceptance: "reviewed",
960
+ tasks: [{ acceptance: { level: "reviewed" } }],
961
+ chain: [
962
+ { acceptance: "reviewed" },
963
+ { parallel: [{ acceptance: { level: "reviewed" } }] },
964
+ { parallel: { acceptance: "reviewed" } },
965
+ ],
966
+ });
967
+
968
+ assert.equal(errors.length, 5);
969
+ assert.match(errors[0] ?? "", /^acceptance is an achieved status/);
970
+ assert.match(errors[1] ?? "", /^tasks\[0\]\.acceptance\.level is an achieved status/);
971
+ assert.match(errors[2] ?? "", /^chain\[0\]\.acceptance is an achieved status/);
972
+ assert.match(errors[3] ?? "", /^chain\[1\]\.parallel\[0\]\.acceptance\.level is an achieved status/);
973
+ assert.match(errors[4] ?? "", /^chain\[2\]\.parallel\.acceptance is an achieved status/);
974
+ assert.match(errors.join("\n"), /acceptance\.review\.required/);
975
+ });
976
+
977
+ it("blanket read-only wording is not inferred as a risky write task", () => {
978
+ const readOnlyWorker = resolveEffectiveAcceptance({
979
+ agentName: "builder",
980
+ task: "Report on the extraction pipeline. Do not modify project/source files.",
981
+ async: true,
982
+ });
983
+ assert.equal(readOnlyWorker.level, "attested");
984
+ for (const task of [
985
+ "Inspect the extraction pipeline",
986
+ "Summarize the extraction pipeline",
987
+ "Review only: return findings",
988
+ "Analyze the extraction pipeline without edits",
989
+ ]) {
990
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task, async: true }).level, "attested", task);
991
+ }
992
+
993
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Inspect the failure and implement the fix" }).level, "checked");
994
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Inspect the failure and implement the fix", async: true }).level, "checked");
995
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests; implement the fix", async: true }).level, "checked");
996
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests but implement the fix", async: true }).level, "checked");
997
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests and implement the fix", async: true }).level, "checked");
998
+ for (const task of [
999
+ "Do not modify tests - implement the fix",
1000
+ "Do not modify tests – implement the fix",
1001
+ "Do not modify tests — implement the fix",
1002
+ ]) {
1003
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task }).level, "checked", task);
1004
+ assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task, async: true }).level, "checked", task);
1005
+ }
1006
+ });
1007
+
1008
+ it("bare write verbs keep their review requirement for async tasks on any agent", () => {
1009
+ const tasks = [
1010
+ "Write the code",
1011
+ "Commit the changes",
1012
+ "Delete temporary data",
1013
+ "Remove obsolete assets",
1014
+ "Update dependencies",
1015
+ ];
1016
+ for (const task of tasks) {
1017
+ const resolved = resolveEffectiveAcceptance({ agentName: "builder", task, async: true });
1018
+ assert.equal(resolved.level, "checked", task);
1019
+ assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true, task);
1020
+ }
1021
+ });
1022
+
1023
+ it("explicit levels are honored over a read-only inference without silent escalation", () => {
1024
+ const resolved = resolveEffectiveAcceptance({
1025
+ agentName: "researcher",
1026
+ task: "Research PDF backends. Do not modify project/source files.",
1027
+ explicit: "checked",
1028
+ async: true,
1029
+ });
1030
+ assert.equal(resolved.level, "checked");
1031
+ });
1032
+
406
1033
  it("validates invalid disable and verify shapes", () => {
407
1034
  assert.deepEqual(validateAcceptanceInput({ level: "none" }), ["acceptance.reason is required when level is none."]);
1035
+ assert.deepEqual(validateAcceptanceInput("none"), ["acceptance level \"none\" requires a reason; use { level: \"none\", reason: \"...\" }."]);
408
1036
  assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "missing-command" }] }), ["acceptance.verify[0].command is required."]);
409
1037
  assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "fractional", command: "npm test", timeoutMs: 1.5 }] }), ["acceptance.verify[0].timeoutMs must be an integer >= 1."]);
410
1038
  assert.deepEqual(validateAcceptanceInput(false), []);
411
1039
  assert.deepEqual(validateAcceptanceInput("checked"), []);
1040
+ assert.match(validateAcceptanceInput("reviewed").join("\n"), /achieved status.*omit acceptance.*acceptance\.review\.required/i);
1041
+ assert.match(validateAcceptanceInput({ level: "reviewed" }).join("\n"), /achieved status.*omit acceptance.*acceptance\.review\.required/i);
412
1042
  assert.deepEqual(validateAcceptanceInput({ criteria: ["ship the fix"], review: false, stopRules: ["stay scoped"] }), []);
413
1043
  assert.match(validateAcceptanceInput({ criteria: [{ id: "missing-must" }] }).join("\n"), /acceptance\.criteria\[0\]\.must is required/);
1044
+ assert.match(validateAcceptanceInput({ criteria: [
1045
+ { id: "Release_Check", must: "first" },
1046
+ { id: "release check", must: "second" },
1047
+ ] }).join("\n"), /acceptance\.criteria\[1\]\.id duplicates normalized criterion id 'release-check'/);
414
1048
  assert.match(validateAcceptanceInput({ criteria: [123] }).join("\n"), /acceptance\.criteria\[0\] must be a string or an object/);
415
1049
  assert.match(validateAcceptanceInput({ evidence: ["bogus"] }).join("\n"), /acceptance\.evidence\[0\] is not a supported evidence kind/);
416
1050
  assert.match(validateAcceptanceInput({ review: true }).join("\n"), /acceptance\.review must be false or an object/);