@tacuchi/agent-workflow-cli 25.6.0 → 25.7.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 (148) hide show
  1. package/CHANGELOG.md +4368 -0
  2. package/README.md +1 -1
  3. package/dist/adapters/git-cli.js +74 -6
  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/checkpoint-write-service.js +161 -39
  10. package/dist/application/checkpoint-write-service.js.map +1 -1
  11. package/dist/application/dates.js +12 -0
  12. package/dist/application/dates.js.map +1 -1
  13. package/dist/application/fix-git-service.js +83 -13
  14. package/dist/application/fix-git-service.js.map +1 -1
  15. package/dist/application/flow/advance.js +24 -3
  16. package/dist/application/flow/advance.js.map +1 -1
  17. package/dist/application/flow/annul-service.js +159 -0
  18. package/dist/application/flow/annul-service.js.map +1 -0
  19. package/dist/application/flow/flow-service.js +212 -49
  20. package/dist/application/flow/flow-service.js.map +1 -1
  21. package/dist/application/flow/internal-actions.js +23 -3
  22. package/dist/application/flow/internal-actions.js.map +1 -1
  23. package/dist/application/flow/internal-drive.js +51 -19
  24. package/dist/application/flow/internal-drive.js.map +1 -1
  25. package/dist/application/flow/run-projection.js +21 -5
  26. package/dist/application/flow/run-projection.js.map +1 -1
  27. package/dist/application/flow/run-state-service.js +108 -35
  28. package/dist/application/flow/run-state-service.js.map +1 -1
  29. package/dist/application/flow/submit.js +63 -144
  30. package/dist/application/flow/submit.js.map +1 -1
  31. package/dist/application/git-flow-service.js +325 -72
  32. package/dist/application/git-flow-service.js.map +1 -1
  33. package/dist/application/hook-sql-mutation-guard.js +155 -27
  34. package/dist/application/hook-sql-mutation-guard.js.map +1 -1
  35. package/dist/application/host-memory/claude-code.js +65 -0
  36. package/dist/application/host-memory/claude-code.js.map +1 -0
  37. package/dist/application/host-memory/codex.js +225 -0
  38. package/dist/application/host-memory/codex.js.map +1 -0
  39. package/dist/application/host-memory/destination.js +52 -0
  40. package/dist/application/host-memory/destination.js.map +1 -0
  41. package/dist/application/host-memory/reader.js +9 -0
  42. package/dist/application/host-memory/reader.js.map +1 -0
  43. package/dist/application/host-memory/report.js +136 -0
  44. package/dist/application/host-memory/report.js.map +1 -0
  45. package/dist/application/lifecycle-target.js +3 -1
  46. package/dist/application/lifecycle-target.js.map +1 -1
  47. package/dist/application/local-proposal.js +13 -3
  48. package/dist/application/local-proposal.js.map +1 -1
  49. package/dist/application/logging/log-events.js +22 -0
  50. package/dist/application/logging/log-events.js.map +1 -1
  51. package/dist/application/parsers/spec-relation.js +20 -7
  52. package/dist/application/parsers/spec-relation.js.map +1 -1
  53. package/dist/application/persist-service.js +45 -1
  54. package/dist/application/persist-service.js.map +1 -1
  55. package/dist/application/plan-exec-batch-service.js +47 -2
  56. package/dist/application/plan-exec-batch-service.js.map +1 -1
  57. package/dist/application/plan-lineage-seal.js +93 -0
  58. package/dist/application/plan-lineage-seal.js.map +1 -0
  59. package/dist/application/plan-lint-service.js +153 -0
  60. package/dist/application/plan-lint-service.js.map +1 -0
  61. package/dist/application/prod-consent.js +75 -0
  62. package/dist/application/prod-consent.js.map +1 -0
  63. package/dist/application/reseal-service.js +9 -3
  64. package/dist/application/reseal-service.js.map +1 -1
  65. package/dist/application/self/install-skill.js +5 -1
  66. package/dist/application/self/install-skill.js.map +1 -1
  67. package/dist/application/self/update-self.js +204 -8
  68. package/dist/application/self/update-self.js.map +1 -1
  69. package/dist/application/session-create-service.js +49 -0
  70. package/dist/application/session-create-service.js.map +1 -1
  71. package/dist/application/session-custody-recorder.js +4 -0
  72. package/dist/application/session-custody-recorder.js.map +1 -1
  73. package/dist/application/session-narrative.js +27 -0
  74. package/dist/application/session-narrative.js.map +1 -1
  75. package/dist/application/session-resolver.js +28 -9
  76. package/dist/application/session-resolver.js.map +1 -1
  77. package/dist/application/source-boundary-policy.js +168 -34
  78. package/dist/application/source-boundary-policy.js.map +1 -1
  79. package/dist/application/working-branch-service.js +68 -0
  80. package/dist/application/working-branch-service.js.map +1 -0
  81. package/dist/application/workline-graph.js +7 -2
  82. package/dist/application/workline-graph.js.map +1 -1
  83. package/dist/cli/commands/checkpoint-write.js +18 -8
  84. package/dist/cli/commands/checkpoint-write.js.map +1 -1
  85. package/dist/cli/commands/fix-git.js +45 -6
  86. package/dist/cli/commands/fix-git.js.map +1 -1
  87. package/dist/cli/commands/flow.js +50 -5
  88. package/dist/cli/commands/flow.js.map +1 -1
  89. package/dist/cli/commands/git-flow.js +103 -29
  90. package/dist/cli/commands/git-flow.js.map +1 -1
  91. package/dist/cli/commands/host-memory.js +33 -0
  92. package/dist/cli/commands/host-memory.js.map +1 -0
  93. package/dist/cli/commands/index.js +6 -0
  94. package/dist/cli/commands/index.js.map +1 -1
  95. package/dist/cli/commands/persist.js +5 -0
  96. package/dist/cli/commands/persist.js.map +1 -1
  97. package/dist/cli/commands/plan.js +50 -0
  98. package/dist/cli/commands/plan.js.map +1 -0
  99. package/dist/cli/commands/resume-summary.js +4 -2
  100. package/dist/cli/commands/resume-summary.js.map +1 -1
  101. package/dist/cli/commands/set-branch.js +40 -6
  102. package/dist/cli/commands/set-branch.js.map +1 -1
  103. package/dist/cli/help-groups.js +6 -0
  104. package/dist/cli/help-groups.js.map +1 -1
  105. package/dist/cli/main.js +6 -2
  106. package/dist/cli/main.js.map +1 -1
  107. package/dist/cli/parser.js +20 -1
  108. package/dist/cli/parser.js.map +1 -1
  109. package/dist/cli/tui/components/git-flow-actions.js +65 -20
  110. package/dist/cli/tui/components/git-flow-actions.js.map +1 -1
  111. package/dist/cli/tui/tabs/project-tab.js +101 -12
  112. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  113. package/dist/domain/changelog-contract.js +172 -0
  114. package/dist/domain/changelog-contract.js.map +1 -0
  115. package/dist/domain/database-tools.js +1 -189
  116. package/dist/domain/database-tools.js.map +1 -1
  117. package/dist/domain/flow/answer.js +3 -0
  118. package/dist/domain/flow/answer.js.map +1 -1
  119. package/dist/domain/flow/authority.js +12 -0
  120. package/dist/domain/flow/authority.js.map +1 -1
  121. package/dist/domain/flow/journey-baseline.js +131 -0
  122. package/dist/domain/flow/journey-baseline.js.map +1 -0
  123. package/dist/domain/flow/run-state.js +257 -62
  124. package/dist/domain/flow/run-state.js.map +1 -1
  125. package/dist/domain/host-memory/model.js +2 -0
  126. package/dist/domain/host-memory/model.js.map +1 -0
  127. package/dist/domain/host-memory/provenance.js +43 -0
  128. package/dist/domain/host-memory/provenance.js.map +1 -0
  129. package/dist/domain/host-memory/topic.js +26 -0
  130. package/dist/domain/host-memory/topic.js.map +1 -0
  131. package/dist/domain/lineage.js +11 -3
  132. package/dist/domain/lineage.js.map +1 -1
  133. package/dist/domain/proposal.js +7 -0
  134. package/dist/domain/proposal.js.map +1 -1
  135. package/dist/domain/session/custody.js.map +1 -1
  136. package/dist/domain/sql-lexer.js +227 -0
  137. package/dist/domain/sql-lexer.js.map +1 -0
  138. package/package.json +2 -2
  139. package/skills/w/SKILL.md +9 -9
  140. package/skills/w/commands/README.md +2 -1
  141. package/skills/w/commands/recall.md +39 -0
  142. package/skills/w/context/MANIFEST.json +4 -0
  143. package/skills/w/harness/HARNESS.md +9 -5
  144. package/skills/w/hooks/README.md +27 -16
  145. package/skills/w/loops/CHASSIS.md +16 -12
  146. package/skills/w/loops/CODE-POLICIES.md +11 -6
  147. package/skills/w/loops/plan-exec-loop/LOOP.md +9 -9
  148. package/skills/w/modules/COMPACTION.md +1 -1
@@ -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 { 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,42 @@ 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 = 12;
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
+ export const FLOW_RUN_STATE_CONTINUABLE = [FLOW_RUN_STATE_VERSION, 11];
42
55
  /**
43
56
  * The versions this CLI READS, newest first.
44
57
  *
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.
58
+ * Versions 10 to 7 remain readable for status and recovery evidence only. They
59
+ * are NOT continuable execution state: continuing an old cursor by merely
60
+ * changing `version` would invent a batch boundary the old run never declared,
61
+ * so their way out is `aw flow restart`.
62
+ */
63
+ export const FLOW_RUN_STATE_READABLE = [
64
+ ...FLOW_RUN_STATE_CONTINUABLE,
65
+ 10,
66
+ 9,
67
+ 8,
68
+ 7,
69
+ ];
70
+ /**
71
+ * One upgrade step per continuable version, keyed by the version it upgrades FROM.
72
+ *
73
+ * A step may only add what the next version introduced with a truthful default;
74
+ * the cursor, the answers, the approvals and the attempts pass through untouched.
50
75
  */
51
- export const FLOW_RUN_STATE_READABLE = [FLOW_RUN_STATE_VERSION, 10, 9, 8, 7];
76
+ const CONTINUABLE_UPGRADES = {
77
+ 11: (state) => ({ ...state, version: 12, journey_base: [...V11_JOURNEY_BASE[state.flow]] }),
78
+ };
52
79
  /** The CLI-owned run state inside the session folder. Machine-local, dotted. */
53
80
  export const FLOW_RUN_STATE_FILE = ".flow-run.json";
54
81
  /**
@@ -348,8 +375,12 @@ export function recoveryBlockedAt(state, transition) {
348
375
  function declaresMaterialEffect(event) {
349
376
  // A reconciliation moved the run's own counters and nothing else: it can no
350
377
  // more block a recovery than reading the file could.
351
- if (event.kind === "reconciled")
378
+ if (event.kind === "reconciled" ||
379
+ event.kind === "aligned" ||
380
+ event.kind === "restarted" ||
381
+ event.kind === "annulled") {
352
382
  return false;
383
+ }
353
384
  if (event.kind !== "failed")
354
385
  return touchesTheWorld(event.effects);
355
386
  if (event.effects === undefined)
@@ -405,6 +436,7 @@ export function newRunState(flow, session) {
405
436
  flow,
406
437
  session,
407
438
  scope: null,
439
+ journey_base: journeyOfFlow(flow).map((decision) => decision.id),
408
440
  applied: [],
409
441
  skipped: [],
410
442
  boundary: null,
@@ -525,32 +557,43 @@ export function grantAttempts(state, transition, attempts) {
525
557
  granted[key] = (granted[key] ?? 0) + attempts;
526
558
  return sealRunState({ ...withoutSeal(state), attempt_grants: granted });
527
559
  }
560
+ /** Whether this exhausted boundary already spent the one re-run the CLI gives it. */
561
+ export function exhaustedRerunSpent(state, transition) {
562
+ return (state.exhausted_reruns ?? []).includes(attemptCounterKey(state, transition));
563
+ }
564
+ /** Record that the CLI's one re-run of this exhausted boundary came back refused. */
565
+ export function withExhaustedRerunSpent(state, transition) {
566
+ if (exhaustedRerunSpent(state, transition))
567
+ return state;
568
+ return sealRunState({
569
+ ...withoutSeal(state),
570
+ exhausted_reruns: [...(state.exhausted_reruns ?? []), attemptCounterKey(state, transition)],
571
+ });
572
+ }
528
573
  /**
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".
574
+ * Forget a spent re-run: `aw flow recover` gives the boundary a fresh budget,
575
+ * and a later exhaustion is a new episode that is owed its own re-run.
536
576
  */
537
- export function atCurrentVersion(state) {
538
- if (state.version === FLOW_RUN_STATE_VERSION)
577
+ export function withoutExhaustedRerun(state, transition) {
578
+ if (!exhaustedRerunSpent(state, transition))
539
579
  return state;
580
+ const key = attemptCounterKey(state, transition);
540
581
  return sealRunState({
541
582
  ...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",
583
+ exhausted_reruns: (state.exhausted_reruns ?? []).filter((spent) => spent !== key),
552
584
  });
553
585
  }
586
+ /**
587
+ * The one exit from a run the CLI cannot continue: archive it inside the session
588
+ * and adopt a new one. Named in every refusal whose state it resolves.
589
+ */
590
+ export function restartInvocation(session) {
591
+ return `aw flow restart --session ${session}`;
592
+ }
593
+ /** The action of every refusal over a registry this CLI cannot use as it is. */
594
+ function unusableRemedy(session) {
595
+ 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`;
596
+ }
554
597
  /** Keep the sealed preview the person is choosing over; no cursor moves here. */
555
598
  export function withRouteProposal(state, proposal) {
556
599
  return sealRunState({
@@ -568,9 +611,28 @@ export function withRouteDecisions(state, decisions) {
568
611
  assurance: assuranceForRoute(decisions, state.applied),
569
612
  });
570
613
  }
571
- /** Whether a parsed state needs the explicit adoption boundary before mutation. */
614
+ /** Whether a parsed state is outside the continuable set and cannot be mutated. */
572
615
  export function legacyRunNeedsAdoption(state) {
573
- return state.version !== FLOW_RUN_STATE_VERSION;
616
+ return !FLOW_RUN_STATE_CONTINUABLE.includes(state.version);
617
+ }
618
+ /**
619
+ * Walk a continuable state up to the version this CLI writes, one step at a time.
620
+ *
621
+ * Re-sealed once at the end: the file on disk was already verified against its
622
+ * own seal, and the upgraded state is persisted by the next write, never here.
623
+ * A legacy state comes back unchanged — reading one must never re-stamp it.
624
+ */
625
+ export function upgradeContinuable(state) {
626
+ if (legacyRunNeedsAdoption(state) || state.version === FLOW_RUN_STATE_VERSION)
627
+ return state;
628
+ let current = state;
629
+ while (current.version !== FLOW_RUN_STATE_VERSION) {
630
+ const step = CONTINUABLE_UPGRADES[current.version];
631
+ if (step === undefined)
632
+ return state;
633
+ current = step(current);
634
+ }
635
+ return sealRunState(withoutSeal(current));
574
636
  }
575
637
  /** Record one inferred or declared batch, refusing duplicate ids at the caller's boundary. */
576
638
  export function withPlanExecBatch(state, batch) {
@@ -1021,20 +1083,26 @@ function union(current, added) {
1021
1083
  *
1022
1084
  * Order matters: shape before version before seal. A file whose version is
1023
1085
  * unknown must not be judged by a digest rule that version may not even use.
1086
+ *
1087
+ * Every refusal names the command that works IN that state: `--adopt` only adopts
1088
+ * a session with no registry at all, so an unreadable, tampered or too-old one
1089
+ * leaves through `aw flow restart`, and one written by a newer CLI through
1090
+ * updating this one. `session` names the run in those commands.
1024
1091
  */
1025
- export function parseRunState(raw) {
1092
+ export function parseRunState(raw, session = "<código>") {
1093
+ const restart = unusableRemedy(session);
1026
1094
  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");
1095
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida está vacío", restart);
1028
1096
  }
1029
1097
  let parsed;
1030
1098
  try {
1031
1099
  parsed = JSON.parse(raw);
1032
1100
  }
1033
1101
  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'");
1102
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida no es JSON válido", restart);
1035
1103
  }
1036
1104
  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'");
1105
+ return refuse("FLOW_RUN_INVALID", "el estado de corrida no es un objeto JSON", restart);
1038
1106
  }
1039
1107
  if (!FLOW_RUN_STATE_READABLE.includes(parsed.version)) {
1040
1108
  // No silent migration, in either direction: a version outside the readable
@@ -1042,14 +1110,17 @@ export function parseRunState(raw) {
1042
1110
  // fabricate the very history this file exists to make trustworthy — or comes
1043
1111
  // from a build ahead of this one, whose fields this build would ignore.
1044
1112
  // 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)`);
1113
+ const newer = typeof parsed.version === "number" && parsed.version > FLOW_RUN_STATE_VERSION;
1114
+ 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
1115
+ ? `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`
1116
+ : restart);
1046
1117
  }
1047
- const shape = checkShape(parsed);
1118
+ const shape = checkShape(parsed, restart);
1048
1119
  if (shape !== null)
1049
1120
  return { ok: false, failure: shape };
1050
1121
  const state = parsed;
1051
1122
  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'");
1123
+ return refuse("FLOW_RUN_TAMPERED", "el estado de corrida no coincide con su propio sello: fue editado fuera del CLI", restart);
1053
1124
  }
1054
1125
  return { ok: true, state };
1055
1126
  }
@@ -1069,10 +1140,15 @@ export function checkAgainstJourney(state, journey) {
1069
1140
  for (const [index, applied] of state.applied.entries()) {
1070
1141
  if (ids[index] === applied)
1071
1142
  continue;
1143
+ // The cause decides the repair, so it is named: a row this build no longer
1144
+ // has was renamed or retired, one it has elsewhere was reordered.
1145
+ const cause = ids.includes(applied)
1146
+ ? "el recorrido instalado la tiene en otra posición"
1147
+ : "el recorrido instalado ya no tiene esa frontera";
1072
1148
  return {
1073
1149
  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",
1150
+ message: `el estado dice haber aplicado '${applied}' donde el recorrido tiene '${ids[index] ?? "(nada)"}': ${cause}`,
1151
+ action: unusableRemedy(state.session),
1076
1152
  };
1077
1153
  }
1078
1154
  const next = ids[state.applied.length] ?? null;
@@ -1080,16 +1156,111 @@ export function checkAgainstJourney(state, journey) {
1080
1156
  return {
1081
1157
  code: "FLOW_RUN_AHEAD_OF_JOURNEY",
1082
1158
  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",
1159
+ action: unusableRemedy(state.session),
1084
1160
  };
1085
1161
  }
1086
1162
  return null;
1087
1163
  }
1088
- function checkShape(parsed) {
1164
+ /** The trace operation that names an alignment, so status and tests read one spelling. */
1165
+ export const JOURNEY_ALIGNMENT_OPERATION = "flow.journey-alignment";
1166
+ const ALIGNMENT_REASON = "la versión instalada agregó esta frontera en un tramo que la corrida ya había recorrido: entra omitida, sin pedirse";
1167
+ /**
1168
+ * Align a continuable run's cursor with the journey the installed build walks.
1169
+ *
1170
+ * The only difference this repairs is a boundary the installed build ADDED to
1171
+ * the base journey the run recorded (`journey_base`) and that sits before the
1172
+ * run's position. "Before the position" includes the rows ahead of the boundary
1173
+ * the run is standing on: that boundary is the one it was answering, and a
1174
+ * sealed or attempted action there must not be thrown away for a row nobody
1175
+ * asked it about. A row added AFTER the boundary is left alone and asked when
1176
+ * the run gets there.
1177
+ *
1178
+ * Every other difference comes back untouched, for {@link checkAgainstJourney}
1179
+ * to refuse with its cause: a row the recorded base already had and the cursor
1180
+ * lacks is a hole, not an addition; a reorder or a retirement moves an id that
1181
+ * keys attempts, counters, trace and approvals.
1182
+ *
1183
+ * `base` is the installed REGISTRY journey; `journey` is what the run walks
1184
+ * (plan-exec's expanded copies included). Pure: the aligned state is persisted
1185
+ * by the run's next write, never by a read.
1186
+ */
1187
+ export function alignToJourney(state, journey, base) {
1188
+ const recorded = state.journey_base;
1189
+ if (recorded === undefined || sameIds(recorded, base))
1190
+ return state;
1191
+ const addedIds = new Set(base.filter((id) => !recorded.includes(id)));
1192
+ const ids = journey.map((decision) => decision.id);
1193
+ const applied = [];
1194
+ const added = [];
1195
+ let cursor = 0;
1196
+ // Everything from `cursor` up to the next occurrence of `target` must be rows
1197
+ // the installed build added; only then are they inserted, in journey order.
1198
+ const insertUntil = (target) => {
1199
+ let next = cursor;
1200
+ while (next < ids.length && ids[next] !== target) {
1201
+ if (!addedIds.has(ids[next]))
1202
+ return false;
1203
+ next += 1;
1204
+ }
1205
+ if (next >= ids.length)
1206
+ return false;
1207
+ for (; cursor < next; cursor += 1) {
1208
+ added.push({ transition: ids[cursor], position: applied.length });
1209
+ applied.push(ids[cursor]);
1210
+ }
1211
+ return true;
1212
+ };
1213
+ for (const id of state.applied) {
1214
+ if (ids[cursor] !== id && !insertUntil(id))
1215
+ return state;
1216
+ applied.push(id);
1217
+ cursor += 1;
1218
+ }
1219
+ if (state.boundary !== null && ids[cursor] !== state.boundary && !insertUntil(state.boundary)) {
1220
+ return state;
1221
+ }
1222
+ const events = added.map((row) => {
1223
+ const iteration = segmentIterationAt(state.flow, applied, row.position);
1224
+ return {
1225
+ kind: "aligned",
1226
+ transition: row.transition,
1227
+ ...(iteration === null ? {} : { batch_iteration: iteration }),
1228
+ operation: JOURNEY_ALIGNMENT_OPERATION,
1229
+ position: row.position,
1230
+ reason: ALIGNMENT_REASON,
1231
+ };
1232
+ });
1233
+ return sealRunState({
1234
+ ...withoutSeal(state),
1235
+ journey_base: [...base],
1236
+ applied,
1237
+ skipped: [...state.skipped, ...added.map((row) => row.transition)],
1238
+ events: [...state.events, ...events],
1239
+ });
1240
+ }
1241
+ function sameIds(left, right) {
1242
+ return left.length === right.length && left.every((id, index) => id === right[index]);
1243
+ }
1244
+ /**
1245
+ * Which plan-exec batch copy a cursor position falls inside, or null outside one.
1246
+ *
1247
+ * Two copies of the same added row differ only by the batch they belong to, so
1248
+ * the trace names it the way every other per-batch event does.
1249
+ */
1250
+ function segmentIterationAt(flow, applied, position) {
1251
+ if (flow !== "plan-exec")
1252
+ return null;
1253
+ const before = applied.slice(0, position);
1254
+ const opened = before.lastIndexOf("plan-exec.batch-eligibility-signal");
1255
+ if (opened < 0 || before.lastIndexOf("plan-exec.batch-close") > opened)
1256
+ return null;
1257
+ return before.filter((id) => id === "plan-exec.batch-eligibility-signal").length;
1258
+ }
1259
+ function checkShape(parsed, remedy) {
1089
1260
  const invalid = (why) => ({
1090
1261
  code: "FLOW_RUN_INVALID",
1091
1262
  message: `el estado de corrida ${why}`,
1092
- action: "restauralo o re-adoptá la sesión con 'aw flow advance --adopt'",
1263
+ action: remedy,
1093
1264
  });
1094
1265
  if (!WORKLINE_FLOWS.includes(parsed.flow)) {
1095
1266
  return invalid(`declara un flow desconocido: ${String(parsed.flow)}`);
@@ -1125,16 +1296,23 @@ function checkRecordShape(parsed, invalid) {
1125
1296
  const common = checkCommonRecordShape(parsed, invalid);
1126
1297
  if (common !== null)
1127
1298
  return common;
1128
- if (parsed.version === FLOW_RUN_STATE_VERSION || parsed.version === 10) {
1299
+ const version = parsed.version;
1300
+ if (version >= 10) {
1129
1301
  const v10 = checkV10RecordShape(parsed, invalid);
1130
1302
  if (v10 !== null)
1131
1303
  return v10;
1132
1304
  }
1133
- if (parsed.version === FLOW_RUN_STATE_VERSION) {
1305
+ if (version >= 11) {
1134
1306
  const v11 = checkV11RecordShape(parsed, invalid);
1135
1307
  if (v11 !== null)
1136
1308
  return v11;
1137
1309
  }
1310
+ if (version >= 12 && !isStringArray(parsed.journey_base)) {
1311
+ return invalid("no dice contra qué recorrido se escribió su cursor");
1312
+ }
1313
+ if (parsed.exhausted_reruns !== undefined && !isStringArray(parsed.exhausted_reruns)) {
1314
+ return invalid("declara vueltas de fronteras agotadas que no son transiciones");
1315
+ }
1138
1316
  if (typeof parsed.digest !== "string")
1139
1317
  return invalid("no trae su sello");
1140
1318
  return null;
@@ -1599,7 +1777,8 @@ function isDecisionPreview(value, baseline) {
1599
1777
  return value.effects.entries.every((entry) => isRecord(entry) &&
1600
1778
  typeof entry.path === "string" &&
1601
1779
  Number.isInteger(entry.bytes) &&
1602
- typeof entry.overwrite === "boolean");
1780
+ typeof entry.overwrite === "boolean" &&
1781
+ isReservedMark(entry.reserved));
1603
1782
  }
1604
1783
  /**
1605
1784
  * The persisted preview's obligations, in EITHER form.
@@ -1634,7 +1813,12 @@ function isProposalArtifacts(value) {
1634
1813
  return value.every((entry) => isRecord(entry) &&
1635
1814
  typeof entry.path === "string" &&
1636
1815
  typeof entry.content === "string" &&
1637
- typeof entry.overwrite === "boolean");
1816
+ typeof entry.overwrite === "boolean" &&
1817
+ isReservedMark(entry.reserved));
1818
+ }
1819
+ /** The optional reservation mark: absent, or exactly `true`. */
1820
+ function isReservedMark(value) {
1821
+ return value === undefined || value === true;
1638
1822
  }
1639
1823
  function isProposalBases(value) {
1640
1824
  if (!Array.isArray(value))
@@ -1682,24 +1866,35 @@ function isEvent(entry) {
1682
1866
  (!Number.isInteger(entry.batch_iteration) || entry.batch_iteration < 1)) {
1683
1867
  return false;
1684
1868
  }
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;
1869
+ return EVENT_BODIES.get(entry.kind)?.(entry) === true;
1702
1870
  }
1871
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
1872
+ /**
1873
+ * What each trace kind demands beyond the common fields; an unknown kind has no
1874
+ * entry. A Map, not an object: a kind spelled like an Object.prototype member
1875
+ * must not find a function nobody declared.
1876
+ */
1877
+ const EVENT_BODIES = new Map(Object.entries({
1878
+ executed: (entry) => typeof entry.summary === "string" &&
1879
+ typeof entry.output_digest === "string" &&
1880
+ isEffectClassArray(entry.effects) &&
1881
+ isStringArray(entry.evidence),
1882
+ reconciled: (entry) => isRepairArray(entry.repairs),
1883
+ restarted: (entry) => nonEmpty(entry.archive) && nonEmpty(entry.cause),
1884
+ annulled: (entry) => isStringArray(entry.batches) &&
1885
+ entry.batches.length > 0 &&
1886
+ Array.isArray(entry.phases) &&
1887
+ entry.phases.every((phase) => Number.isInteger(phase) && phase > 0) &&
1888
+ isStringArray(entry.tasks) &&
1889
+ nonEmpty(entry.digest),
1890
+ aligned: (entry) => Number.isInteger(entry.position) && entry.position >= 0 && nonEmpty(entry.reason),
1891
+ failed: (entry) => typeof entry.code === "string" &&
1892
+ typeof entry.message === "string" &&
1893
+ typeof entry.recovery === "string" &&
1894
+ // ABSENT-or-well-formed, never defaulted: a version 7 trace has no such
1895
+ // field, and the guard that reads it treats its absence as the refusal.
1896
+ (entry.effects === undefined || isEffectClassArray(entry.effects)),
1897
+ }));
1703
1898
  /** A repair is well-formed when its rule is one of the three and its values are numbers. */
1704
1899
  function isRepairArray(value) {
1705
1900
  if (!Array.isArray(value))