@tacuchi/agent-workflow-cli 22.4.1 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/git-cli.js +59 -17
  3. package/dist/adapters/git-cli.js.map +1 -1
  4. package/dist/adapters/node-file-system.js +55 -1
  5. package/dist/adapters/node-file-system.js.map +1 -1
  6. package/dist/application/artifacts-service.js +2 -2
  7. package/dist/application/artifacts-service.js.map +1 -1
  8. package/dist/application/capability/dispatcher.js +2 -2
  9. package/dist/application/capability/dispatcher.js.map +1 -1
  10. package/dist/application/capability/installed-inventory.js +2 -2
  11. package/dist/application/capability/installed-inventory.js.map +1 -1
  12. package/dist/application/capability/readiness.js +1 -1
  13. package/dist/application/capability/readiness.js.map +1 -1
  14. package/dist/application/check-branch-service.js +2 -2
  15. package/dist/application/check-branch-service.js.map +1 -1
  16. package/dist/application/checkpoint/files-touched.js +271 -0
  17. package/dist/application/checkpoint/files-touched.js.map +1 -0
  18. package/dist/application/checkpoint/markdown.js +61 -8
  19. package/dist/application/checkpoint/markdown.js.map +1 -1
  20. package/dist/application/checkpoint/state-reader.js +8 -2
  21. package/dist/application/checkpoint/state-reader.js.map +1 -1
  22. package/dist/application/checkpoint-write-service.js +16 -5
  23. package/dist/application/checkpoint-write-service.js.map +1 -1
  24. package/dist/application/claims-ledger.js +330 -0
  25. package/dist/application/claims-ledger.js.map +1 -0
  26. package/dist/application/claims-recovery.js +338 -0
  27. package/dist/application/claims-recovery.js.map +1 -0
  28. package/dist/application/dev-only-services.js +154 -44
  29. package/dist/application/dev-only-services.js.map +1 -1
  30. package/dist/application/elicitation-server.js +382 -0
  31. package/dist/application/elicitation-server.js.map +1 -0
  32. package/dist/application/elicitation-stdio.js +48 -0
  33. package/dist/application/elicitation-stdio.js.map +1 -0
  34. package/dist/application/flow/advance.js +86 -5
  35. package/dist/application/flow/advance.js.map +1 -1
  36. package/dist/application/flow/checkout-observation.js +152 -0
  37. package/dist/application/flow/checkout-observation.js.map +1 -0
  38. package/dist/application/flow/flow-service.js +72 -13
  39. package/dist/application/flow/flow-service.js.map +1 -1
  40. package/dist/application/flow/internal-actions.js +460 -1
  41. package/dist/application/flow/internal-actions.js.map +1 -1
  42. package/dist/application/flow/internal-drive.js +22 -9
  43. package/dist/application/flow/internal-drive.js.map +1 -1
  44. package/dist/application/flow/prove.js +205 -0
  45. package/dist/application/flow/prove.js.map +1 -0
  46. package/dist/application/flow/run-journey.js +15 -0
  47. package/dist/application/flow/run-journey.js.map +1 -0
  48. package/dist/application/flow/run-projection.js +15 -3
  49. package/dist/application/flow/run-projection.js.map +1 -1
  50. package/dist/application/flow/run-state-service.js +19 -4
  51. package/dist/application/flow/run-state-service.js.map +1 -1
  52. package/dist/application/flow/submit.js +250 -72
  53. package/dist/application/flow/submit.js.map +1 -1
  54. package/dist/application/generate-launch-service.js +4 -4
  55. package/dist/application/generate-launch-service.js.map +1 -1
  56. package/dist/application/git-flow-service.js +9 -0
  57. package/dist/application/git-flow-service.js.map +1 -1
  58. package/dist/application/local-proposal.js +9 -0
  59. package/dist/application/local-proposal.js.map +1 -1
  60. package/dist/application/lock-service.js +6 -3
  61. package/dist/application/lock-service.js.map +1 -1
  62. package/dist/application/mcp-doctor-service.js.map +1 -1
  63. package/dist/application/mcp-host-reader.js +30 -13
  64. package/dist/application/mcp-host-reader.js.map +1 -1
  65. package/dist/application/mcp-host-writer.js +136 -112
  66. package/dist/application/mcp-host-writer.js.map +1 -1
  67. package/dist/application/mcp-remove-service.js +7 -3
  68. package/dist/application/mcp-remove-service.js.map +1 -1
  69. package/dist/application/mcp-scope-common.js +1 -3
  70. package/dist/application/mcp-scope-common.js.map +1 -1
  71. package/dist/application/mcp-setup-service.js +7 -3
  72. package/dist/application/mcp-setup-service.js.map +1 -1
  73. package/dist/application/merge-state-service.js +10 -16
  74. package/dist/application/merge-state-service.js.map +1 -1
  75. package/dist/application/multiroot/claude.js +34 -15
  76. package/dist/application/multiroot/claude.js.map +1 -1
  77. package/dist/application/multiroot/codex.js +9 -5
  78. package/dist/application/multiroot/codex.js.map +1 -1
  79. package/dist/application/multiroot-service.js +20 -11
  80. package/dist/application/multiroot-service.js.map +1 -1
  81. package/dist/application/parsers/project-block.js +25 -2
  82. package/dist/application/parsers/project-block.js.map +1 -1
  83. package/dist/application/paths-service.js +30 -31
  84. package/dist/application/paths-service.js.map +1 -1
  85. package/dist/application/plan-exec-batch-service.js +549 -0
  86. package/dist/application/plan-exec-batch-service.js.map +1 -0
  87. package/dist/application/plan-exec-decision-service.js +273 -0
  88. package/dist/application/plan-exec-decision-service.js.map +1 -0
  89. package/dist/application/project-md-service.js +2 -2
  90. package/dist/application/project-md-service.js.map +1 -1
  91. package/dist/application/project-md-upsert-service.js +4 -4
  92. package/dist/application/project-md-upsert-service.js.map +1 -1
  93. package/dist/application/project-tab-data.js +12 -7
  94. package/dist/application/project-tab-data.js.map +1 -1
  95. package/dist/application/release-data-service.js +2 -2
  96. package/dist/application/release-data-service.js.map +1 -1
  97. package/dist/application/resume-service.js +32 -11
  98. package/dist/application/resume-service.js.map +1 -1
  99. package/dist/application/retirement/apply.js +84 -0
  100. package/dist/application/retirement/apply.js.map +1 -1
  101. package/dist/application/retirement/attribution.js +31 -1
  102. package/dist/application/retirement/attribution.js.map +1 -1
  103. package/dist/application/retirement/graph.js +19 -1
  104. package/dist/application/retirement/graph.js.map +1 -1
  105. package/dist/application/retirement/journal.js +11 -0
  106. package/dist/application/retirement/journal.js.map +1 -1
  107. package/dist/application/retirement/prepare.js +89 -3
  108. package/dist/application/retirement/prepare.js.map +1 -1
  109. package/dist/application/retirement/preview.js +16 -0
  110. package/dist/application/retirement/preview.js.map +1 -1
  111. package/dist/application/retirement/resolve.js +65 -0
  112. package/dist/application/retirement/resolve.js.map +1 -1
  113. package/dist/application/self/host-states.js +2 -0
  114. package/dist/application/self/host-states.js.map +1 -1
  115. package/dist/application/self/install-skill.js +53 -19
  116. package/dist/application/self/install-skill.js.map +1 -1
  117. package/dist/application/self/mcp-config.js +28 -30
  118. package/dist/application/self/mcp-config.js.map +1 -1
  119. package/dist/application/self/mcp-offer.js +96 -0
  120. package/dist/application/self/mcp-offer.js.map +1 -0
  121. package/dist/application/self/mcp-via-state.js +59 -0
  122. package/dist/application/self/mcp-via-state.js.map +1 -0
  123. package/dist/application/self/uninstall-skill.js +18 -3
  124. package/dist/application/self/uninstall-skill.js.map +1 -1
  125. package/dist/application/self/uninstall.js +37 -8
  126. package/dist/application/self/uninstall.js.map +1 -1
  127. package/dist/application/session-close-service.js +24 -1
  128. package/dist/application/session-close-service.js.map +1 -1
  129. package/dist/application/session-create-service.js +7 -0
  130. package/dist/application/session-create-service.js.map +1 -1
  131. package/dist/application/session-resume-service.js +2 -2
  132. package/dist/application/session-resume-service.js.map +1 -1
  133. package/dist/application/sessions-service.js +2 -4
  134. package/dist/application/sessions-service.js.map +1 -1
  135. package/dist/application/skills-resolver-service.js +4 -4
  136. package/dist/application/skills-resolver-service.js.map +1 -1
  137. package/dist/application/source-boundary-policy.js +18 -7
  138. package/dist/application/source-boundary-policy.js.map +1 -1
  139. package/dist/application/source-remove-service.js +12 -1
  140. package/dist/application/source-remove-service.js.map +1 -1
  141. package/dist/application/sources-service.js +2 -2
  142. package/dist/application/sources-service.js.map +1 -1
  143. package/dist/application/status-service.js +4 -0
  144. package/dist/application/status-service.js.map +1 -1
  145. package/dist/application/visibility-doctor-service.js +1 -1
  146. package/dist/application/visibility-doctor-service.js.map +1 -1
  147. package/dist/application/workline-index-service.js +182 -27
  148. package/dist/application/workline-index-service.js.map +1 -1
  149. package/dist/application/workspace-init-service.js +85 -265
  150. package/dist/application/workspace-init-service.js.map +1 -1
  151. package/dist/application/workspace-materialization-service.js +300 -0
  152. package/dist/application/workspace-materialization-service.js.map +1 -0
  153. package/dist/application/worktree-service.js +32 -3
  154. package/dist/application/worktree-service.js.map +1 -1
  155. package/dist/cli/commands/capability.js +4 -7
  156. package/dist/cli/commands/capability.js.map +1 -1
  157. package/dist/cli/commands/claims.js +72 -0
  158. package/dist/cli/commands/claims.js.map +1 -0
  159. package/dist/cli/commands/dev-only.js +73 -25
  160. package/dist/cli/commands/dev-only.js.map +1 -1
  161. package/dist/cli/commands/flow.js +145 -42
  162. package/dist/cli/commands/flow.js.map +1 -1
  163. package/dist/cli/commands/index.js +2 -0
  164. package/dist/cli/commands/index.js.map +1 -1
  165. package/dist/cli/commands/mcp.js +124 -35
  166. package/dist/cli/commands/mcp.js.map +1 -1
  167. package/dist/cli/commands/multiroot.js +40 -7
  168. package/dist/cli/commands/multiroot.js.map +1 -1
  169. package/dist/cli/commands/resume.js +9 -3
  170. package/dist/cli/commands/resume.js.map +1 -1
  171. package/dist/cli/commands/retirement.js +9 -0
  172. package/dist/cli/commands/retirement.js.map +1 -1
  173. package/dist/cli/commands/session-create.js +1 -1
  174. package/dist/cli/commands/session-create.js.map +1 -1
  175. package/dist/cli/commands/skills.js +1 -1
  176. package/dist/cli/commands/skills.js.map +1 -1
  177. package/dist/cli/commands/status.js +72 -24
  178. package/dist/cli/commands/status.js.map +1 -1
  179. package/dist/cli/commands/workspace-init.js +5 -5
  180. package/dist/cli/commands/workspace-init.js.map +1 -1
  181. package/dist/cli/help-groups.js +5 -0
  182. package/dist/cli/help-groups.js.map +1 -1
  183. package/dist/cli/main.js +112 -39
  184. package/dist/cli/main.js.map +1 -1
  185. package/dist/cli/parser.js +3 -0
  186. package/dist/cli/parser.js.map +1 -1
  187. package/dist/cli/tui/app.js +3 -2
  188. package/dist/cli/tui/app.js.map +1 -1
  189. package/dist/cli/tui/components/host-admin-section.js +46 -2
  190. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  191. package/dist/cli/tui/components/workspace-init-form.js +7 -7
  192. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  193. package/dist/cli/tui/data/workflow-content.js +2 -2
  194. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  195. package/dist/cli/tui/tabs/config-tab.js +4 -3
  196. package/dist/cli/tui/tabs/config-tab.js.map +1 -1
  197. package/dist/cli/tui/tabs/mcp-tab-helpers.js +1 -1
  198. package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
  199. package/dist/cli/tui/tabs/project-tab.js +18 -21
  200. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  201. package/dist/cli/tui/workspace-root.js +13 -0
  202. package/dist/cli/tui/workspace-root.js.map +1 -0
  203. package/dist/domain/capability/protocol.js +2 -2
  204. package/dist/domain/capability/protocol.js.map +1 -1
  205. package/dist/domain/degradation-notice.js +60 -0
  206. package/dist/domain/degradation-notice.js.map +1 -0
  207. package/dist/domain/elicitation.js +152 -0
  208. package/dist/domain/elicitation.js.map +1 -0
  209. package/dist/domain/flow/answer.js +173 -42
  210. package/dist/domain/flow/answer.js.map +1 -1
  211. package/dist/domain/flow/authority.js +180 -127
  212. package/dist/domain/flow/authority.js.map +1 -1
  213. package/dist/domain/flow/directive.js +47 -1
  214. package/dist/domain/flow/directive.js.map +1 -1
  215. package/dist/domain/flow/rules.js +7 -4
  216. package/dist/domain/flow/rules.js.map +1 -1
  217. package/dist/domain/flow/run-state.js +636 -31
  218. package/dist/domain/flow/run-state.js.map +1 -1
  219. package/dist/domain/harnesses.js +17 -1
  220. package/dist/domain/harnesses.js.map +1 -1
  221. package/dist/domain/mcp-entry.js +36 -14
  222. package/dist/domain/mcp-entry.js.map +1 -1
  223. package/dist/domain/retirement/proposal.js +1 -0
  224. package/dist/domain/retirement/proposal.js.map +1 -1
  225. package/dist/domain/structured-choice-stamp.js +32 -4
  226. package/dist/domain/structured-choice-stamp.js.map +1 -1
  227. package/dist/domain/workline-mcp-entry.js +37 -0
  228. package/dist/domain/workline-mcp-entry.js.map +1 -0
  229. package/dist/runtime/namespace-resolver.js +104 -18
  230. package/dist/runtime/namespace-resolver.js.map +1 -1
  231. package/package.json +1 -1
  232. package/skills/w/README.md +2 -2
  233. package/skills/w/SKILL.md +9 -10
  234. package/skills/w/artifacts/README.md +1 -1
  235. package/skills/w/commands/README.md +3 -3
  236. package/skills/w/commands/persist.md +3 -1
  237. package/skills/w/commands/resume.md +1 -1
  238. package/skills/w/commands/spec-new.md +2 -2
  239. package/skills/w/commands/status.md +1 -1
  240. package/skills/w/commands/workspace-init.md +9 -10
  241. package/skills/w/context/MANIFEST.json +1 -4
  242. package/skills/w/harness/HARNESS.md +1 -1
  243. package/skills/w/loops/quick-loop/LOOP.md +1 -1
  244. package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
  245. package/skills/w/modules/WORKSPACE-SCAFFOLD.md +17 -20
  246. package/skills/w/schemas/capability-descriptor.schema.json +1 -1
@@ -24,6 +24,8 @@
24
24
  import { WORKLINE_FLOWS } from "../../application/capability/compose.js";
25
25
  import { canonicalJson, semanticDigest } from "../../application/semantic-operation/protocol.js";
26
26
  import { isEffectClass, touchesTheWorld } from "../capability/effects.js";
27
+ import { AttemptLedger } from "../capability/protocol.js";
28
+ import { validateDecisionNote } from "../decision-note.js";
27
29
  /**
28
30
  * The version this CLI WRITES. Every state it persists carries it.
29
31
  *
@@ -35,21 +37,17 @@ import { isEffectClass, touchesTheWorld } from "../capability/effects.js";
35
37
  * turning the cap off in silence while somebody alternates CLI versions over one
36
38
  * run. Failing with a cause is the requirement; failing silently is the defect.
37
39
  */
38
- export const FLOW_RUN_STATE_VERSION = 9;
40
+ export const FLOW_RUN_STATE_VERSION = 10;
39
41
  /**
40
42
  * The versions this CLI READS, newest first.
41
43
  *
42
- * Three, and none of the older two is a migration: every field versions 8 and 9
43
- * added is OPTIONAL and its absence is the conservative reading — no floor
44
- * beyond the ledger, nothing forgiven, no degradation declared, nothing owed
45
- * recorded. So a run written before them is read exactly as it always was, keeps
46
- * walking, and is re-stamped current by its first write. Refusing it instead
47
- * would strand runs that were mid-journey when the CLI was upgraded, which is
48
- * the compatibility the spec demands; inventing values for the missing fields
49
- * would be the fabrication the version gate exists to refuse. Neither happens
50
- * here.
51
- */
52
- export const FLOW_RUN_STATE_READABLE = [FLOW_RUN_STATE_VERSION, 8, 7];
44
+ * Versions 9, 8 and 7 remain readable for status, recovery evidence and an
45
+ * explicit adoption. They are NOT writable execution state: v10 records batches
46
+ * and their append-only iteration trace, so continuing an old cursor by merely
47
+ * changing `version` would invent a batch boundary the old run never declared.
48
+ * An active legacy run must be adopted explicitly before any mutation.
49
+ */
50
+ export const FLOW_RUN_STATE_READABLE = [FLOW_RUN_STATE_VERSION, 9, 8, 7];
53
51
  /** The CLI-owned run state inside the session folder. Machine-local, dotted. */
54
52
  export const FLOW_RUN_STATE_FILE = ".flow-run.json";
55
53
  /**
@@ -81,13 +79,116 @@ export const MAX_BOUNDARY_ATTEMPTS = 3;
81
79
  * as a reason to refuse a run that was walking fine.
82
80
  */
83
81
  export function attemptsAt(state, transition) {
84
- const spent = state.attempts.filter((attempt) => attempt.transition === transition).length;
85
- const floor = state.attempt_floor?.[transition] ?? 0;
86
- const granted = state.attempt_grants?.[transition] ?? 0;
87
- return Math.max(0, Math.max(spent, floor) - granted);
82
+ return spendAt(state, transition).spent;
83
+ }
84
+ /** The monotone-counter key for one boundary, scoped when that boundary repeats in a batch. */
85
+ export function attemptCounterKey(state, transition) {
86
+ const iteration = currentBatchIteration(state, transition);
87
+ return attemptCounterKeyForIteration(transition, iteration ?? undefined);
88
+ }
89
+ /** Stable counter key for a historical attempt whose batch iteration is recorded on the row. */
90
+ export function attemptCounterKeyForIteration(transition, iteration) {
91
+ return iteration === undefined ? transition : `${transition}@batch-${iteration}`;
92
+ }
93
+ /**
94
+ * The three numbers and the difference between them, computed once.
95
+ *
96
+ * Kept apart from {@link attemptAccountingAt} so the cap check stays what it was
97
+ * — three lookups and a subtraction — while the richer reading built on top does
98
+ * not restate the arithmetic. Two copies of this formula is exactly how the
99
+ * directive and the recovery guard would end up disagreeing about the same run.
100
+ */
101
+ function spendAt(state, transition) {
102
+ const iteration = currentBatchIteration(state, transition);
103
+ const rows = state.attempts.filter((attempt) => attempt.transition === transition &&
104
+ (iteration === null
105
+ ? attempt.batch_iteration === undefined
106
+ : attempt.batch_iteration === iteration));
107
+ const key = attemptCounterKey(state, transition);
108
+ const floor = state.attempt_floor?.[key] ?? 0;
109
+ const granted = state.attempt_grants?.[key] ?? 0;
110
+ return { rows, floor, granted, spent: Math.max(0, Math.max(rows.length, floor) - granted) };
111
+ }
112
+ /**
113
+ * The whole accounting of one transition, conflicts named and answerability read.
114
+ *
115
+ * The one reading the engine, the directive and the recovery guard share.
116
+ */
117
+ export function attemptAccountingAt(state, transition) {
118
+ const { rows, floor: read, granted, spent } = spendAt(state, transition);
119
+ // The floor the FILE will hold, not the one this invocation happened to read.
120
+ // `raiseCounters` lifts it to at least the row count on every write, so the
121
+ // pre-raise value left the directive reporting a floor one write behind itself
122
+ // — and a projection that disagrees with the state it describes is the very
123
+ // thing this reading exists to stop. It cannot mask a real divergence: a
124
+ // counter that runs AHEAD of the rows stays ahead of this maximum.
125
+ const floor = Math.max(read, rows.length);
126
+ const ordinals = rows.map((attempt) => attempt.attempt);
127
+ const conflicts = [];
128
+ if (floor > rows.length) {
129
+ conflicts.push({
130
+ between: [
131
+ { name: "attempt_floor", value: floor },
132
+ { name: "attempts", value: rows.length },
133
+ ],
134
+ cause: "el contador monótono declara más intentos que las filas persistidas: o se restauró una copia anterior del ledger, o una escritura murió entre el contador y el estado",
135
+ });
136
+ }
137
+ if (granted > Math.max(rows.length, floor)) {
138
+ conflicts.push({
139
+ between: [
140
+ { name: "attempt_grants", value: granted },
141
+ { name: "attempts", value: Math.max(rows.length, floor) },
142
+ ],
143
+ cause: "se perdonan más intentos de los que esta transición llegó a registrar",
144
+ });
145
+ }
146
+ const highest = ordinals.length === 0 ? 0 : Math.max(...ordinals);
147
+ if (highest !== rows.length) {
148
+ conflicts.push({
149
+ between: [
150
+ { name: "attempts[].attempt", value: highest },
151
+ { name: "attempts", value: rows.length },
152
+ ],
153
+ cause: "el ordinal más alto que las filas declaran no coincide con cuántas filas hay: la cadena tiene un hueco, un número repetido, o se reinició bajo otro sello",
154
+ });
155
+ }
156
+ return {
157
+ transition,
158
+ rows: rows.length,
159
+ floor,
160
+ granted,
161
+ spent,
162
+ available: Math.max(0, MAX_BOUNDARY_ATTEMPTS - spent),
163
+ ordinals,
164
+ conflicts,
165
+ unanswerable: unanswerableAt(state),
166
+ };
167
+ }
168
+ /**
169
+ * The refusal a submit would hit replaying this run's own history, or `null`.
170
+ *
171
+ * The ledger validates ordinals keyed by the boundary SEAL rather than by
172
+ * transition, so the question cannot be asked about one transition in isolation:
173
+ * it is asked the way `submit` asks it, by replaying every row the run persisted.
174
+ * A failed replay is not a prediction — it is the same failure the next submit
175
+ * returns, produced before anybody pays an attempt to discover it.
176
+ */
177
+ function unanswerableAt(state) {
178
+ const ledger = new AttemptLedger();
179
+ for (const past of state.attempts) {
180
+ const replay = ledger.record(past);
181
+ if (!replay.ok)
182
+ return replay.failure;
183
+ }
184
+ return null;
88
185
  }
89
186
  export function recoveryBlockedAt(state, transition) {
90
- const trace = state.events.filter((event) => event.transition === transition);
187
+ const iteration = currentBatchIteration(state, transition);
188
+ const trace = state.events.filter((event) => event.transition === transition &&
189
+ (iteration === null
190
+ ? event.batch_iteration === undefined
191
+ : event.batch_iteration === iteration));
91
192
  const material = [...trace].reverse().find(declaresMaterialEffect);
92
193
  if (material !== undefined)
93
194
  return { reason: "materialized", event: material };
@@ -134,6 +235,19 @@ export function positionDigest(state) {
134
235
  const { attempts: _spent, attempt_floor: _floor, attempt_grants: _granted, digest: _seal, ...position } = state;
135
236
  return semanticDigest(position);
136
237
  }
238
+ /** The bounded lifecycle of one repeatable plan-execution batch. */
239
+ export const PLAN_EXEC_BATCH_STAGES = [
240
+ "inferred",
241
+ "isolated",
242
+ "implementing",
243
+ "deviation",
244
+ "validating",
245
+ "reviewing",
246
+ "closed",
247
+ "committing",
248
+ "integrating",
249
+ "done",
250
+ ];
137
251
  /** The state minus its own seal — exactly what {@link sealRunState} digests. */
138
252
  function withoutSeal(state) {
139
253
  const { digest: _seal, ...rest } = state;
@@ -158,6 +272,12 @@ export function newRunState(flow, session) {
158
272
  proposal: null,
159
273
  effects: { planned: [], approved: [], applied: [] },
160
274
  attempts: [],
275
+ batches: [],
276
+ batch_trace: [],
277
+ batch_loop: { pending: true, iteration: 1 },
278
+ handoff: null,
279
+ selected_choice: null,
280
+ decision_preparation: null,
161
281
  });
162
282
  }
163
283
  /**
@@ -223,25 +343,257 @@ export function degradeTransition(state, decisionId, cause) {
223
343
  * were forgiven, which only ever grows; the effective count is the difference.
224
344
  * See {@link attemptsAt}.
225
345
  */
346
+ /**
347
+ * Relabel the attempt chain so the ledger accepts it again — losing no row.
348
+ *
349
+ * The ledger validates ordinals per boundary SEAL: contiguous from 1, each row
350
+ * linked to the digest of the one before it. A run whose chain was broken by an
351
+ * older build of this very CLI — the recovery that rewound `attempts` and then
352
+ * appended a duplicate — stays readable and keeps every fact worth keeping, but
353
+ * every future submit dies on the replay. Renumbering is the smallest repair that
354
+ * makes it answerable: the rows, their digests and their seals all survive in the
355
+ * order they happened, and only the LABELS are made consistent with that order.
356
+ *
357
+ * It is not a way around the cap, which is the reason it is safe to do at all:
358
+ * the monotone floor and the grants are untouched here, so what the boundary has
359
+ * already spent is exactly what it spent before. And it is not a repair of a
360
+ * TAMPERED ledger either — the seal is verified before anything gets this far.
361
+ */
362
+ export function normalizeAttemptChain(state) {
363
+ const ordinal = new Map();
364
+ const previous = new Map();
365
+ const attempts = state.attempts.map((row) => {
366
+ const next = (ordinal.get(row.invocation_id) ?? 0) + 1;
367
+ ordinal.set(row.invocation_id, next);
368
+ const parent = next === 1 ? null : (previous.get(row.invocation_id) ?? null);
369
+ previous.set(row.invocation_id, row.request_digest);
370
+ return { ...row, attempt: next, parent_request_digest: parent };
371
+ });
372
+ return sealRunState({ ...withoutSeal(state), attempts });
373
+ }
226
374
  export function grantAttempts(state, transition, attempts) {
227
375
  const granted = { ...(state.attempt_grants ?? {}) };
228
- granted[transition] = (granted[transition] ?? 0) + attempts;
376
+ const key = attemptCounterKey(state, transition);
377
+ granted[key] = (granted[key] ?? 0) + attempts;
229
378
  return sealRunState({ ...withoutSeal(state), attempt_grants: granted });
230
379
  }
231
380
  /**
232
- * Stamp a state read from an older readable version with the current one.
381
+ * Explicitly adopt a readable legacy state into v10.
233
382
  *
234
- * Applied on the way OUT of the reader, so the run walks — and is written back —
235
- * as one format from the first read onwards, and the version stops being a fact
236
- * that varies per file within one workspace. Nothing else is touched: the older
237
- * versions this build reads differ from the current one only by fields whose
238
- * absence is already the conservative reading, so there is nothing to fill in
239
- * and nothing is filled in. Re-sealed because the version is inside the seal.
383
+ * This function is intentionally NOT called by the reader. Calling it is the
384
+ * durable act of adoption after the caller has named the flow and accepted that
385
+ * no pre-v10 batch is being reconstructed. Empty batch arrays mean exactly
386
+ * "legacy history preserved; no v10 batch claimed", never "the old run had no
387
+ * batches".
240
388
  */
241
389
  export function atCurrentVersion(state) {
242
390
  if (state.version === FLOW_RUN_STATE_VERSION)
243
391
  return state;
244
- return sealRunState({ ...withoutSeal(state), version: FLOW_RUN_STATE_VERSION });
392
+ return sealRunState({
393
+ ...withoutSeal(state),
394
+ version: FLOW_RUN_STATE_VERSION,
395
+ batches: [],
396
+ batch_trace: [],
397
+ batch_loop: { pending: true, iteration: 1 },
398
+ handoff: null,
399
+ selected_choice: null,
400
+ decision_preparation: null,
401
+ });
402
+ }
403
+ /** Whether a parsed state needs the explicit adoption boundary before mutation. */
404
+ export function legacyRunNeedsAdoption(state) {
405
+ return state.version !== FLOW_RUN_STATE_VERSION;
406
+ }
407
+ /** Record one inferred or declared batch, refusing duplicate ids at the caller's boundary. */
408
+ export function withPlanExecBatch(state, batch) {
409
+ const batches = state.batches ?? [];
410
+ if (batches.some((current) => current.id === batch.id))
411
+ return state;
412
+ const trace = state.batch_trace ?? [];
413
+ return sealRunState({
414
+ ...withoutSeal(state),
415
+ batches: [...batches, batch],
416
+ batch_trace: [
417
+ ...trace,
418
+ {
419
+ sequence: trace.length + 1,
420
+ batch_id: batch.id,
421
+ iteration: batch.iteration,
422
+ stage: "inferred",
423
+ transition: "plan-exec.batch-inference",
424
+ kind: "entered",
425
+ },
426
+ ],
427
+ });
428
+ }
429
+ /** Advance one batch phase and append the evidence row in the same sealed state. */
430
+ export function withPlanExecBatchStage(state, batchId, stage, transition, kind = "completed") {
431
+ const batches = state.batches ?? [];
432
+ const index = batches.findIndex((batch) => batch.id === batchId);
433
+ if (index < 0)
434
+ return state;
435
+ const current = batches[index];
436
+ if (current === undefined)
437
+ return state;
438
+ const nextBatches = [...batches];
439
+ nextBatches[index] = { ...current, stage };
440
+ const trace = state.batch_trace ?? [];
441
+ const entry = {
442
+ sequence: trace.length + 1,
443
+ batch_id: current.id,
444
+ iteration: current.iteration,
445
+ stage,
446
+ transition,
447
+ kind,
448
+ };
449
+ return sealRunState({
450
+ ...withoutSeal(state),
451
+ batches: nextBatches,
452
+ batch_trace: [...trace, entry],
453
+ });
454
+ }
455
+ /**
456
+ * Record the lifecycle evidence a real PLAN-exec boundary just crossed.
457
+ *
458
+ * The batch itself is sealed at `batch-inference`, before implementation. Later
459
+ * transitions do not re-infer it; they only append the stage that the cursor
460
+ * actually reached. The small mapping lives beside the state shape so CLI walks,
461
+ * submitted execution results and internal actions cannot drift into three
462
+ * different notions of "the current batch".
463
+ */
464
+ const PLAN_EXEC_BATCH_STAGE_BY_TRANSITION = {
465
+ "plan-exec.batch-isolation": "isolated",
466
+ "plan-exec.implementation": "implementing",
467
+ "plan-exec.deviation-recognition": "deviation",
468
+ "plan-exec.validation-execution": "validating",
469
+ "plan-exec.review-findings": "reviewing",
470
+ // These only occur after the final batch publication. They therefore extend
471
+ // the last closed batch's trace instead of fabricating a new one for Git/done.
472
+ "plan-exec.final-validation": "validating",
473
+ "plan-exec.commit-execution": "committing",
474
+ "plan-exec.unit-integration": "integrating",
475
+ "plan-exec.plan-done": "done",
476
+ };
477
+ const PLAN_EXEC_TERMINAL_BATCH_TRANSITIONS = new Set([
478
+ "plan-exec.final-validation",
479
+ "plan-exec.commit-execution",
480
+ "plan-exec.unit-integration",
481
+ "plan-exec.plan-done",
482
+ ]);
483
+ /** Append the stage for a crossed batch boundary, if that boundary owns one. */
484
+ export function withPlanExecBatchStageForTransition(state, transition) {
485
+ if (state.flow !== "plan-exec")
486
+ return state;
487
+ const stage = PLAN_EXEC_BATCH_STAGE_BY_TRANSITION[transition];
488
+ if (stage === undefined)
489
+ return state;
490
+ const reverse = [...(state.batches ?? [])].reverse();
491
+ const current = PLAN_EXEC_TERMINAL_BATCH_TRANSITIONS.has(transition)
492
+ ? reverse.find((batch) => batch.published_plan_digest !== undefined)
493
+ : reverse.find((batch) => batch.published_plan_digest === undefined);
494
+ if (current === undefined)
495
+ return state;
496
+ const last = state.batch_trace?.at(-1);
497
+ if (current.stage === stage && last?.batch_id === current.id && last.transition === transition) {
498
+ return state;
499
+ }
500
+ return withPlanExecBatchStage(state, current.id, stage, transition, "completed");
501
+ }
502
+ /**
503
+ * Seal whether the just-published batch opens another iteration.
504
+ *
505
+ * The final document bytes, not an agent assertion, decide this field. Keeping
506
+ * it in the same state publication as the `closed` trace makes a crash recover
507
+ * to one exact cursor shape.
508
+ */
509
+ export function withPlanExecBatchLoop(state, loop) {
510
+ const current = state.batch_loop;
511
+ if (current?.pending === loop.pending && current?.iteration === loop.iteration)
512
+ return state;
513
+ return sealRunState({ ...withoutSeal(state), batch_loop: loop });
514
+ }
515
+ /**
516
+ * The iteration identity for a transition in the repeatable PLAN-exec segment.
517
+ *
518
+ * Prefix/tail rows deliberately return `null`: they run once per flow and keep
519
+ * their historical retry/observation identity. The loop field is absent on an
520
+ * older v10 state, in which case preserving its old unscoped accounting is safer
521
+ * than pretending we know which historical batch it belonged to.
522
+ */
523
+ export function currentBatchIteration(state, transition) {
524
+ if (state.flow !== "plan-exec" || !PLAN_EXEC_BATCH_LOOP_TRANSITIONS.has(transition))
525
+ return null;
526
+ const loop = state.batch_loop;
527
+ return loop?.pending === true && loop.iteration !== null ? loop.iteration : null;
528
+ }
529
+ /** The rows that repeat between batch closures; kept local to the state contract. */
530
+ export const PLAN_EXEC_BATCH_LOOP_TRANSITIONS = new Set([
531
+ "plan-exec.batch-eligibility-signal",
532
+ "plan-exec.batch-inference",
533
+ "plan-exec.batch-isolation",
534
+ "plan-exec.design-precondition",
535
+ "plan-exec.unit-acquisition",
536
+ "plan-exec.branch-precondition",
537
+ "plan-exec.implementation",
538
+ "plan-exec.deviation-recognition",
539
+ "plan-exec.deviation-eligibility",
540
+ "plan-exec.deviation-gate",
541
+ "plan-exec.escalation-package",
542
+ "plan-exec.validation-execution",
543
+ "plan-exec.deferred-check",
544
+ "plan-exec.review-findings",
545
+ "plan-exec.batch-close",
546
+ ]);
547
+ /** Seal the plan bytes the batch publication actually produced. */
548
+ export function withPlanExecBatchPublication(state, batchId, publishedPlanDigest) {
549
+ const batches = state.batches ?? [];
550
+ const index = batches.findIndex((batch) => batch.id === batchId);
551
+ if (index < 0)
552
+ return state;
553
+ const current = batches[index];
554
+ if (current === undefined)
555
+ return state;
556
+ const next = [...batches];
557
+ const publication = current.publication === undefined
558
+ ? {}
559
+ : {
560
+ publication: {
561
+ ...current.publication,
562
+ after_plan_digest: publishedPlanDigest,
563
+ status: "applied",
564
+ },
565
+ };
566
+ next[index] = {
567
+ ...current,
568
+ published_plan_digest: publishedPlanDigest,
569
+ ...publication,
570
+ };
571
+ return sealRunState({ ...withoutSeal(state), batches: next });
572
+ }
573
+ /** Seal the exact before/after pair before the plan document is touched. */
574
+ export function withPlanExecBatchPublicationPrepared(state, batchId, publication) {
575
+ const batches = state.batches ?? [];
576
+ const index = batches.findIndex((batch) => batch.id === batchId);
577
+ if (index < 0)
578
+ return state;
579
+ const current = batches[index];
580
+ if (current === undefined)
581
+ return state;
582
+ const next = [...batches];
583
+ next[index] = { ...current, publication: { ...publication, status: "prepared" } };
584
+ return sealRunState({ ...withoutSeal(state), batches: next });
585
+ }
586
+ /** Persist a selected handoff; the advancement engine then refuses to continue this run. */
587
+ export function withHandoff(state, handoff) {
588
+ return sealRunState({ ...withoutSeal(state), handoff });
589
+ }
590
+ /** Persist the gate's executable consequence before the run leaves that boundary. */
591
+ export function withSelectedChoice(state, selected) {
592
+ return sealRunState({ ...withoutSeal(state), selected_choice: selected });
593
+ }
594
+ /** Persist or clear the exact decision view offered at the human gate. */
595
+ export function withDecisionPreparation(state, preparation) {
596
+ return sealRunState({ ...withoutSeal(state), decision_preparation: preparation });
245
597
  }
246
598
  /**
247
599
  * Reconcile the state with the monotone counter that lives outside its seal.
@@ -329,6 +681,7 @@ export function restatesLastEvent(state, event) {
329
681
  if (last === undefined || last.kind !== "failed" || event.kind !== "failed")
330
682
  return false;
331
683
  return (last.transition === event.transition &&
684
+ last.batch_iteration === event.batch_iteration &&
332
685
  last.code === event.code &&
333
686
  last.message === event.message);
334
687
  }
@@ -348,7 +701,9 @@ export function restatesLastEvent(state, event) {
348
701
  * into a retry counter, which is what the attempt history is for.
349
702
  */
350
703
  export function withObservation(state, observation) {
351
- const repeated = state.observations.some((past) => past.transition === observation.transition && sameSignals(past.signals, observation.signals));
704
+ const repeated = state.observations.some((past) => past.transition === observation.transition &&
705
+ past.batch_iteration === observation.batch_iteration &&
706
+ sameSignals(past.signals, observation.signals));
352
707
  if (repeated)
353
708
  return state;
354
709
  return sealRunState({
@@ -552,6 +907,20 @@ function checkShape(parsed) {
552
907
  * digest matches.
553
908
  */
554
909
  function checkRecordShape(parsed, invalid) {
910
+ const common = checkCommonRecordShape(parsed, invalid);
911
+ if (common !== null)
912
+ return common;
913
+ if (parsed.version === FLOW_RUN_STATE_VERSION) {
914
+ const v10 = checkV10RecordShape(parsed, invalid);
915
+ if (v10 !== null)
916
+ return v10;
917
+ }
918
+ if (typeof parsed.digest !== "string")
919
+ return invalid("no trae su sello");
920
+ return null;
921
+ }
922
+ /** Record fields shared by every readable flow-run version, in refusal order. */
923
+ function checkCommonRecordShape(parsed, invalid) {
555
924
  if (!isPendingAction(parsed.pending_action)) {
556
925
  return invalid("declara una acción pendiente sin transición o sin sello");
557
926
  }
@@ -583,8 +952,28 @@ function checkRecordShape(parsed, invalid) {
583
952
  if (!isCounterMap(parsed.attempt_floor) || !isCounterMap(parsed.attempt_grants)) {
584
953
  return invalid("trae una contabilidad de intentos que no es un contador por transición");
585
954
  }
586
- if (typeof parsed.digest !== "string")
587
- return invalid("no trae su sello");
955
+ return null;
956
+ }
957
+ /** Fields added by v10 that have no truthful default on a persisted run. */
958
+ function checkV10RecordShape(parsed, invalid) {
959
+ if (!isPlanExecBatchArray(parsed.batches)) {
960
+ return invalid("no trae batches v10 con fases, tareas y sello de plan");
961
+ }
962
+ if (!isPlanExecBatchTraceArray(parsed.batch_trace, parsed.batches)) {
963
+ return invalid("trae una traza de batches que no es append-only ni corresponde a sus iteraciones");
964
+ }
965
+ if (!isPlanExecBatchLoop(parsed.batch_loop)) {
966
+ return invalid("declara un ciclo de batches sin su próxima iteración sellada");
967
+ }
968
+ if (!isHandoff(parsed.handoff)) {
969
+ return invalid("declara una entrega de flow sin destino, comando o sello de paquete");
970
+ }
971
+ if (!isSelectedChoice(parsed.selected_choice)) {
972
+ return invalid("declara una consecuencia elegida sin frontera, etiqueta o ruta válida");
973
+ }
974
+ if (!isDecisionPreparation(parsed.decision_preparation)) {
975
+ return invalid("declara una vista de decisión sin nota, preview o linaje sellados");
976
+ }
588
977
  return null;
589
978
  }
590
979
  function refuse(code, message, action) {
@@ -633,6 +1022,212 @@ function isScope(value) {
633
1022
  return false;
634
1023
  return value.sources.every((alias) => alias.trim().length > 0);
635
1024
  }
1025
+ function isPlanExecBatchArray(value) {
1026
+ if (!Array.isArray(value))
1027
+ return false;
1028
+ const ids = new Set();
1029
+ const iterations = new Set();
1030
+ return value.every((entry) => {
1031
+ if (!isRecord(entry))
1032
+ return false;
1033
+ if (typeof entry.id !== "string" ||
1034
+ entry.id.trim().length === 0 ||
1035
+ ids.has(entry.id) ||
1036
+ typeof entry.iteration !== "number" ||
1037
+ !Number.isInteger(entry.iteration) ||
1038
+ entry.iteration < 1 ||
1039
+ iterations.has(entry.iteration) ||
1040
+ (entry.mode !== "continuous" && entry.mode !== "isolated") ||
1041
+ !Array.isArray(entry.phases) ||
1042
+ entry.phases.length === 0 ||
1043
+ !entry.phases.every((phase) => typeof phase === "number" && Number.isInteger(phase) && phase > 0) ||
1044
+ !isStringArray(entry.tasks) ||
1045
+ entry.tasks.length === 0 ||
1046
+ new Set(entry.tasks).size !== entry.tasks.length ||
1047
+ typeof entry.plan_digest !== "string" ||
1048
+ entry.plan_digest.length === 0 ||
1049
+ (entry.published_plan_digest !== undefined &&
1050
+ (typeof entry.published_plan_digest !== "string" ||
1051
+ entry.published_plan_digest.length === 0)) ||
1052
+ !isPlanExecBatchPublication(entry.publication) ||
1053
+ (entry.publication !== undefined &&
1054
+ entry.publication.status === "applied" &&
1055
+ entry.published_plan_digest !== entry.publication.after_plan_digest) ||
1056
+ !PLAN_EXEC_BATCH_STAGES.includes(entry.stage)) {
1057
+ return false;
1058
+ }
1059
+ ids.add(entry.id);
1060
+ iterations.add(entry.iteration);
1061
+ return true;
1062
+ });
1063
+ }
1064
+ function isPlanExecBatchPublication(value) {
1065
+ if (value === undefined)
1066
+ return true;
1067
+ return (isRecord(value) &&
1068
+ typeof value.plan === "string" &&
1069
+ value.plan.trim().length > 0 &&
1070
+ typeof value.before_plan_digest === "string" &&
1071
+ value.before_plan_digest.length > 0 &&
1072
+ typeof value.after_plan_digest === "string" &&
1073
+ value.after_plan_digest.length > 0 &&
1074
+ typeof value.transition === "string" &&
1075
+ value.transition.length > 0 &&
1076
+ (value.status === "prepared" || value.status === "applied"));
1077
+ }
1078
+ /** Absent is only the compatibility reading of a v10 file written before the loop cursor. */
1079
+ function isPlanExecBatchLoop(value) {
1080
+ if (value === undefined)
1081
+ return true;
1082
+ if (!isRecord(value) || typeof value.pending !== "boolean")
1083
+ return false;
1084
+ if (value.pending)
1085
+ return Number.isInteger(value.iteration) && value.iteration >= 1;
1086
+ return value.iteration === null;
1087
+ }
1088
+ function isPlanExecBatchTraceArray(value, batches) {
1089
+ if (!Array.isArray(value) || !Array.isArray(batches))
1090
+ return false;
1091
+ const byId = new Map(batches.map((batch) => [batch.id, batch.iteration]));
1092
+ return value.every((entry, index) => {
1093
+ if (!isRecord(entry))
1094
+ return false;
1095
+ return (entry.sequence === index + 1 &&
1096
+ typeof entry.batch_id === "string" &&
1097
+ byId.get(entry.batch_id) === entry.iteration &&
1098
+ typeof entry.iteration === "number" &&
1099
+ Number.isInteger(entry.iteration) &&
1100
+ typeof entry.transition === "string" &&
1101
+ entry.transition.length > 0 &&
1102
+ PLAN_EXEC_BATCH_STAGES.includes(entry.stage) &&
1103
+ (entry.kind === "entered" || entry.kind === "completed" || entry.kind === "blocked"));
1104
+ });
1105
+ }
1106
+ function isHandoff(value) {
1107
+ if (value === null)
1108
+ return true;
1109
+ if (!isRecord(value))
1110
+ return false;
1111
+ return ((value.destination === "plan-refine" ||
1112
+ value.destination === "spec-refine" ||
1113
+ value.destination === "spec-new") &&
1114
+ typeof value.command === "string" &&
1115
+ value.command.trim().length > 0 &&
1116
+ isEscalationPackage(value.package) &&
1117
+ typeof value.package_digest === "string" &&
1118
+ value.package_digest === semanticDigest(value.package));
1119
+ }
1120
+ function isEscalationPackage(value) {
1121
+ return (isRecord(value) &&
1122
+ (value.plan === null || (typeof value.plan === "string" && value.plan.trim().length > 0)) &&
1123
+ isObservationArray(value.observations) &&
1124
+ isRecord(value.decisions) &&
1125
+ isJsonValue(value.decisions) &&
1126
+ typeof value.selection === "string" &&
1127
+ value.selection.trim().length > 0);
1128
+ }
1129
+ function isJsonValue(value) {
1130
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1131
+ return true;
1132
+ if (typeof value === "number")
1133
+ return Number.isFinite(value);
1134
+ if (Array.isArray(value))
1135
+ return value.every(isJsonValue);
1136
+ return isRecord(value) && Object.values(value).every(isJsonValue);
1137
+ }
1138
+ function isSelectedChoice(value) {
1139
+ if (value === null)
1140
+ return true;
1141
+ if (!isRecord(value) || typeof value.transition !== "string" || typeof value.label !== "string") {
1142
+ return false;
1143
+ }
1144
+ if (!isRecord(value.outcome))
1145
+ return false;
1146
+ return (value.outcome.kind === "register-decision" ||
1147
+ (value.outcome.kind === "handoff" &&
1148
+ (value.outcome.destination === "plan-refine" ||
1149
+ value.outcome.destination === "spec-refine" ||
1150
+ value.outcome.destination === "spec-new")));
1151
+ }
1152
+ /** A v10 file written before preview persistence may omit this field. */
1153
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: each discriminant validates one durable preparation variant.
1154
+ function isDecisionPreparation(value) {
1155
+ if (value === undefined || value === null)
1156
+ return true;
1157
+ if (!isRecord(value) || typeof value.kind !== "string")
1158
+ return false;
1159
+ if (value.kind === "settled")
1160
+ return typeof value.decision === "string" && value.decision.trim().length > 0;
1161
+ if (value.kind === "reused") {
1162
+ return (typeof value.note === "string" &&
1163
+ value.note.trim().length > 0 &&
1164
+ typeof value.decision === "string" &&
1165
+ value.decision.trim().length > 0 &&
1166
+ typeof value.resume_point === "string" &&
1167
+ value.resume_point.trim().length > 0);
1168
+ }
1169
+ if (value.kind !== "prepared" || !isRecord(value.baseline))
1170
+ return false;
1171
+ const baseline = value.baseline;
1172
+ if (typeof baseline.path !== "string" ||
1173
+ baseline.path.trim().length === 0 ||
1174
+ typeof baseline.number !== "string" ||
1175
+ baseline.number.trim().length === 0 ||
1176
+ typeof baseline.digest !== "string" ||
1177
+ baseline.digest.trim().length === 0 ||
1178
+ !isStringArray(baseline.criteria) ||
1179
+ typeof value.index_path !== "string" ||
1180
+ value.index_path.trim().length === 0) {
1181
+ return false;
1182
+ }
1183
+ const checkedBaseline = {
1184
+ path: baseline.path,
1185
+ number: baseline.number,
1186
+ digest: baseline.digest,
1187
+ };
1188
+ const note = validateDecisionNote(value.note);
1189
+ if (!note.ok || note.value === null)
1190
+ return false;
1191
+ if (note.value.lineage.spec.path !== checkedBaseline.path ||
1192
+ note.value.lineage.spec.number !== checkedBaseline.number ||
1193
+ note.value.lineage.spec.digest !== checkedBaseline.digest) {
1194
+ return false;
1195
+ }
1196
+ return isDecisionPreview(value.preview, checkedBaseline);
1197
+ }
1198
+ function isDecisionPreview(value, baseline) {
1199
+ if (!isRecord(value) ||
1200
+ !isRecord(value.baseline) ||
1201
+ !isRecord(value.impact) ||
1202
+ !isRecord(value.evidence)) {
1203
+ return false;
1204
+ }
1205
+ const previewBaseline = value.baseline;
1206
+ if (previewBaseline.path !== baseline.path ||
1207
+ previewBaseline.number !== baseline.number ||
1208
+ previewBaseline.digest !== baseline.digest ||
1209
+ !Array.isArray(value.effective_change) ||
1210
+ !isStringArray(value.consumers) ||
1211
+ !isStringArray(value.obligations) ||
1212
+ typeof value.resume_point !== "string" ||
1213
+ value.resume_point.trim().length === 0 ||
1214
+ !isStringArray(value.evidence.preserved) ||
1215
+ !isStringArray(value.evidence.invalidated) ||
1216
+ typeof value.impact.scope !== "string" ||
1217
+ (value.impact.scope !== "functional" && value.impact.scope !== "plan-only") ||
1218
+ !Number.isInteger(value.impact.assertions) ||
1219
+ !Number.isInteger(value.impact.consumers) ||
1220
+ !isRecord(value.effects) ||
1221
+ !isEffectClassArray(value.effects.classes) ||
1222
+ !Array.isArray(value.effects.entries) ||
1223
+ !isProposal(value.proposal)) {
1224
+ return false;
1225
+ }
1226
+ return value.effects.entries.every((entry) => isRecord(entry) &&
1227
+ typeof entry.path === "string" &&
1228
+ Number.isInteger(entry.bytes) &&
1229
+ typeof entry.overwrite === "boolean");
1230
+ }
636
1231
  function isProposal(value) {
637
1232
  if (value === null)
638
1233
  return true;
@@ -694,8 +1289,12 @@ function isEventArray(value) {
694
1289
  return value.every((entry) => {
695
1290
  if (!isRecord(entry))
696
1291
  return false;
697
- if (typeof entry.transition !== "string" || typeof entry.operation !== "string")
1292
+ if (typeof entry.transition !== "string" ||
1293
+ typeof entry.operation !== "string" ||
1294
+ (entry.batch_iteration !== undefined &&
1295
+ (!Number.isInteger(entry.batch_iteration) || entry.batch_iteration < 1))) {
698
1296
  return false;
1297
+ }
699
1298
  if (entry.kind === "executed") {
700
1299
  return (typeof entry.summary === "string" &&
701
1300
  typeof entry.output_digest === "string" &&
@@ -716,7 +1315,11 @@ function isEventArray(value) {
716
1315
  function isObservationArray(value) {
717
1316
  if (!Array.isArray(value))
718
1317
  return false;
719
- return value.every((entry) => isRecord(entry) && typeof entry.transition === "string" && isStringArray(entry.signals));
1318
+ return value.every((entry) => isRecord(entry) &&
1319
+ typeof entry.transition === "string" &&
1320
+ isStringArray(entry.signals) &&
1321
+ (entry.batch_iteration === undefined ||
1322
+ (Number.isInteger(entry.batch_iteration) && entry.batch_iteration >= 1)));
720
1323
  }
721
1324
  /**
722
1325
  * The two optional fields are checked as ABSENT-or-well-formed, never defaulted.
@@ -752,6 +1355,8 @@ function isAttemptArray(value) {
752
1355
  Number.isInteger(entry.attempt) &&
753
1356
  typeof entry.request_digest === "string" &&
754
1357
  typeof entry.transition === "string" &&
1358
+ (entry.batch_iteration === undefined ||
1359
+ (Number.isInteger(entry.batch_iteration) && entry.batch_iteration >= 1)) &&
755
1360
  (entry.parent_request_digest === null || typeof entry.parent_request_digest === "string"));
756
1361
  }
757
1362
  //# sourceMappingURL=run-state.js.map