@tacuchi/agent-workflow-cli 25.6.1 → 25.8.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 (279) hide show
  1. package/CHANGELOG.md +4398 -0
  2. package/README.md +1 -1
  3. package/dist/adapters/git-cli.js +160 -13
  4. package/dist/adapters/git-cli.js.map +1 -1
  5. package/dist/adapters/node-file-system.js +39 -2
  6. package/dist/adapters/node-file-system.js.map +1 -1
  7. package/dist/application/branch-resolver.js +34 -1
  8. package/dist/application/branch-resolver.js.map +1 -1
  9. package/dist/application/capability/readiness.js +26 -10
  10. package/dist/application/capability/readiness.js.map +1 -1
  11. package/dist/application/capability/wrapper.js +10 -3
  12. package/dist/application/capability/wrapper.js.map +1 -1
  13. package/dist/application/checkpoint-write-service.js +161 -39
  14. package/dist/application/checkpoint-write-service.js.map +1 -1
  15. package/dist/application/dates.js +12 -0
  16. package/dist/application/dates.js.map +1 -1
  17. package/dist/application/dev-only-services.js +5 -7
  18. package/dist/application/dev-only-services.js.map +1 -1
  19. package/dist/application/doctor/provider-mcps.js +1 -1
  20. package/dist/application/doctor/provider-mcps.js.map +1 -1
  21. package/dist/application/fix-git-service.js +83 -13
  22. package/dist/application/fix-git-service.js.map +1 -1
  23. package/dist/application/flow/advance.js +64 -9
  24. package/dist/application/flow/advance.js.map +1 -1
  25. package/dist/application/flow/annul-service.js +159 -0
  26. package/dist/application/flow/annul-service.js.map +1 -0
  27. package/dist/application/flow/checkout-observation.js +49 -0
  28. package/dist/application/flow/checkout-observation.js.map +1 -1
  29. package/dist/application/flow/close-at-boundary.js +144 -0
  30. package/dist/application/flow/close-at-boundary.js.map +1 -0
  31. package/dist/application/flow/flow-service.js +213 -49
  32. package/dist/application/flow/flow-service.js.map +1 -1
  33. package/dist/application/flow/internal-actions.js +87 -4
  34. package/dist/application/flow/internal-actions.js.map +1 -1
  35. package/dist/application/flow/internal-drive.js +70 -33
  36. package/dist/application/flow/internal-drive.js.map +1 -1
  37. package/dist/application/flow/reopen-run.js +152 -0
  38. package/dist/application/flow/reopen-run.js.map +1 -0
  39. package/dist/application/flow/run-journey.js +11 -8
  40. package/dist/application/flow/run-journey.js.map +1 -1
  41. package/dist/application/flow/run-projection.js +37 -10
  42. package/dist/application/flow/run-projection.js.map +1 -1
  43. package/dist/application/flow/run-state-service.js +109 -36
  44. package/dist/application/flow/run-state-service.js.map +1 -1
  45. package/dist/application/flow/submit.js +217 -188
  46. package/dist/application/flow/submit.js.map +1 -1
  47. package/dist/application/git-flow-service.js +325 -72
  48. package/dist/application/git-flow-service.js.map +1 -1
  49. package/dist/application/hook-sql-mutation-guard.js +290 -27
  50. package/dist/application/hook-sql-mutation-guard.js.map +1 -1
  51. package/dist/application/host-memory/claude-code.js +65 -0
  52. package/dist/application/host-memory/claude-code.js.map +1 -0
  53. package/dist/application/host-memory/codex.js +225 -0
  54. package/dist/application/host-memory/codex.js.map +1 -0
  55. package/dist/application/host-memory/destination.js +52 -0
  56. package/dist/application/host-memory/destination.js.map +1 -0
  57. package/dist/application/host-memory/reader.js +9 -0
  58. package/dist/application/host-memory/reader.js.map +1 -0
  59. package/dist/application/host-memory/report.js +136 -0
  60. package/dist/application/host-memory/report.js.map +1 -0
  61. package/dist/application/lifecycle-target.js +3 -1
  62. package/dist/application/lifecycle-target.js.map +1 -1
  63. package/dist/application/local-proposal.js +13 -3
  64. package/dist/application/local-proposal.js.map +1 -1
  65. package/dist/application/logging/log-events.js +22 -0
  66. package/dist/application/logging/log-events.js.map +1 -1
  67. package/dist/application/parsers/spec-relation.js +20 -7
  68. package/dist/application/parsers/spec-relation.js.map +1 -1
  69. package/dist/application/persist-service.js +45 -1
  70. package/dist/application/persist-service.js.map +1 -1
  71. package/dist/application/plan-exec-batch-service.js +47 -2
  72. package/dist/application/plan-exec-batch-service.js.map +1 -1
  73. package/dist/application/plan-lineage-seal.js +93 -0
  74. package/dist/application/plan-lineage-seal.js.map +1 -0
  75. package/dist/application/plan-lint-service.js +153 -0
  76. package/dist/application/plan-lint-service.js.map +1 -0
  77. package/dist/application/prod-consent.js +75 -0
  78. package/dist/application/prod-consent.js.map +1 -0
  79. package/dist/application/reseal-service.js +9 -3
  80. package/dist/application/reseal-service.js.map +1 -1
  81. package/dist/application/self/install-skill.js +40 -12
  82. package/dist/application/self/install-skill.js.map +1 -1
  83. package/dist/application/self/install-targets.js +73 -2
  84. package/dist/application/self/install-targets.js.map +1 -1
  85. package/dist/application/self/uninstall.js +21 -7
  86. package/dist/application/self/uninstall.js.map +1 -1
  87. package/dist/application/self/update-self.js +204 -8
  88. package/dist/application/self/update-self.js.map +1 -1
  89. package/dist/application/session-create-service.js +49 -0
  90. package/dist/application/session-create-service.js.map +1 -1
  91. package/dist/application/session-custody-recorder.js +4 -0
  92. package/dist/application/session-custody-recorder.js.map +1 -1
  93. package/dist/application/session-narrative.js +36 -0
  94. package/dist/application/session-narrative.js.map +1 -1
  95. package/dist/application/session-resolver.js +28 -9
  96. package/dist/application/session-resolver.js.map +1 -1
  97. package/dist/application/session-resume-service.js +65 -16
  98. package/dist/application/session-resume-service.js.map +1 -1
  99. package/dist/application/source-boundary-policy.js +168 -34
  100. package/dist/application/source-boundary-policy.js.map +1 -1
  101. package/dist/application/working-branch-service.js +68 -0
  102. package/dist/application/working-branch-service.js.map +1 -0
  103. package/dist/application/workline-graph.js +7 -2
  104. package/dist/application/workline-graph.js.map +1 -1
  105. package/dist/cli/ascii.js +84 -0
  106. package/dist/cli/ascii.js.map +1 -0
  107. package/dist/cli/commands/amend.js +1 -0
  108. package/dist/cli/commands/amend.js.map +1 -1
  109. package/dist/cli/commands/capability.js +15 -0
  110. package/dist/cli/commands/capability.js.map +1 -1
  111. package/dist/cli/commands/check-branch.js +1 -0
  112. package/dist/cli/commands/check-branch.js.map +1 -1
  113. package/dist/cli/commands/checkpoint-read.js +1 -0
  114. package/dist/cli/commands/checkpoint-read.js.map +1 -1
  115. package/dist/cli/commands/checkpoint-write.js +22 -9
  116. package/dist/cli/commands/checkpoint-write.js.map +1 -1
  117. package/dist/cli/commands/claims.js +1 -0
  118. package/dist/cli/commands/claims.js.map +1 -1
  119. package/dist/cli/commands/code-scan.js +1 -0
  120. package/dist/cli/commands/code-scan.js.map +1 -1
  121. package/dist/cli/commands/context-budget.js +1 -0
  122. package/dist/cli/commands/context-budget.js.map +1 -1
  123. package/dist/cli/commands/context-plan.js +1 -0
  124. package/dist/cli/commands/context-plan.js.map +1 -1
  125. package/dist/cli/commands/cut-intent.js +1 -0
  126. package/dist/cli/commands/cut-intent.js.map +1 -1
  127. package/dist/cli/commands/designs.js +1 -0
  128. package/dist/cli/commands/designs.js.map +1 -1
  129. package/dist/cli/commands/dev-only.js +4 -0
  130. package/dist/cli/commands/dev-only.js.map +1 -1
  131. package/dist/cli/commands/doctor.js +5 -0
  132. package/dist/cli/commands/doctor.js.map +1 -1
  133. package/dist/cli/commands/export.js +13 -0
  134. package/dist/cli/commands/export.js.map +1 -1
  135. package/dist/cli/commands/fix-git.js +46 -6
  136. package/dist/cli/commands/fix-git.js.map +1 -1
  137. package/dist/cli/commands/flow.js +61 -5
  138. package/dist/cli/commands/flow.js.map +1 -1
  139. package/dist/cli/commands/generate-launch.js +1 -0
  140. package/dist/cli/commands/generate-launch.js.map +1 -1
  141. package/dist/cli/commands/git-flow.js +104 -29
  142. package/dist/cli/commands/git-flow.js.map +1 -1
  143. package/dist/cli/commands/history-update.js +7 -9
  144. package/dist/cli/commands/history-update.js.map +1 -1
  145. package/dist/cli/commands/hook.js +11 -3
  146. package/dist/cli/commands/hook.js.map +1 -1
  147. package/dist/cli/commands/host-doctor.js +1 -0
  148. package/dist/cli/commands/host-doctor.js.map +1 -1
  149. package/dist/cli/commands/host-memory.js +26 -0
  150. package/dist/cli/commands/host-memory.js.map +1 -0
  151. package/dist/cli/commands/index.js +6 -0
  152. package/dist/cli/commands/index.js.map +1 -1
  153. package/dist/cli/commands/mcp.js +30 -4
  154. package/dist/cli/commands/mcp.js.map +1 -1
  155. package/dist/cli/commands/merge-state.js +1 -0
  156. package/dist/cli/commands/merge-state.js.map +1 -1
  157. package/dist/cli/commands/multiroot.js +16 -0
  158. package/dist/cli/commands/multiroot.js.map +1 -1
  159. package/dist/cli/commands/persist.js +6 -0
  160. package/dist/cli/commands/persist.js.map +1 -1
  161. package/dist/cli/commands/plan.js +51 -0
  162. package/dist/cli/commands/plan.js.map +1 -0
  163. package/dist/cli/commands/plugin-cache.js +4 -0
  164. package/dist/cli/commands/plugin-cache.js.map +1 -1
  165. package/dist/cli/commands/plugin-doctor.js +3 -0
  166. package/dist/cli/commands/plugin-doctor.js.map +1 -1
  167. package/dist/cli/commands/project-md-upsert.js +3 -0
  168. package/dist/cli/commands/project-md-upsert.js.map +1 -1
  169. package/dist/cli/commands/release-data.js +12 -0
  170. package/dist/cli/commands/release-data.js.map +1 -1
  171. package/dist/cli/commands/release-pass.js +10 -0
  172. package/dist/cli/commands/release-pass.js.map +1 -1
  173. package/dist/cli/commands/remove-source.js +1 -0
  174. package/dist/cli/commands/remove-source.js.map +1 -1
  175. package/dist/cli/commands/reseal.js +1 -0
  176. package/dist/cli/commands/reseal.js.map +1 -1
  177. package/dist/cli/commands/resume-summary.js +9 -2
  178. package/dist/cli/commands/resume-summary.js.map +1 -1
  179. package/dist/cli/commands/resume.js +1 -0
  180. package/dist/cli/commands/resume.js.map +1 -1
  181. package/dist/cli/commands/retirement.js +1 -0
  182. package/dist/cli/commands/retirement.js.map +1 -1
  183. package/dist/cli/commands/self.js +33 -0
  184. package/dist/cli/commands/self.js.map +1 -1
  185. package/dist/cli/commands/session-artifacts.js +1 -0
  186. package/dist/cli/commands/session-artifacts.js.map +1 -1
  187. package/dist/cli/commands/session-close.js +129 -30
  188. package/dist/cli/commands/session-close.js.map +1 -1
  189. package/dist/cli/commands/session-create.js +1 -0
  190. package/dist/cli/commands/session-create.js.map +1 -1
  191. package/dist/cli/commands/session-resume.js +1 -0
  192. package/dist/cli/commands/session-resume.js.map +1 -1
  193. package/dist/cli/commands/sessions.js +1 -0
  194. package/dist/cli/commands/sessions.js.map +1 -1
  195. package/dist/cli/commands/set-branch.js +41 -6
  196. package/dist/cli/commands/set-branch.js.map +1 -1
  197. package/dist/cli/commands/settle.js +1 -0
  198. package/dist/cli/commands/settle.js.map +1 -1
  199. package/dist/cli/commands/skill-index.js +1 -0
  200. package/dist/cli/commands/skill-index.js.map +1 -1
  201. package/dist/cli/commands/skills.js +1 -0
  202. package/dist/cli/commands/skills.js.map +1 -1
  203. package/dist/cli/commands/sources.js +1 -0
  204. package/dist/cli/commands/sources.js.map +1 -1
  205. package/dist/cli/commands/stack.js +1 -0
  206. package/dist/cli/commands/stack.js.map +1 -1
  207. package/dist/cli/commands/status.js +1 -0
  208. package/dist/cli/commands/status.js.map +1 -1
  209. package/dist/cli/commands/tool.js +5 -21
  210. package/dist/cli/commands/tool.js.map +1 -1
  211. package/dist/cli/commands/unknown-flags.js +67 -16
  212. package/dist/cli/commands/unknown-flags.js.map +1 -1
  213. package/dist/cli/commands/visibility.js +1 -0
  214. package/dist/cli/commands/visibility.js.map +1 -1
  215. package/dist/cli/commands/workspace-init.js +12 -0
  216. package/dist/cli/commands/workspace-init.js.map +1 -1
  217. package/dist/cli/commands/workspace-migrate.js +2 -13
  218. package/dist/cli/commands/workspace-migrate.js.map +1 -1
  219. package/dist/cli/commands/worktree.js +1 -0
  220. package/dist/cli/commands/worktree.js.map +1 -1
  221. package/dist/cli/help-groups.js +41 -0
  222. package/dist/cli/help-groups.js.map +1 -1
  223. package/dist/cli/main.js +41 -58
  224. package/dist/cli/main.js.map +1 -1
  225. package/dist/cli/output-mode.js +11 -2
  226. package/dist/cli/output-mode.js.map +1 -1
  227. package/dist/cli/parser.js +14 -5
  228. package/dist/cli/parser.js.map +1 -1
  229. package/dist/cli/registry.js.map +1 -1
  230. package/dist/cli/render.js +30 -1
  231. package/dist/cli/render.js.map +1 -1
  232. package/dist/cli/tui/components/git-flow-actions.js +65 -20
  233. package/dist/cli/tui/components/git-flow-actions.js.map +1 -1
  234. package/dist/cli/tui/tabs/project-tab.js +101 -12
  235. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  236. package/dist/domain/changelog-contract.js +172 -0
  237. package/dist/domain/changelog-contract.js.map +1 -0
  238. package/dist/domain/database-tools.js +1 -189
  239. package/dist/domain/database-tools.js.map +1 -1
  240. package/dist/domain/flow/answer.js +11 -0
  241. package/dist/domain/flow/answer.js.map +1 -1
  242. package/dist/domain/flow/authority.js +153 -14
  243. package/dist/domain/flow/authority.js.map +1 -1
  244. package/dist/domain/flow/execution-result.js +78 -7
  245. package/dist/domain/flow/execution-result.js.map +1 -1
  246. package/dist/domain/flow/journey-baseline.js +131 -0
  247. package/dist/domain/flow/journey-baseline.js.map +1 -0
  248. package/dist/domain/flow/route.js.map +1 -1
  249. package/dist/domain/flow/rules.js +5 -6
  250. package/dist/domain/flow/rules.js.map +1 -1
  251. package/dist/domain/flow/run-state.js +440 -89
  252. package/dist/domain/flow/run-state.js.map +1 -1
  253. package/dist/domain/harnesses.js +8 -1
  254. package/dist/domain/harnesses.js.map +1 -1
  255. package/dist/domain/host-memory/model.js +2 -0
  256. package/dist/domain/host-memory/model.js.map +1 -0
  257. package/dist/domain/host-memory/provenance.js +43 -0
  258. package/dist/domain/host-memory/provenance.js.map +1 -0
  259. package/dist/domain/host-memory/topic.js +26 -0
  260. package/dist/domain/host-memory/topic.js.map +1 -0
  261. package/dist/domain/lineage.js +11 -3
  262. package/dist/domain/lineage.js.map +1 -1
  263. package/dist/domain/proposal.js +7 -0
  264. package/dist/domain/proposal.js.map +1 -1
  265. package/dist/domain/session/custody.js.map +1 -1
  266. package/dist/domain/sql-lexer.js +240 -0
  267. package/dist/domain/sql-lexer.js.map +1 -0
  268. package/package.json +2 -2
  269. package/skills/w/SKILL.md +9 -9
  270. package/skills/w/commands/README.md +2 -1
  271. package/skills/w/commands/recall.md +39 -0
  272. package/skills/w/context/MANIFEST.json +4 -0
  273. package/skills/w/harness/HARNESS.md +12 -6
  274. package/skills/w/hooks/README.md +27 -16
  275. package/skills/w/loops/CHASSIS.md +16 -12
  276. package/skills/w/loops/CODE-POLICIES.md +11 -6
  277. package/skills/w/loops/plan-exec-loop/LOOP.md +9 -9
  278. package/skills/w/modules/COMPACTION.md +1 -1
  279. package/skills/w/roles/design/CONTRACT.md +10 -3
@@ -25,12 +25,12 @@ 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
30
  import { PAUSE_LABEL, STOP_LABEL, stepOf, } from "../../domain/flow/directive.js";
31
- import { executionVerdict } from "../../domain/flow/execution-result.js";
31
+ import { batchCreditVerdict, executionVerdict, } from "../../domain/flow/execution-result.js";
32
32
  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";
33
+ import { applyTransition, checkAgainstJourney, iterationOf, restartInvocation, restatesLastEvent, sameIteration, settlementAmbiguous, withApproval, withAttempt, withBoundary, withContinuation, withDecisionPreparation, withEvent, withFixPreview, withHandoff, withObservation, withPlanExecBatchCredit, withPlanExecBatchStageForTransition, withProposal, withReentry, withRouteDecisions, withRouteProposal, withScope, withSelectedChoice, withSettlementDeclarations, } from "../../domain/flow/run-state.js";
34
34
  import { withSpecBaseline } from "../../domain/lineage.js";
35
35
  import { destinationsOf, observedEffects, sealProposal } from "../../domain/proposal.js";
36
36
  import { baseDigest } from "../../domain/proposal.js";
@@ -38,19 +38,19 @@ import { reservationMarker } from "../../domain/reservation.js";
38
38
  import { checkSafeRelativePath } from "../../domain/safe-path.js";
39
39
  import { resolveCoreDocsCanon } from "../docs-canon-service.js";
40
40
  import { readWorkspaceBlock } from "../parsers/project-block.js";
41
- import { functionalSpecDigest } from "../parsers/spec-functional.js";
42
41
  import { parseDerivedFromPath, parseSpecRelation } from "../parsers/spec-relation.js";
43
42
  import { resolveWorkspaceRootFrom } from "../paths-service.js";
44
43
  import { commitStoredPlanExecDecision, preparePlanExecDecision, } from "../plan-exec-decision-service.js";
44
+ import { planBoundaryAction, planGrammarAtEntry, planGrammarAtPublication, } from "../plan-lint-service.js";
45
45
  import { semanticDigest } from "../semantic-operation/protocol.js";
46
46
  import { resolveSessionTarget } from "../session-resolver.js";
47
- import { sourceAliasesOfPlan, validatePlanSourceBoundary, validateSourceBoundedSemantics, } from "../source-boundary-policy.js";
48
- import { actionDigest, advanceFlowRun, directiveFor, effectsOfTransition, resolveBoundary, } from "./advance.js";
49
- import { observeCheckout, withObservedCheckouts } from "./checkout-observation.js";
47
+ import { sourceAliasesOfPlan, } from "../source-boundary-policy.js";
48
+ import { actionDigest, advanceFlowRun, awaitingCliRerun, directiveFor, effectsOfTransition, resolveBoundary, } from "./advance.js";
49
+ import { observeCheckout, observeScopedFingerprints, withObservedCheckouts, } from "./checkout-observation.js";
50
50
  import { resolveCheckoutCandidates } from "./checkout-observation.js";
51
51
  import { driveInternalActions } from "./internal-drive.js";
52
52
  import { journeyForRun } from "./run-journey.js";
53
- import { applyUnderLock, locateRun } from "./run-state-service.js";
53
+ import { applyUnderLock, locateRun, readRun } from "./run-state-service.js";
54
54
  export async function submitFlow(fs, paths, input) {
55
55
  const canon = await resolveCoreDocsCanon(fs, paths);
56
56
  if (!canon.ok) {
@@ -119,125 +119,79 @@ export async function submitFlow(fs, paths, input) {
119
119
  }
120
120
  async function observe(fs, paths, raw, session, git) {
121
121
  const root = await resolveWorkspaceRootFrom(fs, paths);
122
+ const plans = await observePlanArtifacts(fs, paths, raw);
122
123
  return {
123
124
  root,
124
125
  destinations: await observeDestinations(fs, paths, raw),
125
126
  scope: await observeScope(fs, paths, raw),
126
- plans: await observePlanEvidence(fs, paths, raw),
127
+ plans: plans.evidence,
127
128
  checkouts: await observeCheckouts(fs, paths, session, git),
128
- baselines: await observeSpecBaselines(fs, paths, raw),
129
+ scoped: await observeBatchSources(fs, paths, session, git),
130
+ baselines: plans.baselines,
129
131
  };
130
132
  }
131
133
  /**
132
- * The baseline one plan document seals, or `null` when it cannot seal one.
133
- *
134
- * What gets sealed is the spec's FUNCTIONAL digest, not its exact bytes: a plan
135
- * derives from what the spec promises, so a later comma or a rewrapped
136
- * paragraph must not turn this plan divergent and make it uncloseable.
137
- *
138
- * Every `null` here is a plan that stays UNSEALED, which is a legitimate
139
- * reading: no `Derived from` path, two contradictory ones, a path that escapes
140
- * the workspace, or a spec nobody can read. None of them is an error — a wrong
141
- * seal would be.
134
+ * The scoped fingerprints a phase validation is judged against, read before the
135
+ * lock like every other observation. The run read here only decides WHETHER to
136
+ * measure: the decision under the lock re-reads it, and a run that moved in
137
+ * between answers a different seal and is refused as stale anyway.
142
138
  */
143
- async function baselineOfPlan(fs, root, content, specDir) {
144
- const specPath = parseDerivedFromPath(content, specDir);
145
- if (specPath === null || !checkSafeRelativePath(specPath).ok)
146
- return null;
147
- const relation = parseSpecRelation(content, specDir);
148
- if (relation.status !== "declared")
149
- return null;
150
- try {
151
- const specText = await fs.readText(join(root, specPath));
152
- return { path: specPath, number: relation.number, digest: functionalSpecDigest(specText) };
153
- }
154
- catch {
139
+ async function observeBatchSources(fs, paths, session, git) {
140
+ if (git === undefined)
155
141
  return null;
156
- }
142
+ // With a reader present, "not measured" is an empty map and never `null`: the
143
+ // judgment then finds every source unobservable and refuses, instead of
144
+ // skipping the one comparison the phase validation exists for.
145
+ const read = await readRun(fs, locateRun(paths, session));
146
+ if (!read.ok || read.state.boundary !== PHASE_VALIDATION || read.state.scope === null) {
147
+ return {};
148
+ }
149
+ const observed = await observeScopedFingerprints(fs, git, paths, session, read.state.scope.sources);
150
+ return observed.ok ? observed.base : {};
157
151
  }
152
+ const PHASE_VALIDATION = "plan-exec.validation-execution";
158
153
  /**
159
- * For every PLAN artifact in the payload, the exact spec bytes it was derived
160
- * from — read here, never taken from the sender.
154
+ * Judge the plan bytes a proposal is carrying, and derive the seal each one gets.
161
155
  *
162
- * A digest supplied by whoever wrote the document would prove only that they
163
- * typed something; the point of the seal is that publication computed it from
164
- * the file the plan names. A plan whose `Derived from` is absent or
165
- * contradictory yields nothing, and publication then writes no seal — an
166
- * unsealed plan is a legitimate diagnostic, a wrong seal is not.
167
- */
168
- async function observeSpecBaselines(fs, paths, raw) {
169
- const out = new Map();
170
- let parsed;
171
- try {
172
- parsed = JSON.parse(raw);
173
- }
174
- catch {
175
- return out;
176
- }
177
- const artifacts = parsed?.artifacts;
178
- if (!Array.isArray(artifacts) || artifacts.length === 0)
179
- return out;
180
- const canon = await resolveCoreDocsCanon(fs, paths);
181
- // An unreadable canon cannot tell a plan from anything else. Sealing on a
182
- // guess would stamp a baseline into whatever the payload happened to carry.
183
- if (!canon.ok)
184
- return out;
185
- const root = await resolveWorkspaceRootFrom(fs, paths);
186
- for (const entry of artifacts) {
187
- const path = entry?.path;
188
- const content = entry?.content;
189
- if (typeof path !== "string" || typeof content !== "string")
190
- continue;
191
- const relative = path.trim();
192
- if (out.has(relative))
193
- continue;
194
- if (coreDocumentKindForPath(relative, canon.canon) !== "plan")
195
- continue;
196
- const baseline = await baselineOfPlan(fs, root, content, canon.canon.spec);
197
- if (baseline !== null)
198
- out.set(relative, baseline);
199
- }
200
- return out;
201
- }
202
- /**
203
- * Run the source-boundary policy over the plan bytes a proposal is carrying.
204
- *
205
- * The same judgment the execution entry makes, one moment earlier — over the
206
- * artifact's content instead of a file on disk — so a plan accepted when it
207
- * closes cannot be rejected for the same cause when somebody tries to run it.
208
- * Nothing is read when the payload carries no artifact.
156
+ * The grammar half is the same judgment the execution entry makes, one moment
157
+ * earlier — over the artifact's content instead of a file on disk — so a plan
158
+ * accepted when it closes cannot be rejected for the same cause when somebody
159
+ * tries to run it. When the WORKSPACE block cannot be read, only the SEMANTIC
160
+ * half runs: answering an unreadable block with "that alias does not exist"
161
+ * would reject a plan for something the plan did not do.
209
162
  *
210
- * When the WORKSPACE block cannot be read, only the SEMANTIC half runs. The
211
- * clause-level judgment needs no alias table, and answering an unreadable block
212
- * with "that alias does not exist" would reject a plan for something the plan
213
- * did not do.
163
+ * The lineage half runs in the same stage, so one refusal carries both. The seal
164
+ * is computed here from the spec the header names — never taken from the sender,
165
+ * whose digest would prove only that they typed something — and a plan that
166
+ * declares neither a lineage nor `> Standalone:` is refused instead of published
167
+ * without a seal. Nothing is read when the payload carries no artifact.
214
168
  */
215
- async function observePlanEvidence(fs, paths, raw) {
169
+ async function observePlanArtifacts(fs, paths, raw) {
170
+ const evidence = [];
171
+ const baselines = new Map();
216
172
  const proposed = proposedArtifacts(raw);
217
173
  if (proposed.length === 0)
218
- return [];
174
+ return { evidence, baselines };
219
175
  const canon = await resolveCoreDocsCanon(fs, paths);
220
- // An unreadable canon cannot tell a plan from anything else, and judging a
221
- // spec's prose by the plan's closure rule would reject what it never promised.
176
+ // An unreadable canon cannot tell a plan from anything else: judging a spec's
177
+ // prose by the plan's closure rule, or sealing on a guess, would both act on
178
+ // whatever the payload happened to carry.
222
179
  if (!canon.ok)
223
- return [];
180
+ return { evidence, baselines };
224
181
  const root = await resolveWorkspaceRootFrom(fs, paths);
225
182
  const block = await readWorkspaceBlock(fs, root, paths.blockMarkers());
226
183
  const declared = block === null ? null : block.fuentes.map((source) => source.alias);
227
- const out = [];
228
184
  for (const artifact of proposed) {
229
185
  if (!checkSafeRelativePath(artifact.path).ok)
230
186
  continue;
231
187
  if (coreDocumentKindForPath(artifact.path, canon.canon) !== "plan")
232
188
  continue;
233
- out.push({
234
- path: artifact.path,
235
- failures: declared === null
236
- ? validateSourceBoundedSemantics(artifact.content)
237
- : validatePlanSourceBoundary(artifact.content, declared),
238
- });
189
+ const { failures, seal } = await planGrammarAtPublication(fs, root, artifact.content, declared, canon.canon.spec);
190
+ if (seal.status === "sealed")
191
+ baselines.set(artifact.path, seal.baseline);
192
+ evidence.push({ path: artifact.path, failures });
239
193
  }
240
- return out;
194
+ return { evidence, baselines };
241
195
  }
242
196
  /** The `path`/`content` pairs a payload proposes, read as data and judged by nobody. */
243
197
  function proposedArtifacts(raw) {
@@ -322,7 +276,7 @@ async function observeScope(fs, paths, raw) {
322
276
  declared,
323
277
  plan,
324
278
  sources: sourceAliasesOfPlan(text),
325
- boundary_failures: validatePlanSourceBoundary(text, declared ?? []),
279
+ boundary_failures: planGrammarAtEntry(text, declared),
326
280
  plan_error: null,
327
281
  };
328
282
  }
@@ -444,6 +398,24 @@ async function decide(fs, paths, state, input, snapshot) {
444
398
  const resend = resendCheck(state, resolved, cost);
445
399
  if (resend !== null)
446
400
  return resend;
401
+ // The one re-run of an exhausted internal boundary belongs to the CLI: an
402
+ // external answer here would be the fourth try the cap exists to refuse.
403
+ if (awaitingCliRerun(state, resolved.stopped)) {
404
+ return reject(state, resolved, `'${resolved.stopped.id}' agotó sus intentos y la vuelve a correr el CLI, no un llamador`, {
405
+ code: "FLOW_ANSWER_NOT_EXPECTED",
406
+ action: "corré 'aw flow advance': el CLI corre una vez más la operación interna y, si reconoce su efecto, avanza sin cobrar",
407
+ });
408
+ }
409
+ // An internal action is credited only by the driver that ran it: that is what
410
+ // leaves its `executed` event in the trace. A result from outside, however well
411
+ // formed, is a claim nobody verified — and applying it used to publish nothing
412
+ // while marking the step done.
413
+ if (resolved.kind === "execution" && internalActionOf(resolved.stopped) !== null) {
414
+ return reject(state, resolved, `'${resolved.stopped.id}' la ejecuta el CLI: un resultado externo no la acredita`, {
415
+ code: "FLOW_INTERNAL_ACTION_EXTERNAL_RESULT",
416
+ 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`,
417
+ }, cost);
418
+ }
447
419
  // The action the run is waiting on is the one that was EMITTED, and the seal
448
420
  // would already refuse a result about any other. What the persisted digest adds
449
421
  // is the diagnosis: when the two differ, the invocation changed underneath a run
@@ -453,7 +425,7 @@ async function decide(fs, paths, state, input, snapshot) {
453
425
  if (drifted !== null)
454
426
  return drifted;
455
427
  // 2 · The boundary in force decides what is admissible.
456
- const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts);
428
+ const admissible = admit(state, resolved, resolved.stopped, input, cost, snapshot.checkouts, snapshot.scoped);
457
429
  if ("decision" in admissible)
458
430
  return admissible.decision;
459
431
  const parsed = admissible;
@@ -467,7 +439,12 @@ async function decide(fs, paths, state, input, snapshot) {
467
439
  if (route.kind === "adjust") {
468
440
  return adjustRouteProposal(state, journey);
469
441
  }
470
- const routed = route.kind === "accept" ? withRouteDecisions(state, route.decisions) : state;
442
+ const accepted = route.kind === "accept" ? withRouteDecisions(state, route.decisions) : state;
443
+ // Sealed with the transition it earned, in the same state publication: a batch
444
+ // is never seen validated without the proofs that validated it.
445
+ const routed = parsed.credit === undefined
446
+ ? accepted
447
+ : withPlanExecBatchCredit(accepted, parsed.credit.batch, parsed.credit.proofs);
471
448
  // The registry, not the Spanish consequence text, owns what a selected
472
449
  // alternative does. A handoff must stop before any later plan-exec row can
473
450
  // mark work, validate or commit; a decision selection is durably recorded
@@ -561,9 +538,12 @@ async function decide(fs, paths, state, input, snapshot) {
561
538
  if (preview.preview !== null) {
562
539
  selectedState = withFixPreview(selectedState, preview.preview);
563
540
  }
541
+ // A redraft recorded by `grantOf` inserted rows after this boundary, so the
542
+ // transition is applied against the journey as it now reads.
543
+ const walked = journeyForRun(selectedState);
564
544
  const outcome = holds
565
- ? holdAfterApproval(selectedState, journey, identity)
566
- : applyAndAdvance(selectedState, journey, resolved.stopped, identity, parsed.answer);
545
+ ? holdAfterApproval(selectedState, walked, identity)
546
+ : applyAndAdvance(selectedState, walked, resolved.stopped, identity, parsed.answer);
567
547
  return standalone === null ? outcome : withStandaloneGuidance(outcome, standalone);
568
548
  }
569
549
  /**
@@ -738,17 +718,28 @@ function routeAnswer(state, stopped, journey, answer) {
738
718
  },
739
719
  };
740
720
  }
721
+ const { decision, control } = configuredControl;
722
+ const consequence = control.consequences[raw.disposition];
723
+ if (consequence === undefined) {
724
+ return {
725
+ kind: "failure",
726
+ failure: {
727
+ code: "FLOW_ROUTE_HARD_GATE",
728
+ message: `'${raw.transition}' no admite '${raw.disposition}': el registro no deja que una ruta la saltee`,
729
+ action: "declará apply o substitute para ese control; la evidencia que acredita el paso no se omite",
730
+ },
731
+ };
732
+ }
741
733
  const substitution = routeSubstitution(raw.disposition, raw.substitution);
742
734
  if ("failure" in substitution)
743
735
  return { kind: "failure", failure: substitution.failure };
744
- const { decision, control } = configuredControl;
745
736
  controls.push({
746
737
  transition: decision.id,
747
738
  title: decision.title,
748
739
  disposition: raw.disposition,
749
740
  recommendation: control.recommendation,
750
741
  alternatives: control.consequences,
751
- consequence: control.consequences[raw.disposition],
742
+ consequence,
752
743
  risk: control.risk,
753
744
  reason: raw.reason.trim(),
754
745
  substitution: substitution.value,
@@ -1275,11 +1266,11 @@ async function registerSelectedDecision(fs, paths, state, snapshot, resolved, co
1275
1266
  * run that merely advanced would be indistinguishable from one that never decided.
1276
1267
  */
1277
1268
  function standaloneDecisionEvent(state, transition, record) {
1278
- const batchIteration = currentBatchIteration(state, transition);
1269
+ const rowIteration = iterationOf(state, transition);
1279
1270
  return {
1280
1271
  kind: "executed",
1281
1272
  transition,
1282
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
1273
+ ...rowIteration,
1283
1274
  operation: "plan-exec.standalone-decision",
1284
1275
  summary: record.decision,
1285
1276
  output_digest: record.digest,
@@ -1322,36 +1313,6 @@ function withStandaloneGuidance(decision, record) {
1322
1313
  function invalidScope(message, action) {
1323
1314
  return { code: "FLOW_SCOPE_INVALID", message, action };
1324
1315
  }
1325
- /**
1326
- * What to do about a source-boundary failure, said at the moment it was found.
1327
- *
1328
- * One generic sentence used to answer all five codes, and it was wrong twice
1329
- * over: it sent somebody to declare sources that were already declared, and it
1330
- * sent a sentence of prose to a refinement. Structure goes to the refinement
1331
- * that owns it; a clause that names no observable check is a phrase to fix where
1332
- * it is being written, and at a save proposal nothing is even saved yet.
1333
- */
1334
- function planBoundaryAction(code, moment) {
1335
- const then = moment === "proposal"
1336
- ? "corregilo en los bytes y volvé a proponer la vista previa"
1337
- : "corregilo con /w:plan-refine antes de volver a entrar a ejecución";
1338
- switch (code) {
1339
- case "PLAN_SOURCE_BOUNDARY_MISSING":
1340
- return `falta la declaración estructural: '> Límite de ejecución: checkout' bajo el título y '> Fuentes:' en cada fase — ${then}`;
1341
- case "PLAN_SOURCE_UNKNOWN":
1342
- return `ese alias no está en la tabla Fuentes del bloque WORKSPACE: declaralo ahí, o usá uno de los que ya están — ${then}`;
1343
- case "PLAN_TASK_SOURCE_OUTSIDE_PHASE":
1344
- return `la fuente de una tarea es un subconjunto de la de su fase: ajustá una de las dos — ${then}`;
1345
- case "PLAN_SOURCE_EXTERNAL_CLOSURE":
1346
- return `una cláusula de cierre no puede apoyarse en una superficie externa: llevá esa comprobación a '## Handoff operativo' y dejá en la cláusula una del checkout — ${then}`;
1347
- case "PLAN_SOURCE_LOCAL_PROOF_MISSING":
1348
- return moment === "proposal"
1349
- ? "nombrá en esa cláusula el comando, el archivo o la ruta que produce la comprobación, y volvé a proponer la vista previa"
1350
- : "es una frase del documento y no su estructura: nombrá en esa cláusula el comando, el archivo o la ruta que produce la comprobación";
1351
- default:
1352
- return then;
1353
- }
1354
- }
1355
1316
  /**
1356
1317
  * The closure-evidence gate, run BEFORE the preview is sealed.
1357
1318
  *
@@ -1370,10 +1331,14 @@ function planEvidenceFrom(stopped, answer, observed) {
1370
1331
  const failure = plan.failures[0];
1371
1332
  if (failure === undefined)
1372
1333
  continue;
1334
+ // Grammar and lineage travel together: whoever fixes the first cause
1335
+ // should not learn of the header on the next attempt.
1336
+ const lineage = plan.failures.find((candidate) => candidate !== failure && candidate.code === "PLAN_LINEAGE_UNSEALED");
1337
+ const also = lineage === undefined ? "" : ` · además: ${lineage.message}`;
1373
1338
  return {
1374
1339
  failure: {
1375
1340
  code: failure.code,
1376
- message: `'${plan.path}': ${failure.message}`,
1341
+ message: `'${plan.path}': ${failure.message}${also}`,
1377
1342
  action: planBoundaryAction(failure.code, "proposal"),
1378
1343
  },
1379
1344
  };
@@ -1415,25 +1380,27 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1415
1380
  const artifacts = answer.artifacts.map((artifact) => {
1416
1381
  const seen = snapshot.get(artifact.path);
1417
1382
  const baseline = baselines.get(artifact.path);
1383
+ /**
1384
+ * Completing THIS run's reservation replaces no document.
1385
+ *
1386
+ * The file is there because this run put it there to hold the number, and
1387
+ * nothing was ever published into it — so the write is additive in the only
1388
+ * sense the classes measure, and the preview says so: `overwrite: false`,
1389
+ * `reserved: true`. A destination that exists for any other reason stays an
1390
+ * overwrite, which is what keeps a save row declaring only `local_additive`
1391
+ * from reaching a real plan.
1392
+ */
1393
+ const reserved = seen?.exists === true && seen.digest === reservation;
1418
1394
  return {
1419
1395
  path: artifact.path,
1420
1396
  // Publication seals the baseline into the very bytes it is about to
1421
1397
  // propose, so preview, approval and write cover it as one thing.
1422
1398
  content: baseline === undefined ? artifact.content : withSpecBaseline(artifact.content, baseline),
1423
- overwrite: seen?.exists === true,
1424
- /**
1425
- * Completing THIS run's reservation replaces no document.
1426
- *
1427
- * The file is there because this run put it there to hold the number, and
1428
- * nothing was ever published into it — so the write is additive in the only
1429
- * sense the classes measure: nobody's content is lost. A destination that
1430
- * exists for any other reason stays `mutate_overwrite`, which is what keeps
1431
- * a save row declaring only `local_additive` from reaching a real plan.
1432
- */
1433
- reserved: seen?.exists === true && seen.digest === reservation,
1399
+ overwrite: seen?.exists === true && !reserved,
1400
+ ...(reserved ? { reserved: true } : {}),
1434
1401
  };
1435
1402
  });
1436
- const effects = observedEffects(artifacts.map((a) => ({ overwrite: a.overwrite && !a.reserved })));
1403
+ const effects = observedEffects(artifacts);
1437
1404
  const beyond = effects.filter((effect) => !contract.effects.includes(effect));
1438
1405
  if (beyond.length > 0) {
1439
1406
  return {
@@ -1444,8 +1411,10 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1444
1411
  },
1445
1412
  };
1446
1413
  }
1414
+ // The reservation is a base too: the marker has to be the one this run left
1415
+ // when the write lands, or somebody published into the slot in between.
1447
1416
  const bases = artifacts
1448
- .filter((a) => a.overwrite)
1417
+ .filter((a) => a.overwrite || a.reserved === true)
1449
1418
  .map((a) => ({ path: a.path, digest: snapshot.get(a.path)?.digest ?? "" }));
1450
1419
  const unreadable = bases.filter((base) => base.digest.length === 0);
1451
1420
  if (unreadable.length > 0) {
@@ -1477,6 +1446,26 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1477
1446
  })),
1478
1447
  };
1479
1448
  }
1449
+ /**
1450
+ * The redraft a confirmation row's alternative asked for, or `null`.
1451
+ *
1452
+ * It unseats any standing proposal and records a reentry from the flow's
1453
+ * authoring row, so the run walks the redraft and its gate again instead of
1454
+ * carrying on to a publication with nothing to publish and a finalize that
1455
+ * closes the session. Independent of whether a proposal is seated: a proposal
1456
+ * the run gave up on is exactly when redrafting matters most.
1457
+ */
1458
+ function redraftOf(state, stopped, answer, journey) {
1459
+ const redraft = reentryOf(stopped);
1460
+ if (redraft === null || answer.choice !== redraft.label)
1461
+ return null;
1462
+ return withReentry(withProposal(state, null), {
1463
+ kind: "refine",
1464
+ transition: stopped.id,
1465
+ occurrence: occurrenceAt(journey, state.applied.length),
1466
+ from: redraft.from,
1467
+ });
1468
+ }
1480
1469
  /**
1481
1470
  * The grant this answer produced, scoped to what it was given over.
1482
1471
  *
@@ -1491,8 +1480,13 @@ function sealFrom(state, stopped, answer, snapshot, baselines) {
1491
1480
  */
1492
1481
  function grantOf(state, resolved, answer, granted, journey) {
1493
1482
  const stopped = resolved.stopped;
1483
+ if (stopped === null)
1484
+ return state;
1485
+ const redrafted = redraftOf(state, stopped, answer, journey);
1486
+ if (redrafted !== null)
1487
+ return redrafted;
1494
1488
  const proposal = state.proposal;
1495
- const approve = stopped === null ? null : publishApprovalOf(stopped);
1489
+ const approve = publishApprovalOf(stopped);
1496
1490
  if (approve !== null && proposal !== null) {
1497
1491
  if (answer.choice === approve) {
1498
1492
  return withApproval(state, {
@@ -1501,13 +1495,11 @@ function grantOf(state, resolved, answer, granted, journey) {
1501
1495
  classes: [...proposal.requires_approval],
1502
1496
  });
1503
1497
  }
1504
- // Declining unseats the proposal, and that is what makes `Refinar` cost
1505
- // nothing: the publication downstream finds nothing to publish and is skipped
1506
- // saying so, instead of stopping to ask for an authorization over bytes the
1498
+ // Declining unseats the proposal: no authorization is asked over bytes the
1507
1499
  // person just turned down.
1508
1500
  return withProposal(state, null);
1509
1501
  }
1510
- const link = stopped === null ? null : approvalGrantOf(stopped);
1502
+ const link = approvalGrantOf(stopped);
1511
1503
  if (link !== null && answer.choice === link.approve) {
1512
1504
  const target = journey.find((row) => row.id === link.transition);
1513
1505
  // A link to a transition this journey does not walk grants nothing: the
@@ -1538,7 +1530,7 @@ function grantOf(state, resolved, answer, granted, journey) {
1538
1530
  * a payload the boundary's own contract rejects, a decline (a real answer that
1539
1531
  * applies nothing), and an execution result that did not earn its transition.
1540
1532
  */
1541
- function admit(state, resolved, stopped, input, cost, checkouts) {
1533
+ function admit(state, resolved, stopped, input, cost, checkouts, scoped) {
1542
1534
  const expectedApproval = resolved.kind === "authorization"
1543
1535
  ? effectApprovalDigest(stopped.id, resolved.authorization?.planned ?? [])
1544
1536
  : null;
@@ -1584,7 +1576,7 @@ function admit(state, resolved, stopped, input, cost, checkouts) {
1584
1576
  return {
1585
1577
  decision: reject(state, resolved, `'${PAUSE_LABEL}': la frontera queda en pie y la corrida retoma acá`, {
1586
1578
  code: "FLOW_BOUNDARY_PAUSED",
1587
- action: "escribí el CHECKPOINT con 'aw checkpoint-write', compactá, y volvé con 'aw flow advance' a esta misma frontera",
1579
+ action: `escribí el CHECKPOINT con 'aw checkpoint-write --code ${state.session}', compactá, y volvé con 'aw flow advance --code ${state.session}' a esta misma frontera`,
1588
1580
  outcome: "needs_input",
1589
1581
  }, cost),
1590
1582
  };
@@ -1601,22 +1593,53 @@ function admit(state, resolved, stopped, input, cost, checkouts) {
1601
1593
  // An execution result has to EARN the transition. Anything short of a completed
1602
1594
  // run with its evidence and its whole effect keeps the boundary standing: the
1603
1595
  // work stays pending, with the recovery the action declared.
1604
- if (resolved.kind === "execution") {
1605
- // The row's `effects` are the ceiling and the sealed proposal is what really
1606
- // happens: demanding an overwrite from a publication that only creates files
1607
- // would leave the transition pending for an effect nobody could produce.
1608
- const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts);
1609
- if (verdict !== null) {
1610
- const trace = declaredTrace(state, stopped, resolved, parsed.answer, verdict);
1611
- return {
1612
- decision: reject(state, resolved, verdict.message, verdict.detail, {
1613
- ...cost,
1614
- ...(trace === null ? {} : { trace }),
1615
- }),
1616
- };
1617
- }
1596
+ if (resolved.kind !== "execution")
1597
+ return parsed;
1598
+ return earned(state, resolved, stopped, parsed, cost, checkouts, scoped);
1599
+ }
1600
+ /** The execution verdict, then — at the phase validation — the batch's own credit. */
1601
+ function earned(state, resolved, stopped, parsed, cost, checkouts, scoped) {
1602
+ // The row's `effects` are the ceiling and the sealed proposal is what really
1603
+ // happens: demanding an overwrite from a publication that only creates files
1604
+ // would leave the transition pending for an effect nobody could produce.
1605
+ const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped), checkouts);
1606
+ if (verdict !== null) {
1607
+ const trace = declaredTrace(state, stopped, resolved, parsed.answer, verdict);
1608
+ return {
1609
+ decision: reject(state, resolved, verdict.message, verdict.detail, {
1610
+ ...cost,
1611
+ ...(trace === null ? {} : { trace }),
1612
+ }),
1613
+ };
1614
+ }
1615
+ const credited = batchCredit(state, stopped, parsed.answer, resolved, scoped);
1616
+ if (credited === null)
1617
+ return parsed;
1618
+ if (!credited.ok) {
1619
+ return {
1620
+ decision: reject(state, resolved, credited.refusal.message, credited.refusal.detail, cost),
1621
+ };
1622
+ }
1623
+ return { ...parsed, credit: credited.credit };
1624
+ }
1625
+ /**
1626
+ * The phase validation's own judgment: the batch it closes must bring a proof of
1627
+ * each of its sources, of its own checkout. `null` on every other boundary, and
1628
+ * on a run with no batch standing, where there is nothing to credit.
1629
+ */
1630
+ function batchCredit(state, stopped, answer, resolved, scoped) {
1631
+ if (stopped.id !== PHASE_VALIDATION || answer.result === null || state.scope === null) {
1632
+ return null;
1618
1633
  }
1619
- return parsed;
1634
+ const iteration = state.batch_loop?.iteration ?? null;
1635
+ const batches = state.batches ?? [];
1636
+ const batch = batches.find((entry) => entry.iteration === iteration);
1637
+ if (batch === undefined)
1638
+ return null;
1639
+ const verdict = batchCreditVerdict(answer.result, { batch, batches, sources: state.scope.sources, scoped }, resolved.action?.recovery ?? "");
1640
+ if (!verdict.ok)
1641
+ return verdict;
1642
+ return { ok: true, credit: { batch: batch.id, proofs: verdict.credit } };
1620
1643
  }
1621
1644
  /**
1622
1645
  * The material fact a REFUSED external result still leaves behind — or `null`.
@@ -1638,11 +1661,11 @@ function declaredTrace(state, stopped, resolved, answer, verdict) {
1638
1661
  if (!touchesTheWorld(applied) && verdict.detail.code !== "FLOW_EFFECT_PARTIAL")
1639
1662
  return null;
1640
1663
  const invocation = resolved.action?.invocation;
1641
- const batchIteration = currentBatchIteration(state, stopped.id);
1664
+ const rowIteration = iterationOf(state, stopped.id);
1642
1665
  return {
1643
1666
  kind: "failed",
1644
1667
  transition: stopped.id,
1645
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
1668
+ ...rowIteration,
1646
1669
  // What was really run, named the way whoever ran it would recognize it: an
1647
1670
  // external execution has no internal operation id to quote.
1648
1671
  operation: invocation === undefined ? stopped.id : [invocation.program, ...invocation.args].join(" "),
@@ -1699,13 +1722,6 @@ function applyAndAdvance(approved, journey, stopped, identity, answer) {
1699
1722
  ? withObservation(approved, observationFor(approved, stopped.id, answer.signals))
1700
1723
  : approved;
1701
1724
  next = withPlanExecBatchStageForTransition(applyTransition(next, stopped.id, effectsOfTransition(next, stopped)), stopped.id);
1702
- // A published proposal is spent, whoever published it. This is the degraded
1703
- // path — no internal executor, so the caller ran the write and returned its
1704
- // result — and leaving the proposal seated would keep a preview of bytes that
1705
- // are already on disk standing in front of the next boundary.
1706
- if (internalActionOf(stopped)?.operation === "proposal.publish") {
1707
- next = withProposal(next, null);
1708
- }
1709
1725
  next = withAttempt(next, identity);
1710
1726
  // The boundary follows the position, always: handing the engine a state whose
1711
1727
  // boundary still names the transition just applied is exactly the incoherence
@@ -1860,7 +1876,7 @@ function resendCheck(state, resolved, cost) {
1860
1876
  // The twin was refused: this is a retry of the same wrong answer, so it gets
1861
1877
  // the same real diagnosis again and counts toward the cap, which is what
1862
1878
  // eventually degrades the boundary instead of leaving the caller looping.
1863
- if (twin !== undefined && !state.applied.includes(twin.transition))
1879
+ if (twin !== undefined && twinWasRefused(state, twin))
1864
1880
  return null;
1865
1881
  return reject(state, resolved, "esta respuesta ya se había aplicado: el recorrido no avanza dos veces", {
1866
1882
  code: "FLOW_ANSWER_RESENT",
@@ -1868,6 +1884,19 @@ function resendCheck(state, resolved, cost) {
1868
1884
  outcome: "completed",
1869
1885
  }, cost);
1870
1886
  }
1887
+ /**
1888
+ * Whether the attempt a resend matches never advanced anything.
1889
+ *
1890
+ * Its transition missing from `applied` says so. So does its transition being
1891
+ * the boundary still standing at the same iteration: a repeated row (a batch or
1892
+ * a redraft copy) has its id in `applied` from an earlier walk, and that earlier
1893
+ * walk is not this one.
1894
+ */
1895
+ function twinWasRefused(state, twin) {
1896
+ if (!state.applied.includes(twin.transition))
1897
+ return true;
1898
+ return (twin.transition === state.boundary && sameIteration(twin, iterationOf(state, twin.transition)));
1899
+ }
1871
1900
  /**
1872
1901
  * A rejection, expressed as the recalculated directive.
1873
1902
  *
@@ -1929,7 +1958,7 @@ function reject(state, resolved, message, detail, cost) {
1929
1958
  */
1930
1959
  function attemptIdentity(state, input, seal, transition) {
1931
1960
  const digest = semanticDigest({ payload: input.raw, approval: input.approval });
1932
- const batchIteration = currentBatchIteration(state, transition);
1961
+ const rowIteration = iterationOf(state, transition);
1933
1962
  const prior = state.attempts.filter((past) => past.invocation_id === seal);
1934
1963
  const twin = prior.find((past) => past.request_digest === digest);
1935
1964
  // The next ordinal, asked with the SAME expression `AttemptLedger.record`
@@ -1953,18 +1982,18 @@ function attemptIdentity(state, input, seal, transition) {
1953
1982
  // The seal moves with the state; the transition does not while the run
1954
1983
  // stands there. It is what the cap counts over.
1955
1984
  transition,
1956
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
1985
+ ...rowIteration,
1957
1986
  };
1958
1987
  }
1959
1988
  function isRecord(value) {
1960
1989
  return typeof value === "object" && value !== null && !Array.isArray(value);
1961
1990
  }
1962
1991
  function observationFor(state, transition, signals) {
1963
- const batchIteration = currentBatchIteration(state, transition);
1992
+ const rowIteration = iterationOf(state, transition);
1964
1993
  return {
1965
1994
  transition,
1966
1995
  signals,
1967
- ...(batchIteration === null ? {} : { batch_iteration: batchIteration }),
1996
+ ...rowIteration,
1968
1997
  };
1969
1998
  }
1970
1999
  //# sourceMappingURL=submit.js.map