@tacuchi/agent-workflow-cli 25.8.0 → 26.0.0

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 (558) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +6 -3
  3. package/dist/adapters/git-cli.js +225 -17
  4. package/dist/adapters/git-cli.js.map +1 -1
  5. package/dist/adapters/node-file-system.js +48 -0
  6. package/dist/adapters/node-file-system.js.map +1 -1
  7. package/dist/application/amend-service.js +51 -14
  8. package/dist/application/amend-service.js.map +1 -1
  9. package/dist/application/branch-resolver.js +6 -3
  10. package/dist/application/branch-resolver.js.map +1 -1
  11. package/dist/application/capability/design-handler.js +218 -43
  12. package/dist/application/capability/design-handler.js.map +1 -1
  13. package/dist/application/capability/dispatcher.js +182 -19
  14. package/dist/application/capability/dispatcher.js.map +1 -1
  15. package/dist/application/capability/durable-effect.js +3 -0
  16. package/dist/application/capability/durable-effect.js.map +1 -1
  17. package/dist/application/capability/readiness.js +8 -54
  18. package/dist/application/capability/readiness.js.map +1 -1
  19. package/dist/application/capability/resolution.js +124 -192
  20. package/dist/application/capability/resolution.js.map +1 -1
  21. package/dist/application/check-branch-service.js +158 -19
  22. package/dist/application/check-branch-service.js.map +1 -1
  23. package/dist/application/checkpoint/markdown.js +2 -2
  24. package/dist/application/checkpoint/markdown.js.map +1 -1
  25. package/dist/application/checkpoint/state-reader.js +12 -0
  26. package/dist/application/checkpoint/state-reader.js.map +1 -1
  27. package/dist/application/checkpoint-service.js +12 -4
  28. package/dist/application/checkpoint-service.js.map +1 -1
  29. package/dist/application/checkpoint-write-service.js +91 -17
  30. package/dist/application/checkpoint-write-service.js.map +1 -1
  31. package/dist/application/claims-ledger.js +137 -11
  32. package/dist/application/claims-ledger.js.map +1 -1
  33. package/dist/application/claims-recovery.js +85 -44
  34. package/dist/application/claims-recovery.js.map +1 -1
  35. package/dist/application/database-tool-catalog.js +33 -0
  36. package/dist/application/database-tool-catalog.js.map +1 -1
  37. package/dist/application/design/consumer-document.js +2 -2
  38. package/dist/application/design/consumer-document.js.map +1 -1
  39. package/dist/application/design/design-content-gate-service.js +20 -2
  40. package/dist/application/design/design-content-gate-service.js.map +1 -1
  41. package/dist/application/design/design-gate-service.js +2 -0
  42. package/dist/application/design/design-gate-service.js.map +1 -1
  43. package/dist/application/design/design-index-service.js +4 -0
  44. package/dist/application/design/design-index-service.js.map +1 -1
  45. package/dist/application/design/design-publish-service.js +1 -4
  46. package/dist/application/design/design-publish-service.js.map +1 -1
  47. package/dist/application/design/design-record-service.js +13 -7
  48. package/dist/application/design/design-record-service.js.map +1 -1
  49. package/dist/application/design/design-resolver-service.js +22 -3
  50. package/dist/application/design/design-resolver-service.js.map +1 -1
  51. package/dist/application/design/design-simple-service.js +24 -5
  52. package/dist/application/design/design-simple-service.js.map +1 -1
  53. package/dist/application/design/digest.js +41 -0
  54. package/dist/application/design/digest.js.map +1 -0
  55. package/dist/application/dev-only-services.js +166 -20
  56. package/dist/application/dev-only-services.js.map +1 -1
  57. package/dist/application/doc-branch-ledger.js +145 -0
  58. package/dist/application/doc-branch-ledger.js.map +1 -0
  59. package/dist/application/docs-canon-service.js +1 -1
  60. package/dist/application/docs-canon-service.js.map +1 -1
  61. package/dist/application/doctor/dist-integrity.js +113 -0
  62. package/dist/application/doctor/dist-integrity.js.map +1 -0
  63. package/dist/application/doctor/hosts.js +5 -2
  64. package/dist/application/doctor/hosts.js.map +1 -1
  65. package/dist/application/doctor/prepare.js +3 -3
  66. package/dist/application/doctor/prepare.js.map +1 -1
  67. package/dist/application/doctor/provider-installation.js +39 -19
  68. package/dist/application/doctor/provider-installation.js.map +1 -1
  69. package/dist/application/doctor/provider-mcps.js +9 -1
  70. package/dist/application/doctor/provider-mcps.js.map +1 -1
  71. package/dist/application/doctor/provider-skills.js +29 -115
  72. package/dist/application/doctor/provider-skills.js.map +1 -1
  73. package/dist/application/doctor/provider-visibility.js +76 -3
  74. package/dist/application/doctor/provider-visibility.js.map +1 -1
  75. package/dist/application/doctor/provider-workspace-block.js +156 -0
  76. package/dist/application/doctor/provider-workspace-block.js.map +1 -0
  77. package/dist/application/doctor/repair-runner.js +36 -39
  78. package/dist/application/doctor/repair-runner.js.map +1 -1
  79. package/dist/application/doctor/report.js +24 -4
  80. package/dist/application/doctor/report.js.map +1 -1
  81. package/dist/application/export-catalog-check.js +200 -0
  82. package/dist/application/export-catalog-check.js.map +1 -0
  83. package/dist/application/export-service.js +716 -79
  84. package/dist/application/export-service.js.map +1 -1
  85. package/dist/application/fix-git-service.js +285 -64
  86. package/dist/application/fix-git-service.js.map +1 -1
  87. package/dist/application/flow/advance.js +202 -25
  88. package/dist/application/flow/advance.js.map +1 -1
  89. package/dist/application/flow/annul-service.js +3 -1
  90. package/dist/application/flow/annul-service.js.map +1 -1
  91. package/dist/application/flow/batch-commit.js +189 -0
  92. package/dist/application/flow/batch-commit.js.map +1 -0
  93. package/dist/application/flow/checkout-observation.js +63 -10
  94. package/dist/application/flow/checkout-observation.js.map +1 -1
  95. package/dist/application/flow/close-artifacts.js +105 -0
  96. package/dist/application/flow/close-artifacts.js.map +1 -0
  97. package/dist/application/flow/close-at-boundary.js +25 -14
  98. package/dist/application/flow/close-at-boundary.js.map +1 -1
  99. package/dist/application/flow/close-document-guidance.js +70 -0
  100. package/dist/application/flow/close-document-guidance.js.map +1 -0
  101. package/dist/application/flow/flow-service.js +46 -3
  102. package/dist/application/flow/flow-service.js.map +1 -1
  103. package/dist/application/flow/internal-actions.js +300 -82
  104. package/dist/application/flow/internal-actions.js.map +1 -1
  105. package/dist/application/flow/internal-drive.js +24 -1
  106. package/dist/application/flow/internal-drive.js.map +1 -1
  107. package/dist/application/flow/plan-entry.js +82 -0
  108. package/dist/application/flow/plan-entry.js.map +1 -0
  109. package/dist/application/flow/prove.js +32 -1
  110. package/dist/application/flow/prove.js.map +1 -1
  111. package/dist/application/flow/quick-checkouts.js +48 -0
  112. package/dist/application/flow/quick-checkouts.js.map +1 -0
  113. package/dist/application/flow/reinfer-batch.js +177 -0
  114. package/dist/application/flow/reinfer-batch.js.map +1 -0
  115. package/dist/application/flow/reopen-run.js +12 -5
  116. package/dist/application/flow/reopen-run.js.map +1 -1
  117. package/dist/application/flow/run-state-service.js +2 -0
  118. package/dist/application/flow/run-state-service.js.map +1 -1
  119. package/dist/application/flow/submit.js +584 -117
  120. package/dist/application/flow/submit.js.map +1 -1
  121. package/dist/application/generate-launch-service.js +16 -6
  122. package/dist/application/generate-launch-service.js.map +1 -1
  123. package/dist/application/git-flow-service.js +34 -3
  124. package/dist/application/git-flow-service.js.map +1 -1
  125. package/dist/application/history-publications.js +24 -0
  126. package/dist/application/history-publications.js.map +1 -1
  127. package/dist/application/history-reconcile-service.js +45 -0
  128. package/dist/application/history-reconcile-service.js.map +1 -0
  129. package/dist/application/history-table.js +61 -12
  130. package/dist/application/history-table.js.map +1 -1
  131. package/dist/application/history-update-service.js +25 -4
  132. package/dist/application/history-update-service.js.map +1 -1
  133. package/dist/application/hook-branch-check.js +25 -3
  134. package/dist/application/hook-branch-check.js.map +1 -1
  135. package/dist/application/hook-git-commit-advisor.js +3 -4
  136. package/dist/application/hook-git-commit-advisor.js.map +1 -1
  137. package/dist/application/hook-turn-start.js +51 -0
  138. package/dist/application/hook-turn-start.js.map +1 -0
  139. package/dist/application/host-memory/codex.js +1 -1
  140. package/dist/application/host-memory/codex.js.map +1 -1
  141. package/dist/application/hub-registry.js +135 -0
  142. package/dist/application/hub-registry.js.map +1 -0
  143. package/dist/application/local-proposal.js +72 -19
  144. package/dist/application/local-proposal.js.map +1 -1
  145. package/dist/application/mcp-host-reader.js +1 -1
  146. package/dist/application/mcp-host-reader.js.map +1 -1
  147. package/dist/application/mcp-host-writer.js +16 -10
  148. package/dist/application/mcp-host-writer.js.map +1 -1
  149. package/dist/application/merge-state-service.js +150 -34
  150. package/dist/application/merge-state-service.js.map +1 -1
  151. package/dist/application/multiroot/claude.js +17 -10
  152. package/dist/application/multiroot/claude.js.map +1 -1
  153. package/dist/application/multiroot/codex.js +17 -11
  154. package/dist/application/multiroot/codex.js.map +1 -1
  155. package/dist/application/multiroot-service.js +52 -28
  156. package/dist/application/multiroot-service.js.map +1 -1
  157. package/dist/application/parsers/execution-batches.js +64 -0
  158. package/dist/application/parsers/execution-batches.js.map +1 -0
  159. package/dist/application/parsers/phases.js +23 -2
  160. package/dist/application/parsers/phases.js.map +1 -1
  161. package/dist/application/parsers/project-block.js +146 -17
  162. package/dist/application/parsers/project-block.js.map +1 -1
  163. package/dist/application/parsers/tasks.js +16 -2
  164. package/dist/application/parsers/tasks.js.map +1 -1
  165. package/dist/application/parsers/toml.js +19 -0
  166. package/dist/application/parsers/toml.js.map +1 -0
  167. package/dist/application/paths-service.js +13 -16
  168. package/dist/application/paths-service.js.map +1 -1
  169. package/dist/application/persist-service.js +118 -5
  170. package/dist/application/persist-service.js.map +1 -1
  171. package/dist/application/plan-exec-batch-service.js +162 -22
  172. package/dist/application/plan-exec-batch-service.js.map +1 -1
  173. package/dist/application/plan-exec-decision-service.js +61 -12
  174. package/dist/application/plan-exec-decision-service.js.map +1 -1
  175. package/dist/application/plan-exec-plan-diff.js +105 -0
  176. package/dist/application/plan-exec-plan-diff.js.map +1 -0
  177. package/dist/application/plan-lint-service.js +33 -5
  178. package/dist/application/plan-lint-service.js.map +1 -1
  179. package/dist/application/plan-passes.js +41 -0
  180. package/dist/application/plan-passes.js.map +1 -0
  181. package/dist/application/preparation-receipts.js +58 -0
  182. package/dist/application/preparation-receipts.js.map +1 -0
  183. package/dist/application/project-md-upsert-service.js +191 -52
  184. package/dist/application/project-md-upsert-service.js.map +1 -1
  185. package/dist/application/project-tab-data.js +40 -10
  186. package/dist/application/project-tab-data.js.map +1 -1
  187. package/dist/application/release-data/artifacts.js +15 -7
  188. package/dist/application/release-data/artifacts.js.map +1 -1
  189. package/dist/application/release-data/bundles.js +52 -6
  190. package/dist/application/release-data/bundles.js.map +1 -1
  191. package/dist/application/release-data/common.js +2 -2
  192. package/dist/application/release-data/common.js.map +1 -1
  193. package/dist/application/release-data/rollback.js +7 -0
  194. package/dist/application/release-data/rollback.js.map +1 -0
  195. package/dist/application/release-data/sessions.js +2 -2
  196. package/dist/application/release-data/sessions.js.map +1 -1
  197. package/dist/application/release-pass-ledger.js +1 -1
  198. package/dist/application/render/history-row.js +2 -0
  199. package/dist/application/render/history-row.js.map +1 -1
  200. package/dist/application/render/project-block.js +28 -11
  201. package/dist/application/render/project-block.js.map +1 -1
  202. package/dist/application/reseal-service.js +1 -0
  203. package/dist/application/reseal-service.js.map +1 -1
  204. package/dist/application/resume-service.js +55 -8
  205. package/dist/application/resume-service.js.map +1 -1
  206. package/dist/application/retirement/apply.js +25 -7
  207. package/dist/application/retirement/apply.js.map +1 -1
  208. package/dist/application/retirement/attribution.js +122 -50
  209. package/dist/application/retirement/attribution.js.map +1 -1
  210. package/dist/application/retirement/prepare.js +17 -11
  211. package/dist/application/retirement/prepare.js.map +1 -1
  212. package/dist/application/retirement/preview.js +2 -1
  213. package/dist/application/retirement/preview.js.map +1 -1
  214. package/dist/application/self/clean-legacy.js +1 -2
  215. package/dist/application/self/clean-legacy.js.map +1 -1
  216. package/dist/application/self/codex-plugin.js +1 -1
  217. package/dist/application/self/codex-plugin.js.map +1 -1
  218. package/dist/application/self/host-states.js +3 -3
  219. package/dist/application/self/host-states.js.map +1 -1
  220. package/dist/application/self/install-hooks.js +1 -1
  221. package/dist/application/self/install-hooks.js.map +1 -1
  222. package/dist/application/self/install-skill.js +5 -49
  223. package/dist/application/self/install-skill.js.map +1 -1
  224. package/dist/application/self/mcp-config.js +19 -11
  225. package/dist/application/self/mcp-config.js.map +1 -1
  226. package/dist/application/self/opencode-plugin.js +1 -0
  227. package/dist/application/self/opencode-plugin.js.map +1 -1
  228. package/dist/application/self/skill-files.js +29 -0
  229. package/dist/application/self/skill-files.js.map +1 -0
  230. package/dist/application/self/uninstall.js +1 -1
  231. package/dist/application/self/uninstall.js.map +1 -1
  232. package/dist/application/semantic-operation/protocol.js +66 -5
  233. package/dist/application/semantic-operation/protocol.js.map +1 -1
  234. package/dist/application/session-archive-service.js +113 -0
  235. package/dist/application/session-archive-service.js.map +1 -0
  236. package/dist/application/session-binding-service.js +35 -0
  237. package/dist/application/session-binding-service.js.map +1 -1
  238. package/dist/application/session-close-service.js +390 -54
  239. package/dist/application/session-close-service.js.map +1 -1
  240. package/dist/application/session-create-service.js +97 -5
  241. package/dist/application/session-create-service.js.map +1 -1
  242. package/dist/application/session-custody-recorder.js +15 -12
  243. package/dist/application/session-custody-recorder.js.map +1 -1
  244. package/dist/application/session-custody-service.js +1 -0
  245. package/dist/application/session-custody-service.js.map +1 -1
  246. package/dist/application/session-narrative.js +42 -12
  247. package/dist/application/session-narrative.js.map +1 -1
  248. package/dist/application/session-pause-service.js +46 -0
  249. package/dist/application/session-pause-service.js.map +1 -0
  250. package/dist/application/session-resolver.js +36 -11
  251. package/dist/application/session-resolver.js.map +1 -1
  252. package/dist/application/session-resume-service.js +134 -13
  253. package/dist/application/session-resume-service.js.map +1 -1
  254. package/dist/application/sessions-service.js +4 -0
  255. package/dist/application/sessions-service.js.map +1 -1
  256. package/dist/application/sessions-sweep-service.js +77 -0
  257. package/dist/application/sessions-sweep-service.js.map +1 -0
  258. package/dist/application/settle-service.js +3 -1
  259. package/dist/application/settle-service.js.map +1 -1
  260. package/dist/application/skills-resolver-service.js +6 -92
  261. package/dist/application/skills-resolver-service.js.map +1 -1
  262. package/dist/application/source-add-service.js +37 -0
  263. package/dist/application/source-add-service.js.map +1 -0
  264. package/dist/application/source-boundary-policy.js +105 -8
  265. package/dist/application/source-boundary-policy.js.map +1 -1
  266. package/dist/application/source-pipeline.js +68 -0
  267. package/dist/application/source-pipeline.js.map +1 -0
  268. package/dist/application/source-remove-service.js +15 -3
  269. package/dist/application/source-remove-service.js.map +1 -1
  270. package/dist/application/sources-service.js +71 -16
  271. package/dist/application/sources-service.js.map +1 -1
  272. package/dist/application/status-service.js +55 -5
  273. package/dist/application/status-service.js.map +1 -1
  274. package/dist/application/unit-dependencies.js +100 -0
  275. package/dist/application/unit-dependencies.js.map +1 -0
  276. package/dist/application/unit-membership.js +37 -0
  277. package/dist/application/unit-membership.js.map +1 -0
  278. package/dist/application/unit-residue.js +72 -0
  279. package/dist/application/unit-residue.js.map +1 -0
  280. package/dist/application/visibility-doctor-service.js +66 -4
  281. package/dist/application/visibility-doctor-service.js.map +1 -1
  282. package/dist/application/working-branch-service.js +6 -0
  283. package/dist/application/working-branch-service.js.map +1 -1
  284. package/dist/application/workline-graph.js +1 -1
  285. package/dist/application/workline-graph.js.map +1 -1
  286. package/dist/application/workline-index-service.js +118 -17
  287. package/dist/application/workline-index-service.js.map +1 -1
  288. package/dist/application/workspace-commit-service.js +218 -0
  289. package/dist/application/workspace-commit-service.js.map +1 -0
  290. package/dist/application/workspace-init-service.js +64 -34
  291. package/dist/application/workspace-init-service.js.map +1 -1
  292. package/dist/application/workspace-local-config.js +90 -0
  293. package/dist/application/workspace-local-config.js.map +1 -0
  294. package/dist/application/workspace-materialization-service.js +52 -3
  295. package/dist/application/workspace-materialization-service.js.map +1 -1
  296. package/dist/application/workspace-migrate/apply.js +525 -4
  297. package/dist/application/workspace-migrate/apply.js.map +1 -1
  298. package/dist/application/workspace-migrate/markers.js +9 -6
  299. package/dist/application/workspace-migrate/markers.js.map +1 -1
  300. package/dist/application/workspace-migrate/plan.js +97 -5
  301. package/dist/application/workspace-migrate/plan.js.map +1 -1
  302. package/dist/application/workspace-move-service.js +270 -0
  303. package/dist/application/workspace-move-service.js.map +1 -0
  304. package/dist/application/workspace-untrack-service.js +28 -0
  305. package/dist/application/workspace-untrack-service.js.map +1 -0
  306. package/dist/application/worktree-service.js +654 -105
  307. package/dist/application/worktree-service.js.map +1 -1
  308. package/dist/cli/ascii.js +2 -0
  309. package/dist/cli/ascii.js.map +1 -1
  310. package/dist/cli/commands/add-source.js +38 -0
  311. package/dist/cli/commands/add-source.js.map +1 -0
  312. package/dist/cli/commands/amend.js +14 -3
  313. package/dist/cli/commands/amend.js.map +1 -1
  314. package/dist/cli/commands/capability.js +23 -4
  315. package/dist/cli/commands/capability.js.map +1 -1
  316. package/dist/cli/commands/designs.js +14 -5
  317. package/dist/cli/commands/designs.js.map +1 -1
  318. package/dist/cli/commands/dev-only.js +19 -6
  319. package/dist/cli/commands/dev-only.js.map +1 -1
  320. package/dist/cli/commands/doc-branch.js +184 -0
  321. package/dist/cli/commands/doc-branch.js.map +1 -0
  322. package/dist/cli/commands/doctor.js +5 -1
  323. package/dist/cli/commands/doctor.js.map +1 -1
  324. package/dist/cli/commands/export.js +78 -18
  325. package/dist/cli/commands/export.js.map +1 -1
  326. package/dist/cli/commands/fix-git.js +89 -14
  327. package/dist/cli/commands/fix-git.js.map +1 -1
  328. package/dist/cli/commands/flow.js +120 -15
  329. package/dist/cli/commands/flow.js.map +1 -1
  330. package/dist/cli/commands/generate-launch.js +14 -2
  331. package/dist/cli/commands/generate-launch.js.map +1 -1
  332. package/dist/cli/commands/history-update.js +1 -1
  333. package/dist/cli/commands/history-update.js.map +1 -1
  334. package/dist/cli/commands/history.js +14 -0
  335. package/dist/cli/commands/history.js.map +1 -0
  336. package/dist/cli/commands/hook.js +15 -2
  337. package/dist/cli/commands/hook.js.map +1 -1
  338. package/dist/cli/commands/index.js +18 -5
  339. package/dist/cli/commands/index.js.map +1 -1
  340. package/dist/cli/commands/merge-state.js +2 -2
  341. package/dist/cli/commands/merge-state.js.map +1 -1
  342. package/dist/cli/commands/persist.js +6 -5
  343. package/dist/cli/commands/persist.js.map +1 -1
  344. package/dist/cli/commands/project-md-upsert.js +5 -3
  345. package/dist/cli/commands/project-md-upsert.js.map +1 -1
  346. package/dist/cli/commands/release-pass.js +3 -3
  347. package/dist/cli/commands/release-pass.js.map +1 -1
  348. package/dist/cli/commands/resume.js +12 -3
  349. package/dist/cli/commands/resume.js.map +1 -1
  350. package/dist/cli/commands/self.js +8 -24
  351. package/dist/cli/commands/self.js.map +1 -1
  352. package/dist/cli/commands/session-close.js +64 -9
  353. package/dist/cli/commands/session-close.js.map +1 -1
  354. package/dist/cli/commands/session-create.js +4 -2
  355. package/dist/cli/commands/session-create.js.map +1 -1
  356. package/dist/cli/commands/session-pause.js +14 -0
  357. package/dist/cli/commands/session-pause.js.map +1 -0
  358. package/dist/cli/commands/sessions.js +16 -3
  359. package/dist/cli/commands/sessions.js.map +1 -1
  360. package/dist/cli/commands/set-branch.js +28 -4
  361. package/dist/cli/commands/set-branch.js.map +1 -1
  362. package/dist/cli/commands/set-edit-mode.js +20 -0
  363. package/dist/cli/commands/set-edit-mode.js.map +1 -0
  364. package/dist/cli/commands/set-pipeline.js +35 -0
  365. package/dist/cli/commands/set-pipeline.js.map +1 -0
  366. package/dist/cli/commands/skills.js +4 -11
  367. package/dist/cli/commands/skills.js.map +1 -1
  368. package/dist/cli/commands/sources.js +4 -0
  369. package/dist/cli/commands/sources.js.map +1 -1
  370. package/dist/cli/commands/status.js +35 -2
  371. package/dist/cli/commands/status.js.map +1 -1
  372. package/dist/cli/commands/unknown-flags.js +1 -0
  373. package/dist/cli/commands/unknown-flags.js.map +1 -1
  374. package/dist/cli/commands/visibility.js +1 -0
  375. package/dist/cli/commands/visibility.js.map +1 -1
  376. package/dist/cli/commands/workspace-commit.js +62 -0
  377. package/dist/cli/commands/workspace-commit.js.map +1 -0
  378. package/dist/cli/commands/workspace-init.js +39 -2
  379. package/dist/cli/commands/workspace-init.js.map +1 -1
  380. package/dist/cli/commands/workspace-migrate.js +31 -3
  381. package/dist/cli/commands/workspace-migrate.js.map +1 -1
  382. package/dist/cli/commands/workspace-move.js +42 -0
  383. package/dist/cli/commands/workspace-move.js.map +1 -0
  384. package/dist/cli/commands/worktree.js +4 -4
  385. package/dist/cli/commands/worktree.js.map +1 -1
  386. package/dist/cli/context-id.js +6 -0
  387. package/dist/cli/context-id.js.map +1 -1
  388. package/dist/cli/full-cli.js +555 -0
  389. package/dist/cli/full-cli.js.map +1 -0
  390. package/dist/cli/help-groups.js +76 -19
  391. package/dist/cli/help-groups.js.map +1 -1
  392. package/dist/cli/hook-stdin-cache.js +12 -0
  393. package/dist/cli/hook-stdin-cache.js.map +1 -0
  394. package/dist/cli/main.js +47 -453
  395. package/dist/cli/main.js.map +1 -1
  396. package/dist/cli/parser.js +4 -0
  397. package/dist/cli/parser.js.map +1 -1
  398. package/dist/cli/tui/app.js +3 -4
  399. package/dist/cli/tui/app.js.map +1 -1
  400. package/dist/cli/tui/components/host-admin-section.js +2 -13
  401. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  402. package/dist/cli/tui/components/tabs-config.js +1 -2
  403. package/dist/cli/tui/components/tabs-config.js.map +1 -1
  404. package/dist/cli/tui/components/workspace-init-form.js +4 -1
  405. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  406. package/dist/cli/tui/data/workflow-content.js +6 -1
  407. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  408. package/dist/cli/tui/tabs/mcp-tab.js +4 -3
  409. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  410. package/dist/cli/tui/tabs/project-tab.js +7 -2
  411. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  412. package/dist/cli/tui/tui-prefs.js +2 -0
  413. package/dist/cli/tui/tui-prefs.js.map +1 -1
  414. package/dist/dist-manifest.json +3973 -0
  415. package/dist/domain/capability/protocol.js +3 -1
  416. package/dist/domain/capability/protocol.js.map +1 -1
  417. package/dist/domain/decision-note.js +13 -0
  418. package/dist/domain/decision-note.js.map +1 -1
  419. package/dist/domain/design/artifact.js +22 -3
  420. package/dist/domain/design/artifact.js.map +1 -1
  421. package/dist/domain/design/capability.js +1 -0
  422. package/dist/domain/design/capability.js.map +1 -1
  423. package/dist/domain/design/expansion.js +1 -1
  424. package/dist/domain/design/expansion.js.map +1 -1
  425. package/dist/domain/design/maturity.js +16 -0
  426. package/dist/domain/design/maturity.js.map +1 -1
  427. package/dist/domain/design/reference.js +5 -0
  428. package/dist/domain/design/reference.js.map +1 -1
  429. package/dist/domain/doctor/operations.js +20 -12
  430. package/dist/domain/doctor/operations.js.map +1 -1
  431. package/dist/domain/effective-contract.js +3 -1
  432. package/dist/domain/effective-contract.js.map +1 -1
  433. package/dist/domain/elicitation.js +1 -1
  434. package/dist/domain/flow/answer.js +265 -130
  435. package/dist/domain/flow/answer.js.map +1 -1
  436. package/dist/domain/flow/authority.js +194 -37
  437. package/dist/domain/flow/authority.js.map +1 -1
  438. package/dist/domain/flow/batch-review.js +65 -0
  439. package/dist/domain/flow/batch-review.js.map +1 -0
  440. package/dist/domain/flow/decision-schema.js +47 -0
  441. package/dist/domain/flow/decision-schema.js.map +1 -0
  442. package/dist/domain/flow/directive.js +16 -0
  443. package/dist/domain/flow/directive.js.map +1 -1
  444. package/dist/domain/flow/execution-result.js +91 -2
  445. package/dist/domain/flow/execution-result.js.map +1 -1
  446. package/dist/domain/flow/run-state.js +352 -24
  447. package/dist/domain/flow/run-state.js.map +1 -1
  448. package/dist/domain/flow/test-run-evidence.js +285 -0
  449. package/dist/domain/flow/test-run-evidence.js.map +1 -0
  450. package/dist/domain/flow/unchanged-phase.js +15 -0
  451. package/dist/domain/flow/unchanged-phase.js.map +1 -0
  452. package/dist/domain/harnesses.js +14 -5
  453. package/dist/domain/harnesses.js.map +1 -1
  454. package/dist/domain/isolation-unit.js +2 -2
  455. package/dist/domain/isolation-unit.js.map +1 -1
  456. package/dist/domain/lineage.js +7 -2
  457. package/dist/domain/lineage.js.map +1 -1
  458. package/dist/domain/obligation-correspondence.js +4 -2
  459. package/dist/domain/obligation-correspondence.js.map +1 -1
  460. package/dist/domain/plan-handoff.js +5 -0
  461. package/dist/domain/plan-handoff.js.map +1 -0
  462. package/dist/domain/proposal.js +18 -7
  463. package/dist/domain/proposal.js.map +1 -1
  464. package/dist/domain/reservation.js +5 -3
  465. package/dist/domain/reservation.js.map +1 -1
  466. package/dist/domain/retirement/proposal.js +1 -0
  467. package/dist/domain/retirement/proposal.js.map +1 -1
  468. package/dist/domain/session/custody.js +10 -6
  469. package/dist/domain/session/custody.js.map +1 -1
  470. package/dist/domain/session/narrative.js +4 -1
  471. package/dist/domain/session/narrative.js.map +1 -1
  472. package/dist/domain/skills.js +4 -6
  473. package/dist/domain/skills.js.map +1 -1
  474. package/dist/domain/sql-lexer.js +1 -1
  475. package/dist/domain/sql-lexer.js.map +1 -1
  476. package/dist/runtime/namespace-resolver.js +4 -0
  477. package/dist/runtime/namespace-resolver.js.map +1 -1
  478. package/dist/runtime/workline-marker.js +35 -8
  479. package/dist/runtime/workline-marker.js.map +1 -1
  480. package/dist/runtime/workspace-resolution.js +123 -0
  481. package/dist/runtime/workspace-resolution.js.map +1 -0
  482. package/package.json +6 -5
  483. package/skills/w/SKILL.md +12 -17
  484. package/skills/w/artifacts/artifacts-core/CHECKPOINT.md +1 -1
  485. package/skills/w/artifacts/artifacts-core/SCRIPTS.rollback.sql +8 -0
  486. package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +4 -8
  487. package/skills/w/commands/export-diagrams.md +3 -3
  488. package/skills/w/commands/export-manuals.md +6 -6
  489. package/skills/w/commands/export-reports.md +3 -3
  490. package/skills/w/commands/export-scripts.md +5 -5
  491. package/skills/w/commands/fix-git.md +12 -13
  492. package/skills/w/commands/persist.md +2 -2
  493. package/skills/w/commands/plan-exec.md +4 -2
  494. package/skills/w/commands/spec-new.md +1 -1
  495. package/skills/w/commands/workspace-init.md +2 -0
  496. package/skills/w/context/MANIFEST.json +1 -1
  497. package/skills/w/exports/README.md +4 -4
  498. package/skills/w/exports/export-diagrams/EXPORT.md +4 -4
  499. package/skills/w/exports/export-manuals/EXPORT.md +14 -16
  500. package/skills/w/exports/export-reports/EXPORT.md +3 -3
  501. package/skills/w/exports/export-scripts/EXPORT.md +46 -22
  502. package/skills/w/harness/HARNESS.md +4 -7
  503. package/skills/w/hooks/hooks.template.json +6 -4
  504. package/skills/w/loops/CHASSIS.md +12 -6
  505. package/skills/w/loops/CODE-POLICIES.md +8 -5
  506. package/skills/w/loops/README.md +2 -2
  507. package/skills/w/loops/plan-exec-loop/LOOP.md +23 -22
  508. package/skills/w/loops/plan-new-loop/LOOP.md +23 -31
  509. package/skills/w/loops/plan-refine-loop/LOOP.md +8 -2
  510. package/skills/w/loops/quick-loop/LOOP.md +7 -6
  511. package/skills/w/loops/spec-refine-loop/LOOP.md +1 -1
  512. package/skills/w/modules/DESIGN-REFERENCES.md +9 -11
  513. package/skills/w/modules/PLAN-EXECUTION-BATCHES.md +4 -4
  514. package/skills/w/modules/SESSION-NUMBERING.md +2 -0
  515. package/skills/w/roles/README.md +11 -47
  516. package/skills/w/roles/design/CONTRACT.md +6 -4
  517. package/skills/w/roles/design/ROLE.md +7 -3
  518. package/skills/w/roles/diagrams/ROLE.md +1 -1
  519. package/skills/w/roles/git/ROLE.md +17 -10
  520. package/skills/w/roles/research/ROLE.md +2 -2
  521. package/skills/w/roles/sql/ROLE.md +17 -15
  522. package/skills/w/schemas/design/ui-screen.v1.schema.json +9 -0
  523. package/dist/application/capability/descriptor-loader.js +0 -109
  524. package/dist/application/capability/descriptor-loader.js.map +0 -1
  525. package/dist/application/capability/installed-inventory.js +0 -168
  526. package/dist/application/capability/installed-inventory.js.map +0 -1
  527. package/dist/application/self/install-plugin-skills-git.js +0 -226
  528. package/dist/application/self/install-plugin-skills-git.js.map +0 -1
  529. package/dist/application/self/install-plugin-skills.js +0 -203
  530. package/dist/application/self/install-plugin-skills.js.map +0 -1
  531. package/dist/application/self/plugin-cache-clear.js +0 -133
  532. package/dist/application/self/plugin-cache-clear.js.map +0 -1
  533. package/dist/application/self/plugin-cache-reload.js +0 -141
  534. package/dist/application/self/plugin-cache-reload.js.map +0 -1
  535. package/dist/application/self/skills-apply.js +0 -623
  536. package/dist/application/self/skills-apply.js.map +0 -1
  537. package/dist/application/self/skills-catalog.js +0 -35
  538. package/dist/application/self/skills-catalog.js.map +0 -1
  539. package/dist/application/self/skills-change.js +0 -479
  540. package/dist/application/self/skills-change.js.map +0 -1
  541. package/dist/application/self/skills-discovery.js +0 -275
  542. package/dist/application/self/skills-discovery.js.map +0 -1
  543. package/dist/application/self/skills-manager.js +0 -253
  544. package/dist/application/self/skills-manager.js.map +0 -1
  545. package/dist/application/self/skills-payload.js +0 -566
  546. package/dist/application/self/skills-payload.js.map +0 -1
  547. package/dist/application/self/skills-registry.js +0 -107
  548. package/dist/application/self/skills-registry.js.map +0 -1
  549. package/dist/application/skill-index-service.js +0 -78
  550. package/dist/application/skill-index-service.js.map +0 -1
  551. package/dist/cli/commands/plugin-cache.js +0 -25
  552. package/dist/cli/commands/plugin-cache.js.map +0 -1
  553. package/dist/cli/commands/skill-index.js +0 -19
  554. package/dist/cli/commands/skill-index.js.map +0 -1
  555. package/dist/cli/tui/data/recommended-skills.js +0 -482
  556. package/dist/cli/tui/data/recommended-skills.js.map +0 -1
  557. package/dist/cli/tui/tabs/skills-tab.js +0 -664
  558. package/dist/cli/tui/tabs/skills-tab.js.map +0 -1
@@ -27,30 +27,86 @@ import { DEFAULT_CORE_DOCS_CANON, coreDocumentKindForPath } from "../../domain/d
27
27
  import { claimedSeal, parseFlowAnswer, spendsAttempt, } from "../../domain/flow/answer.js";
28
28
  import { FIX_PREVIEW_TRANSITION, SETTLEMENT_READINGS, actionOf, alternativesOf, approvalGrantOf, effectsOf, internalActionOf, isRouteEvaluation, occurrenceAt, proposalContractOf, publishApprovalOf, reentryOf, routeControlOf, scopesSources, } from "../../domain/flow/authority.js";
29
29
  import { effectApprovalDigest } from "../../domain/flow/authorization.js";
30
+ import { isBatchReview } from "../../domain/flow/batch-review.js";
31
+ import { routeShape, settlementList, sourceList } from "../../domain/flow/decision-schema.js";
30
32
  import { PAUSE_LABEL, STOP_LABEL, stepOf, } from "../../domain/flow/directive.js";
31
33
  import { batchCreditVerdict, executionVerdict, } from "../../domain/flow/execution-result.js";
32
34
  import { ROUTE_ACCEPT_LABEL, ROUTE_ADJUST_LABEL, isRouteDisposition, } from "../../domain/flow/route.js";
33
- import { applyTransition, checkAgainstJourney, iterationOf, restartInvocation, restatesLastEvent, sameIteration, settlementAmbiguous, withApproval, withAttempt, withBoundary, withContinuation, withDecisionPreparation, withEvent, withFixPreview, withHandoff, withObservation, withPlanExecBatchCredit, withPlanExecBatchStageForTransition, withProposal, withReentry, withRouteDecisions, withRouteProposal, withScope, withSelectedChoice, withSettlementDeclarations, } from "../../domain/flow/run-state.js";
35
+ import { applyTransition, checkAgainstJourney, iterationOf, planRefineHandoff, restartInvocation, restatesLastEvent, sameIteration, settlementAmbiguous, withApproval, withAttempt, withBoundary, withContinuation, withDecisionDate, withDecisionPreparation, withEvent, withFixPreview, withHandoff, withObservation, withPlanExecBatchCredit, withPlanExecBatchReview, withPlanExecBatchStageForTransition, withPlanExecBatchUpdate, withPlanExecEntry, withProposal, withQuickCheckouts, withReentry, withRouteDecisions, withRouteProposal, withScope, withSelectedChoice, withSettlementDeclarations, } from "../../domain/flow/run-state.js";
36
+ import { APPROVE_UNCHANGED_PHASE, UNCHANGED_PHASE_CONSENT, approvedValidationOnly, } from "../../domain/flow/unchanged-phase.js";
34
37
  import { withSpecBaseline } from "../../domain/lineage.js";
35
38
  import { destinationsOf, observedEffects, sealProposal } from "../../domain/proposal.js";
36
39
  import { baseDigest } from "../../domain/proposal.js";
37
40
  import { reservationMarker } from "../../domain/reservation.js";
38
41
  import { checkSafeRelativePath } from "../../domain/safe-path.js";
39
42
  import { resolveCoreDocsCanon } from "../docs-canon-service.js";
43
+ import { parseMdSectionBilingual } from "../markdown.js";
44
+ import { parsePhases } from "../parsers/phases.js";
40
45
  import { readWorkspaceBlock } from "../parsers/project-block.js";
41
46
  import { parseDerivedFromPath, parseSpecRelation } from "../parsers/spec-relation.js";
47
+ import { parseTasks } from "../parsers/tasks.js";
42
48
  import { resolveWorkspaceRootFrom } from "../paths-service.js";
43
49
  import { commitStoredPlanExecDecision, preparePlanExecDecision, } from "../plan-exec-decision-service.js";
44
50
  import { planBoundaryAction, planGrammarAtEntry, planGrammarAtPublication, } from "../plan-lint-service.js";
45
51
  import { semanticDigest } from "../semantic-operation/protocol.js";
46
52
  import { resolveSessionTarget } from "../session-resolver.js";
47
53
  import { sourceAliasesOfPlan, } from "../source-boundary-policy.js";
54
+ import { readSourcePipelines, resolveFinalValidation } from "../source-pipeline.js";
55
+ import { runWorkspaceCommit } from "../workspace-commit-service.js";
48
56
  import { actionDigest, advanceFlowRun, awaitingCliRerun, directiveFor, effectsOfTransition, resolveBoundary, } from "./advance.js";
49
57
  import { observeCheckout, observeScopedFingerprints, withObservedCheckouts, } from "./checkout-observation.js";
50
58
  import { resolveCheckoutCandidates } from "./checkout-observation.js";
59
+ import { closeAtBoundaryState } from "./close-at-boundary.js";
51
60
  import { driveInternalActions } from "./internal-drive.js";
61
+ import { observePlanEntry } from "./plan-entry.js";
62
+ import { observeQuickCheckouts } from "./quick-checkouts.js";
52
63
  import { journeyForRun } from "./run-journey.js";
53
64
  import { applyUnderLock, locateRun, readRun } from "./run-state-service.js";
65
+ /** The same decision against the current boundary, without a run lock or writes. */
66
+ export async function checkFlow(fs, paths, input) {
67
+ const canon = await resolveCoreDocsCanon(fs, paths);
68
+ if (!canon.ok)
69
+ return {
70
+ ok: false,
71
+ failure: { code: "DOCS_CANON_INVALID", message: canon.error, action: "corregí [docs]" },
72
+ };
73
+ const resolution = await resolveSessionTarget(fs, paths, {
74
+ intent: "read",
75
+ ...(input.code !== undefined ? { code: input.code } : {}),
76
+ ...(input.contextId !== undefined ? { contextId: input.contextId } : {}),
77
+ allowClosed: false,
78
+ bind: false,
79
+ });
80
+ if (resolution.outcome !== "resolved")
81
+ return { ok: false, session: resolution };
82
+ const session = resolution.session.folder;
83
+ const read = await readRun(fs, locateRun(paths, session));
84
+ if (!read.ok)
85
+ return { ok: false, failure: read.failure };
86
+ const snapshot = await observe(fs, paths, input.raw, session, input.git);
87
+ const decision = await decide(fs, paths, read.state, input, snapshot, true);
88
+ if (!decision.ok)
89
+ return { ok: false, failure: decision.failure };
90
+ const error = decision.value.directive.error;
91
+ return {
92
+ ok: true,
93
+ receipt: {
94
+ check: true,
95
+ valid: error === null,
96
+ error,
97
+ violations: error?.violations ??
98
+ (error === null
99
+ ? []
100
+ : [
101
+ {
102
+ code: error.code,
103
+ field: "answer",
104
+ message: error.message,
105
+ },
106
+ ]),
107
+ },
108
+ };
109
+ }
54
110
  export async function submitFlow(fs, paths, input) {
55
111
  const canon = await resolveCoreDocsCanon(fs, paths);
56
112
  if (!canon.ok) {
@@ -112,7 +168,7 @@ export async function submitFlow(fs, paths, input) {
112
168
  ok: true,
113
169
  state: applied.state,
114
170
  value: applied.value.directive,
115
- });
171
+ }, paths);
116
172
  if (!driven.ok)
117
173
  return { ok: false, failure: driven.failure };
118
174
  return { ok: true, directive: withObservedCheckouts(driven.value, observed) };
@@ -120,13 +176,18 @@ export async function submitFlow(fs, paths, input) {
120
176
  async function observe(fs, paths, raw, session, git) {
121
177
  const root = await resolveWorkspaceRootFrom(fs, paths);
122
178
  const plans = await observePlanArtifacts(fs, paths, raw);
179
+ const checkouts = await observeCheckouts(fs, paths, session, git);
123
180
  return {
124
181
  root,
182
+ quickCheckouts: await observeQuickCheckouts(fs, paths, session, git),
125
183
  destinations: await observeDestinations(fs, paths, raw),
126
184
  scope: await observeScope(fs, paths, raw),
127
185
  plans: plans.evidence,
128
- checkouts: await observeCheckouts(fs, paths, session, git),
186
+ checkouts: checkouts.states,
187
+ checkoutFailure: checkouts.failure,
129
188
  scoped: await observeBatchSources(fs, paths, session, git),
189
+ preexisting: await observePreexistingFailures(fs, paths, session, root),
190
+ validation_only_current: await observeValidationOnly(fs, paths, session, root),
130
191
  baselines: plans.baselines,
131
192
  };
132
193
  }
@@ -150,6 +211,43 @@ async function observeBatchSources(fs, paths, session, git) {
150
211
  return observed.ok ? observed.base : {};
151
212
  }
152
213
  const PHASE_VALIDATION = "plan-exec.validation-execution";
214
+ async function observeValidationOnly(fs, paths, session, root) {
215
+ const read = await readRun(fs, locateRun(paths, session));
216
+ if (!read.ok || read.state.boundary !== PHASE_VALIDATION || read.state.scope === null)
217
+ return false;
218
+ const batch = read.state.batches?.find((item) => item.iteration === read.state.batch_loop?.iteration);
219
+ if (batch === undefined || !approvedValidationOnly(read.state, batch))
220
+ return false;
221
+ try {
222
+ const text = await fs.readText(join(root, read.state.scope.plan));
223
+ return !parseTasks(text).items.some((task) => task.status === "open" && batch.phases.includes(task.phase ?? -1));
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ async function observePreexistingFailures(fs, paths, session, root) {
230
+ const read = await readRun(fs, locateRun(paths, session));
231
+ if (!read.ok || read.state.boundary !== PHASE_VALIDATION)
232
+ return [];
233
+ const state = read.state;
234
+ if (state.scope === null)
235
+ return null;
236
+ const batch = state.batches?.find((entry) => entry.iteration === state.batch_loop?.iteration);
237
+ if (batch === undefined)
238
+ return [];
239
+ try {
240
+ const phases = parsePhases(await fs.readText(join(root, state.scope.plan))).items;
241
+ const selected = phases.filter((phase) => batch.phases.includes(phase.n));
242
+ if (selected.length !== batch.phases.length ||
243
+ selected.some((phase) => phase.preexisting_failures === null))
244
+ return null;
245
+ return selected.flatMap((phase) => phase.preexisting_failures ?? []);
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }
153
251
  /**
154
252
  * Judge the plan bytes a proposal is carrying, and derive the seal each one gets.
155
253
  *
@@ -186,7 +284,7 @@ async function observePlanArtifacts(fs, paths, raw) {
186
284
  continue;
187
285
  if (coreDocumentKindForPath(artifact.path, canon.canon) !== "plan")
188
286
  continue;
189
- const { failures, seal } = await planGrammarAtPublication(fs, root, artifact.content, declared, canon.canon.spec);
287
+ const { failures, seal } = await planGrammarAtPublication(fs, root, artifact.content, declared, canon.canon.spec, paths);
190
288
  if (seal.status === "sealed")
191
289
  baselines.set(artifact.path, seal.baseline);
192
290
  evidence.push({ path: artifact.path, failures });
@@ -230,8 +328,10 @@ async function observeScope(fs, paths, raw) {
230
328
  declared: null,
231
329
  plan: null,
232
330
  sources: null,
331
+ final_validation: null,
233
332
  boundary_failures: null,
234
333
  plan_error: null,
334
+ isolation: "unit",
235
335
  };
236
336
  let parsed;
237
337
  try {
@@ -252,7 +352,7 @@ async function observeScope(fs, paths, raw) {
252
352
  const named = decisions.plan;
253
353
  const plan = typeof named === "string" ? named.trim() : null;
254
354
  if (plan === null || plan.length === 0 || !checkSafeRelativePath(plan).ok) {
255
- return { ...empty, declared, plan };
355
+ return { ...empty, declared, plan, isolation: block?.edit_mode ?? "unit" };
256
356
  }
257
357
  const canon = await resolveCoreDocsCanon(fs, paths);
258
358
  if (!canon.ok)
@@ -276,8 +376,12 @@ async function observeScope(fs, paths, raw) {
276
376
  declared,
277
377
  plan,
278
378
  sources: sourceAliasesOfPlan(text),
379
+ final_validation: resolveFinalValidation(text, sourceAliasesOfPlan(text), await readSourcePipelines(fs, paths)),
279
380
  boundary_failures: planGrammarAtEntry(text, declared),
280
381
  plan_error: null,
382
+ isolation: /^> Aislamiento: unidad\s*$/m.test(text.split(/^## /m, 1)[0] ?? "")
383
+ ? "unit"
384
+ : (block?.edit_mode ?? "unit"),
281
385
  };
282
386
  }
283
387
  /**
@@ -289,7 +393,7 @@ async function observeScope(fs, paths, raw) {
289
393
  */
290
394
  async function observeCheckouts(fs, paths, session, git) {
291
395
  if (git === undefined)
292
- return null;
396
+ return { states: null, failure: null };
293
397
  // The SAME resolution the directive published, from the same function: two
294
398
  // callers computing this apart is what made a rejection claim the tree moved
295
399
  // when all that differed was the directory each side measured.
@@ -298,11 +402,24 @@ async function observeCheckouts(fs, paths, session, git) {
298
402
  for (const candidate of candidates) {
299
403
  // An unreadable checkout is deliberately absent from the eligible set: a proof
300
404
  // against it fails closed instead of being treated as a clean tree.
301
- const state = await observeCheckout(fs, git, candidate);
405
+ let state;
406
+ try {
407
+ state = await observeCheckout(fs, git, candidate);
408
+ }
409
+ catch (error) {
410
+ return {
411
+ states,
412
+ failure: {
413
+ code: "WORKLINE_CHECKOUT_UNOBSERVABLE",
414
+ message: error instanceof Error ? error.message : String(error),
415
+ action: "corregí el fallo de git en ese checkout y recapturá la prueba",
416
+ },
417
+ };
418
+ }
302
419
  if (state !== null)
303
420
  states.push(state);
304
421
  }
305
- return states;
422
+ return { states, failure: null };
306
423
  }
307
424
  /**
308
425
  * The alias→root pairs worth publishing: the ones actually OBSERVED.
@@ -369,11 +486,25 @@ async function observeDestinations(fs, paths, raw) {
369
486
  return snapshot;
370
487
  }
371
488
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: resend, drift, admission, scope, sealing, preview, grant and handoff checks are one intentional precedence contract.
372
- async function decide(fs, paths, state, input, snapshot) {
489
+ async function decide(fs, paths, state, input, snapshot, checkOnly = false) {
373
490
  const journey = journeyForRun(state);
491
+ // Human controls are exits from the run, not answers to the current kind of
492
+ // boundary. They remain usable with an exhausted or incoherent attempt ledger.
493
+ const control = controlFromRaw(input.raw);
494
+ if (control === STOP_LABEL)
495
+ return closeFromAnswer(state);
496
+ if (control === PAUSE_LABEL) {
497
+ const standing = resolveBoundary(state, journey);
498
+ return reject(state, standing, `'${PAUSE_LABEL}': la frontera queda en pie`, {
499
+ code: "FLOW_BOUNDARY_PAUSED",
500
+ action: `escribí el CHECKPOINT con 'aw checkpoint-write --code ${state.session}' y retomá con 'aw flow advance --code ${state.session}'`,
501
+ outcome: "needs_input",
502
+ });
503
+ }
374
504
  const incoherent = checkAgainstJourney(state, journey);
375
- if (incoherent !== null)
376
- return { ok: false, failure: incoherent };
505
+ if (incoherent !== null) {
506
+ return reject(state, resolveBoundary(state, journey), incoherent.message, incoherent);
507
+ }
377
508
  const resolved = resolveBoundary(state, journey);
378
509
  if (resolved.stopped === null) {
379
510
  return reject(state, resolved, "el recorrido ya terminó: no hay frontera que responder", {
@@ -424,8 +555,11 @@ async function decide(fs, paths, state, input, snapshot) {
424
555
  const drifted = actionDrift(state, resolved, cost);
425
556
  if (drifted !== null)
426
557
  return drifted;
558
+ if (resolved.kind === "execution" && snapshot.checkoutFailure !== null) {
559
+ return reject(state, resolved, snapshot.checkoutFailure.message, snapshot.checkoutFailure, cost);
560
+ }
427
561
  // 2 · The boundary in force decides what is admissible.
428
- const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts, snapshot.scoped);
562
+ const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts, snapshot.scoped, snapshot.preexisting, snapshot.validation_only_current);
429
563
  if ("decision" in admissible)
430
564
  return admissible.decision;
431
565
  const parsed = admissible;
@@ -439,24 +573,73 @@ async function decide(fs, paths, state, input, snapshot) {
439
573
  if (route.kind === "adjust") {
440
574
  return adjustRouteProposal(state, journey);
441
575
  }
576
+ if (checkOnly && resolved.kind === "execution")
577
+ return checked(state, resolved);
442
578
  const accepted = route.kind === "accept" ? withRouteDecisions(state, route.decisions) : state;
443
579
  // Sealed with the transition it earned, in the same state publication: a batch
444
580
  // is never seen validated without the proofs that validated it.
445
- const routed = parsed.credit === undefined
581
+ let routed = parsed.credit === undefined
446
582
  ? accepted
447
583
  : withPlanExecBatchCredit(accepted, parsed.credit.batch, parsed.credit.proofs);
584
+ if (state.flow === "plan-exec" &&
585
+ [
586
+ "plan-exec.entry-gate",
587
+ "plan-exec.entry-gap-recognition",
588
+ "plan-exec.normalization-consent",
589
+ ].includes(resolved.stopped.id)) {
590
+ routed = withPlanExecEntry(routed, await observePlanEntry(fs, paths, state));
591
+ }
592
+ if (resolved.stopped.id === UNCHANGED_PHASE_CONSENT &&
593
+ parsed.answer.choice === APPROVE_UNCHANGED_PHASE) {
594
+ const entry = state.plan_exec_entry;
595
+ if (entry?.plan && entry.phases_without_open_tasks) {
596
+ routed = withPlanExecEntry(routed, {
597
+ ...entry,
598
+ approved_without_changes: [...entry.phases_without_open_tasks],
599
+ });
600
+ }
601
+ }
448
602
  // The registry, not the Spanish consequence text, owns what a selected
449
603
  // alternative does. A handoff must stop before any later plan-exec row can
450
604
  // mark work, validate or commit; a decision selection is durably recorded
451
605
  // even while its registration is handled by the decision bridge below.
452
606
  const selected = choiceOutcomeOf(resolved.stopped, parsed.answer.choice);
607
+ const withEvidence = parsed.answer.choice === "Copiar evidencia y aprobar commit del workspace";
608
+ let approvedWorkspaceCommit = null;
609
+ if (resolved.stopped.id === "chassis.commit-choice" &&
610
+ (parsed.answer.choice === "Aprobar commit del workspace" || withEvidence)) {
611
+ if (!input.git || !input.process)
612
+ return reject(state, resolved, "no se puede observar Git para aprobar el commit", {
613
+ code: "WORKSPACE_COMMIT_UNAVAILABLE",
614
+ action: "cerrá sin commit o reintentá con un repositorio Git legible",
615
+ }, cost);
616
+ const prepared = await runWorkspaceCommit(fs, input.git, input.process, paths, {
617
+ code: state.session,
618
+ withEvidence,
619
+ });
620
+ if ("error" in prepared)
621
+ return reject(state, resolved, prepared.error, {
622
+ code: "WORKSPACE_COMMIT_UNAVAILABLE",
623
+ action: "cerrá sin commit o repará la propuesta y volvé a prepararla",
624
+ }, cost);
625
+ if (parsed.answer.decisions.commit_approval !== prepared.proposal.approval) {
626
+ const command = `aw workspace-commit prepare --code ${state.session}${withEvidence ? " --with-evidence" : ""}`;
627
+ return reject(state, resolved, "la aprobación no coincide con la lista de rutas y el mensaje", {
628
+ code: "WORKSPACE_COMMIT_APPROVAL_INVALID",
629
+ action: `revisá ${command} y contestá con decisions.commit_approval: ${prepared.proposal.approval}`,
630
+ }, cost);
631
+ }
632
+ approvedWorkspaceCommit = prepared.proposal;
633
+ }
453
634
  if (selected?.kind === "handoff") {
454
635
  // Only `spec-refine` needs a document the run never declared: the plan's
455
636
  // scope names the plan, and the spec is one hop away through its lineage.
456
637
  const specPath = selected.destination === "spec-refine"
457
638
  ? await specPathOfScopedPlan(fs, paths, snapshot.root, state.scope?.plan ?? null)
458
639
  : null;
459
- return applyAndHandoff(state, journey, resolved.stopped, identity, parsed.answer, selected, specPath);
640
+ return applyAndHandoff(routed, journey, resolved.stopped, identity, parsed.answer, selected, specPath, state.flow === "quick"
641
+ ? (parseMdSectionBilingual(await fs.readText(join(paths.cwdSessionsDir(), state.session, "SESSION.md")), "Objective")?.trim() ?? "")
642
+ : null);
460
643
  }
461
644
  // The fix preview is checked HERE and nowhere else: it is a declaration, so no
462
645
  // seal, no destination and no evidence would ever notice its absence, and the
@@ -474,27 +657,63 @@ async function decide(fs, paths, state, input, snapshot) {
474
657
  // preview showed. Neither ever applies the step by itself.
475
658
  const scoped = scopeFrom(routed, resolved.stopped, parsed.answer, snapshot.scope);
476
659
  if ("failure" in scoped) {
477
- return reject(state, resolved, scoped.failure.message, { code: scoped.failure.code, action: scoped.failure.action }, cost);
660
+ // A run without an input plan only learns the plan's identity at source-scope.
661
+ // Once its grammar is read, asking for another scope cannot repair the plan:
662
+ // retain that observation and show a blocked boundary with the refine action.
663
+ const grammar = snapshot.scope.boundary_failures?.[0];
664
+ if (grammar !== undefined && scoped.failure.code === grammar.code && snapshot.scope.plan) {
665
+ const blocked = withPlanExecEntry(state, {
666
+ plan: snapshot.scope.plan,
667
+ phases_without_open_tasks: state.plan_exec_entry?.phases_without_open_tasks ?? null,
668
+ grammar_failure: {
669
+ ...scoped.failure,
670
+ action: `${scoped.failure.action}; corregí el plan con /w:plan-refine antes de reanudar ejecución`,
671
+ },
672
+ });
673
+ const refusal = reject(blocked, resolveBoundary(blocked, journey), scoped.failure.message, scoped.failure, cost);
674
+ return refusal.ok ? { ...refusal, persist: true } : refusal;
675
+ }
676
+ return reject(state, resolved, scoped.failure.message, scoped.failure, cost);
677
+ }
678
+ const batchProposal = await commitProposalFrom(scoped.state, resolved.stopped, parsed.answer, input.git, fs, paths);
679
+ if ("failure" in batchProposal) {
680
+ return reject(state, resolved, batchProposal.failure.message, batchProposal.failure, cost);
478
681
  }
479
682
  // The closure-evidence gate runs over the proposed bytes BEFORE the seal: a
480
683
  // plan whose validation names no observable check of the checkout is refused
481
684
  // while it is being written, not when somebody tries to execute it.
482
685
  const evidence = planEvidenceFrom(resolved.stopped, parsed.answer, snapshot.plans);
483
686
  if (evidence !== null) {
484
- return reject(state, resolved, evidence.failure.message, { code: evidence.failure.code, action: evidence.failure.action }, cost);
687
+ return reject(state, resolved, evidence.failure.message, evidence.failure, cost);
485
688
  }
486
- const sealed = sealFrom(scoped.state, resolved.stopped, parsed.answer, snapshot.destinations, snapshot.baselines);
689
+ const sealed = sealFrom(batchProposal.state, resolved.stopped, parsed.answer, snapshot.destinations, snapshot.baselines);
487
690
  if ("failure" in sealed) {
488
- return reject(state, resolved, sealed.failure.message, { code: sealed.failure.code, action: sealed.failure.action }, cost);
691
+ return reject(state, resolved, sealed.failure.message, sealed.failure, cost);
489
692
  }
490
693
  const preparedForGate = await prepareDecisionForGate(fs, sealed.state, snapshot, parsed.answer, resolved, cost);
491
694
  if (!preparedForGate.ok)
492
695
  return preparedForGate.decision;
696
+ if (checkOnly)
697
+ return checked(state, resolved);
493
698
  const settlement = recordSettlement(preparedForGate.state, parsed.answer, resolved, cost);
494
699
  if (!settlement.ok)
495
700
  return settlement.decision;
496
701
  const granted = resolved.kind === "authorization" ? (resolved.authorization?.planned ?? []) : [];
497
- const approved = grantOf(settlement.state, resolved, parsed.answer, granted, journey);
702
+ let approved = grantOf(settlement.state, resolved, parsed.answer, granted, journey);
703
+ if (resolved.stopped.id === "plan-exec.batch-commit-authorization" &&
704
+ parsed.answer.choice === "Aprobar los commits del lote") {
705
+ const batch = (approved.batches ?? []).find((entry) => entry.iteration === approved.batch_loop?.iteration);
706
+ if (batch?.commit_proposal !== undefined) {
707
+ const proposal = batch.commit_proposal;
708
+ approved = withPlanExecBatchUpdate(approved, batch.id, (entry) => ({
709
+ ...entry,
710
+ commit_proposal: {
711
+ ...proposal,
712
+ approved_digest: proposal.digest,
713
+ },
714
+ }));
715
+ }
716
+ }
498
717
  // All pure scope/proposal checks have now passed. A durable decision is a
499
718
  // material write, so it must never land when a later validation of the same
500
719
  // answer would refuse the boundary and leave its cursor standing.
@@ -517,6 +736,29 @@ async function decide(fs, paths, state, input, snapshot) {
517
736
  outcome: selected,
518
737
  })
519
738
  : approved;
739
+ if (approvedWorkspaceCommit !== null) {
740
+ selectedState = withEvent(selectedState, {
741
+ kind: "executed",
742
+ transition: "chassis.commit-choice",
743
+ operation: "workspace.commit-approved",
744
+ summary: JSON.stringify(approvedWorkspaceCommit),
745
+ output_digest: semanticDigest(approvedWorkspaceCommit),
746
+ effects: [],
747
+ evidence: ["workspace.commit-paths-approved"],
748
+ });
749
+ }
750
+ if (resolved.stopped.id === "chassis.commit-choice" &&
751
+ (parsed.answer.choice === "Copiar evidencia y cerrar" || withEvidence)) {
752
+ selectedState = withEvent(selectedState, {
753
+ kind: "executed",
754
+ transition: "chassis.commit-choice",
755
+ operation: "workspace.evidence-approved",
756
+ summary: "copia explícita de evidencia efímera",
757
+ output_digest: semanticDigest("workspace.evidence-approved"),
758
+ effects: [],
759
+ evidence: ["workspace.evidence-copy-approved"],
760
+ });
761
+ }
520
762
  if (registered?.ok && registered.reconciliation !== null) {
521
763
  selectedState = withContinuation(selectedState, registered.reconciliation);
522
764
  }
@@ -536,15 +778,160 @@ async function decide(fs, paths, state, input, snapshot) {
536
778
  // cuya respuesta se tiraba —así que `Compactar` ahí volvía a una pregunta que
537
779
  // ya nadie podía mostrar.
538
780
  if (preview.preview !== null) {
539
- selectedState = withFixPreview(selectedState, preview.preview);
781
+ selectedState = withFixPreview(selectedState, {
782
+ ...preview.preview,
783
+ ...(snapshot.quickCheckouts === null ? {} : { checkouts: snapshot.quickCheckouts }),
784
+ });
540
785
  }
786
+ selectedState = withQuickCheckouts(selectedState, snapshot.quickCheckouts);
541
787
  // A redraft recorded by `grantOf` inserted rows after this boundary, so the
542
788
  // transition is applied against the journey as it now reads.
543
789
  const walked = journeyForRun(selectedState);
544
790
  const outcome = holds
545
791
  ? holdAfterApproval(selectedState, walked, identity)
546
792
  : applyAndAdvance(selectedState, walked, resolved.stopped, identity, parsed.answer);
547
- return standalone === null ? outcome : withStandaloneGuidance(outcome, standalone);
793
+ const directed = standalone === null ? outcome : withStandaloneGuidance(outcome, standalone);
794
+ if (!directed.ok || !preparedForGate.warnings?.length)
795
+ return directed;
796
+ return {
797
+ ...directed,
798
+ value: {
799
+ ...directed.value,
800
+ directive: {
801
+ ...directed.value.directive,
802
+ next_action: `${preparedForGate.warnings.join("; ")}; ${directed.value.directive.next_action}`,
803
+ },
804
+ },
805
+ };
806
+ }
807
+ function controlFromRaw(raw) {
808
+ try {
809
+ const body = JSON.parse(raw);
810
+ if (!isRecord(body))
811
+ return null;
812
+ return body.choice === PAUSE_LABEL || body.choice === STOP_LABEL ? body.choice : null;
813
+ }
814
+ catch {
815
+ return null;
816
+ }
817
+ }
818
+ function checked(state, resolved) {
819
+ const built = directiveFor(state, resolved, []);
820
+ if (!built.ok)
821
+ return { ok: false, failure: built.failure };
822
+ return { ok: true, state, value: { directive: built.directive, advanced: false } };
823
+ }
824
+ async function commitProposalFrom(state, stopped, answer, git, fs, paths) {
825
+ if (stopped.id !== "plan-exec.batch-commit-proposal")
826
+ return { state };
827
+ const batch = (state.batches ?? []).find((entry) => entry.iteration === state.batch_loop?.iteration);
828
+ if (batch === undefined || batch.snapshot === undefined || git === undefined) {
829
+ return {
830
+ failure: {
831
+ code: "PLAN_EXEC_BATCH_COMMIT_UNOBSERVABLE",
832
+ message: "falta la instantánea git o el lector de las fuentes",
833
+ action: "volvé a adquirir la unidad del lote",
834
+ },
835
+ };
836
+ }
837
+ const messages = answer.decisions.messages;
838
+ const declaredPaths = answer.decisions.paths;
839
+ const pathMap = typeof declaredPaths === "object" && declaredPaths !== null && !Array.isArray(declaredPaths)
840
+ ? declaredPaths
841
+ : {};
842
+ const inPlace = state.scope?.isolation === "in-place";
843
+ const given = typeof messages === "object" && messages !== null && !Array.isArray(messages)
844
+ ? messages
845
+ : {};
846
+ const candidates = await resolveCheckoutCandidates(fs, paths, state.session);
847
+ const sources = [];
848
+ for (const [alias, base] of Object.entries(batch.snapshot).sort(([a], [b]) => a.localeCompare(b))) {
849
+ const root = candidates.find((candidate) => candidate.source === alias)?.root;
850
+ if (root === undefined)
851
+ return {
852
+ failure: {
853
+ code: "PLAN_EXEC_BATCH_COMMIT_UNOBSERVABLE",
854
+ message: `no se puede ubicar la unidad ${alias}`,
855
+ action: "restaurá la unidad de esta sesión",
856
+ },
857
+ };
858
+ const dirty = await git.dirtyPaths(root);
859
+ const changed = dirty
860
+ .filter((entry) => !base.dirty.some((previous) => previous.path === entry.path && previous.digest === entry.digest))
861
+ .map((entry) => entry.path);
862
+ const askedPaths = pathMap[alias];
863
+ if (inPlace &&
864
+ (!Array.isArray(askedPaths) ||
865
+ !askedPaths.length ||
866
+ askedPaths.some((path) => typeof path !== "string" || !path.trim()) ||
867
+ new Set(askedPaths).size !== askedPaths.length))
868
+ return {
869
+ failure: {
870
+ code: "PLAN_EXEC_BATCH_PATHS_REQUIRED",
871
+ message: `${alias}: declarás decisions.paths[${alias}] como lista exacta de rutas de la corrida`,
872
+ action: "indicá sólo rutas de la corrida en este checkout",
873
+ },
874
+ };
875
+ const paths = inPlace ? askedPaths : changed;
876
+ if (inPlace) {
877
+ for (const path of paths) {
878
+ const before = base.dirty.find((entry) => entry.path === path);
879
+ const after = dirty.find((entry) => entry.path === path);
880
+ if (before && after?.digest !== before.digest)
881
+ return {
882
+ failure: {
883
+ code: "PLAN_EXEC_BATCH_SHARED_PATH",
884
+ message: `${alias}: ${path} fue tocada por el usuario y por la corrida`,
885
+ action: "detené el lote sin commitear y acordá la separación de cambios",
886
+ },
887
+ };
888
+ if (before || !after)
889
+ return {
890
+ failure: {
891
+ code: "PLAN_EXEC_BATCH_PATH_NOT_OWNED",
892
+ message: `${alias}: ${path} ya era del usuario o no está sucia`,
893
+ action: "declarás únicamente rutas nuevas de la corrida",
894
+ },
895
+ };
896
+ }
897
+ }
898
+ if (paths.length === 0)
899
+ continue;
900
+ const message = given[alias];
901
+ if (typeof message !== "string" ||
902
+ message.trim() !== message ||
903
+ message.length === 0 ||
904
+ /[\r\n]/.test(message) ||
905
+ /\b(?:Co-authored-by|Signed-off-by|Reviewed-by):/i.test(message)) {
906
+ return {
907
+ failure: {
908
+ code: "PLAN_EXEC_BATCH_COMMIT_MESSAGE_INVALID",
909
+ message: `${alias}: el mensaje debe ser una sola línea sin trailers`,
910
+ action: "devolvé decisions.messages por alias con un mensaje de una línea",
911
+ },
912
+ };
913
+ }
914
+ sources.push({
915
+ alias,
916
+ paths: paths.sort(),
917
+ dirty: dirty.filter((entry) => paths.includes(entry.path)),
918
+ message,
919
+ ...(inPlace
920
+ ? {
921
+ foreign_paths: dirty
922
+ .filter((entry) => !paths.includes(entry.path))
923
+ .map((entry) => entry.path),
924
+ }
925
+ : {}),
926
+ });
927
+ }
928
+ const digest = semanticDigest({ batch: batch.id, snapshot: batch.snapshot, sources });
929
+ return {
930
+ state: withPlanExecBatchUpdate(state, batch.id, (item) => ({
931
+ ...item,
932
+ commit_proposal: { sources, digest },
933
+ })),
934
+ };
548
935
  }
549
936
  /**
550
937
  * The declared fix preview, checked for the three things it has to say.
@@ -640,10 +1027,7 @@ function routeAnswer(state, stopped, journey, answer) {
640
1027
  };
641
1028
  }
642
1029
  const candidate = answer.decisions.route;
643
- if (!isRecord(candidate) ||
644
- !isRecord(candidate.summary) ||
645
- !isRecord(candidate.basis) ||
646
- !Array.isArray(candidate.controls)) {
1030
+ if (!routeShape(candidate)) {
647
1031
  return {
648
1032
  kind: "failure",
649
1033
  failure: {
@@ -668,17 +1052,25 @@ function routeAnswer(state, stopped, journey, answer) {
668
1052
  const declaredSummary = summary;
669
1053
  const basis = candidate.basis;
670
1054
  const basisFields = ["intention", "checkout", "conventions", "adopted_decisions"];
671
- if (basisFields.some((field) => !nonBlank(basis[field]))) {
1055
+ const asText = (value) => {
1056
+ if (nonBlank(value))
1057
+ return value.trim();
1058
+ if (Array.isArray(value) && value.length > 0 && value.every(nonBlank))
1059
+ return value.map((entry) => entry.trim()).join("\n");
1060
+ return null;
1061
+ };
1062
+ const invalidBasis = basisFields.find((field) => asText(basis[field]) === null);
1063
+ if (invalidBasis !== undefined) {
672
1064
  return {
673
1065
  kind: "failure",
674
1066
  failure: {
675
1067
  code: "FLOW_ROUTE_PROPOSAL_INVALID",
676
- message: "la ruta no explica intención, checkout, convenciones y decisiones adoptadas",
1068
+ message: `decisions.route.basis.${invalidBasis} espera texto o lista de textos no vacíos; recibió ${Array.isArray(basis[invalidBasis]) ? "lista inválida" : typeof basis[invalidBasis]}`,
677
1069
  action: "completá cada campo de decisions.route.basis con una observación concreta",
678
1070
  },
679
1071
  };
680
1072
  }
681
- const declaredBasis = basis;
1073
+ const declaredBasis = Object.fromEntries(basisFields.map((field) => [field, asText(basis[field])]));
682
1074
  const configured = new Map(journey.flatMap((decision) => {
683
1075
  const control = routeControlOf(decision);
684
1076
  return control === null ? [] : [[decision.id, { decision, control }]];
@@ -824,15 +1216,26 @@ function scopeFrom(state, stopped, answer, snapshot) {
824
1216
  if (!scopesSources(stopped))
825
1217
  return { state };
826
1218
  const asked = answer.decisions.sources;
827
- const sources = Array.isArray(asked)
828
- ? asked.filter((alias) => typeof alias === "string").map((a) => a.trim())
1219
+ const sources = sourceList(asked)
1220
+ ? asked.map((entry) => typeof entry === "string"
1221
+ ? entry.trim()
1222
+ : isRecord(entry) && typeof entry.alias === "string" && Object.keys(entry).length === 1
1223
+ ? entry.alias.trim()
1224
+ : null)
829
1225
  : null;
830
- if (sources === null || sources.length === 0 || sources.some((alias) => alias.length === 0)) {
1226
+ if (sources?.some((alias) => alias === null)) {
1227
+ const at = sources.findIndex((alias) => alias === null);
831
1228
  return {
832
- failure: invalidScope("esta frontera fija qué fuentes edita la corrida y la respuesta no trae ninguna", "devolvé en 'decisions.sources' los alias exactos que el plan afecta, y en 'decisions.plan' su documento"),
1229
+ failure: invalidScope(`decisions.sources[${at}] espera un alias de texto o {alias: string}; recibió ${Array.isArray(asked) && asked[at] === null ? "null" : typeof asked[at]}`, "cada fuente es un alias de texto o un objeto {alias} sin otras claves"),
833
1230
  };
834
1231
  }
835
- if (new Set(sources).size !== sources.length) {
1232
+ if (sources === null || sources.length === 0 || sources.some((alias) => alias?.length === 0)) {
1233
+ return {
1234
+ failure: invalidScope(`decisions.sources espera una lista no vacía de alias de texto o {alias: string}; recibió ${Array.isArray(asked) ? "lista vacía" : typeof asked}`, "devolvé en 'decisions.sources' los alias exactos que el plan afecta, y en 'decisions.plan' su documento"),
1235
+ };
1236
+ }
1237
+ const aliases = sources;
1238
+ if (new Set(aliases).size !== aliases.length) {
836
1239
  return {
837
1240
  failure: invalidScope("el scope declara la misma fuente dos veces", "una unidad por alias: declarás cada fuente una sola vez"),
838
1241
  };
@@ -840,7 +1243,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
840
1243
  const plan = snapshot.plan;
841
1244
  if (plan === null || plan.length === 0) {
842
1245
  return {
843
- failure: invalidScope("el scope no dice qué plan ejecuta esta corrida", `devolvé en 'decisions.plan' la ruta del plan dentro del workspace (${DEFAULT_CORE_DOCS_CANON.plan}/PPP-plan-<slug>.md)`),
1246
+ failure: invalidScope(`decisions.plan espera una ruta de texto no vacía; recibió ${typeof answer.decisions.plan}`, `devolvé en 'decisions.plan' la ruta del plan dentro del workspace (${DEFAULT_CORE_DOCS_CANON.plan}/PPP-plan-<slug>.md)`),
844
1247
  };
845
1248
  }
846
1249
  if (snapshot.plan_error !== null) {
@@ -853,7 +1256,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
853
1256
  };
854
1257
  }
855
1258
  const declared = snapshot.declared;
856
- if (declared === null && sources.some((alias) => alias !== "workspace")) {
1259
+ if (declared === null && aliases.some((alias) => alias !== "workspace")) {
857
1260
  return {
858
1261
  failure: {
859
1262
  code: "FLOW_SCOPE_UNKNOWN_SOURCE",
@@ -863,7 +1266,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
863
1266
  };
864
1267
  }
865
1268
  const declaredAliases = declared ?? [];
866
- const unknown = sources.filter((alias) => alias !== "workspace" && !declaredAliases.includes(alias));
1269
+ const unknown = aliases.filter((alias) => alias !== "workspace" && !declaredAliases.includes(alias));
867
1270
  if (unknown.length > 0) {
868
1271
  return {
869
1272
  failure: {
@@ -889,12 +1292,19 @@ function scopeFrom(state, stopped, answer, snapshot) {
889
1292
  code: structural.code,
890
1293
  message: structural.message,
891
1294
  action: planBoundaryAction(structural.code, "execution-entry"),
1295
+ violations: snapshot.boundary_failures.map((failure) => ({
1296
+ code: failure.code,
1297
+ field: failure.line ? `plan:${failure.line}` : "plan",
1298
+ expected: "gramática de fuentes y prueba local del plan",
1299
+ received: failure.message,
1300
+ message: failure.message,
1301
+ })),
892
1302
  },
893
1303
  };
894
1304
  }
895
1305
  const declaredByPlan = snapshot.sources;
896
- const missing = declaredByPlan.filter((alias) => !sources.includes(alias));
897
- const extra = sources.filter((alias) => !declaredByPlan.includes(alias));
1306
+ const missing = declaredByPlan.filter((alias) => !aliases.includes(alias));
1307
+ const extra = aliases.filter((alias) => !declaredByPlan.includes(alias));
898
1308
  if (missing.length > 0 || extra.length > 0) {
899
1309
  const difference = [
900
1310
  ...(missing.length > 0 ? [`faltan ${missing.join(", ")}`] : []),
@@ -908,7 +1318,16 @@ function scopeFrom(state, stopped, answer, snapshot) {
908
1318
  },
909
1319
  };
910
1320
  }
911
- return { state: withScope(state, { plan, sources }) };
1321
+ return {
1322
+ state: withScope(state, {
1323
+ plan,
1324
+ sources: aliases,
1325
+ isolation: snapshot.isolation,
1326
+ ...(snapshot.final_validation === null
1327
+ ? {}
1328
+ : { final_validation: snapshot.final_validation }),
1329
+ }),
1330
+ };
912
1331
  }
913
1332
  const SETTLEMENT_OUTCOMES = SETTLEMENT_READINGS.map((reading) => reading.outcome);
914
1333
  /** The reading each label of the question stands for — from the row's own table. */
@@ -1048,7 +1467,7 @@ function ratified(declaration, outcome) {
1048
1467
  : { note: declaration.note, index: declaration.index, outcome };
1049
1468
  }
1050
1469
  function readDeclarations(value) {
1051
- if (!Array.isArray(value))
1470
+ if (!settlementList(value))
1052
1471
  return null;
1053
1472
  const out = [];
1054
1473
  for (const entry of value) {
@@ -1113,6 +1532,7 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1113
1532
  }, cost),
1114
1533
  };
1115
1534
  }
1535
+ const dated = withDecisionDate(state, state.decision_date ?? new Date().toISOString().slice(0, 10));
1116
1536
  // The plan of the scope decides WHICH preparation this is. A standalone plan has
1117
1537
  // no spec to compose against, so calling `preparePlanExecDecision` for it would
1118
1538
  // refuse with `FLOW_DECISION_LINEAGE_INVALID` and leave the run with handoffs as
@@ -1120,7 +1540,7 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1120
1540
  const standalone = await standaloneDecisionFor(fs, snapshot.root, plan, candidate);
1121
1541
  if (standalone !== null) {
1122
1542
  return standalone.ok
1123
- ? { ok: true, state: withDecisionPreparation(state, standalone.preparation) }
1543
+ ? { ok: true, state: withDecisionPreparation(dated, standalone.preparation) }
1124
1544
  : {
1125
1545
  ok: false,
1126
1546
  decision: reject(state, resolved, standalone.failure.message, standalone.failure, cost),
@@ -1131,23 +1551,27 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1131
1551
  session: state.session,
1132
1552
  plan,
1133
1553
  value: candidate,
1554
+ date: dated.decision_date ?? new Date().toISOString().slice(0, 10),
1134
1555
  });
1135
1556
  if (!prepared.ok) {
1557
+ const refused = reject(dated, resolveBoundary(dated, journeyForRun(dated)), prepared.failure.message, prepared.failure, cost);
1136
1558
  return {
1137
1559
  ok: false,
1138
- decision: reject(state, resolved, prepared.failure.message, prepared.failure, cost),
1560
+ decision: refused.ok ? { ...refused, persist: true } : refused,
1139
1561
  };
1140
1562
  }
1141
1563
  switch (prepared.kind) {
1142
1564
  case "settled":
1143
1565
  return {
1144
1566
  ok: true,
1145
- state: withDecisionPreparation(state, { kind: "settled", decision: prepared.decision }),
1567
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1568
+ state: withDecisionPreparation(dated, { kind: "settled", decision: prepared.decision }),
1146
1569
  };
1147
1570
  case "reused":
1148
1571
  return {
1149
1572
  ok: true,
1150
- state: withDecisionPreparation(state, {
1573
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1574
+ state: withDecisionPreparation(dated, {
1151
1575
  kind: "reused",
1152
1576
  note: prepared.note,
1153
1577
  decision: prepared.decision,
@@ -1157,7 +1581,8 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1157
1581
  case "prepared":
1158
1582
  return {
1159
1583
  ok: true,
1160
- state: withDecisionPreparation(state, {
1584
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1585
+ state: withDecisionPreparation(dated, {
1161
1586
  kind: "prepared",
1162
1587
  note: prepared.prepared.note,
1163
1588
  preview: prepared.prepared.preview,
@@ -1311,7 +1736,7 @@ function withStandaloneGuidance(decision, record) {
1311
1736
  * cannot tell them apart, and neither could the table that decides the charge.
1312
1737
  */
1313
1738
  function invalidScope(message, action) {
1314
- return { code: "FLOW_SCOPE_INVALID", message, action };
1739
+ return { code: "FLOW_SCOPE_SHAPE_INVALID", message, action };
1315
1740
  }
1316
1741
  /**
1317
1742
  * The closure-evidence gate, run BEFORE the preview is sealed.
@@ -1325,6 +1750,8 @@ function planEvidenceFrom(stopped, answer, observed) {
1325
1750
  if (proposalContractOf(stopped) === null || answer.artifacts.length === 0)
1326
1751
  return null;
1327
1752
  const proposed = new Set(answer.artifacts.map((artifact) => artifact.path.trim()));
1753
+ const violations = [];
1754
+ let first = null;
1328
1755
  for (const plan of observed) {
1329
1756
  if (!proposed.has(plan.path))
1330
1757
  continue;
@@ -1335,15 +1762,20 @@ function planEvidenceFrom(stopped, answer, observed) {
1335
1762
  // should not learn of the header on the next attempt.
1336
1763
  const lineage = plan.failures.find((candidate) => candidate !== failure && candidate.code === "PLAN_LINEAGE_UNSEALED");
1337
1764
  const also = lineage === undefined ? "" : ` · además: ${lineage.message}`;
1338
- return {
1339
- failure: {
1340
- code: failure.code,
1341
- message: `'${plan.path}': ${failure.message}${also}`,
1342
- action: planBoundaryAction(failure.code, "proposal"),
1343
- },
1765
+ first ??= {
1766
+ code: failure.code,
1767
+ message: `'${plan.path}': ${failure.message}${also}`,
1768
+ action: planBoundaryAction(failure.code, "proposal"),
1344
1769
  };
1345
- }
1346
- return null;
1770
+ violations.push(...plan.failures.map((entry) => ({
1771
+ code: entry.code,
1772
+ field: entry.line ? `${plan.path}:${entry.line}` : plan.path,
1773
+ expected: "plan con prueba local y linaje sellado",
1774
+ received: entry.message,
1775
+ message: entry.message,
1776
+ })));
1777
+ }
1778
+ return first === null ? null : { failure: { ...first, violations } };
1347
1779
  }
1348
1780
  /**
1349
1781
  * Seat the proposal an authoring answer just handed over — sealed, not believed.
@@ -1530,7 +1962,7 @@ function grantOf(state, resolved, answer, granted, journey) {
1530
1962
  * a payload the boundary's own contract rejects, a decline (a real answer that
1531
1963
  * applies nothing), and an execution result that did not earn its transition.
1532
1964
  */
1533
- function admit(state, resolved, stopped, input, cost, checkouts, scoped) {
1965
+ function admit(state, resolved, stopped, input, cost, checkouts, scoped, preexisting, validationOnlyCurrent) {
1534
1966
  const expectedApproval = resolved.kind === "authorization"
1535
1967
  ? effectApprovalDigest(stopped.id, resolved.authorization?.planned ?? [])
1536
1968
  : null;
@@ -1564,14 +1996,11 @@ function admit(state, resolved, stopped, input, cost, checkouts, scoped) {
1564
1996
  // consults. A resend, a decline and a pause are not failed tries either, and
1565
1997
  // they are classified there rather than being exempted here.
1566
1998
  return {
1567
- decision: reject(state, resolved, parsed.failure.message, { code: parsed.failure.code, action: parsed.failure.action }, cost),
1999
+ decision: reject(state, resolved, parsed.failure.message, parsed.failure, cost),
1568
2000
  };
1569
2001
  }
1570
- // The flow control is a real answer, and neither half applies anything. They
1571
- // are kept apart because the outcomes differ and the difference is the point:
1572
- // stopping ends the run here (`cancelled`), pausing keeps the very same
1573
- // boundary standing so the run picks it up after compacting (`needs_input`).
1574
- // Collapsing them would report a paused run as a cancelled one.
2002
+ // Pause leaves the boundary standing; stop places the shared close intention.
2003
+ // The driver runs finalize after this transaction releases the run lock.
1575
2004
  if (parsed.answer.choice === PAUSE_LABEL) {
1576
2005
  return {
1577
2006
  decision: reject(state, resolved, `'${PAUSE_LABEL}': la frontera queda en pie y la corrida retoma acá`, {
@@ -1582,27 +2011,37 @@ function admit(state, resolved, stopped, input, cost, checkouts, scoped) {
1582
2011
  };
1583
2012
  }
1584
2013
  if (parsed.answer.choice === STOP_LABEL) {
1585
- return {
1586
- decision: reject(state, resolved, `'${STOP_LABEL}': el recorrido queda detenido en esta frontera`, {
1587
- code: "FLOW_BOUNDARY_DECLINED",
1588
- action: "reanudá con 'aw flow advance' cuando quieras retomar esta frontera",
1589
- outcome: "cancelled",
1590
- }, cost),
1591
- };
2014
+ return { decision: closeFromAnswer(state) };
1592
2015
  }
1593
2016
  // An execution result has to EARN the transition. Anything short of a completed
1594
2017
  // run with its evidence and its whole effect keeps the boundary standing: the
1595
2018
  // work stays pending, with the recovery the action declared.
1596
2019
  if (resolved.kind !== "execution")
1597
2020
  return parsed;
1598
- return earned(state, resolved, stopped, parsed, cost, checkouts, scoped);
2021
+ return earned(state, resolved, stopped, parsed, cost, checkouts, scoped, preexisting, validationOnlyCurrent);
2022
+ }
2023
+ function closeFromAnswer(state) {
2024
+ const closing = closeAtBoundaryState(state);
2025
+ if (!closing.ok)
2026
+ return closing;
2027
+ const advanced = advanceFlowRun({
2028
+ state: closing.state,
2029
+ journey: journeyForRun(closing.state),
2030
+ });
2031
+ if (!advanced.ok)
2032
+ return advanced;
2033
+ return {
2034
+ ok: true,
2035
+ state: advanced.state,
2036
+ value: { directive: advanced.directive, advanced: true },
2037
+ };
1599
2038
  }
1600
2039
  /** The execution verdict, then — at the phase validation — the batch's own credit. */
1601
- function earned(state, resolved, stopped, parsed, cost, checkouts, scoped) {
2040
+ function earned(state, resolved, stopped, parsed, cost, checkouts, scoped, preexisting, validationOnlyCurrent) {
1602
2041
  // The row's `effects` are the ceiling and the sealed proposal is what really
1603
2042
  // happens: demanding an overwrite from a publication that only creates files
1604
2043
  // would leave the transition pending for an effect nobody could produce.
1605
- const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts);
2044
+ const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts, preexisting);
1606
2045
  if (verdict !== null) {
1607
2046
  const trace = declaredTrace(state, stopped, resolved, parsed.answer, verdict);
1608
2047
  return {
@@ -1612,7 +2051,7 @@ function earned(state, resolved, stopped, parsed, cost, checkouts, scoped) {
1612
2051
  }),
1613
2052
  };
1614
2053
  }
1615
- const credited = batchCredit(state, stopped, parsed.answer, resolved, scoped);
2054
+ const credited = batchCredit(state, stopped, parsed.answer, resolved, scoped, validationOnlyCurrent);
1616
2055
  if (credited === null)
1617
2056
  return parsed;
1618
2057
  if (!credited.ok) {
@@ -1627,7 +2066,7 @@ function earned(state, resolved, stopped, parsed, cost, checkouts, scoped) {
1627
2066
  * each of its sources, of its own checkout. `null` on every other boundary, and
1628
2067
  * on a run with no batch standing, where there is nothing to credit.
1629
2068
  */
1630
- function batchCredit(state, stopped, answer, resolved, scoped) {
2069
+ function batchCredit(state, stopped, answer, resolved, scoped, validationOnlyCurrent) {
1631
2070
  if (stopped.id !== PHASE_VALIDATION || answer.result === null || state.scope === null) {
1632
2071
  return null;
1633
2072
  }
@@ -1636,7 +2075,13 @@ function batchCredit(state, stopped, answer, resolved, scoped) {
1636
2075
  const batch = batches.find((entry) => entry.iteration === iteration);
1637
2076
  if (batch === undefined)
1638
2077
  return null;
1639
- const verdict = batchCreditVerdict(answer.result, { batch, batches, sources: state.scope.sources, scoped }, resolved.action?.recovery ?? "");
2078
+ const verdict = batchCreditVerdict(answer.result, {
2079
+ batch,
2080
+ batches,
2081
+ sources: state.scope.sources,
2082
+ scoped,
2083
+ validation_only_approved: validationOnlyCurrent && approvedValidationOnly(state, batch),
2084
+ }, resolved.action?.recovery ?? "");
1640
2085
  if (!verdict.ok)
1641
2086
  return verdict;
1642
2087
  return { ok: true, credit: { batch: batch.id, proofs: verdict.credit } };
@@ -1708,7 +2153,11 @@ function actionDrift(state, resolved, cost) {
1708
2153
  * was written" are different facts, and only the second one advances a run.
1709
2154
  */
1710
2155
  function holdAfterApproval(approved, journey, identity) {
1711
- const held = advanceFlowRun({ state: withAttempt(approved, identity), journey, applied: [] });
2156
+ const held = advanceFlowRun({
2157
+ state: withAttempt(approved, { ...identity, approval: true }),
2158
+ journey,
2159
+ applied: [],
2160
+ });
1712
2161
  if (!held.ok)
1713
2162
  return { ok: false, failure: held.failure };
1714
2163
  return { ok: true, state: held.state, value: { directive: held.directive, advanced: true } };
@@ -1721,6 +2170,9 @@ function applyAndAdvance(approved, journey, stopped, identity, answer) {
1721
2170
  let next = answer.signals.length > 0
1722
2171
  ? withObservation(approved, observationFor(approved, stopped.id, answer.signals))
1723
2172
  : approved;
2173
+ if (stopped.answer_contract === "batch-review" && isBatchReview(answer.decisions.review)) {
2174
+ next = withPlanExecBatchReview(next, answer.decisions.review);
2175
+ }
1724
2176
  next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1725
2177
  next = withAttempt(next, identity);
1726
2178
  // The boundary follows the position, always: handing the engine a state whose
@@ -1774,15 +2226,8 @@ function choiceOutcomeOf(stopped, label) {
1774
2226
  * the state. A retry is recognized by the attempt ledger, so it cannot create a
1775
2227
  * second package or walk the batch toward a commit.
1776
2228
  */
1777
- function applyAndHandoff(state, journey, stopped, identity, answer, outcome, specPath) {
1778
- const plan = state.scope?.plan ?? null;
1779
- if (outcome.destination === "plan-refine" && plan === null) {
1780
- const resolved = resolveBoundary(state, journey);
1781
- return reject(state, resolved, "la entrega a plan-refine no tiene el plan que debe reabrir", {
1782
- code: "FLOW_HANDOFF_PLAN_MISSING",
1783
- action: "fijá el scope de plan-exec antes de elegir una escalación a plan-refine",
1784
- }, { journey, identity });
1785
- }
2229
+ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, specPath, request) {
2230
+ const plan = state.scope?.plan ?? state.plan_exec_entry?.plan ?? null;
1786
2231
  const fixPreview = state.fix_preview ?? null;
1787
2232
  const packageBody = {
1788
2233
  plan,
@@ -1792,7 +2237,11 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1792
2237
  // declared preview travels with it — the destination is the one place that
1793
2238
  // rewrites it, and rediscovering it there is exactly what the package exists
1794
2239
  // to avoid.
1795
- decisions: fixPreview === null ? answer.decisions : { ...answer.decisions, preview: fixPreview },
2240
+ decisions: {
2241
+ ...answer.decisions,
2242
+ ...(fixPreview === null ? {} : { preview: fixPreview }),
2243
+ ...(request === null ? {} : { request }),
2244
+ },
1796
2245
  selection: answer.choice ?? "",
1797
2246
  };
1798
2247
  // The handoff to `spec-refine` used to be the bare command while the one to
@@ -1800,13 +2249,7 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1800
2249
  // spec again — the escalation package exists precisely so nothing is
1801
2250
  // re-derived at the destination. An unreadable plan degrades to the bare
1802
2251
  // command: an escalation is never blocked over the convenience of an argument.
1803
- const command = outcome.destination === "plan-refine"
1804
- ? `/w:plan-refine ${plan}`
1805
- : outcome.destination === "spec-refine"
1806
- ? specPath === null
1807
- ? "/w:spec-refine"
1808
- : `/w:spec-refine ${specPath}`
1809
- : "/w:spec-new";
2252
+ const command = handoffCommand(outcome.destination, plan, specPath);
1810
2253
  let next = withSelectedChoice(state, {
1811
2254
  transition: stopped.id,
1812
2255
  label: answer.choice ?? "",
@@ -1814,14 +2257,26 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1814
2257
  });
1815
2258
  next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1816
2259
  next = withAttempt(next, identity);
1817
- next = withHandoff(next, {
1818
- destination: outcome.destination,
1819
- command,
1820
- package: packageBody,
1821
- package_digest: semanticDigest(packageBody),
1822
- });
2260
+ next = withHandoff(next, outcome.destination === "plan-refine"
2261
+ ? planRefineHandoff(state, answer.decisions, answer.choice ?? "")
2262
+ : {
2263
+ destination: outcome.destination,
2264
+ command,
2265
+ package: packageBody,
2266
+ package_digest: semanticDigest(packageBody),
2267
+ });
1823
2268
  next = withBoundary(next, journey[next.applied.length]?.id ?? null);
1824
- const advanced = advanceFlowRun({ state: next, journey, applied: [stepOf(stopped)] });
2269
+ if (state.flow === "quick" && outcome.destination === "spec-new") {
2270
+ const closing = closeAtBoundaryState(next);
2271
+ if (!closing.ok)
2272
+ return closing;
2273
+ next = closing.state;
2274
+ }
2275
+ const advanced = advanceFlowRun({
2276
+ state: next,
2277
+ journey: journeyForRun(next),
2278
+ applied: [stepOf(stopped)],
2279
+ });
1825
2280
  if (!advanced.ok)
1826
2281
  return { ok: false, failure: advanced.failure };
1827
2282
  return {
@@ -1830,6 +2285,13 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1830
2285
  value: { directive: advanced.directive, advanced: true },
1831
2286
  };
1832
2287
  }
2288
+ function handoffCommand(destination, plan, spec) {
2289
+ if (destination === "plan-refine")
2290
+ return `/w:plan-refine ${plan}`;
2291
+ if (destination === "spec-refine")
2292
+ return spec === null ? "/w:spec-refine" : `/w:spec-refine ${spec}`;
2293
+ return "/w:spec-new";
2294
+ }
1833
2295
  /**
1834
2296
  * Whether this submission is a resend, judged by the ledger that already knows
1835
2297
  * how.
@@ -1861,11 +2323,11 @@ function resendCheck(state, resolved, cost) {
1861
2323
  for (const past of state.attempts) {
1862
2324
  const replay = ledger.record(past);
1863
2325
  if (!replay.ok)
1864
- return { ok: false, failure: replay.failure };
2326
+ return reject(state, resolved, replay.failure.message, replay.failure);
1865
2327
  }
1866
2328
  const verdict = ledger.record(identity);
1867
2329
  if (!verdict.ok)
1868
- return { ok: false, failure: verdict.failure };
2330
+ return reject(state, resolved, verdict.failure.message, verdict.failure);
1869
2331
  if (verdict.kind === "new")
1870
2332
  return null;
1871
2333
  // The TWIN's transition, not this submission's: the identity is built over the
@@ -1873,9 +2335,8 @@ function resendCheck(state, resolved, cost) {
1873
2335
  // What is being asked is whether the step the twin answered ever moved.
1874
2336
  const twin = state.attempts.find((past) => past.invocation_id === identity.invocation_id &&
1875
2337
  past.request_digest === identity.request_digest);
1876
- // The twin was refused: this is a retry of the same wrong answer, so it gets
1877
- // the same real diagnosis again and counts toward the cap, which is what
1878
- // eventually degrades the boundary instead of leaving the caller looping.
2338
+ // A refused twin gets its real diagnosis again, but the same request cannot
2339
+ // create another row. Only a different insufficient answer spends again.
1879
2340
  if (twin !== undefined && twinWasRefused(state, twin))
1880
2341
  return null;
1881
2342
  return reject(state, resolved, "esta respuesta ya se había aplicado: el recorrido no avanza dos veces", {
@@ -1907,8 +2368,8 @@ function twinWasRefused(state, twin) {
1907
2368
  * recoverable — an unbounded loop inside the very mechanism that exists to bound
1908
2369
  * one. One place decides, one table says which codes count.
1909
2370
  *
1910
- * `cost` is absent only where there is no boundary to charge: a run that already
1911
- * finished, or one that never stopped at anything.
2371
+ * `cost` is also absent for mechanical controls and replay failures, which never
2372
+ * charge a boundary even when it is still open.
1912
2373
  */
1913
2374
  function reject(state, resolved, message, detail, cost) {
1914
2375
  // A refused answer is the one rejection that CHANGES the run: the attempt is
@@ -1919,14 +2380,20 @@ function reject(state, resolved, message, detail, cost) {
1919
2380
  const traced = cost?.trace === undefined || restatesLastEvent(state, cost.trace)
1920
2381
  ? state
1921
2382
  : withEvent(state, cost.trace);
1922
- const after = cost !== undefined && spendsAttempt(detail.code) ? withAttempt(traced, cost.identity) : traced;
2383
+ const after = cost !== undefined &&
2384
+ spendsAttempt(detail.code) &&
2385
+ !state.attempts.some((past) => past.invocation_id === cost.identity.invocation_id &&
2386
+ past.attempt === cost.identity.attempt &&
2387
+ past.request_digest === cost.identity.request_digest)
2388
+ ? withAttempt(traced, cost.identity)
2389
+ : traced;
1923
2390
  const now = cost === undefined || after === state ? resolved : resolveBoundary(after, cost.journey);
1924
2391
  const built = directiveFor(after, now, [], {
1925
2392
  // No outcome named ⇒ the boundary decides it: a finished journey reports
1926
2393
  // `completed`, an open one `needs_input`. Hardcoding one here would let a
1927
2394
  // rejection at the end of a run claim work is still pending.
1928
2395
  ...(detail.outcome === undefined ? {} : { outcome: detail.outcome }),
1929
- nextAction: `${message} — ${now.error?.action ?? detail.action}`,
2396
+ nextAction: `${message} — ${detail.code === "FLOW_BOUNDARY_PAUSED" ? detail.action : (now.error?.action ?? detail.action)}`,
1930
2397
  });
1931
2398
  if (!built.ok)
1932
2399
  return { ok: false, failure: built.failure };
@@ -1936,7 +2403,12 @@ function reject(state, resolved, message, detail, cost) {
1936
2403
  // longer a way forward, and reporting the payload's error would suggest it is.
1937
2404
  const directive = {
1938
2405
  ...built.directive,
1939
- error: now.error ?? { code: detail.code, message, action: detail.action },
2406
+ error: (detail.code === "FLOW_BOUNDARY_PAUSED" ? null : now.error) ?? {
2407
+ code: detail.code,
2408
+ message,
2409
+ action: detail.action,
2410
+ ...(detail.violations ? { violations: detail.violations } : {}),
2411
+ },
1940
2412
  };
1941
2413
  return {
1942
2414
  ok: true,
@@ -1961,14 +2433,9 @@ function attemptIdentity(state, input, seal, transition) {
1961
2433
  const rowIteration = iterationOf(state, transition);
1962
2434
  const prior = state.attempts.filter((past) => past.invocation_id === seal);
1963
2435
  const twin = prior.find((past) => past.request_digest === digest);
1964
- // The next ordinal, asked with the SAME expression `AttemptLedger.record`
1965
- // validates it against. Counting rows instead is a second reading of one fact,
1966
- // and the two part the moment a resend is persisted: a recognized resend is
1967
- // re-evaluated and its row kept wearing the twin's ordinal — deliberately, so
1968
- // it still counts toward the cap — so the rows say two while the highest
1969
- // ordinal says one, and the next answer arrives as attempt 3 where the ledger
1970
- // accepts only 2. Nobody can answer that boundary, and `recover` reads the
1971
- // chain as coherent because replaying two identical rows never fails.
2436
+ // A twin reuses its ordinal only for the ledger's resend verdict; reject does
2437
+ // not persist it again. A new answer takes highest + 1, never the row count of
2438
+ // a legacy ledger that may still contain identical twins.
1972
2439
  const highest = Math.max(0, ...prior.map((past) => past.attempt));
1973
2440
  const attempt = twin?.attempt ?? highest + 1;
1974
2441
  const parent = attempt === 1