@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
@@ -26,6 +26,8 @@ import { canonicalJson, semanticDigest } from "../../application/semantic-operat
26
26
  import { isEffectClass, touchesTheWorld } from "../capability/effects.js";
27
27
  import { AttemptLedger } from "../capability/protocol.js";
28
28
  import { normalizeObligations, validateDecisionNote } from "../decision-note.js";
29
+ import { expandJourney, journeyOfFlow, } from "./authority.js";
30
+ import { V11_JOURNEY_BASE } from "./journey-baseline.js";
29
31
  import { assuranceForRoute, isAssuranceStatus, isRouteDisposition, } from "./route.js";
30
32
  /**
31
33
  * The version this CLI WRITES. Every state it persists carries it.
@@ -38,17 +40,50 @@ import { assuranceForRoute, isAssuranceStatus, isRouteDisposition, } from "./rou
38
40
  * turning the cap off in silence while somebody alternates CLI versions over one
39
41
  * run. Failing with a cause is the requirement; failing silently is the defect.
40
42
  */
41
- export const FLOW_RUN_STATE_VERSION = 11;
43
+ export const FLOW_RUN_STATE_VERSION = 13;
44
+ /**
45
+ * The versions this CLI CONTINUES without adoption, newest first.
46
+ *
47
+ * v12 added the `aligned`, `restarted` and `annulled` trace kinds and `journey_base`, whose truthful value
48
+ * for a v11 run is the journey 25.6.1 walked. So a v11 run keeps walking after
49
+ * an upgrade: {@link upgradeContinuable} re-stamps it without touching the
50
+ * cursor, the answers or the attempts. Every later version joins this set with its own
51
+ * upgrade step, which is what keeps "v11 or later survives an update" true
52
+ * without an exception per release.
53
+ *
54
+ * v13 added `reentries` and the `reentry_iteration` of attempts, observations and
55
+ * events. All three are absent on a run that never re-entered, which is every
56
+ * v12 run, so its upgrade step only re-stamps the version.
57
+ * It also added each batch's `base` and `credit`, a reseated run's `inherited_bases`
58
+ * and the `route-refused` trace kind. A v12 run has none of them either, and a
59
+ * batch without `base` is read as the one that was in flight when the CLI updated.
60
+ */
61
+ export const FLOW_RUN_STATE_CONTINUABLE = [FLOW_RUN_STATE_VERSION, 12, 11];
42
62
  /**
43
63
  * The versions this CLI READS, newest first.
44
64
  *
45
- * Versions 9, 8 and 7 remain readable for status, recovery evidence and an
46
- * explicit adoption. They are NOT writable execution state: v10 records batches
47
- * and their append-only iteration trace, so continuing an old cursor by merely
48
- * changing `version` would invent a batch boundary the old run never declared.
49
- * An active legacy run must be adopted explicitly before any mutation.
65
+ * Versions 10 to 7 remain readable for status and recovery evidence only. They
66
+ * are NOT continuable execution state: continuing an old cursor by merely
67
+ * changing `version` would invent a batch boundary the old run never declared,
68
+ * so their way out is `aw flow restart`.
50
69
  */
51
- export const FLOW_RUN_STATE_READABLE = [FLOW_RUN_STATE_VERSION, 10, 9, 8, 7];
70
+ export const FLOW_RUN_STATE_READABLE = [
71
+ ...FLOW_RUN_STATE_CONTINUABLE,
72
+ 10,
73
+ 9,
74
+ 8,
75
+ 7,
76
+ ];
77
+ /**
78
+ * One upgrade step per continuable version, keyed by the version it upgrades FROM.
79
+ *
80
+ * A step may only add what the next version introduced with a truthful default;
81
+ * the cursor, the answers, the approvals and the attempts pass through untouched.
82
+ */
83
+ const CONTINUABLE_UPGRADES = {
84
+ 11: (state) => ({ ...state, version: 12, journey_base: [...V11_JOURNEY_BASE[state.flow]] }),
85
+ 12: (state) => ({ ...state, version: 13 }),
86
+ };
52
87
  /** The CLI-owned run state inside the session folder. Machine-local, dotted. */
53
88
  export const FLOW_RUN_STATE_FILE = ".flow-run.json";
54
89
  /**
@@ -84,12 +119,47 @@ export function attemptsAt(state, transition) {
84
119
  }
85
120
  /** The monotone-counter key for one boundary, scoped when that boundary repeats in a batch. */
86
121
  export function attemptCounterKey(state, transition) {
87
- const iteration = currentBatchIteration(state, transition);
88
- return attemptCounterKeyForIteration(transition, iteration ?? undefined);
122
+ return attemptCounterKeyForIteration(transition, iterationOf(state, transition));
89
123
  }
90
- /** Stable counter key for a historical attempt whose batch iteration is recorded on the row. */
124
+ /** Stable counter key for a historical attempt whose iteration is recorded on the row. */
91
125
  export function attemptCounterKeyForIteration(transition, iteration) {
92
- return iteration === undefined ? transition : `${transition}@batch-${iteration}`;
126
+ const batch = iteration.batch_iteration === undefined ? "" : `@batch-${iteration.batch_iteration}`;
127
+ const reentry = iteration.reentry_iteration === undefined ? "" : `@reentry-${iteration.reentry_iteration}`;
128
+ return `${transition}${batch}${reentry}`;
129
+ }
130
+ /**
131
+ * The iteration of `transition` where the run stands: its batch copy and its
132
+ * reentry copy. Spread it into any record keyed per boundary.
133
+ */
134
+ export function iterationOf(state, transition) {
135
+ const batch = currentBatchIteration(state, transition);
136
+ const reentry = currentReentryIteration(state, transition);
137
+ return {
138
+ ...(batch === null ? {} : { batch_iteration: batch }),
139
+ ...(reentry === null ? {} : { reentry_iteration: reentry }),
140
+ };
141
+ }
142
+ /** Whether a persisted record belongs to this iteration of its boundary. */
143
+ export function sameIteration(record, iteration) {
144
+ return (record.batch_iteration === iteration.batch_iteration &&
145
+ record.reentry_iteration === iteration.reentry_iteration);
146
+ }
147
+ /**
148
+ * Which reentry inserted the row of `transition` the cursor is on, or `null` on
149
+ * the row's own place. Read at the cursor and backwards from it, so it answers
150
+ * the same before the row is applied and right after.
151
+ */
152
+ export function currentReentryIteration(state, transition) {
153
+ if ((state.reentries ?? []).length === 0)
154
+ return null;
155
+ const { rows, copies } = expandJourney(state);
156
+ for (let index = Math.min(state.applied.length, rows.length - 1); index >= 0; index -= 1) {
157
+ if (rows[index]?.id !== transition)
158
+ continue;
159
+ const copy = copies[index] ?? 0;
160
+ return copy === 0 ? null : copy;
161
+ }
162
+ return null;
93
163
  }
94
164
  /**
95
165
  * The three numbers and the difference between them, computed once.
@@ -100,11 +170,8 @@ export function attemptCounterKeyForIteration(transition, iteration) {
100
170
  * directive and the recovery guard would end up disagreeing about the same run.
101
171
  */
102
172
  function spendAt(state, transition) {
103
- const iteration = currentBatchIteration(state, transition);
104
- const rows = state.attempts.filter((attempt) => attempt.transition === transition &&
105
- (iteration === null
106
- ? attempt.batch_iteration === undefined
107
- : attempt.batch_iteration === iteration));
173
+ const iteration = iterationOf(state, transition);
174
+ const rows = state.attempts.filter((attempt) => attempt.transition === transition && sameIteration(attempt, iteration));
108
175
  const key = attemptCounterKey(state, transition);
109
176
  const floor = state.attempt_floor?.[key] ?? 0;
110
177
  const granted = state.attempt_grants?.[key] ?? 0;
@@ -289,11 +356,10 @@ export function applyAttemptReconciliation(state, reconciliation) {
289
356
  grants[key] = repair.after;
290
357
  next = sealRunState({ ...withoutSeal(next), attempt_grants: grants });
291
358
  }
292
- const iteration = currentBatchIteration(next, reconciliation.transition);
293
359
  return withEvent(next, {
294
360
  kind: "reconciled",
295
361
  transition: reconciliation.transition,
296
- ...(iteration === null ? {} : { batch_iteration: iteration }),
362
+ ...iterationOf(next, reconciliation.transition),
297
363
  operation: "flow.attempt-reconciliation",
298
364
  repairs: reconciliation.repairs,
299
365
  });
@@ -323,11 +389,8 @@ function unanswerableAt(state) {
323
389
  return null;
324
390
  }
325
391
  export function recoveryBlockedAt(state, transition) {
326
- const iteration = currentBatchIteration(state, transition);
327
- const trace = state.events.filter((event) => event.transition === transition &&
328
- (iteration === null
329
- ? event.batch_iteration === undefined
330
- : event.batch_iteration === iteration));
392
+ const iteration = iterationOf(state, transition);
393
+ const trace = state.events.filter((event) => event.transition === transition && sameIteration(event, iteration));
331
394
  const material = [...trace].reverse().find(declaresMaterialEffect);
332
395
  if (material !== undefined)
333
396
  return { reason: "materialized", event: material };
@@ -348,8 +411,13 @@ export function recoveryBlockedAt(state, transition) {
348
411
  function declaresMaterialEffect(event) {
349
412
  // A reconciliation moved the run's own counters and nothing else: it can no
350
413
  // more block a recovery than reading the file could.
351
- if (event.kind === "reconciled")
414
+ if (event.kind === "reconciled" ||
415
+ event.kind === "aligned" ||
416
+ event.kind === "route-refused" ||
417
+ event.kind === "restarted" ||
418
+ event.kind === "annulled") {
352
419
  return false;
420
+ }
353
421
  if (event.kind !== "failed")
354
422
  return touchesTheWorld(event.effects);
355
423
  if (event.effects === undefined)
@@ -405,6 +473,7 @@ export function newRunState(flow, session) {
405
473
  flow,
406
474
  session,
407
475
  scope: null,
476
+ journey_base: journeyOfFlow(flow).map((decision) => decision.id),
408
477
  applied: [],
409
478
  skipped: [],
410
479
  boundary: null,
@@ -525,32 +594,43 @@ export function grantAttempts(state, transition, attempts) {
525
594
  granted[key] = (granted[key] ?? 0) + attempts;
526
595
  return sealRunState({ ...withoutSeal(state), attempt_grants: granted });
527
596
  }
597
+ /** Whether this exhausted boundary already spent the one re-run the CLI gives it. */
598
+ export function exhaustedRerunSpent(state, transition) {
599
+ return (state.exhausted_reruns ?? []).includes(attemptCounterKey(state, transition));
600
+ }
601
+ /** Record that the CLI's one re-run of this exhausted boundary came back refused. */
602
+ export function withExhaustedRerunSpent(state, transition) {
603
+ if (exhaustedRerunSpent(state, transition))
604
+ return state;
605
+ return sealRunState({
606
+ ...withoutSeal(state),
607
+ exhausted_reruns: [...(state.exhausted_reruns ?? []), attemptCounterKey(state, transition)],
608
+ });
609
+ }
528
610
  /**
529
- * Explicitly adopt a readable legacy state into v10.
530
- *
531
- * This function is intentionally NOT called by the reader. Calling it is the
532
- * durable act of adoption after the caller has named the flow and accepted that
533
- * no pre-v10 batch is being reconstructed. Empty batch arrays mean exactly
534
- * "legacy history preserved; no v10 batch claimed", never "the old run had no
535
- * batches".
611
+ * Forget a spent re-run: `aw flow recover` gives the boundary a fresh budget,
612
+ * and a later exhaustion is a new episode that is owed its own re-run.
536
613
  */
537
- export function atCurrentVersion(state) {
538
- if (state.version === FLOW_RUN_STATE_VERSION)
614
+ export function withoutExhaustedRerun(state, transition) {
615
+ if (!exhaustedRerunSpent(state, transition))
539
616
  return state;
617
+ const key = attemptCounterKey(state, transition);
540
618
  return sealRunState({
541
619
  ...withoutSeal(state),
542
- version: FLOW_RUN_STATE_VERSION,
543
- batches: [],
544
- batch_trace: [],
545
- batch_loop: { pending: true, iteration: 1 },
546
- handoff: null,
547
- selected_choice: null,
548
- decision_preparation: null,
549
- route_proposal: null,
550
- route_decisions: null,
551
- assurance: "verified",
620
+ exhausted_reruns: (state.exhausted_reruns ?? []).filter((spent) => spent !== key),
552
621
  });
553
622
  }
623
+ /**
624
+ * The one exit from a run the CLI cannot continue: archive it inside the session
625
+ * and adopt a new one. Named in every refusal whose state it resolves.
626
+ */
627
+ export function restartInvocation(session) {
628
+ return `aw flow restart --session ${session}`;
629
+ }
630
+ /** The action of every refusal over a registry this CLI cannot use as it is. */
631
+ function unusableRemedy(session) {
632
+ return `corré '${restartInvocation(session)}': archiva el registro dentro de la sesión, re-adopta su flow y lo deja en la traza — o restaurá una copia íntegra del archivo`;
633
+ }
554
634
  /** Keep the sealed preview the person is choosing over; no cursor moves here. */
555
635
  export function withRouteProposal(state, proposal) {
556
636
  return sealRunState({
@@ -568,9 +648,68 @@ export function withRouteDecisions(state, decisions) {
568
648
  assurance: assuranceForRoute(decisions, state.applied),
569
649
  });
570
650
  }
571
- /** Whether a parsed state needs the explicit adoption boundary before mutation. */
651
+ /** Whether a parsed state is outside the continuable set and cannot be mutated. */
572
652
  export function legacyRunNeedsAdoption(state) {
573
- return state.version !== FLOW_RUN_STATE_VERSION;
653
+ return !FLOW_RUN_STATE_CONTINUABLE.includes(state.version);
654
+ }
655
+ /**
656
+ * Walk a continuable state up to the version this CLI writes, one step at a time.
657
+ *
658
+ * Re-sealed once at the end: the file on disk was already verified against its
659
+ * own seal, and the upgraded state is persisted by the next write, never here.
660
+ * A legacy state comes back unchanged — reading one must never re-stamp it.
661
+ */
662
+ export function upgradeContinuable(state) {
663
+ if (legacyRunNeedsAdoption(state) || state.version === FLOW_RUN_STATE_VERSION)
664
+ return state;
665
+ let current = state;
666
+ while (current.version !== FLOW_RUN_STATE_VERSION) {
667
+ const step = CONTINUABLE_UPGRADES[current.version];
668
+ if (step === undefined)
669
+ return state;
670
+ current = step(current);
671
+ }
672
+ return sealRunState(withoutSeal(current));
673
+ }
674
+ /** Record a reentry of the journey, after every one already recorded. */
675
+ export function withReentry(state, reentry) {
676
+ return sealRunState({
677
+ ...withoutSeal(state),
678
+ reentries: [...(state.reentries ?? []), reentry],
679
+ });
680
+ }
681
+ /**
682
+ * Take back the last reentry, for the close that recorded its intention and then
683
+ * could not happen. The field goes back to absent when it empties, so the run
684
+ * serializes the bytes it had before the intention.
685
+ */
686
+ export function withoutLastReentry(state) {
687
+ const { reentries: _all, ...rest } = withoutSeal(state);
688
+ const kept = (state.reentries ?? []).slice(0, -1);
689
+ return sealRunState(kept.length === 0 ? rest : { ...rest, reentries: kept });
690
+ }
691
+ /** Seal the base of one batch; a batch that already has one keeps it. */
692
+ export function withPlanExecBatchBase(state, batchId, base) {
693
+ const batches = state.batches ?? [];
694
+ const target = batches.find((batch) => batch.id === batchId);
695
+ // A re-run of the acquisition on the same batch must not move its base: the
696
+ // batch would then be compared against its own half-done work.
697
+ if (target === undefined || target.base !== undefined)
698
+ return state;
699
+ return sealRunState({
700
+ ...withoutSeal(state),
701
+ batches: batches.map((batch) => (batch.id === batchId ? { ...batch, base } : batch)),
702
+ });
703
+ }
704
+ /** Record the proofs that credited one batch. */
705
+ export function withPlanExecBatchCredit(state, batchId, credit) {
706
+ const batches = state.batches ?? [];
707
+ if (!batches.some((batch) => batch.id === batchId))
708
+ return state;
709
+ return sealRunState({
710
+ ...withoutSeal(state),
711
+ batches: batches.map((batch) => (batch.id === batchId ? { ...batch, credit } : batch)),
712
+ });
574
713
  }
575
714
  /** Record one inferred or declared batch, refusing duplicate ids at the caller's boundary. */
576
715
  export function withPlanExecBatch(state, batch) {
@@ -892,7 +1031,7 @@ export function restatesLastEvent(state, event) {
892
1031
  if (last === undefined || last.kind !== "failed" || event.kind !== "failed")
893
1032
  return false;
894
1033
  return (last.transition === event.transition &&
895
- last.batch_iteration === event.batch_iteration &&
1034
+ sameIteration(last, event) &&
896
1035
  last.code === event.code &&
897
1036
  last.message === event.message);
898
1037
  }
@@ -913,7 +1052,7 @@ export function restatesLastEvent(state, event) {
913
1052
  */
914
1053
  export function withObservation(state, observation) {
915
1054
  const repeated = state.observations.some((past) => past.transition === observation.transition &&
916
- past.batch_iteration === observation.batch_iteration &&
1055
+ sameIteration(past, observation) &&
917
1056
  sameSignals(past.signals, observation.signals));
918
1057
  if (repeated)
919
1058
  return state;
@@ -1021,20 +1160,26 @@ function union(current, added) {
1021
1160
  *
1022
1161
  * Order matters: shape before version before seal. A file whose version is
1023
1162
  * unknown must not be judged by a digest rule that version may not even use.
1163
+ *
1164
+ * Every refusal names the command that works IN that state: `--adopt` only adopts
1165
+ * a session with no registry at all, so an unreadable, tampered or too-old one
1166
+ * leaves through `aw flow restart`, and one written by a newer CLI through
1167
+ * updating this one. `session` names the run in those commands.
1024
1168
  */
1025
- export function parseRunState(raw) {
1169
+ export function parseRunState(raw, session = "<código>") {
1170
+ const restart = unusableRemedy(session);
1026
1171
  if (raw.trim().length === 0) {
1027
- return refuse("FLOW_RUN_INVALID", "el estado de corrida está vacío", "reconstruí la corrida con 'aw flow advance --adopt', o restaurá el archivo");
1172
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida está vacío", restart);
1028
1173
  }
1029
1174
  let parsed;
1030
1175
  try {
1031
1176
  parsed = JSON.parse(raw);
1032
1177
  }
1033
1178
  catch {
1034
- return refuse("FLOW_RUN_INVALID", "el estado de corrida no es JSON válido", "no se avanza sobre un estado ilegible: restauralo o re-adoptá la sesión con 'aw flow advance --adopt'");
1179
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida no es JSON válido", restart);
1035
1180
  }
1036
1181
  if (!isRecord(parsed)) {
1037
- return refuse("FLOW_RUN_INVALID", "el estado de corrida no es un objeto JSON", "restauralo o re-adoptá la sesión con 'aw flow advance --adopt'");
1182
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida no es un objeto JSON", restart);
1038
1183
  }
1039
1184
  if (!FLOW_RUN_STATE_READABLE.includes(parsed.version)) {
1040
1185
  // No silent migration, in either direction: a version outside the readable
@@ -1042,14 +1187,17 @@ export function parseRunState(raw) {
1042
1187
  // fabricate the very history this file exists to make trustworthy — or comes
1043
1188
  // from a build ahead of this one, whose fields this build would ignore.
1044
1189
  // Refusing with the cause is what keeps the second case from being silent.
1045
- return refuse("FLOW_RUN_VERSION_UNSUPPORTED", `versión de estado de corrida no soportada: ${String(parsed.version)}`, `esta versión del CLI lee ${FLOW_RUN_STATE_READABLE.join(" y ")} y escribe la ${FLOW_RUN_STATE_VERSION}: actualizá el CLI, o re-adoptá la sesión con 'aw flow advance --flow <flow> --adopt' (no hay migración automática)`);
1190
+ const newer = typeof parsed.version === "number" && parsed.version > FLOW_RUN_STATE_VERSION;
1191
+ return refuse("FLOW_RUN_VERSION_UNSUPPORTED", `versión de estado de corrida no soportada: ${String(parsed.version)} (este CLI lee de la ${FLOW_RUN_STATE_READABLE.at(-1)} a la ${FLOW_RUN_STATE_VERSION})`, newer
1192
+ ? `la escribió un CLI más nuevo que este: actualizalo con 'aw self update' y seguí la corrida — reiniciarla descartaría lo que esa versión sí continúa`
1193
+ : restart);
1046
1194
  }
1047
- const shape = checkShape(parsed);
1195
+ const shape = checkShape(parsed, restart);
1048
1196
  if (shape !== null)
1049
1197
  return { ok: false, failure: shape };
1050
1198
  const state = parsed;
1051
1199
  if (semanticDigest(withoutSeal(state)) !== state.digest) {
1052
- return refuse("FLOW_RUN_TAMPERED", "el estado de corrida no coincide con su propio sello", "fue editado fuera del CLI: descartá el archivo y re-adoptá la sesión con 'aw flow advance --adopt'");
1200
+ return refuse("FLOW_RUN_TAMPERED", "el estado de corrida no coincide con su propio sello: fue editado fuera del CLI", restart);
1053
1201
  }
1054
1202
  return { ok: true, state };
1055
1203
  }
@@ -1069,10 +1217,15 @@ export function checkAgainstJourney(state, journey) {
1069
1217
  for (const [index, applied] of state.applied.entries()) {
1070
1218
  if (ids[index] === applied)
1071
1219
  continue;
1220
+ // The cause decides the repair, so it is named: a row this build no longer
1221
+ // has was renamed or retired, one it has elsewhere was reordered.
1222
+ const cause = ids.includes(applied)
1223
+ ? "el recorrido instalado la tiene en otra posición"
1224
+ : "el recorrido instalado ya no tiene esa frontera";
1072
1225
  return {
1073
1226
  code: "FLOW_RUN_AHEAD_OF_JOURNEY",
1074
- message: `el estado dice haber aplicado '${applied}' donde el recorrido tiene '${ids[index] ?? "(nada)"}'`,
1075
- action: "el estado no corresponde a este recorrido: revisá el flow de la corrida o re-adoptá la sesión",
1227
+ message: `el estado dice haber aplicado '${applied}' donde el recorrido tiene '${ids[index] ?? "(nada)"}': ${cause}`,
1228
+ action: unusableRemedy(state.session),
1076
1229
  };
1077
1230
  }
1078
1231
  const next = ids[state.applied.length] ?? null;
@@ -1080,16 +1233,111 @@ export function checkAgainstJourney(state, journey) {
1080
1233
  return {
1081
1234
  code: "FLOW_RUN_AHEAD_OF_JOURNEY",
1082
1235
  message: `la frontera vigente dice '${state.boundary}' y el recorrido sigue en '${next ?? "(nada)"}'`,
1083
- action: "recalculá la frontera avanzando de nuevo; no se responde sobre una frontera que el estado no sostiene",
1236
+ action: unusableRemedy(state.session),
1084
1237
  };
1085
1238
  }
1086
1239
  return null;
1087
1240
  }
1088
- function checkShape(parsed) {
1241
+ /** The trace operation that names an alignment, so status and tests read one spelling. */
1242
+ export const JOURNEY_ALIGNMENT_OPERATION = "flow.journey-alignment";
1243
+ const ALIGNMENT_REASON = "la versión instalada agregó esta frontera en un tramo que la corrida ya había recorrido: entra omitida, sin pedirse";
1244
+ /**
1245
+ * Align a continuable run's cursor with the journey the installed build walks.
1246
+ *
1247
+ * The only difference this repairs is a boundary the installed build ADDED to
1248
+ * the base journey the run recorded (`journey_base`) and that sits before the
1249
+ * run's position. "Before the position" includes the rows ahead of the boundary
1250
+ * the run is standing on: that boundary is the one it was answering, and a
1251
+ * sealed or attempted action there must not be thrown away for a row nobody
1252
+ * asked it about. A row added AFTER the boundary is left alone and asked when
1253
+ * the run gets there.
1254
+ *
1255
+ * Every other difference comes back untouched, for {@link checkAgainstJourney}
1256
+ * to refuse with its cause: a row the recorded base already had and the cursor
1257
+ * lacks is a hole, not an addition; a reorder or a retirement moves an id that
1258
+ * keys attempts, counters, trace and approvals.
1259
+ *
1260
+ * `base` is the installed REGISTRY journey; `journey` is what the run walks
1261
+ * (plan-exec's expanded copies included). Pure: the aligned state is persisted
1262
+ * by the run's next write, never by a read.
1263
+ */
1264
+ export function alignToJourney(state, journey, base) {
1265
+ const recorded = state.journey_base;
1266
+ if (recorded === undefined || sameIds(recorded, base))
1267
+ return state;
1268
+ const addedIds = new Set(base.filter((id) => !recorded.includes(id)));
1269
+ const ids = journey.map((decision) => decision.id);
1270
+ const applied = [];
1271
+ const added = [];
1272
+ let cursor = 0;
1273
+ // Everything from `cursor` up to the next occurrence of `target` must be rows
1274
+ // the installed build added; only then are they inserted, in journey order.
1275
+ const insertUntil = (target) => {
1276
+ let next = cursor;
1277
+ while (next < ids.length && ids[next] !== target) {
1278
+ if (!addedIds.has(ids[next]))
1279
+ return false;
1280
+ next += 1;
1281
+ }
1282
+ if (next >= ids.length)
1283
+ return false;
1284
+ for (; cursor < next; cursor += 1) {
1285
+ added.push({ transition: ids[cursor], position: applied.length });
1286
+ applied.push(ids[cursor]);
1287
+ }
1288
+ return true;
1289
+ };
1290
+ for (const id of state.applied) {
1291
+ if (ids[cursor] !== id && !insertUntil(id))
1292
+ return state;
1293
+ applied.push(id);
1294
+ cursor += 1;
1295
+ }
1296
+ if (state.boundary !== null && ids[cursor] !== state.boundary && !insertUntil(state.boundary)) {
1297
+ return state;
1298
+ }
1299
+ const events = added.map((row) => {
1300
+ const iteration = segmentIterationAt(state.flow, applied, row.position);
1301
+ return {
1302
+ kind: "aligned",
1303
+ transition: row.transition,
1304
+ ...(iteration === null ? {} : { batch_iteration: iteration }),
1305
+ operation: JOURNEY_ALIGNMENT_OPERATION,
1306
+ position: row.position,
1307
+ reason: ALIGNMENT_REASON,
1308
+ };
1309
+ });
1310
+ return sealRunState({
1311
+ ...withoutSeal(state),
1312
+ journey_base: [...base],
1313
+ applied,
1314
+ skipped: [...state.skipped, ...added.map((row) => row.transition)],
1315
+ events: [...state.events, ...events],
1316
+ });
1317
+ }
1318
+ function sameIds(left, right) {
1319
+ return left.length === right.length && left.every((id, index) => id === right[index]);
1320
+ }
1321
+ /**
1322
+ * Which plan-exec batch copy a cursor position falls inside, or null outside one.
1323
+ *
1324
+ * Two copies of the same added row differ only by the batch they belong to, so
1325
+ * the trace names it the way every other per-batch event does.
1326
+ */
1327
+ function segmentIterationAt(flow, applied, position) {
1328
+ if (flow !== "plan-exec")
1329
+ return null;
1330
+ const before = applied.slice(0, position);
1331
+ const opened = before.lastIndexOf("plan-exec.batch-eligibility-signal");
1332
+ if (opened < 0 || before.lastIndexOf("plan-exec.batch-close") > opened)
1333
+ return null;
1334
+ return before.filter((id) => id === "plan-exec.batch-eligibility-signal").length;
1335
+ }
1336
+ function checkShape(parsed, remedy) {
1089
1337
  const invalid = (why) => ({
1090
1338
  code: "FLOW_RUN_INVALID",
1091
1339
  message: `el estado de corrida ${why}`,
1092
- action: "restauralo o re-adoptá la sesión con 'aw flow advance --adopt'",
1340
+ action: remedy,
1093
1341
  });
1094
1342
  if (!WORKLINE_FLOWS.includes(parsed.flow)) {
1095
1343
  return invalid(`declara un flow desconocido: ${String(parsed.flow)}`);
@@ -1125,16 +1373,26 @@ function checkRecordShape(parsed, invalid) {
1125
1373
  const common = checkCommonRecordShape(parsed, invalid);
1126
1374
  if (common !== null)
1127
1375
  return common;
1128
- if (parsed.version === FLOW_RUN_STATE_VERSION || parsed.version === 10) {
1376
+ const version = parsed.version;
1377
+ if (version >= 10) {
1129
1378
  const v10 = checkV10RecordShape(parsed, invalid);
1130
1379
  if (v10 !== null)
1131
1380
  return v10;
1132
1381
  }
1133
- if (parsed.version === FLOW_RUN_STATE_VERSION) {
1382
+ if (version >= 11) {
1134
1383
  const v11 = checkV11RecordShape(parsed, invalid);
1135
1384
  if (v11 !== null)
1136
1385
  return v11;
1137
1386
  }
1387
+ if (version >= 12 && !isStringArray(parsed.journey_base)) {
1388
+ return invalid("no dice contra qué recorrido se escribió su cursor");
1389
+ }
1390
+ if (!isReentryArray(parsed.reentries)) {
1391
+ return invalid("declara una reentrada sin tipo, frontera, ocurrencia u origen");
1392
+ }
1393
+ if (parsed.exhausted_reruns !== undefined && !isStringArray(parsed.exhausted_reruns)) {
1394
+ return invalid("declara vueltas de fronteras agotadas que no son transiciones");
1395
+ }
1138
1396
  if (typeof parsed.digest !== "string")
1139
1397
  return invalid("no trae su sello");
1140
1398
  return null;
@@ -1172,8 +1430,55 @@ function checkCommonRecordShape(parsed, invalid) {
1172
1430
  if (!isCounterMap(parsed.attempt_floor) || !isCounterMap(parsed.attempt_grants)) {
1173
1431
  return invalid("trae una contabilidad de intentos que no es un contador por transición");
1174
1432
  }
1433
+ if (!isInheritedBaseArray(parsed.inherited_bases)) {
1434
+ return invalid("hereda una base de batch sin sus fases o sin sus fuentes");
1435
+ }
1175
1436
  return null;
1176
1437
  }
1438
+ function isInheritedBaseArray(value) {
1439
+ if (value === undefined)
1440
+ return true;
1441
+ return (Array.isArray(value) &&
1442
+ value.every((entry) => isRecord(entry) &&
1443
+ Array.isArray(entry.phases) &&
1444
+ entry.phases.length > 0 &&
1445
+ entry.phases.every((phase) => Number.isInteger(phase) && phase > 0) &&
1446
+ isSourceMap(entry.base, true) &&
1447
+ entry.base !== undefined));
1448
+ }
1449
+ /**
1450
+ * The bases a reseated run inherits from the archived registry, read tolerantly:
1451
+ * the archive may be the very state that could not be continued, so anything
1452
+ * that is not a well-formed batch base is left behind rather than trusted.
1453
+ */
1454
+ export function inheritedBasesFrom(archived) {
1455
+ if (archived === null)
1456
+ return [];
1457
+ let parsed;
1458
+ try {
1459
+ parsed = JSON.parse(archived);
1460
+ }
1461
+ catch {
1462
+ return [];
1463
+ }
1464
+ if (!isRecord(parsed) || !Array.isArray(parsed.batches))
1465
+ return [];
1466
+ const inherited = parsed.batches.flatMap((batch) => {
1467
+ if (!isRecord(batch) || batch.base === undefined)
1468
+ return [];
1469
+ const candidate = { phases: batch.phases, base: batch.base };
1470
+ return isInheritedBaseArray([candidate]) ? [candidate] : [];
1471
+ });
1472
+ const earlier = isInheritedBaseArray(parsed.inherited_bases)
1473
+ ? (parsed.inherited_bases ?? [])
1474
+ : [];
1475
+ return [...earlier, ...inherited];
1476
+ }
1477
+ export function withInheritedBases(state, inherited) {
1478
+ if (inherited.length === 0)
1479
+ return state;
1480
+ return sealRunState({ ...withoutSeal(state), inherited_bases: [...inherited] });
1481
+ }
1177
1482
  /** Fields added by v10 that have no truthful default on a persisted run. */
1178
1483
  function checkV10RecordShape(parsed, invalid) {
1179
1484
  if (!isPlanExecBatchArray(parsed.batches)) {
@@ -1262,7 +1567,7 @@ function isRouteProposal(value) {
1262
1567
  !isRouteDisposition(control.recommendation) ||
1263
1568
  !isRecord(control.alternatives) ||
1264
1569
  !isNonEmptyString(control.alternatives.apply) ||
1265
- !isNonEmptyString(control.alternatives.omit) ||
1570
+ (control.alternatives.omit !== undefined && !isNonEmptyString(control.alternatives.omit)) ||
1266
1571
  !isNonEmptyString(control.alternatives.substitute) ||
1267
1572
  !isNonEmptyString(control.consequence) ||
1268
1573
  !isNonEmptyString(control.risk) ||
@@ -1364,7 +1669,9 @@ function isPlanExecBatchArray(value) {
1364
1669
  (entry.publication !== undefined &&
1365
1670
  entry.publication.status === "applied" &&
1366
1671
  entry.published_plan_digest !== entry.publication.after_plan_digest) ||
1367
- !PLAN_EXEC_BATCH_STAGES.includes(entry.stage)) {
1672
+ !PLAN_EXEC_BATCH_STAGES.includes(entry.stage) ||
1673
+ !isSourceMap(entry.base, true) ||
1674
+ !isSourceMap(entry.credit, false)) {
1368
1675
  return false;
1369
1676
  }
1370
1677
  ids.add(entry.id);
@@ -1372,6 +1679,14 @@ function isPlanExecBatchArray(value) {
1372
1679
  return true;
1373
1680
  });
1374
1681
  }
1682
+ /** Absent, or a non-empty alias → digest map; `nullable` admits an unobserved source. */
1683
+ function isSourceMap(value, nullable) {
1684
+ if (value === undefined)
1685
+ return true;
1686
+ if (!isRecord(value) || Object.keys(value).length === 0)
1687
+ return false;
1688
+ return Object.values(value).every((digest) => (nullable && digest === null) || (typeof digest === "string" && digest.length > 0));
1689
+ }
1375
1690
  function isPlanExecBatchPublication(value) {
1376
1691
  if (value === undefined)
1377
1692
  return true;
@@ -1599,7 +1914,8 @@ function isDecisionPreview(value, baseline) {
1599
1914
  return value.effects.entries.every((entry) => isRecord(entry) &&
1600
1915
  typeof entry.path === "string" &&
1601
1916
  Number.isInteger(entry.bytes) &&
1602
- typeof entry.overwrite === "boolean");
1917
+ typeof entry.overwrite === "boolean" &&
1918
+ isReservedMark(entry.reserved));
1603
1919
  }
1604
1920
  /**
1605
1921
  * The persisted preview's obligations, in EITHER form.
@@ -1634,7 +1950,12 @@ function isProposalArtifacts(value) {
1634
1950
  return value.every((entry) => isRecord(entry) &&
1635
1951
  typeof entry.path === "string" &&
1636
1952
  typeof entry.content === "string" &&
1637
- typeof entry.overwrite === "boolean");
1953
+ typeof entry.overwrite === "boolean" &&
1954
+ isReservedMark(entry.reserved));
1955
+ }
1956
+ /** The optional reservation mark: absent, or exactly `true`. */
1957
+ function isReservedMark(value) {
1958
+ return value === undefined || value === true;
1638
1959
  }
1639
1960
  function isProposalBases(value) {
1640
1961
  if (!Array.isArray(value))
@@ -1678,28 +1999,38 @@ function isEvent(entry) {
1678
1999
  return false;
1679
2000
  if (typeof entry.transition !== "string" || typeof entry.operation !== "string")
1680
2001
  return false;
1681
- if (entry.batch_iteration !== undefined &&
1682
- (!Number.isInteger(entry.batch_iteration) || entry.batch_iteration < 1)) {
2002
+ if (!isIterationRecord(entry))
1683
2003
  return false;
1684
- }
1685
- if (entry.kind === "executed") {
1686
- return (typeof entry.summary === "string" &&
1687
- typeof entry.output_digest === "string" &&
1688
- isEffectClassArray(entry.effects) &&
1689
- isStringArray(entry.evidence));
1690
- }
1691
- if (entry.kind === "reconciled")
1692
- return isRepairArray(entry.repairs);
1693
- if (entry.kind === "failed") {
1694
- return (typeof entry.code === "string" &&
1695
- typeof entry.message === "string" &&
1696
- typeof entry.recovery === "string" &&
1697
- // ABSENT-or-well-formed, never defaulted: a version 7 trace has no such
1698
- // field, and the guard that reads it treats its absence as the refusal.
1699
- (entry.effects === undefined || isEffectClassArray(entry.effects)));
1700
- }
1701
- return false;
2004
+ return EVENT_BODIES.get(entry.kind)?.(entry) === true;
1702
2005
  }
2006
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
2007
+ /**
2008
+ * What each trace kind demands beyond the common fields; an unknown kind has no
2009
+ * entry. A Map, not an object: a kind spelled like an Object.prototype member
2010
+ * must not find a function nobody declared.
2011
+ */
2012
+ const EVENT_BODIES = new Map(Object.entries({
2013
+ executed: (entry) => typeof entry.summary === "string" &&
2014
+ typeof entry.output_digest === "string" &&
2015
+ isEffectClassArray(entry.effects) &&
2016
+ isStringArray(entry.evidence),
2017
+ reconciled: (entry) => isRepairArray(entry.repairs),
2018
+ restarted: (entry) => nonEmpty(entry.archive) && nonEmpty(entry.cause),
2019
+ annulled: (entry) => isStringArray(entry.batches) &&
2020
+ entry.batches.length > 0 &&
2021
+ Array.isArray(entry.phases) &&
2022
+ entry.phases.every((phase) => Number.isInteger(phase) && phase > 0) &&
2023
+ isStringArray(entry.tasks) &&
2024
+ nonEmpty(entry.digest),
2025
+ aligned: (entry) => Number.isInteger(entry.position) && entry.position >= 0 && nonEmpty(entry.reason),
2026
+ "route-refused": (entry) => isRouteDisposition(entry.disposition) && nonEmpty(entry.reason),
2027
+ failed: (entry) => typeof entry.code === "string" &&
2028
+ typeof entry.message === "string" &&
2029
+ typeof entry.recovery === "string" &&
2030
+ // ABSENT-or-well-formed, never defaulted: a version 7 trace has no such
2031
+ // field, and the guard that reads it treats its absence as the refusal.
2032
+ (entry.effects === undefined || isEffectClassArray(entry.effects)),
2033
+ }));
1703
2034
  /** A repair is well-formed when its rule is one of the three and its values are numbers. */
1704
2035
  function isRepairArray(value) {
1705
2036
  if (!Array.isArray(value))
@@ -1718,8 +2049,29 @@ function isObservationArray(value) {
1718
2049
  return value.every((entry) => isRecord(entry) &&
1719
2050
  typeof entry.transition === "string" &&
1720
2051
  isStringArray(entry.signals) &&
1721
- (entry.batch_iteration === undefined ||
1722
- (Number.isInteger(entry.batch_iteration) && entry.batch_iteration >= 1)));
2052
+ isIterationRecord(entry));
2053
+ }
2054
+ const REENTRY_KINDS = ["refine", "close", "reopen"];
2055
+ /** Absent, or reentries each with its kind, its boundary, its occurrence and its origin. */
2056
+ function isReentryArray(value) {
2057
+ if (value === undefined)
2058
+ return true;
2059
+ if (!Array.isArray(value))
2060
+ return false;
2061
+ return value.every((entry) => isRecord(entry) &&
2062
+ REENTRY_KINDS.includes(entry.kind) &&
2063
+ nonEmpty(entry.transition) &&
2064
+ Number.isInteger(entry.occurrence) &&
2065
+ entry.occurrence >= 1 &&
2066
+ // A redraft names where it re-enters; the other kinds repeat from their row.
2067
+ (entry.kind === "refine"
2068
+ ? nonEmpty(entry.from)
2069
+ : entry.from === null || nonEmpty(entry.from)));
2070
+ }
2071
+ /** Both iteration fields, each absent or a positive integer. */
2072
+ function isIterationRecord(entry) {
2073
+ const ordinal = (value) => value === undefined || (Number.isInteger(value) && value >= 1);
2074
+ return ordinal(entry.batch_iteration) && ordinal(entry.reentry_iteration);
1723
2075
  }
1724
2076
  /**
1725
2077
  * The two optional fields are checked as ABSENT-or-well-formed, never defaulted.
@@ -1755,8 +2107,7 @@ function isAttemptArray(value) {
1755
2107
  Number.isInteger(entry.attempt) &&
1756
2108
  typeof entry.request_digest === "string" &&
1757
2109
  typeof entry.transition === "string" &&
1758
- (entry.batch_iteration === undefined ||
1759
- (Number.isInteger(entry.batch_iteration) && entry.batch_iteration >= 1)) &&
2110
+ isIterationRecord(entry) &&
1760
2111
  (entry.parent_request_digest === null || typeof entry.parent_request_digest === "string"));
1761
2112
  }
1762
2113
  //# sourceMappingURL=run-state.js.map