@tacuchi/agent-workflow-cli 25.7.0 → 25.9.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 (580) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +5 -1
  3. package/dist/adapters/git-cli.js +311 -24
  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 +124 -43
  12. package/dist/application/capability/design-handler.js.map +1 -1
  13. package/dist/application/capability/dispatcher.js +4 -1
  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 +26 -10
  18. package/dist/application/capability/readiness.js.map +1 -1
  19. package/dist/application/capability/wrapper.js +10 -3
  20. package/dist/application/capability/wrapper.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 +171 -27
  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 +4 -1
  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 +10 -2
  70. package/dist/application/doctor/provider-mcps.js.map +1 -1
  71. package/dist/application/doctor/provider-skills.js +25 -0
  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 +47 -3
  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/doctor/skills-toml-migrate.js +76 -0
  82. package/dist/application/doctor/skills-toml-migrate.js.map +1 -0
  83. package/dist/application/export-catalog-check.js +200 -0
  84. package/dist/application/export-catalog-check.js.map +1 -0
  85. package/dist/application/export-service.js +716 -79
  86. package/dist/application/export-service.js.map +1 -1
  87. package/dist/application/fix-git-service.js +285 -64
  88. package/dist/application/fix-git-service.js.map +1 -1
  89. package/dist/application/flow/advance.js +241 -30
  90. package/dist/application/flow/advance.js.map +1 -1
  91. package/dist/application/flow/annul-service.js +3 -1
  92. package/dist/application/flow/annul-service.js.map +1 -1
  93. package/dist/application/flow/batch-commit.js +189 -0
  94. package/dist/application/flow/batch-commit.js.map +1 -0
  95. package/dist/application/flow/checkout-observation.js +110 -8
  96. package/dist/application/flow/checkout-observation.js.map +1 -1
  97. package/dist/application/flow/close-artifacts.js +105 -0
  98. package/dist/application/flow/close-artifacts.js.map +1 -0
  99. package/dist/application/flow/close-at-boundary.js +155 -0
  100. package/dist/application/flow/close-at-boundary.js.map +1 -0
  101. package/dist/application/flow/close-document-guidance.js +70 -0
  102. package/dist/application/flow/close-document-guidance.js.map +1 -0
  103. package/dist/application/flow/flow-service.js +49 -5
  104. package/dist/application/flow/flow-service.js.map +1 -1
  105. package/dist/application/flow/internal-actions.js +361 -80
  106. package/dist/application/flow/internal-actions.js.map +1 -1
  107. package/dist/application/flow/internal-drive.js +47 -19
  108. package/dist/application/flow/internal-drive.js.map +1 -1
  109. package/dist/application/flow/plan-entry.js +82 -0
  110. package/dist/application/flow/plan-entry.js.map +1 -0
  111. package/dist/application/flow/prove.js +32 -1
  112. package/dist/application/flow/prove.js.map +1 -1
  113. package/dist/application/flow/quick-checkouts.js +48 -0
  114. package/dist/application/flow/quick-checkouts.js.map +1 -0
  115. package/dist/application/flow/reinfer-batch.js +177 -0
  116. package/dist/application/flow/reinfer-batch.js.map +1 -0
  117. package/dist/application/flow/reopen-run.js +159 -0
  118. package/dist/application/flow/reopen-run.js.map +1 -0
  119. package/dist/application/flow/run-journey.js +11 -8
  120. package/dist/application/flow/run-journey.js.map +1 -1
  121. package/dist/application/flow/run-projection.js +16 -5
  122. package/dist/application/flow/run-projection.js.map +1 -1
  123. package/dist/application/flow/run-state-service.js +3 -1
  124. package/dist/application/flow/run-state-service.js.map +1 -1
  125. package/dist/application/flow/submit.js +731 -154
  126. package/dist/application/flow/submit.js.map +1 -1
  127. package/dist/application/generate-launch-service.js +16 -6
  128. package/dist/application/generate-launch-service.js.map +1 -1
  129. package/dist/application/git-flow-service.js +34 -3
  130. package/dist/application/git-flow-service.js.map +1 -1
  131. package/dist/application/history-publications.js +24 -0
  132. package/dist/application/history-publications.js.map +1 -1
  133. package/dist/application/history-reconcile-service.js +45 -0
  134. package/dist/application/history-reconcile-service.js.map +1 -0
  135. package/dist/application/history-table.js +61 -12
  136. package/dist/application/history-table.js.map +1 -1
  137. package/dist/application/history-update-service.js +25 -4
  138. package/dist/application/history-update-service.js.map +1 -1
  139. package/dist/application/hook-branch-check.js +25 -3
  140. package/dist/application/hook-branch-check.js.map +1 -1
  141. package/dist/application/hook-git-commit-advisor.js +3 -4
  142. package/dist/application/hook-git-commit-advisor.js.map +1 -1
  143. package/dist/application/hook-sql-mutation-guard.js +147 -12
  144. package/dist/application/hook-sql-mutation-guard.js.map +1 -1
  145. package/dist/application/hook-turn-start.js +51 -0
  146. package/dist/application/hook-turn-start.js.map +1 -0
  147. package/dist/application/host-memory/codex.js +1 -1
  148. package/dist/application/host-memory/codex.js.map +1 -1
  149. package/dist/application/hub-registry.js +135 -0
  150. package/dist/application/hub-registry.js.map +1 -0
  151. package/dist/application/local-proposal.js +72 -19
  152. package/dist/application/local-proposal.js.map +1 -1
  153. package/dist/application/mcp-host-reader.js +1 -1
  154. package/dist/application/mcp-host-reader.js.map +1 -1
  155. package/dist/application/mcp-host-writer.js +16 -10
  156. package/dist/application/mcp-host-writer.js.map +1 -1
  157. package/dist/application/merge-state-service.js +150 -34
  158. package/dist/application/merge-state-service.js.map +1 -1
  159. package/dist/application/multiroot/claude.js +17 -10
  160. package/dist/application/multiroot/claude.js.map +1 -1
  161. package/dist/application/multiroot/codex.js +17 -11
  162. package/dist/application/multiroot/codex.js.map +1 -1
  163. package/dist/application/multiroot-service.js +52 -28
  164. package/dist/application/multiroot-service.js.map +1 -1
  165. package/dist/application/parsers/execution-batches.js +64 -0
  166. package/dist/application/parsers/execution-batches.js.map +1 -0
  167. package/dist/application/parsers/phases.js +23 -2
  168. package/dist/application/parsers/phases.js.map +1 -1
  169. package/dist/application/parsers/project-block.js +146 -17
  170. package/dist/application/parsers/project-block.js.map +1 -1
  171. package/dist/application/parsers/tasks.js +16 -2
  172. package/dist/application/parsers/tasks.js.map +1 -1
  173. package/dist/application/parsers/toml.js +19 -0
  174. package/dist/application/parsers/toml.js.map +1 -0
  175. package/dist/application/paths-service.js +13 -16
  176. package/dist/application/paths-service.js.map +1 -1
  177. package/dist/application/persist-service.js +118 -5
  178. package/dist/application/persist-service.js.map +1 -1
  179. package/dist/application/plan-exec-batch-service.js +162 -22
  180. package/dist/application/plan-exec-batch-service.js.map +1 -1
  181. package/dist/application/plan-exec-decision-service.js +61 -12
  182. package/dist/application/plan-exec-decision-service.js.map +1 -1
  183. package/dist/application/plan-exec-plan-diff.js +105 -0
  184. package/dist/application/plan-exec-plan-diff.js.map +1 -0
  185. package/dist/application/plan-lint-service.js +2 -0
  186. package/dist/application/plan-lint-service.js.map +1 -1
  187. package/dist/application/plan-passes.js +41 -0
  188. package/dist/application/plan-passes.js.map +1 -0
  189. package/dist/application/preparation-receipts.js +58 -0
  190. package/dist/application/preparation-receipts.js.map +1 -0
  191. package/dist/application/project-md-upsert-service.js +191 -52
  192. package/dist/application/project-md-upsert-service.js.map +1 -1
  193. package/dist/application/project-tab-data.js +40 -10
  194. package/dist/application/project-tab-data.js.map +1 -1
  195. package/dist/application/release-data/artifacts.js +15 -7
  196. package/dist/application/release-data/artifacts.js.map +1 -1
  197. package/dist/application/release-data/bundles.js +52 -6
  198. package/dist/application/release-data/bundles.js.map +1 -1
  199. package/dist/application/release-data/common.js +2 -2
  200. package/dist/application/release-data/common.js.map +1 -1
  201. package/dist/application/release-data/rollback.js +7 -0
  202. package/dist/application/release-data/rollback.js.map +1 -0
  203. package/dist/application/release-data/sessions.js +2 -2
  204. package/dist/application/release-data/sessions.js.map +1 -1
  205. package/dist/application/release-pass-ledger.js +1 -1
  206. package/dist/application/render/history-row.js +2 -0
  207. package/dist/application/render/history-row.js.map +1 -1
  208. package/dist/application/render/project-block.js +28 -11
  209. package/dist/application/render/project-block.js.map +1 -1
  210. package/dist/application/reseal-service.js +1 -0
  211. package/dist/application/reseal-service.js.map +1 -1
  212. package/dist/application/resume-service.js +55 -8
  213. package/dist/application/resume-service.js.map +1 -1
  214. package/dist/application/retirement/apply.js +25 -7
  215. package/dist/application/retirement/apply.js.map +1 -1
  216. package/dist/application/retirement/attribution.js +121 -48
  217. package/dist/application/retirement/attribution.js.map +1 -1
  218. package/dist/application/retirement/prepare.js +17 -11
  219. package/dist/application/retirement/prepare.js.map +1 -1
  220. package/dist/application/retirement/preview.js +2 -1
  221. package/dist/application/retirement/preview.js.map +1 -1
  222. package/dist/application/self/codex-plugin.js +1 -1
  223. package/dist/application/self/codex-plugin.js.map +1 -1
  224. package/dist/application/self/host-states.js +3 -3
  225. package/dist/application/self/host-states.js.map +1 -1
  226. package/dist/application/self/install-hooks.js +1 -1
  227. package/dist/application/self/install-hooks.js.map +1 -1
  228. package/dist/application/self/install-skill.js +35 -11
  229. package/dist/application/self/install-skill.js.map +1 -1
  230. package/dist/application/self/install-targets.js +73 -2
  231. package/dist/application/self/install-targets.js.map +1 -1
  232. package/dist/application/self/mcp-config.js +19 -11
  233. package/dist/application/self/mcp-config.js.map +1 -1
  234. package/dist/application/self/opencode-plugin.js +1 -0
  235. package/dist/application/self/opencode-plugin.js.map +1 -1
  236. package/dist/application/self/uninstall.js +22 -8
  237. package/dist/application/self/uninstall.js.map +1 -1
  238. package/dist/application/semantic-operation/protocol.js +66 -5
  239. package/dist/application/semantic-operation/protocol.js.map +1 -1
  240. package/dist/application/session-archive-service.js +113 -0
  241. package/dist/application/session-archive-service.js.map +1 -0
  242. package/dist/application/session-binding-service.js +35 -0
  243. package/dist/application/session-binding-service.js.map +1 -1
  244. package/dist/application/session-close-service.js +390 -54
  245. package/dist/application/session-close-service.js.map +1 -1
  246. package/dist/application/session-create-service.js +97 -5
  247. package/dist/application/session-create-service.js.map +1 -1
  248. package/dist/application/session-custody-recorder.js +15 -12
  249. package/dist/application/session-custody-recorder.js.map +1 -1
  250. package/dist/application/session-custody-service.js +1 -0
  251. package/dist/application/session-custody-service.js.map +1 -1
  252. package/dist/application/session-narrative.js +51 -12
  253. package/dist/application/session-narrative.js.map +1 -1
  254. package/dist/application/session-pause-service.js +46 -0
  255. package/dist/application/session-pause-service.js.map +1 -0
  256. package/dist/application/session-resolver.js +36 -11
  257. package/dist/application/session-resolver.js.map +1 -1
  258. package/dist/application/session-resume-service.js +190 -20
  259. package/dist/application/session-resume-service.js.map +1 -1
  260. package/dist/application/sessions-service.js +4 -0
  261. package/dist/application/sessions-service.js.map +1 -1
  262. package/dist/application/sessions-sweep-service.js +77 -0
  263. package/dist/application/sessions-sweep-service.js.map +1 -0
  264. package/dist/application/settle-service.js +3 -1
  265. package/dist/application/settle-service.js.map +1 -1
  266. package/dist/application/skills-resolver-service.js +1 -1
  267. package/dist/application/skills-resolver-service.js.map +1 -1
  268. package/dist/application/source-add-service.js +37 -0
  269. package/dist/application/source-add-service.js.map +1 -0
  270. package/dist/application/source-boundary-policy.js +97 -8
  271. package/dist/application/source-boundary-policy.js.map +1 -1
  272. package/dist/application/source-pipeline.js +68 -0
  273. package/dist/application/source-pipeline.js.map +1 -0
  274. package/dist/application/source-remove-service.js +15 -3
  275. package/dist/application/source-remove-service.js.map +1 -1
  276. package/dist/application/sources-service.js +71 -16
  277. package/dist/application/sources-service.js.map +1 -1
  278. package/dist/application/status-service.js +55 -5
  279. package/dist/application/status-service.js.map +1 -1
  280. package/dist/application/unit-dependencies.js +100 -0
  281. package/dist/application/unit-dependencies.js.map +1 -0
  282. package/dist/application/unit-membership.js +37 -0
  283. package/dist/application/unit-membership.js.map +1 -0
  284. package/dist/application/unit-residue.js +72 -0
  285. package/dist/application/unit-residue.js.map +1 -0
  286. package/dist/application/visibility-doctor-service.js +66 -4
  287. package/dist/application/visibility-doctor-service.js.map +1 -1
  288. package/dist/application/working-branch-service.js +6 -0
  289. package/dist/application/working-branch-service.js.map +1 -1
  290. package/dist/application/workline-graph.js +1 -1
  291. package/dist/application/workline-graph.js.map +1 -1
  292. package/dist/application/workline-index-service.js +118 -17
  293. package/dist/application/workline-index-service.js.map +1 -1
  294. package/dist/application/workspace-commit-service.js +218 -0
  295. package/dist/application/workspace-commit-service.js.map +1 -0
  296. package/dist/application/workspace-init-service.js +64 -34
  297. package/dist/application/workspace-init-service.js.map +1 -1
  298. package/dist/application/workspace-local-config.js +90 -0
  299. package/dist/application/workspace-local-config.js.map +1 -0
  300. package/dist/application/workspace-materialization-service.js +52 -3
  301. package/dist/application/workspace-materialization-service.js.map +1 -1
  302. package/dist/application/workspace-migrate/apply.js +525 -4
  303. package/dist/application/workspace-migrate/apply.js.map +1 -1
  304. package/dist/application/workspace-migrate/markers.js +9 -6
  305. package/dist/application/workspace-migrate/markers.js.map +1 -1
  306. package/dist/application/workspace-migrate/plan.js +97 -5
  307. package/dist/application/workspace-migrate/plan.js.map +1 -1
  308. package/dist/application/workspace-move-service.js +270 -0
  309. package/dist/application/workspace-move-service.js.map +1 -0
  310. package/dist/application/workspace-untrack-service.js +28 -0
  311. package/dist/application/workspace-untrack-service.js.map +1 -0
  312. package/dist/application/worktree-service.js +654 -105
  313. package/dist/application/worktree-service.js.map +1 -1
  314. package/dist/cli/ascii.js +86 -0
  315. package/dist/cli/ascii.js.map +1 -0
  316. package/dist/cli/commands/add-source.js +38 -0
  317. package/dist/cli/commands/add-source.js.map +1 -0
  318. package/dist/cli/commands/amend.js +14 -2
  319. package/dist/cli/commands/amend.js.map +1 -1
  320. package/dist/cli/commands/capability.js +26 -0
  321. package/dist/cli/commands/capability.js.map +1 -1
  322. package/dist/cli/commands/check-branch.js +1 -0
  323. package/dist/cli/commands/check-branch.js.map +1 -1
  324. package/dist/cli/commands/checkpoint-read.js +1 -0
  325. package/dist/cli/commands/checkpoint-read.js.map +1 -1
  326. package/dist/cli/commands/checkpoint-write.js +4 -1
  327. package/dist/cli/commands/checkpoint-write.js.map +1 -1
  328. package/dist/cli/commands/claims.js +1 -0
  329. package/dist/cli/commands/claims.js.map +1 -1
  330. package/dist/cli/commands/code-scan.js +1 -0
  331. package/dist/cli/commands/code-scan.js.map +1 -1
  332. package/dist/cli/commands/context-budget.js +1 -0
  333. package/dist/cli/commands/context-budget.js.map +1 -1
  334. package/dist/cli/commands/context-plan.js +1 -0
  335. package/dist/cli/commands/context-plan.js.map +1 -1
  336. package/dist/cli/commands/cut-intent.js +1 -0
  337. package/dist/cli/commands/cut-intent.js.map +1 -1
  338. package/dist/cli/commands/designs.js +15 -5
  339. package/dist/cli/commands/designs.js.map +1 -1
  340. package/dist/cli/commands/dev-only.js +22 -5
  341. package/dist/cli/commands/dev-only.js.map +1 -1
  342. package/dist/cli/commands/doc-branch.js +184 -0
  343. package/dist/cli/commands/doc-branch.js.map +1 -0
  344. package/dist/cli/commands/doctor.js +9 -0
  345. package/dist/cli/commands/doctor.js.map +1 -1
  346. package/dist/cli/commands/export.js +89 -16
  347. package/dist/cli/commands/export.js.map +1 -1
  348. package/dist/cli/commands/fix-git.js +89 -13
  349. package/dist/cli/commands/fix-git.js.map +1 -1
  350. package/dist/cli/commands/flow.js +129 -13
  351. package/dist/cli/commands/flow.js.map +1 -1
  352. package/dist/cli/commands/generate-launch.js +15 -2
  353. package/dist/cli/commands/generate-launch.js.map +1 -1
  354. package/dist/cli/commands/git-flow.js +1 -0
  355. package/dist/cli/commands/git-flow.js.map +1 -1
  356. package/dist/cli/commands/history-update.js +7 -9
  357. package/dist/cli/commands/history-update.js.map +1 -1
  358. package/dist/cli/commands/history.js +14 -0
  359. package/dist/cli/commands/history.js.map +1 -0
  360. package/dist/cli/commands/hook.js +26 -5
  361. package/dist/cli/commands/hook.js.map +1 -1
  362. package/dist/cli/commands/host-doctor.js +1 -0
  363. package/dist/cli/commands/host-doctor.js.map +1 -1
  364. package/dist/cli/commands/host-memory.js +2 -9
  365. package/dist/cli/commands/host-memory.js.map +1 -1
  366. package/dist/cli/commands/index.js +18 -1
  367. package/dist/cli/commands/index.js.map +1 -1
  368. package/dist/cli/commands/mcp.js +30 -4
  369. package/dist/cli/commands/mcp.js.map +1 -1
  370. package/dist/cli/commands/merge-state.js +3 -2
  371. package/dist/cli/commands/merge-state.js.map +1 -1
  372. package/dist/cli/commands/multiroot.js +16 -0
  373. package/dist/cli/commands/multiroot.js.map +1 -1
  374. package/dist/cli/commands/persist.js +7 -5
  375. package/dist/cli/commands/persist.js.map +1 -1
  376. package/dist/cli/commands/plan.js +1 -0
  377. package/dist/cli/commands/plan.js.map +1 -1
  378. package/dist/cli/commands/plugin-cache.js +4 -0
  379. package/dist/cli/commands/plugin-cache.js.map +1 -1
  380. package/dist/cli/commands/plugin-doctor.js +3 -0
  381. package/dist/cli/commands/plugin-doctor.js.map +1 -1
  382. package/dist/cli/commands/project-md-upsert.js +8 -3
  383. package/dist/cli/commands/project-md-upsert.js.map +1 -1
  384. package/dist/cli/commands/release-data.js +12 -0
  385. package/dist/cli/commands/release-data.js.map +1 -1
  386. package/dist/cli/commands/release-pass.js +13 -3
  387. package/dist/cli/commands/release-pass.js.map +1 -1
  388. package/dist/cli/commands/remove-source.js +1 -0
  389. package/dist/cli/commands/remove-source.js.map +1 -1
  390. package/dist/cli/commands/reseal.js +1 -0
  391. package/dist/cli/commands/reseal.js.map +1 -1
  392. package/dist/cli/commands/resume-summary.js +5 -0
  393. package/dist/cli/commands/resume-summary.js.map +1 -1
  394. package/dist/cli/commands/resume.js +13 -3
  395. package/dist/cli/commands/resume.js.map +1 -1
  396. package/dist/cli/commands/retirement.js +1 -0
  397. package/dist/cli/commands/retirement.js.map +1 -1
  398. package/dist/cli/commands/self.js +39 -1
  399. package/dist/cli/commands/self.js.map +1 -1
  400. package/dist/cli/commands/session-artifacts.js +1 -0
  401. package/dist/cli/commands/session-artifacts.js.map +1 -1
  402. package/dist/cli/commands/session-close.js +185 -31
  403. package/dist/cli/commands/session-close.js.map +1 -1
  404. package/dist/cli/commands/session-create.js +4 -1
  405. package/dist/cli/commands/session-create.js.map +1 -1
  406. package/dist/cli/commands/session-pause.js +14 -0
  407. package/dist/cli/commands/session-pause.js.map +1 -0
  408. package/dist/cli/commands/session-resume.js +1 -0
  409. package/dist/cli/commands/session-resume.js.map +1 -1
  410. package/dist/cli/commands/sessions.js +16 -2
  411. package/dist/cli/commands/sessions.js.map +1 -1
  412. package/dist/cli/commands/set-branch.js +29 -4
  413. package/dist/cli/commands/set-branch.js.map +1 -1
  414. package/dist/cli/commands/set-edit-mode.js +20 -0
  415. package/dist/cli/commands/set-edit-mode.js.map +1 -0
  416. package/dist/cli/commands/set-pipeline.js +35 -0
  417. package/dist/cli/commands/set-pipeline.js.map +1 -0
  418. package/dist/cli/commands/settle.js +1 -0
  419. package/dist/cli/commands/settle.js.map +1 -1
  420. package/dist/cli/commands/skill-index.js +1 -0
  421. package/dist/cli/commands/skill-index.js.map +1 -1
  422. package/dist/cli/commands/skills.js +1 -0
  423. package/dist/cli/commands/skills.js.map +1 -1
  424. package/dist/cli/commands/sources.js +5 -0
  425. package/dist/cli/commands/sources.js.map +1 -1
  426. package/dist/cli/commands/stack.js +1 -0
  427. package/dist/cli/commands/stack.js.map +1 -1
  428. package/dist/cli/commands/status.js +36 -2
  429. package/dist/cli/commands/status.js.map +1 -1
  430. package/dist/cli/commands/tool.js +5 -21
  431. package/dist/cli/commands/tool.js.map +1 -1
  432. package/dist/cli/commands/unknown-flags.js +68 -16
  433. package/dist/cli/commands/unknown-flags.js.map +1 -1
  434. package/dist/cli/commands/visibility.js +2 -0
  435. package/dist/cli/commands/visibility.js.map +1 -1
  436. package/dist/cli/commands/workspace-commit.js +62 -0
  437. package/dist/cli/commands/workspace-commit.js.map +1 -0
  438. package/dist/cli/commands/workspace-init.js +51 -2
  439. package/dist/cli/commands/workspace-init.js.map +1 -1
  440. package/dist/cli/commands/workspace-migrate.js +31 -14
  441. package/dist/cli/commands/workspace-migrate.js.map +1 -1
  442. package/dist/cli/commands/workspace-move.js +42 -0
  443. package/dist/cli/commands/workspace-move.js.map +1 -0
  444. package/dist/cli/commands/worktree.js +5 -4
  445. package/dist/cli/commands/worktree.js.map +1 -1
  446. package/dist/cli/context-id.js +6 -0
  447. package/dist/cli/context-id.js.map +1 -1
  448. package/dist/cli/full-cli.js +555 -0
  449. package/dist/cli/full-cli.js.map +1 -0
  450. package/dist/cli/help-groups.js +108 -14
  451. package/dist/cli/help-groups.js.map +1 -1
  452. package/dist/cli/hook-stdin-cache.js +12 -0
  453. package/dist/cli/hook-stdin-cache.js.map +1 -0
  454. package/dist/cli/main.js +47 -471
  455. package/dist/cli/main.js.map +1 -1
  456. package/dist/cli/output-mode.js +11 -2
  457. package/dist/cli/output-mode.js.map +1 -1
  458. package/dist/cli/parser.js +18 -5
  459. package/dist/cli/parser.js.map +1 -1
  460. package/dist/cli/registry.js.map +1 -1
  461. package/dist/cli/render.js +30 -1
  462. package/dist/cli/render.js.map +1 -1
  463. package/dist/cli/tui/app.js +1 -1
  464. package/dist/cli/tui/app.js.map +1 -1
  465. package/dist/cli/tui/components/workspace-init-form.js +4 -1
  466. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  467. package/dist/cli/tui/data/recommended-skills.js +2 -2
  468. package/dist/cli/tui/data/recommended-skills.js.map +1 -1
  469. package/dist/cli/tui/data/workflow-content.js +6 -1
  470. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  471. package/dist/cli/tui/tabs/mcp-tab.js +4 -3
  472. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  473. package/dist/cli/tui/tabs/project-tab.js +7 -2
  474. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  475. package/dist/dist-manifest.json +4117 -0
  476. package/dist/domain/capability/protocol.js.map +1 -1
  477. package/dist/domain/decision-note.js +13 -0
  478. package/dist/domain/decision-note.js.map +1 -1
  479. package/dist/domain/design/artifact.js +22 -3
  480. package/dist/domain/design/artifact.js.map +1 -1
  481. package/dist/domain/design/capability.js +1 -0
  482. package/dist/domain/design/capability.js.map +1 -1
  483. package/dist/domain/design/expansion.js +1 -1
  484. package/dist/domain/design/expansion.js.map +1 -1
  485. package/dist/domain/design/maturity.js +16 -0
  486. package/dist/domain/design/maturity.js.map +1 -1
  487. package/dist/domain/design/reference.js +5 -0
  488. package/dist/domain/design/reference.js.map +1 -1
  489. package/dist/domain/doctor/operations.js +28 -2
  490. package/dist/domain/doctor/operations.js.map +1 -1
  491. package/dist/domain/effective-contract.js +3 -1
  492. package/dist/domain/effective-contract.js.map +1 -1
  493. package/dist/domain/elicitation.js +1 -1
  494. package/dist/domain/flow/answer.js +273 -130
  495. package/dist/domain/flow/answer.js.map +1 -1
  496. package/dist/domain/flow/authority.js +330 -44
  497. package/dist/domain/flow/authority.js.map +1 -1
  498. package/dist/domain/flow/batch-review.js +65 -0
  499. package/dist/domain/flow/batch-review.js.map +1 -0
  500. package/dist/domain/flow/decision-schema.js +47 -0
  501. package/dist/domain/flow/decision-schema.js.map +1 -0
  502. package/dist/domain/flow/directive.js +16 -0
  503. package/dist/domain/flow/directive.js.map +1 -1
  504. package/dist/domain/flow/execution-result.js +169 -9
  505. package/dist/domain/flow/execution-result.js.map +1 -1
  506. package/dist/domain/flow/route.js.map +1 -1
  507. package/dist/domain/flow/rules.js +5 -6
  508. package/dist/domain/flow/rules.js.map +1 -1
  509. package/dist/domain/flow/run-state.js +531 -47
  510. package/dist/domain/flow/run-state.js.map +1 -1
  511. package/dist/domain/flow/test-run-evidence.js +285 -0
  512. package/dist/domain/flow/test-run-evidence.js.map +1 -0
  513. package/dist/domain/flow/unchanged-phase.js +15 -0
  514. package/dist/domain/flow/unchanged-phase.js.map +1 -0
  515. package/dist/domain/harnesses.js +22 -6
  516. package/dist/domain/harnesses.js.map +1 -1
  517. package/dist/domain/isolation-unit.js +2 -2
  518. package/dist/domain/isolation-unit.js.map +1 -1
  519. package/dist/domain/lineage.js +7 -2
  520. package/dist/domain/lineage.js.map +1 -1
  521. package/dist/domain/obligation-correspondence.js +4 -2
  522. package/dist/domain/obligation-correspondence.js.map +1 -1
  523. package/dist/domain/plan-handoff.js +5 -0
  524. package/dist/domain/plan-handoff.js.map +1 -0
  525. package/dist/domain/proposal.js +18 -7
  526. package/dist/domain/proposal.js.map +1 -1
  527. package/dist/domain/reservation.js +5 -3
  528. package/dist/domain/reservation.js.map +1 -1
  529. package/dist/domain/retirement/proposal.js +1 -0
  530. package/dist/domain/retirement/proposal.js.map +1 -1
  531. package/dist/domain/session/custody.js +10 -6
  532. package/dist/domain/session/custody.js.map +1 -1
  533. package/dist/domain/session/narrative.js +4 -1
  534. package/dist/domain/session/narrative.js.map +1 -1
  535. package/dist/domain/skills.js +2 -0
  536. package/dist/domain/skills.js.map +1 -1
  537. package/dist/domain/sql-lexer.js +15 -2
  538. package/dist/domain/sql-lexer.js.map +1 -1
  539. package/dist/runtime/namespace-resolver.js +4 -0
  540. package/dist/runtime/namespace-resolver.js.map +1 -1
  541. package/dist/runtime/workline-marker.js +35 -8
  542. package/dist/runtime/workline-marker.js.map +1 -1
  543. package/dist/runtime/workspace-resolution.js +123 -0
  544. package/dist/runtime/workspace-resolution.js.map +1 -0
  545. package/package.json +5 -4
  546. package/skills/w/SKILL.md +3 -1
  547. package/skills/w/artifacts/artifacts-core/CHECKPOINT.md +1 -1
  548. package/skills/w/artifacts/artifacts-core/SCRIPTS.rollback.sql +8 -0
  549. package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +4 -8
  550. package/skills/w/commands/export-diagrams.md +3 -3
  551. package/skills/w/commands/export-manuals.md +6 -6
  552. package/skills/w/commands/export-reports.md +3 -3
  553. package/skills/w/commands/export-scripts.md +5 -5
  554. package/skills/w/commands/fix-git.md +12 -13
  555. package/skills/w/commands/persist.md +2 -2
  556. package/skills/w/commands/plan-exec.md +4 -2
  557. package/skills/w/commands/spec-new.md +1 -1
  558. package/skills/w/commands/workspace-init.md +2 -0
  559. package/skills/w/context/MANIFEST.json +1 -1
  560. package/skills/w/exports/export-diagrams/EXPORT.md +3 -3
  561. package/skills/w/exports/export-manuals/EXPORT.md +14 -16
  562. package/skills/w/exports/export-reports/EXPORT.md +3 -3
  563. package/skills/w/exports/export-scripts/EXPORT.md +45 -21
  564. package/skills/w/harness/HARNESS.md +5 -3
  565. package/skills/w/hooks/hooks.template.json +6 -4
  566. package/skills/w/loops/CHASSIS.md +12 -6
  567. package/skills/w/loops/CODE-POLICIES.md +7 -4
  568. package/skills/w/loops/plan-exec-loop/LOOP.md +23 -22
  569. package/skills/w/loops/plan-new-loop/LOOP.md +23 -31
  570. package/skills/w/loops/plan-refine-loop/LOOP.md +8 -2
  571. package/skills/w/loops/quick-loop/LOOP.md +6 -5
  572. package/skills/w/modules/DESIGN-REFERENCES.md +9 -11
  573. package/skills/w/modules/PLAN-EXECUTION-BATCHES.md +4 -4
  574. package/skills/w/modules/SESSION-NUMBERING.md +2 -0
  575. package/skills/w/roles/design/CONTRACT.md +16 -7
  576. package/skills/w/roles/design/ROLE.md +4 -2
  577. package/skills/w/roles/git/ROLE.md +16 -9
  578. package/skills/w/roles/research/ROLE.md +1 -1
  579. package/skills/w/roles/sql/ROLE.md +16 -14
  580. package/skills/w/schemas/design/ui-screen.v1.schema.json +9 -0
@@ -25,32 +25,88 @@ import { authorizeEffects, touchesTheWorld, } from "../../domain/capability/effe
25
25
  import { AttemptLedger } from "../../domain/capability/protocol.js";
26
26
  import { DEFAULT_CORE_DOCS_CANON, coreDocumentKindForPath } from "../../domain/docs-canon.js";
27
27
  import { claimedSeal, parseFlowAnswer, spendsAttempt, } from "../../domain/flow/answer.js";
28
- import { FIX_PREVIEW_TRANSITION, SETTLEMENT_READINGS, actionOf, alternativesOf, approvalGrantOf, effectsOf, internalActionOf, isRouteEvaluation, proposalContractOf, publishApprovalOf, routeControlOf, scopesSources, } from "../../domain/flow/authority.js";
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
- import { executionVerdict } from "../../domain/flow/execution-result.js";
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, currentBatchIteration, restatesLastEvent, settlementAmbiguous, withApproval, withAttempt, withBoundary, withContinuation, withDecisionPreparation, withEvent, withFixPreview, withHandoff, withObservation, withPlanExecBatchStageForTransition, withProposal, 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
- import { observeCheckout, withObservedCheckouts } from "./checkout-observation.js";
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
- import { applyUnderLock, locateRun } from "./run-state-service.js";
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,15 +176,78 @@ 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,
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),
129
191
  baselines: plans.baselines,
130
192
  };
131
193
  }
194
+ /**
195
+ * The scoped fingerprints a phase validation is judged against, read before the
196
+ * lock like every other observation. The run read here only decides WHETHER to
197
+ * measure: the decision under the lock re-reads it, and a run that moved in
198
+ * between answers a different seal and is refused as stale anyway.
199
+ */
200
+ async function observeBatchSources(fs, paths, session, git) {
201
+ if (git === undefined)
202
+ return null;
203
+ // With a reader present, "not measured" is an empty map and never `null`: the
204
+ // judgment then finds every source unobservable and refuses, instead of
205
+ // skipping the one comparison the phase validation exists for.
206
+ const read = await readRun(fs, locateRun(paths, session));
207
+ if (!read.ok || read.state.boundary !== PHASE_VALIDATION || read.state.scope === null) {
208
+ return {};
209
+ }
210
+ const observed = await observeScopedFingerprints(fs, git, paths, session, read.state.scope.sources);
211
+ return observed.ok ? observed.base : {};
212
+ }
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
+ }
132
251
  /**
133
252
  * Judge the plan bytes a proposal is carrying, and derive the seal each one gets.
134
253
  *
@@ -209,8 +328,10 @@ async function observeScope(fs, paths, raw) {
209
328
  declared: null,
210
329
  plan: null,
211
330
  sources: null,
331
+ final_validation: null,
212
332
  boundary_failures: null,
213
333
  plan_error: null,
334
+ isolation: "unit",
214
335
  };
215
336
  let parsed;
216
337
  try {
@@ -231,7 +352,7 @@ async function observeScope(fs, paths, raw) {
231
352
  const named = decisions.plan;
232
353
  const plan = typeof named === "string" ? named.trim() : null;
233
354
  if (plan === null || plan.length === 0 || !checkSafeRelativePath(plan).ok) {
234
- return { ...empty, declared, plan };
355
+ return { ...empty, declared, plan, isolation: block?.edit_mode ?? "unit" };
235
356
  }
236
357
  const canon = await resolveCoreDocsCanon(fs, paths);
237
358
  if (!canon.ok)
@@ -255,8 +376,12 @@ async function observeScope(fs, paths, raw) {
255
376
  declared,
256
377
  plan,
257
378
  sources: sourceAliasesOfPlan(text),
379
+ final_validation: resolveFinalValidation(text, sourceAliasesOfPlan(text), await readSourcePipelines(fs, paths)),
258
380
  boundary_failures: planGrammarAtEntry(text, declared),
259
381
  plan_error: null,
382
+ isolation: /^> Aislamiento: unidad\s*$/m.test(text.split(/^## /m, 1)[0] ?? "")
383
+ ? "unit"
384
+ : (block?.edit_mode ?? "unit"),
260
385
  };
261
386
  }
262
387
  /**
@@ -268,7 +393,7 @@ async function observeScope(fs, paths, raw) {
268
393
  */
269
394
  async function observeCheckouts(fs, paths, session, git) {
270
395
  if (git === undefined)
271
- return null;
396
+ return { states: null, failure: null };
272
397
  // The SAME resolution the directive published, from the same function: two
273
398
  // callers computing this apart is what made a rejection claim the tree moved
274
399
  // when all that differed was the directory each side measured.
@@ -277,11 +402,24 @@ async function observeCheckouts(fs, paths, session, git) {
277
402
  for (const candidate of candidates) {
278
403
  // An unreadable checkout is deliberately absent from the eligible set: a proof
279
404
  // against it fails closed instead of being treated as a clean tree.
280
- 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
+ }
281
419
  if (state !== null)
282
420
  states.push(state);
283
421
  }
284
- return states;
422
+ return { states, failure: null };
285
423
  }
286
424
  /**
287
425
  * The alias→root pairs worth publishing: the ones actually OBSERVED.
@@ -348,11 +486,25 @@ async function observeDestinations(fs, paths, raw) {
348
486
  return snapshot;
349
487
  }
350
488
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: resend, drift, admission, scope, sealing, preview, grant and handoff checks are one intentional precedence contract.
351
- async function decide(fs, paths, state, input, snapshot) {
489
+ async function decide(fs, paths, state, input, snapshot, checkOnly = false) {
352
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
+ }
353
504
  const incoherent = checkAgainstJourney(state, journey);
354
- if (incoherent !== null)
355
- return { ok: false, failure: incoherent };
505
+ if (incoherent !== null) {
506
+ return reject(state, resolveBoundary(state, journey), incoherent.message, incoherent);
507
+ }
356
508
  const resolved = resolveBoundary(state, journey);
357
509
  if (resolved.stopped === null) {
358
510
  return reject(state, resolved, "el recorrido ya terminó: no hay frontera que responder", {
@@ -385,6 +537,16 @@ async function decide(fs, paths, state, input, snapshot) {
385
537
  action: "corré 'aw flow advance': el CLI corre una vez más la operación interna y, si reconoce su efecto, avanza sin cobrar",
386
538
  });
387
539
  }
540
+ // An internal action is credited only by the driver that ran it: that is what
541
+ // leaves its `executed` event in the trace. A result from outside, however well
542
+ // formed, is a claim nobody verified — and applying it used to publish nothing
543
+ // while marking the step done.
544
+ if (resolved.kind === "execution" && internalActionOf(resolved.stopped) !== null) {
545
+ return reject(state, resolved, `'${resolved.stopped.id}' la ejecuta el CLI: un resultado externo no la acredita`, {
546
+ code: "FLOW_INTERNAL_ACTION_EXTERNAL_RESULT",
547
+ action: `corré 'aw flow advance --session ${state.session}': el CLI corre la operación y la acredita; si la corrida no sale de esta frontera, '${restartInvocation(state.session)}' la destraba`,
548
+ }, cost);
549
+ }
388
550
  // The action the run is waiting on is the one that was EMITTED, and the seal
389
551
  // would already refuse a result about any other. What the persisted digest adds
390
552
  // is the diagnosis: when the two differ, the invocation changed underneath a run
@@ -393,8 +555,11 @@ async function decide(fs, paths, state, input, snapshot) {
393
555
  const drifted = actionDrift(state, resolved, cost);
394
556
  if (drifted !== null)
395
557
  return drifted;
558
+ if (resolved.kind === "execution" && snapshot.checkoutFailure !== null) {
559
+ return reject(state, resolved, snapshot.checkoutFailure.message, snapshot.checkoutFailure, cost);
560
+ }
396
561
  // 2 · The boundary in force decides what is admissible.
397
- const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts);
562
+ const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts, snapshot.scoped, snapshot.preexisting, snapshot.validation_only_current);
398
563
  if ("decision" in admissible)
399
564
  return admissible.decision;
400
565
  const parsed = admissible;
@@ -408,19 +573,73 @@ async function decide(fs, paths, state, input, snapshot) {
408
573
  if (route.kind === "adjust") {
409
574
  return adjustRouteProposal(state, journey);
410
575
  }
411
- const routed = route.kind === "accept" ? withRouteDecisions(state, route.decisions) : state;
576
+ if (checkOnly && resolved.kind === "execution")
577
+ return checked(state, resolved);
578
+ const accepted = route.kind === "accept" ? withRouteDecisions(state, route.decisions) : state;
579
+ // Sealed with the transition it earned, in the same state publication: a batch
580
+ // is never seen validated without the proofs that validated it.
581
+ let routed = parsed.credit === undefined
582
+ ? accepted
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
+ }
412
602
  // The registry, not the Spanish consequence text, owns what a selected
413
603
  // alternative does. A handoff must stop before any later plan-exec row can
414
604
  // mark work, validate or commit; a decision selection is durably recorded
415
605
  // even while its registration is handled by the decision bridge below.
416
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
+ }
417
634
  if (selected?.kind === "handoff") {
418
635
  // Only `spec-refine` needs a document the run never declared: the plan's
419
636
  // scope names the plan, and the spec is one hop away through its lineage.
420
637
  const specPath = selected.destination === "spec-refine"
421
638
  ? await specPathOfScopedPlan(fs, paths, snapshot.root, state.scope?.plan ?? null)
422
639
  : null;
423
- 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);
424
643
  }
425
644
  // The fix preview is checked HERE and nowhere else: it is a declaration, so no
426
645
  // seal, no destination and no evidence would ever notice its absence, and the
@@ -438,27 +657,63 @@ async function decide(fs, paths, state, input, snapshot) {
438
657
  // preview showed. Neither ever applies the step by itself.
439
658
  const scoped = scopeFrom(routed, resolved.stopped, parsed.answer, snapshot.scope);
440
659
  if ("failure" in scoped) {
441
- 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);
442
681
  }
443
682
  // The closure-evidence gate runs over the proposed bytes BEFORE the seal: a
444
683
  // plan whose validation names no observable check of the checkout is refused
445
684
  // while it is being written, not when somebody tries to execute it.
446
685
  const evidence = planEvidenceFrom(resolved.stopped, parsed.answer, snapshot.plans);
447
686
  if (evidence !== null) {
448
- 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);
449
688
  }
450
- 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);
451
690
  if ("failure" in sealed) {
452
- 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);
453
692
  }
454
693
  const preparedForGate = await prepareDecisionForGate(fs, sealed.state, snapshot, parsed.answer, resolved, cost);
455
694
  if (!preparedForGate.ok)
456
695
  return preparedForGate.decision;
696
+ if (checkOnly)
697
+ return checked(state, resolved);
457
698
  const settlement = recordSettlement(preparedForGate.state, parsed.answer, resolved, cost);
458
699
  if (!settlement.ok)
459
700
  return settlement.decision;
460
701
  const granted = resolved.kind === "authorization" ? (resolved.authorization?.planned ?? []) : [];
461
- 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
+ }
462
717
  // All pure scope/proposal checks have now passed. A durable decision is a
463
718
  // material write, so it must never land when a later validation of the same
464
719
  // answer would refuse the boundary and leave its cursor standing.
@@ -481,6 +736,29 @@ async function decide(fs, paths, state, input, snapshot) {
481
736
  outcome: selected,
482
737
  })
483
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
+ }
484
762
  if (registered?.ok && registered.reconciliation !== null) {
485
763
  selectedState = withContinuation(selectedState, registered.reconciliation);
486
764
  }
@@ -500,12 +778,160 @@ async function decide(fs, paths, state, input, snapshot) {
500
778
  // cuya respuesta se tiraba —así que `Compactar` ahí volvía a una pregunta que
501
779
  // ya nadie podía mostrar.
502
780
  if (preview.preview !== null) {
503
- selectedState = withFixPreview(selectedState, preview.preview);
781
+ selectedState = withFixPreview(selectedState, {
782
+ ...preview.preview,
783
+ ...(snapshot.quickCheckouts === null ? {} : { checkouts: snapshot.quickCheckouts }),
784
+ });
504
785
  }
786
+ selectedState = withQuickCheckouts(selectedState, snapshot.quickCheckouts);
787
+ // A redraft recorded by `grantOf` inserted rows after this boundary, so the
788
+ // transition is applied against the journey as it now reads.
789
+ const walked = journeyForRun(selectedState);
505
790
  const outcome = holds
506
- ? holdAfterApproval(selectedState, journey, identity)
507
- : applyAndAdvance(selectedState, journey, resolved.stopped, identity, parsed.answer);
508
- return standalone === null ? outcome : withStandaloneGuidance(outcome, standalone);
791
+ ? holdAfterApproval(selectedState, walked, identity)
792
+ : applyAndAdvance(selectedState, walked, resolved.stopped, identity, parsed.answer);
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
+ };
509
935
  }
510
936
  /**
511
937
  * The declared fix preview, checked for the three things it has to say.
@@ -601,10 +1027,7 @@ function routeAnswer(state, stopped, journey, answer) {
601
1027
  };
602
1028
  }
603
1029
  const candidate = answer.decisions.route;
604
- if (!isRecord(candidate) ||
605
- !isRecord(candidate.summary) ||
606
- !isRecord(candidate.basis) ||
607
- !Array.isArray(candidate.controls)) {
1030
+ if (!routeShape(candidate)) {
608
1031
  return {
609
1032
  kind: "failure",
610
1033
  failure: {
@@ -629,17 +1052,25 @@ function routeAnswer(state, stopped, journey, answer) {
629
1052
  const declaredSummary = summary;
630
1053
  const basis = candidate.basis;
631
1054
  const basisFields = ["intention", "checkout", "conventions", "adopted_decisions"];
632
- 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) {
633
1064
  return {
634
1065
  kind: "failure",
635
1066
  failure: {
636
1067
  code: "FLOW_ROUTE_PROPOSAL_INVALID",
637
- 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]}`,
638
1069
  action: "completá cada campo de decisions.route.basis con una observación concreta",
639
1070
  },
640
1071
  };
641
1072
  }
642
- const declaredBasis = basis;
1073
+ const declaredBasis = Object.fromEntries(basisFields.map((field) => [field, asText(basis[field])]));
643
1074
  const configured = new Map(journey.flatMap((decision) => {
644
1075
  const control = routeControlOf(decision);
645
1076
  return control === null ? [] : [[decision.id, { decision, control }]];
@@ -679,17 +1110,28 @@ function routeAnswer(state, stopped, journey, answer) {
679
1110
  },
680
1111
  };
681
1112
  }
1113
+ const { decision, control } = configuredControl;
1114
+ const consequence = control.consequences[raw.disposition];
1115
+ if (consequence === undefined) {
1116
+ return {
1117
+ kind: "failure",
1118
+ failure: {
1119
+ code: "FLOW_ROUTE_HARD_GATE",
1120
+ message: `'${raw.transition}' no admite '${raw.disposition}': el registro no deja que una ruta la saltee`,
1121
+ action: "declará apply o substitute para ese control; la evidencia que acredita el paso no se omite",
1122
+ },
1123
+ };
1124
+ }
682
1125
  const substitution = routeSubstitution(raw.disposition, raw.substitution);
683
1126
  if ("failure" in substitution)
684
1127
  return { kind: "failure", failure: substitution.failure };
685
- const { decision, control } = configuredControl;
686
1128
  controls.push({
687
1129
  transition: decision.id,
688
1130
  title: decision.title,
689
1131
  disposition: raw.disposition,
690
1132
  recommendation: control.recommendation,
691
1133
  alternatives: control.consequences,
692
- consequence: control.consequences[raw.disposition],
1134
+ consequence,
693
1135
  risk: control.risk,
694
1136
  reason: raw.reason.trim(),
695
1137
  substitution: substitution.value,
@@ -774,15 +1216,26 @@ function scopeFrom(state, stopped, answer, snapshot) {
774
1216
  if (!scopesSources(stopped))
775
1217
  return { state };
776
1218
  const asked = answer.decisions.sources;
777
- const sources = Array.isArray(asked)
778
- ? 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)
779
1225
  : null;
780
- 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);
781
1228
  return {
782
- 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"),
783
1230
  };
784
1231
  }
785
- 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) {
786
1239
  return {
787
1240
  failure: invalidScope("el scope declara la misma fuente dos veces", "una unidad por alias: declarás cada fuente una sola vez"),
788
1241
  };
@@ -790,7 +1243,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
790
1243
  const plan = snapshot.plan;
791
1244
  if (plan === null || plan.length === 0) {
792
1245
  return {
793
- 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)`),
794
1247
  };
795
1248
  }
796
1249
  if (snapshot.plan_error !== null) {
@@ -803,7 +1256,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
803
1256
  };
804
1257
  }
805
1258
  const declared = snapshot.declared;
806
- if (declared === null && sources.some((alias) => alias !== "workspace")) {
1259
+ if (declared === null && aliases.some((alias) => alias !== "workspace")) {
807
1260
  return {
808
1261
  failure: {
809
1262
  code: "FLOW_SCOPE_UNKNOWN_SOURCE",
@@ -813,7 +1266,7 @@ function scopeFrom(state, stopped, answer, snapshot) {
813
1266
  };
814
1267
  }
815
1268
  const declaredAliases = declared ?? [];
816
- const unknown = sources.filter((alias) => alias !== "workspace" && !declaredAliases.includes(alias));
1269
+ const unknown = aliases.filter((alias) => alias !== "workspace" && !declaredAliases.includes(alias));
817
1270
  if (unknown.length > 0) {
818
1271
  return {
819
1272
  failure: {
@@ -839,12 +1292,19 @@ function scopeFrom(state, stopped, answer, snapshot) {
839
1292
  code: structural.code,
840
1293
  message: structural.message,
841
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
+ })),
842
1302
  },
843
1303
  };
844
1304
  }
845
1305
  const declaredByPlan = snapshot.sources;
846
- const missing = declaredByPlan.filter((alias) => !sources.includes(alias));
847
- 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));
848
1308
  if (missing.length > 0 || extra.length > 0) {
849
1309
  const difference = [
850
1310
  ...(missing.length > 0 ? [`faltan ${missing.join(", ")}`] : []),
@@ -858,7 +1318,16 @@ function scopeFrom(state, stopped, answer, snapshot) {
858
1318
  },
859
1319
  };
860
1320
  }
861
- 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
+ };
862
1331
  }
863
1332
  const SETTLEMENT_OUTCOMES = SETTLEMENT_READINGS.map((reading) => reading.outcome);
864
1333
  /** The reading each label of the question stands for — from the row's own table. */
@@ -998,7 +1467,7 @@ function ratified(declaration, outcome) {
998
1467
  : { note: declaration.note, index: declaration.index, outcome };
999
1468
  }
1000
1469
  function readDeclarations(value) {
1001
- if (!Array.isArray(value))
1470
+ if (!settlementList(value))
1002
1471
  return null;
1003
1472
  const out = [];
1004
1473
  for (const entry of value) {
@@ -1063,6 +1532,7 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1063
1532
  }, cost),
1064
1533
  };
1065
1534
  }
1535
+ const dated = withDecisionDate(state, state.decision_date ?? new Date().toISOString().slice(0, 10));
1066
1536
  // The plan of the scope decides WHICH preparation this is. A standalone plan has
1067
1537
  // no spec to compose against, so calling `preparePlanExecDecision` for it would
1068
1538
  // refuse with `FLOW_DECISION_LINEAGE_INVALID` and leave the run with handoffs as
@@ -1070,7 +1540,7 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1070
1540
  const standalone = await standaloneDecisionFor(fs, snapshot.root, plan, candidate);
1071
1541
  if (standalone !== null) {
1072
1542
  return standalone.ok
1073
- ? { ok: true, state: withDecisionPreparation(state, standalone.preparation) }
1543
+ ? { ok: true, state: withDecisionPreparation(dated, standalone.preparation) }
1074
1544
  : {
1075
1545
  ok: false,
1076
1546
  decision: reject(state, resolved, standalone.failure.message, standalone.failure, cost),
@@ -1081,23 +1551,27 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1081
1551
  session: state.session,
1082
1552
  plan,
1083
1553
  value: candidate,
1554
+ date: dated.decision_date ?? new Date().toISOString().slice(0, 10),
1084
1555
  });
1085
1556
  if (!prepared.ok) {
1557
+ const refused = reject(dated, resolveBoundary(dated, journeyForRun(dated)), prepared.failure.message, prepared.failure, cost);
1086
1558
  return {
1087
1559
  ok: false,
1088
- decision: reject(state, resolved, prepared.failure.message, prepared.failure, cost),
1560
+ decision: refused.ok ? { ...refused, persist: true } : refused,
1089
1561
  };
1090
1562
  }
1091
1563
  switch (prepared.kind) {
1092
1564
  case "settled":
1093
1565
  return {
1094
1566
  ok: true,
1095
- state: withDecisionPreparation(state, { kind: "settled", decision: prepared.decision }),
1567
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1568
+ state: withDecisionPreparation(dated, { kind: "settled", decision: prepared.decision }),
1096
1569
  };
1097
1570
  case "reused":
1098
1571
  return {
1099
1572
  ok: true,
1100
- state: withDecisionPreparation(state, {
1573
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1574
+ state: withDecisionPreparation(dated, {
1101
1575
  kind: "reused",
1102
1576
  note: prepared.note,
1103
1577
  decision: prepared.decision,
@@ -1107,7 +1581,8 @@ async function prepareDecisionForGate(fs, state, snapshot, answer, resolved, cos
1107
1581
  case "prepared":
1108
1582
  return {
1109
1583
  ok: true,
1110
- state: withDecisionPreparation(state, {
1584
+ ...(prepared.warnings === undefined ? {} : { warnings: prepared.warnings }),
1585
+ state: withDecisionPreparation(dated, {
1111
1586
  kind: "prepared",
1112
1587
  note: prepared.prepared.note,
1113
1588
  preview: prepared.prepared.preview,
@@ -1216,11 +1691,11 @@ async function registerSelectedDecision(fs, paths, state, snapshot, resolved, co
1216
1691
  * run that merely advanced would be indistinguishable from one that never decided.
1217
1692
  */
1218
1693
  function standaloneDecisionEvent(state, transition, record) {
1219
- const batchIteration = currentBatchIteration(state, transition);
1694
+ const rowIteration = iterationOf(state, transition);
1220
1695
  return {
1221
1696
  kind: "executed",
1222
1697
  transition,
1223
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
1698
+ ...rowIteration,
1224
1699
  operation: "plan-exec.standalone-decision",
1225
1700
  summary: record.decision,
1226
1701
  output_digest: record.digest,
@@ -1261,7 +1736,7 @@ function withStandaloneGuidance(decision, record) {
1261
1736
  * cannot tell them apart, and neither could the table that decides the charge.
1262
1737
  */
1263
1738
  function invalidScope(message, action) {
1264
- return { code: "FLOW_SCOPE_INVALID", message, action };
1739
+ return { code: "FLOW_SCOPE_SHAPE_INVALID", message, action };
1265
1740
  }
1266
1741
  /**
1267
1742
  * The closure-evidence gate, run BEFORE the preview is sealed.
@@ -1275,6 +1750,8 @@ function planEvidenceFrom(stopped, answer, observed) {
1275
1750
  if (proposalContractOf(stopped) === null || answer.artifacts.length === 0)
1276
1751
  return null;
1277
1752
  const proposed = new Set(answer.artifacts.map((artifact) => artifact.path.trim()));
1753
+ const violations = [];
1754
+ let first = null;
1278
1755
  for (const plan of observed) {
1279
1756
  if (!proposed.has(plan.path))
1280
1757
  continue;
@@ -1285,15 +1762,20 @@ function planEvidenceFrom(stopped, answer, observed) {
1285
1762
  // should not learn of the header on the next attempt.
1286
1763
  const lineage = plan.failures.find((candidate) => candidate !== failure && candidate.code === "PLAN_LINEAGE_UNSEALED");
1287
1764
  const also = lineage === undefined ? "" : ` · además: ${lineage.message}`;
1288
- return {
1289
- failure: {
1290
- code: failure.code,
1291
- message: `'${plan.path}': ${failure.message}${also}`,
1292
- action: planBoundaryAction(failure.code, "proposal"),
1293
- },
1765
+ first ??= {
1766
+ code: failure.code,
1767
+ message: `'${plan.path}': ${failure.message}${also}`,
1768
+ action: planBoundaryAction(failure.code, "proposal"),
1294
1769
  };
1295
- }
1296
- 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 } };
1297
1779
  }
1298
1780
  /**
1299
1781
  * Seat the proposal an authoring answer just handed over — sealed, not believed.
@@ -1396,6 +1878,26 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1396
1878
  })),
1397
1879
  };
1398
1880
  }
1881
+ /**
1882
+ * The redraft a confirmation row's alternative asked for, or `null`.
1883
+ *
1884
+ * It unseats any standing proposal and records a reentry from the flow's
1885
+ * authoring row, so the run walks the redraft and its gate again instead of
1886
+ * carrying on to a publication with nothing to publish and a finalize that
1887
+ * closes the session. Independent of whether a proposal is seated: a proposal
1888
+ * the run gave up on is exactly when redrafting matters most.
1889
+ */
1890
+ function redraftOf(state, stopped, answer, journey) {
1891
+ const redraft = reentryOf(stopped);
1892
+ if (redraft === null || answer.choice !== redraft.label)
1893
+ return null;
1894
+ return withReentry(withProposal(state, null), {
1895
+ kind: "refine",
1896
+ transition: stopped.id,
1897
+ occurrence: occurrenceAt(journey, state.applied.length),
1898
+ from: redraft.from,
1899
+ });
1900
+ }
1399
1901
  /**
1400
1902
  * The grant this answer produced, scoped to what it was given over.
1401
1903
  *
@@ -1410,8 +1912,13 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1410
1912
  */
1411
1913
  function grantOf(state, resolved, answer, granted, journey) {
1412
1914
  const stopped = resolved.stopped;
1915
+ if (stopped === null)
1916
+ return state;
1917
+ const redrafted = redraftOf(state, stopped, answer, journey);
1918
+ if (redrafted !== null)
1919
+ return redrafted;
1413
1920
  const proposal = state.proposal;
1414
- const approve = stopped === null ? null : publishApprovalOf(stopped);
1921
+ const approve = publishApprovalOf(stopped);
1415
1922
  if (approve !== null && proposal !== null) {
1416
1923
  if (answer.choice === approve) {
1417
1924
  return withApproval(state, {
@@ -1420,13 +1927,11 @@ function grantOf(state, resolved, answer, granted, journey) {
1420
1927
  classes: [...proposal.requires_approval],
1421
1928
  });
1422
1929
  }
1423
- // Declining unseats the proposal, and that is what makes `Refinar` cost
1424
- // nothing: the publication downstream finds nothing to publish and is skipped
1425
- // saying so, instead of stopping to ask for an authorization over bytes the
1930
+ // Declining unseats the proposal: no authorization is asked over bytes the
1426
1931
  // person just turned down.
1427
1932
  return withProposal(state, null);
1428
1933
  }
1429
- const link = stopped === null ? null : approvalGrantOf(stopped);
1934
+ const link = approvalGrantOf(stopped);
1430
1935
  if (link !== null && answer.choice === link.approve) {
1431
1936
  const target = journey.find((row) => row.id === link.transition);
1432
1937
  // A link to a transition this journey does not walk grants nothing: the
@@ -1457,7 +1962,7 @@ function grantOf(state, resolved, answer, granted, journey) {
1457
1962
  * a payload the boundary's own contract rejects, a decline (a real answer that
1458
1963
  * applies nothing), and an execution result that did not earn its transition.
1459
1964
  */
1460
- function admit(state, resolved, stopped, input, cost, checkouts) {
1965
+ function admit(state, resolved, stopped, input, cost, checkouts, scoped, preexisting, validationOnlyCurrent) {
1461
1966
  const expectedApproval = resolved.kind === "authorization"
1462
1967
  ? effectApprovalDigest(stopped.id, resolved.authorization?.planned ?? [])
1463
1968
  : null;
@@ -1491,14 +1996,11 @@ function admit(state, resolved, stopped, input, cost, checkouts) {
1491
1996
  // consults. A resend, a decline and a pause are not failed tries either, and
1492
1997
  // they are classified there rather than being exempted here.
1493
1998
  return {
1494
- 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),
1495
2000
  };
1496
2001
  }
1497
- // The flow control is a real answer, and neither half applies anything. They
1498
- // are kept apart because the outcomes differ and the difference is the point:
1499
- // stopping ends the run here (`cancelled`), pausing keeps the very same
1500
- // boundary standing so the run picks it up after compacting (`needs_input`).
1501
- // 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.
1502
2004
  if (parsed.answer.choice === PAUSE_LABEL) {
1503
2005
  return {
1504
2006
  decision: reject(state, resolved, `'${PAUSE_LABEL}': la frontera queda en pie y la corrida retoma acá`, {
@@ -1509,33 +2011,80 @@ function admit(state, resolved, stopped, input, cost, checkouts) {
1509
2011
  };
1510
2012
  }
1511
2013
  if (parsed.answer.choice === STOP_LABEL) {
1512
- return {
1513
- decision: reject(state, resolved, `'${STOP_LABEL}': el recorrido queda detenido en esta frontera`, {
1514
- code: "FLOW_BOUNDARY_DECLINED",
1515
- action: "reanudá con 'aw flow advance' cuando quieras retomar esta frontera",
1516
- outcome: "cancelled",
1517
- }, cost),
1518
- };
2014
+ return { decision: closeFromAnswer(state) };
1519
2015
  }
1520
2016
  // An execution result has to EARN the transition. Anything short of a completed
1521
2017
  // run with its evidence and its whole effect keeps the boundary standing: the
1522
2018
  // work stays pending, with the recovery the action declared.
1523
- if (resolved.kind === "execution") {
1524
- // The row's `effects` are the ceiling and the sealed proposal is what really
1525
- // happens: demanding an overwrite from a publication that only creates files
1526
- // would leave the transition pending for an effect nobody could produce.
1527
- const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts);
1528
- if (verdict !== null) {
1529
- const trace = declaredTrace(state, stopped, resolved, parsed.answer, verdict);
1530
- return {
1531
- decision: reject(state, resolved, verdict.message, verdict.detail, {
1532
- ...cost,
1533
- ...(trace === null ? {} : { trace }),
1534
- }),
1535
- };
1536
- }
2019
+ if (resolved.kind !== "execution")
2020
+ return parsed;
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
+ };
2038
+ }
2039
+ /** The execution verdict, then — at the phase validation — the batch's own credit. */
2040
+ function earned(state, resolved, stopped, parsed, cost, checkouts, scoped, preexisting, validationOnlyCurrent) {
2041
+ // The row's `effects` are the ceiling and the sealed proposal is what really
2042
+ // happens: demanding an overwrite from a publication that only creates files
2043
+ // would leave the transition pending for an effect nobody could produce.
2044
+ const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts, preexisting);
2045
+ if (verdict !== null) {
2046
+ const trace = declaredTrace(state, stopped, resolved, parsed.answer, verdict);
2047
+ return {
2048
+ decision: reject(state, resolved, verdict.message, verdict.detail, {
2049
+ ...cost,
2050
+ ...(trace === null ? {} : { trace }),
2051
+ }),
2052
+ };
2053
+ }
2054
+ const credited = batchCredit(state, stopped, parsed.answer, resolved, scoped, validationOnlyCurrent);
2055
+ if (credited === null)
2056
+ return parsed;
2057
+ if (!credited.ok) {
2058
+ return {
2059
+ decision: reject(state, resolved, credited.refusal.message, credited.refusal.detail, cost),
2060
+ };
1537
2061
  }
1538
- return parsed;
2062
+ return { ...parsed, credit: credited.credit };
2063
+ }
2064
+ /**
2065
+ * The phase validation's own judgment: the batch it closes must bring a proof of
2066
+ * each of its sources, of its own checkout. `null` on every other boundary, and
2067
+ * on a run with no batch standing, where there is nothing to credit.
2068
+ */
2069
+ function batchCredit(state, stopped, answer, resolved, scoped, validationOnlyCurrent) {
2070
+ if (stopped.id !== PHASE_VALIDATION || answer.result === null || state.scope === null) {
2071
+ return null;
2072
+ }
2073
+ const iteration = state.batch_loop?.iteration ?? null;
2074
+ const batches = state.batches ?? [];
2075
+ const batch = batches.find((entry) => entry.iteration === iteration);
2076
+ if (batch === undefined)
2077
+ return null;
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 ?? "");
2085
+ if (!verdict.ok)
2086
+ return verdict;
2087
+ return { ok: true, credit: { batch: batch.id, proofs: verdict.credit } };
1539
2088
  }
1540
2089
  /**
1541
2090
  * The material fact a REFUSED external result still leaves behind — or `null`.
@@ -1557,11 +2106,11 @@ function declaredTrace(state, stopped, resolved, answer, verdict) {
1557
2106
  if (!touchesTheWorld(applied) && verdict.detail.code !== "FLOW_EFFECT_PARTIAL")
1558
2107
  return null;
1559
2108
  const invocation = resolved.action?.invocation;
1560
- const batchIteration = currentBatchIteration(state, stopped.id);
2109
+ const rowIteration = iterationOf(state, stopped.id);
1561
2110
  return {
1562
2111
  kind: "failed",
1563
2112
  transition: stopped.id,
1564
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
2113
+ ...rowIteration,
1565
2114
  // What was really run, named the way whoever ran it would recognize it: an
1566
2115
  // external execution has no internal operation id to quote.
1567
2116
  operation: invocation === undefined ? stopped.id : [invocation.program, ...invocation.args].join(" "),
@@ -1604,7 +2153,11 @@ function actionDrift(state, resolved, cost) {
1604
2153
  * was written" are different facts, and only the second one advances a run.
1605
2154
  */
1606
2155
  function holdAfterApproval(approved, journey, identity) {
1607
- 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
+ });
1608
2161
  if (!held.ok)
1609
2162
  return { ok: false, failure: held.failure };
1610
2163
  return { ok: true, state: held.state, value: { directive: held.directive, advanced: true } };
@@ -1617,14 +2170,10 @@ function applyAndAdvance(approved, journey, stopped, identity, answer) {
1617
2170
  let next = answer.signals.length > 0
1618
2171
  ? withObservation(approved, observationFor(approved, stopped.id, answer.signals))
1619
2172
  : approved;
1620
- next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1621
- // A published proposal is spent, whoever published it. This is the degraded
1622
- // path — no internal executor, so the caller ran the write and returned its
1623
- // result — and leaving the proposal seated would keep a preview of bytes that
1624
- // are already on disk standing in front of the next boundary.
1625
- if (internalActionOf(stopped)?.operation === "proposal.publish") {
1626
- next = withProposal(next, null);
2173
+ if (stopped.answer_contract === "batch-review" && isBatchReview(answer.decisions.review)) {
2174
+ next = withPlanExecBatchReview(next, answer.decisions.review);
1627
2175
  }
2176
+ next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1628
2177
  next = withAttempt(next, identity);
1629
2178
  // The boundary follows the position, always: handing the engine a state whose
1630
2179
  // boundary still names the transition just applied is exactly the incoherence
@@ -1677,15 +2226,8 @@ function choiceOutcomeOf(stopped, label) {
1677
2226
  * the state. A retry is recognized by the attempt ledger, so it cannot create a
1678
2227
  * second package or walk the batch toward a commit.
1679
2228
  */
1680
- function applyAndHandoff(state, journey, stopped, identity, answer, outcome, specPath) {
1681
- const plan = state.scope?.plan ?? null;
1682
- if (outcome.destination === "plan-refine" && plan === null) {
1683
- const resolved = resolveBoundary(state, journey);
1684
- return reject(state, resolved, "la entrega a plan-refine no tiene el plan que debe reabrir", {
1685
- code: "FLOW_HANDOFF_PLAN_MISSING",
1686
- action: "fijá el scope de plan-exec antes de elegir una escalación a plan-refine",
1687
- }, { journey, identity });
1688
- }
2229
+ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, specPath, request) {
2230
+ const plan = state.scope?.plan ?? state.plan_exec_entry?.plan ?? null;
1689
2231
  const fixPreview = state.fix_preview ?? null;
1690
2232
  const packageBody = {
1691
2233
  plan,
@@ -1695,7 +2237,11 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1695
2237
  // declared preview travels with it — the destination is the one place that
1696
2238
  // rewrites it, and rediscovering it there is exactly what the package exists
1697
2239
  // to avoid.
1698
- decisions: fixPreview === null ? answer.decisions : { ...answer.decisions, preview: fixPreview },
2240
+ decisions: {
2241
+ ...answer.decisions,
2242
+ ...(fixPreview === null ? {} : { preview: fixPreview }),
2243
+ ...(request === null ? {} : { request }),
2244
+ },
1699
2245
  selection: answer.choice ?? "",
1700
2246
  };
1701
2247
  // The handoff to `spec-refine` used to be the bare command while the one to
@@ -1703,13 +2249,7 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1703
2249
  // spec again — the escalation package exists precisely so nothing is
1704
2250
  // re-derived at the destination. An unreadable plan degrades to the bare
1705
2251
  // command: an escalation is never blocked over the convenience of an argument.
1706
- const command = outcome.destination === "plan-refine"
1707
- ? `/w:plan-refine ${plan}`
1708
- : outcome.destination === "spec-refine"
1709
- ? specPath === null
1710
- ? "/w:spec-refine"
1711
- : `/w:spec-refine ${specPath}`
1712
- : "/w:spec-new";
2252
+ const command = handoffCommand(outcome.destination, plan, specPath);
1713
2253
  let next = withSelectedChoice(state, {
1714
2254
  transition: stopped.id,
1715
2255
  label: answer.choice ?? "",
@@ -1717,14 +2257,26 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1717
2257
  });
1718
2258
  next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1719
2259
  next = withAttempt(next, identity);
1720
- next = withHandoff(next, {
1721
- destination: outcome.destination,
1722
- command,
1723
- package: packageBody,
1724
- package_digest: semanticDigest(packageBody),
1725
- });
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
+ });
1726
2268
  next = withBoundary(next, journey[next.applied.length]?.id ?? null);
1727
- 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
+ });
1728
2280
  if (!advanced.ok)
1729
2281
  return { ok: false, failure: advanced.failure };
1730
2282
  return {
@@ -1733,6 +2285,13 @@ function applyAndHandoff(state, journey, stopped, identity, answer, outcome, spe
1733
2285
  value: { directive: advanced.directive, advanced: true },
1734
2286
  };
1735
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
+ }
1736
2295
  /**
1737
2296
  * Whether this submission is a resend, judged by the ledger that already knows
1738
2297
  * how.
@@ -1764,11 +2323,11 @@ function resendCheck(state, resolved, cost) {
1764
2323
  for (const past of state.attempts) {
1765
2324
  const replay = ledger.record(past);
1766
2325
  if (!replay.ok)
1767
- return { ok: false, failure: replay.failure };
2326
+ return reject(state, resolved, replay.failure.message, replay.failure);
1768
2327
  }
1769
2328
  const verdict = ledger.record(identity);
1770
2329
  if (!verdict.ok)
1771
- return { ok: false, failure: verdict.failure };
2330
+ return reject(state, resolved, verdict.failure.message, verdict.failure);
1772
2331
  if (verdict.kind === "new")
1773
2332
  return null;
1774
2333
  // The TWIN's transition, not this submission's: the identity is built over the
@@ -1776,10 +2335,9 @@ function resendCheck(state, resolved, cost) {
1776
2335
  // What is being asked is whether the step the twin answered ever moved.
1777
2336
  const twin = state.attempts.find((past) => past.invocation_id === identity.invocation_id &&
1778
2337
  past.request_digest === identity.request_digest);
1779
- // The twin was refused: this is a retry of the same wrong answer, so it gets
1780
- // the same real diagnosis again and counts toward the cap, which is what
1781
- // eventually degrades the boundary instead of leaving the caller looping.
1782
- if (twin !== undefined && !state.applied.includes(twin.transition))
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.
2340
+ if (twin !== undefined && twinWasRefused(state, twin))
1783
2341
  return null;
1784
2342
  return reject(state, resolved, "esta respuesta ya se había aplicado: el recorrido no avanza dos veces", {
1785
2343
  code: "FLOW_ANSWER_RESENT",
@@ -1787,6 +2345,19 @@ function resendCheck(state, resolved, cost) {
1787
2345
  outcome: "completed",
1788
2346
  }, cost);
1789
2347
  }
2348
+ /**
2349
+ * Whether the attempt a resend matches never advanced anything.
2350
+ *
2351
+ * Its transition missing from `applied` says so. So does its transition being
2352
+ * the boundary still standing at the same iteration: a repeated row (a batch or
2353
+ * a redraft copy) has its id in `applied` from an earlier walk, and that earlier
2354
+ * walk is not this one.
2355
+ */
2356
+ function twinWasRefused(state, twin) {
2357
+ if (!state.applied.includes(twin.transition))
2358
+ return true;
2359
+ return (twin.transition === state.boundary && sameIteration(twin, iterationOf(state, twin.transition)));
2360
+ }
1790
2361
  /**
1791
2362
  * A rejection, expressed as the recalculated directive.
1792
2363
  *
@@ -1797,8 +2368,8 @@ function resendCheck(state, resolved, cost) {
1797
2368
  * recoverable — an unbounded loop inside the very mechanism that exists to bound
1798
2369
  * one. One place decides, one table says which codes count.
1799
2370
  *
1800
- * `cost` is absent only where there is no boundary to charge: a run that already
1801
- * 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.
1802
2373
  */
1803
2374
  function reject(state, resolved, message, detail, cost) {
1804
2375
  // A refused answer is the one rejection that CHANGES the run: the attempt is
@@ -1809,14 +2380,20 @@ function reject(state, resolved, message, detail, cost) {
1809
2380
  const traced = cost?.trace === undefined || restatesLastEvent(state, cost.trace)
1810
2381
  ? state
1811
2382
  : withEvent(state, cost.trace);
1812
- 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;
1813
2390
  const now = cost === undefined || after === state ? resolved : resolveBoundary(after, cost.journey);
1814
2391
  const built = directiveFor(after, now, [], {
1815
2392
  // No outcome named ⇒ the boundary decides it: a finished journey reports
1816
2393
  // `completed`, an open one `needs_input`. Hardcoding one here would let a
1817
2394
  // rejection at the end of a run claim work is still pending.
1818
2395
  ...(detail.outcome === undefined ? {} : { outcome: detail.outcome }),
1819
- nextAction: `${message} — ${now.error?.action ?? detail.action}`,
2396
+ nextAction: `${message} — ${detail.code === "FLOW_BOUNDARY_PAUSED" ? detail.action : (now.error?.action ?? detail.action)}`,
1820
2397
  });
1821
2398
  if (!built.ok)
1822
2399
  return { ok: false, failure: built.failure };
@@ -1826,7 +2403,12 @@ function reject(state, resolved, message, detail, cost) {
1826
2403
  // longer a way forward, and reporting the payload's error would suggest it is.
1827
2404
  const directive = {
1828
2405
  ...built.directive,
1829
- 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
+ },
1830
2412
  };
1831
2413
  return {
1832
2414
  ok: true,
@@ -1848,17 +2430,12 @@ function reject(state, resolved, message, detail, cost) {
1848
2430
  */
1849
2431
  function attemptIdentity(state, input, seal, transition) {
1850
2432
  const digest = semanticDigest({ payload: input.raw, approval: input.approval });
1851
- const batchIteration = currentBatchIteration(state, transition);
2433
+ const rowIteration = iterationOf(state, transition);
1852
2434
  const prior = state.attempts.filter((past) => past.invocation_id === seal);
1853
2435
  const twin = prior.find((past) => past.request_digest === digest);
1854
- // The next ordinal, asked with the SAME expression `AttemptLedger.record`
1855
- // validates it against. Counting rows instead is a second reading of one fact,
1856
- // and the two part the moment a resend is persisted: a recognized resend is
1857
- // re-evaluated and its row kept wearing the twin's ordinal — deliberately, so
1858
- // it still counts toward the cap — so the rows say two while the highest
1859
- // ordinal says one, and the next answer arrives as attempt 3 where the ledger
1860
- // accepts only 2. Nobody can answer that boundary, and `recover` reads the
1861
- // 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.
1862
2439
  const highest = Math.max(0, ...prior.map((past) => past.attempt));
1863
2440
  const attempt = twin?.attempt ?? highest + 1;
1864
2441
  const parent = attempt === 1
@@ -1872,18 +2449,18 @@ function attemptIdentity(state, input, seal, transition) {
1872
2449
  // The seal moves with the state; the transition does not while the run
1873
2450
  // stands there. It is what the cap counts over.
1874
2451
  transition,
1875
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
2452
+ ...rowIteration,
1876
2453
  };
1877
2454
  }
1878
2455
  function isRecord(value) {
1879
2456
  return typeof value === "object" && value !== null && !Array.isArray(value);
1880
2457
  }
1881
2458
  function observationFor(state, transition, signals) {
1882
- const batchIteration = currentBatchIteration(state, transition);
2459
+ const rowIteration = iterationOf(state, transition);
1883
2460
  return {
1884
2461
  transition,
1885
2462
  signals,
1886
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
2463
+ ...rowIteration,
1887
2464
  };
1888
2465
  }
1889
2466
  //# sourceMappingURL=submit.js.map