loopforge 3.9.2 → 4.0.7

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 (285) hide show
  1. package/README.md +236 -112
  2. package/dist/audit.d.ts +73 -15
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +131 -61
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +152 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +382 -206
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +41 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +0 -2
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +0 -4
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/committed-round.d.ts +105 -141
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +249 -372
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/constraint-lifecycle.d.ts +73 -0
  22. package/dist/constraint-lifecycle.d.ts.map +1 -0
  23. package/dist/constraint-lifecycle.js +174 -0
  24. package/dist/constraint-lifecycle.js.map +1 -0
  25. package/dist/context-boundary.d.ts +83 -0
  26. package/dist/context-boundary.d.ts.map +1 -0
  27. package/dist/context-boundary.js +101 -0
  28. package/dist/context-boundary.js.map +1 -0
  29. package/dist/contract-items.d.ts +12 -8
  30. package/dist/contract-items.d.ts.map +1 -1
  31. package/dist/contract-items.js +31 -26
  32. package/dist/contract-items.js.map +1 -1
  33. package/dist/contract-requirement.d.ts +47 -0
  34. package/dist/contract-requirement.d.ts.map +1 -0
  35. package/dist/contract-requirement.js +51 -0
  36. package/dist/contract-requirement.js.map +1 -0
  37. package/dist/criterion-status.d.ts +35 -0
  38. package/dist/criterion-status.d.ts.map +1 -0
  39. package/dist/criterion-status.js +89 -0
  40. package/dist/criterion-status.js.map +1 -0
  41. package/dist/enforcement-gate.d.ts +32 -67
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +203 -439
  44. package/dist/enforcement-gate.js.map +1 -1
  45. package/dist/engine.d.ts +9 -103
  46. package/dist/engine.d.ts.map +1 -1
  47. package/dist/engine.js +28 -887
  48. package/dist/engine.js.map +1 -1
  49. package/dist/errors.d.ts +41 -0
  50. package/dist/errors.d.ts.map +1 -0
  51. package/dist/errors.js +69 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/evidence-claims.d.ts +33 -31
  54. package/dist/evidence-claims.d.ts.map +1 -1
  55. package/dist/evidence-claims.js +58 -72
  56. package/dist/evidence-claims.js.map +1 -1
  57. package/dist/evidence-posture-view.d.ts +19 -0
  58. package/dist/evidence-posture-view.d.ts.map +1 -0
  59. package/dist/evidence-posture-view.js +116 -0
  60. package/dist/evidence-posture-view.js.map +1 -0
  61. package/dist/evidence-posture.d.ts +73 -0
  62. package/dist/evidence-posture.d.ts.map +1 -0
  63. package/dist/evidence-posture.js +281 -0
  64. package/dist/evidence-posture.js.map +1 -0
  65. package/dist/evidence-provider.d.ts +55 -37
  66. package/dist/evidence-provider.d.ts.map +1 -1
  67. package/dist/evidence-provider.js +182 -332
  68. package/dist/evidence-provider.js.map +1 -1
  69. package/dist/explain.d.ts +167 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +373 -58
  72. package/dist/explain.js.map +1 -1
  73. package/dist/field-accounting.d.ts.map +1 -1
  74. package/dist/field-accounting.js +3 -22
  75. package/dist/field-accounting.js.map +1 -1
  76. package/dist/generate-schema.js +13 -8
  77. package/dist/generate-schema.js.map +1 -1
  78. package/dist/git-boundary.d.ts +128 -0
  79. package/dist/git-boundary.d.ts.map +1 -0
  80. package/dist/git-boundary.js +625 -0
  81. package/dist/git-boundary.js.map +1 -0
  82. package/dist/git-facts.d.ts +22 -0
  83. package/dist/git-facts.d.ts.map +1 -0
  84. package/dist/git-facts.js +29 -0
  85. package/dist/git-facts.js.map +1 -0
  86. package/dist/git-motion.d.ts +37 -0
  87. package/dist/git-motion.d.ts.map +1 -0
  88. package/dist/git-motion.js +59 -0
  89. package/dist/git-motion.js.map +1 -0
  90. package/dist/history.d.ts +44 -0
  91. package/dist/history.d.ts.map +1 -0
  92. package/dist/history.js +77 -0
  93. package/dist/history.js.map +1 -0
  94. package/dist/index.d.ts +19 -9
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -4
  97. package/dist/index.js.map +1 -1
  98. package/dist/init.d.ts +16 -0
  99. package/dist/init.d.ts.map +1 -1
  100. package/dist/init.js +80 -14
  101. package/dist/init.js.map +1 -1
  102. package/dist/loop-compiler.d.ts +51 -19
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +423 -507
  105. package/dist/loop-compiler.js.map +1 -1
  106. package/dist/loop-store.d.ts +6 -5
  107. package/dist/loop-store.d.ts.map +1 -1
  108. package/dist/loop-store.js +153 -35
  109. package/dist/loop-store.js.map +1 -1
  110. package/dist/mcp/round-lifecycle.d.ts +285 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1629 -574
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts +5 -0
  115. package/dist/mcp/server.d.ts.map +1 -1
  116. package/dist/mcp/server.js +61 -19
  117. package/dist/mcp/server.js.map +1 -1
  118. package/dist/mcp/session.d.ts +81 -39
  119. package/dist/mcp/session.d.ts.map +1 -1
  120. package/dist/mcp/session.js +963 -271
  121. package/dist/mcp/session.js.map +1 -1
  122. package/dist/mcp/submission-boundary.d.ts +23 -31
  123. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  124. package/dist/mcp/submission-boundary.js +242 -82
  125. package/dist/mcp/submission-boundary.js.map +1 -1
  126. package/dist/mcp/submission-handle.d.ts +66 -0
  127. package/dist/mcp/submission-handle.d.ts.map +1 -0
  128. package/dist/mcp/submission-handle.js +173 -0
  129. package/dist/mcp/submission-handle.js.map +1 -0
  130. package/dist/mcp/tools.d.ts +17 -7
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +763 -244
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/open-question.d.ts +7 -0
  135. package/dist/open-question.d.ts.map +1 -0
  136. package/dist/open-question.js +14 -0
  137. package/dist/open-question.js.map +1 -0
  138. package/dist/policy-metrics.d.ts +31 -21
  139. package/dist/policy-metrics.d.ts.map +1 -1
  140. package/dist/policy-metrics.js +117 -64
  141. package/dist/policy-metrics.js.map +1 -1
  142. package/dist/policy.d.ts +15 -40
  143. package/dist/policy.d.ts.map +1 -1
  144. package/dist/policy.js +13 -13
  145. package/dist/policy.js.map +1 -1
  146. package/dist/progress.d.ts +45 -0
  147. package/dist/progress.d.ts.map +1 -0
  148. package/dist/progress.js +136 -0
  149. package/dist/progress.js.map +1 -0
  150. package/dist/prompt-assembler.d.ts +28 -9
  151. package/dist/prompt-assembler.d.ts.map +1 -1
  152. package/dist/prompt-assembler.js +592 -603
  153. package/dist/prompt-assembler.js.map +1 -1
  154. package/dist/prompt-coverage.d.ts +39 -0
  155. package/dist/prompt-coverage.d.ts.map +1 -0
  156. package/dist/prompt-coverage.js +80 -0
  157. package/dist/prompt-coverage.js.map +1 -0
  158. package/dist/prompt-freshness.d.ts +25 -0
  159. package/dist/prompt-freshness.d.ts.map +1 -0
  160. package/dist/prompt-freshness.js +88 -0
  161. package/dist/prompt-freshness.js.map +1 -0
  162. package/dist/prompt-policy.d.ts +14 -4
  163. package/dist/prompt-policy.d.ts.map +1 -1
  164. package/dist/prompt-policy.js +29 -10
  165. package/dist/prompt-policy.js.map +1 -1
  166. package/dist/protocol.d.ts +875 -419
  167. package/dist/protocol.d.ts.map +1 -1
  168. package/dist/protocol.js +73 -99
  169. package/dist/protocol.js.map +1 -1
  170. package/dist/recovery-guidance.d.ts +13 -0
  171. package/dist/recovery-guidance.d.ts.map +1 -0
  172. package/dist/recovery-guidance.js +15 -0
  173. package/dist/recovery-guidance.js.map +1 -0
  174. package/dist/recovery.d.ts +68 -0
  175. package/dist/recovery.d.ts.map +1 -0
  176. package/dist/recovery.js +234 -0
  177. package/dist/recovery.js.map +1 -0
  178. package/dist/replay.d.ts +42 -8
  179. package/dist/replay.d.ts.map +1 -1
  180. package/dist/replay.js +311 -84
  181. package/dist/replay.js.map +1 -1
  182. package/dist/result-facts.d.ts +45 -0
  183. package/dist/result-facts.d.ts.map +1 -0
  184. package/dist/result-facts.js +86 -0
  185. package/dist/result-facts.js.map +1 -0
  186. package/dist/round-contract.d.ts +38 -8
  187. package/dist/round-contract.d.ts.map +1 -1
  188. package/dist/round-contract.js +71 -31
  189. package/dist/round-contract.js.map +1 -1
  190. package/dist/round-coordinator.d.ts +45 -91
  191. package/dist/round-coordinator.d.ts.map +1 -1
  192. package/dist/round-coordinator.js +254 -271
  193. package/dist/round-coordinator.js.map +1 -1
  194. package/dist/round-driver.d.ts +56 -16
  195. package/dist/round-driver.d.ts.map +1 -1
  196. package/dist/round-driver.js +225 -39
  197. package/dist/round-driver.js.map +1 -1
  198. package/dist/round-facts.d.ts +33 -52
  199. package/dist/round-facts.d.ts.map +1 -1
  200. package/dist/round-facts.js +54 -157
  201. package/dist/round-facts.js.map +1 -1
  202. package/dist/round-guidance.d.ts +56 -0
  203. package/dist/round-guidance.d.ts.map +1 -0
  204. package/dist/round-guidance.js +161 -0
  205. package/dist/round-guidance.js.map +1 -0
  206. package/dist/round-outcome.d.ts +21 -0
  207. package/dist/round-outcome.d.ts.map +1 -0
  208. package/dist/round-outcome.js +31 -0
  209. package/dist/round-outcome.js.map +1 -0
  210. package/dist/round-transaction.d.ts +168 -21
  211. package/dist/round-transaction.d.ts.map +1 -1
  212. package/dist/round-transaction.js +856 -39
  213. package/dist/round-transaction.js.map +1 -1
  214. package/dist/self-eval.d.ts +40 -60
  215. package/dist/self-eval.d.ts.map +1 -1
  216. package/dist/self-eval.js +455 -384
  217. package/dist/self-eval.js.map +1 -1
  218. package/dist/storage.d.ts +16 -0
  219. package/dist/storage.d.ts.map +1 -1
  220. package/dist/storage.js +54 -4
  221. package/dist/storage.js.map +1 -1
  222. package/dist/subgoal-state.d.ts +2 -3
  223. package/dist/subgoal-state.d.ts.map +1 -1
  224. package/dist/subgoal-state.js +2 -3
  225. package/dist/subgoal-state.js.map +1 -1
  226. package/dist/submission-guidance.d.ts +43 -21
  227. package/dist/submission-guidance.d.ts.map +1 -1
  228. package/dist/submission-guidance.js +368 -262
  229. package/dist/submission-guidance.js.map +1 -1
  230. package/dist/submission-receipt.d.ts +16 -8
  231. package/dist/submission-receipt.d.ts.map +1 -1
  232. package/dist/submission-receipt.js +146 -51
  233. package/dist/submission-receipt.js.map +1 -1
  234. package/dist/token-utils.d.ts +47 -7
  235. package/dist/token-utils.d.ts.map +1 -1
  236. package/dist/token-utils.js +66 -15
  237. package/dist/token-utils.js.map +1 -1
  238. package/dist/transaction-identity.d.ts +9 -0
  239. package/dist/transaction-identity.d.ts.map +1 -0
  240. package/dist/transaction-identity.js +69 -0
  241. package/dist/transaction-identity.js.map +1 -0
  242. package/dist/verification-actions.d.ts +13 -0
  243. package/dist/verification-actions.d.ts.map +1 -0
  244. package/dist/verification-actions.js +44 -0
  245. package/dist/verification-actions.js.map +1 -0
  246. package/dist/verification-gate.d.ts +26 -46
  247. package/dist/verification-gate.d.ts.map +1 -1
  248. package/dist/verification-gate.js +218 -427
  249. package/dist/verification-gate.js.map +1 -1
  250. package/dist/version.d.ts +1 -1
  251. package/dist/version.js +1 -1
  252. package/dist/working-memory.d.ts +187 -0
  253. package/dist/working-memory.d.ts.map +1 -0
  254. package/dist/working-memory.js +767 -0
  255. package/dist/working-memory.js.map +1 -0
  256. package/dist/workspace-checkpoint.d.ts +88 -0
  257. package/dist/workspace-checkpoint.d.ts.map +1 -0
  258. package/dist/workspace-checkpoint.js +326 -0
  259. package/dist/workspace-checkpoint.js.map +1 -0
  260. package/dist/workspace-restore.d.ts +68 -0
  261. package/dist/workspace-restore.d.ts.map +1 -0
  262. package/dist/workspace-restore.js +604 -0
  263. package/dist/workspace-restore.js.map +1 -0
  264. package/dist/workspace.d.ts +0 -9
  265. package/dist/workspace.d.ts.map +1 -1
  266. package/dist/workspace.js +0 -17
  267. package/dist/workspace.js.map +1 -1
  268. package/loop_policy.json +5 -19
  269. package/package.json +2 -2
  270. package/skills/forge/SKILL.md +212 -0
  271. package/skills/forge/references/evidence-and-contracts.md +204 -0
  272. package/skills/forge/references/recovery-and-views.md +188 -0
  273. package/dist/cognitive-facts.d.ts +0 -30
  274. package/dist/cognitive-facts.d.ts.map +0 -1
  275. package/dist/cognitive-facts.js +0 -113
  276. package/dist/cognitive-facts.js.map +0 -1
  277. package/dist/loop-extras-parser.d.ts +0 -81
  278. package/dist/loop-extras-parser.d.ts.map +0 -1
  279. package/dist/loop-extras-parser.js +0 -213
  280. package/dist/loop-extras-parser.js.map +0 -1
  281. package/dist/loop-projection.d.ts +0 -6
  282. package/dist/loop-projection.d.ts.map +0 -1
  283. package/dist/loop-projection.js +0 -28
  284. package/dist/loop-projection.js.map +0 -1
  285. package/skills/perception/SKILL.md +0 -370
@@ -5,17 +5,64 @@
5
5
  * feedback storage. The committed feedback embeds the decision so replay after
6
6
  * a process crash is idempotent.
7
7
  */
8
- import { deriveObservationDeltas } from "./evidence-provider.js";
8
+ import { createHash } from "node:crypto";
9
+ import { deriveObservationDeltas, observationVersionIssue } from "./evidence-provider.js";
10
+ import { EVIDENCE_POSTURE_CAPS, rollbackEvidencePosture, } from "./evidence-posture.js";
9
11
  import { RoundCoordinator, } from "./round-coordinator.js";
10
12
  import { logEvent } from "./observability.js";
11
13
  import { deriveContractId, deriveContractItemIds, isRecord } from "./token-utils.js";
12
- import { GIT_OBSERVATION_SCHEMA_VERSION, PROMPT_ARTIFACT_SCHEMA_VERSION, } from "./protocol.js";
14
+ import { PROMPT_ARTIFACT_SCHEMA_VERSION, isPromptContextMode, isPromptContextReason, } from "./protocol.js";
15
+ import { promptContextPairIssue } from "./context-boundary.js";
13
16
  import { commandConfigHash, getPolicy } from "./policy.js";
14
17
  import { policyMetrics } from "./policy-metrics.js";
15
18
  import { VaultRoundCommitStore } from "./storage.js";
16
- /** v3.9.1: schema 3 embeds Git observation schema 2 (complete boundary
17
- * fingerprints) and later adds optional diagnostics/manifest fields. */
18
- export const ROUND_TRANSACTION_SCHEMA_VERSION = 3;
19
+ import { transactionIdentityIssue } from "./transaction-identity.js";
20
+ /** v3.9.7: schema 5 adds the transaction before-image, explicit mid-round
21
+ * checkpoints and the redo restore marker. v4.0.3: schema 7 requires the
22
+ * persisted `EvidencePosture` on every result and adds the transient
23
+ * `RetryContext`. v4.0.4: schema 8 embeds the schema-10 PromptArtifact
24
+ * (`fresh_invocation`) and the renamed `SelfEvaluation.agent_stop_reason`.
25
+ * v4.0.6: schema 9 is a BREAKING shape break — the committed evaluation
26
+ * carries a required `outcome`, one `execution_report.claims` channel
27
+ * (`cr-`/`rci-`) and `round_contract.supersedes`; `success`,
28
+ * `agent_stop_reason`, `contract_transition`, `tests_reported`,
29
+ * `criterion_claims` and `contract_item_claims` are gone. Every earlier
30
+ * version is reported as legacy by `legacyTransactionRounds`; there is no
31
+ * compatibility migration. */
32
+ export const ROUND_TRANSACTION_SCHEMA_VERSION = 9;
33
+ /** Maximum distinct mid-round checkpoints one transaction may reference. */
34
+ export const MAX_MID_CHECKPOINTS_PER_TRANSACTION = 32;
35
+ /** The root branch identity for every loop. */
36
+ export function rootBranchId(loopId) {
37
+ return `loop:${loopId}:branch:root`;
38
+ }
39
+ /** v3.9.6: deterministic transaction identity. The logical round identity is
40
+ * still `roundId`; this id prevents a deep redo from replaying an abandoned
41
+ * commit at the same numeric round. */
42
+ export function deriveTransactionId(input) {
43
+ const payload = [
44
+ input.kind,
45
+ input.branchId,
46
+ input.roundId,
47
+ String(input.attempt),
48
+ input.promptHash ?? "",
49
+ input.kind === "rollback" && input.recovery
50
+ ? [
51
+ input.recovery.target.checkpoint_id,
52
+ input.recovery.target.content_hash,
53
+ String(input.recovery.target.redo_round),
54
+ input.recovery.reason,
55
+ input.recovery.target.transaction_id,
56
+ (input.recovery.abandonedRounds ?? []).join(","),
57
+ (input.recovery.failedRounds ?? []).join(","),
58
+ [...(input.recovery.observedSkippedFiles ?? [])].sort().join(","),
59
+ [...(input.recovery.reportedSkippedFiles ?? [])].sort().join(","),
60
+ [...(input.recovery.triggerFindings ?? [])].sort().join(","),
61
+ ].join("\n")
62
+ : "",
63
+ ].join("\n");
64
+ return `tx-${createHash("sha256").update(payload).digest("hex").slice(0, 24)}`;
65
+ }
19
66
  /** v3.8: the closed set of machine-observation statuses. */
20
67
  const OBSERVATION_STATUSES = [
21
68
  "observed", "passed", "failed", "timeout", "unavailable", "error", "aborted",
@@ -64,16 +111,45 @@ export function makeRoundId(loopId, round) {
64
111
  }
65
112
  return `loop:${loopId}:round:${round}`;
66
113
  }
67
- export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArtifact) {
114
+ export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArtifact, opts = {}) {
68
115
  const now = Date.now();
116
+ const roundId = makeRoundId(loopId, round);
117
+ const branchId = opts.branchId ?? rootBranchId(loopId);
118
+ const kind = opts.kind ?? "evaluation";
119
+ const observedGit = beforeEvidence.some((observation) => observation.kind === "git" && observation.status === "observed");
120
+ // A direct library caller may have no checkpoint store attached. The
121
+ // schema-5 invariant still records the explicit machine fact.
122
+ const beforeCheckpointError = opts.beforeCheckpointError ??
123
+ (observedGit && !opts.beforeCheckpoint
124
+ ? {
125
+ code: "store_unavailable",
126
+ detail: "no workspace checkpoint store was attached to this prepare",
127
+ at: now,
128
+ }
129
+ : undefined);
69
130
  return {
70
131
  schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
71
- roundId: makeRoundId(loopId, round),
132
+ branchId,
133
+ transactionId: deriveTransactionId({
134
+ kind,
135
+ branchId,
136
+ roundId,
137
+ attempt: 1,
138
+ promptHash: promptArtifact?.promptHash,
139
+ }),
140
+ kind,
141
+ roundId,
72
142
  loopId,
73
143
  round,
74
144
  attempt: 1,
75
145
  phase: promptArtifact ? "prompted" : "prepared",
76
146
  beforeEvidence,
147
+ ...(opts.beforeCheckpoint ? { beforeCheckpoint: opts.beforeCheckpoint } : {}),
148
+ ...(beforeCheckpointError ? { beforeCheckpointError } : {}),
149
+ ...(opts.operatorAcceptance
150
+ ? { operatorAcceptance: opts.operatorAcceptance }
151
+ : {}),
152
+ ...(opts.compiled ? { compiled: opts.compiled } : {}),
77
153
  createdAt: now,
78
154
  updatedAt: now,
79
155
  promptArtifact,
@@ -82,7 +158,7 @@ export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArt
82
158
  /** Attach the next prompt attempt to a rejected logical round without changing
83
159
  * its identity or evidence baseline. Evaluation fields belong to the previous
84
160
  * attempt and are cleared before the Agent receives the retry prompt. */
85
- export function prepareRejectedAttempt(rejected, promptArtifact) {
161
+ export function prepareRejectedAttempt(rejected, promptArtifact, compiled, operatorAcceptance) {
86
162
  if (rejected.phase !== "rejected") {
87
163
  throw new Error(`Cannot retry transaction in phase ${rejected.phase}`);
88
164
  }
@@ -92,13 +168,49 @@ export function prepareRejectedAttempt(rejected, promptArtifact) {
92
168
  if (promptArtifact.attempt !== rejected.attempt + 1) {
93
169
  throw new Error(`Retry prompt attempt mismatch: ${promptArtifact.attempt} !== ${rejected.attempt + 1}`);
94
170
  }
171
+ if (rejected.retryContext) {
172
+ if (rejected.retryContext.rejectedAttempt !== rejected.attempt ||
173
+ rejected.retryContext.attempt !== promptArtifact.attempt) {
174
+ throw new Error("Retry context identity does not match the rejected attempt");
175
+ }
176
+ }
177
+ const transactionId = deriveTransactionId({
178
+ kind: rejected.kind,
179
+ branchId: rejected.branchId,
180
+ roundId: rejected.roundId,
181
+ attempt: promptArtifact.attempt,
182
+ promptHash: promptArtifact.promptHash,
183
+ });
184
+ // The round-start before-image is the SAME workspace state across attempts;
185
+ // only its owning transaction identity changes. Mid-round savepoints belong
186
+ // to the rejected attempt and are not carried into the retry.
187
+ const beforeCheckpoint = rejected.beforeCheckpoint
188
+ ? { ...rejected.beforeCheckpoint, transactionId }
189
+ : undefined;
95
190
  return {
96
191
  ...rejected,
192
+ transactionId,
193
+ ...(beforeCheckpoint ? { beforeCheckpoint } : {}),
194
+ ...(rejected.beforeCheckpointError
195
+ ? { beforeCheckpointError: rejected.beforeCheckpointError }
196
+ : {}),
197
+ ...(rejected.restore ? { restore: rejected.restore } : {}),
198
+ ...(compiled !== undefined
199
+ ? { compiled }
200
+ : rejected.compiled
201
+ ? { compiled: rejected.compiled }
202
+ : {}),
203
+ ...(operatorAcceptance !== undefined
204
+ ? { operatorAcceptance }
205
+ : rejected.operatorAcceptance
206
+ ? { operatorAcceptance: rejected.operatorAcceptance }
207
+ : {}),
97
208
  attempt: promptArtifact.attempt,
98
209
  phase: "prompted",
99
210
  afterEvidence: undefined,
100
211
  evaluation: undefined,
101
212
  result: undefined,
213
+ checkpoints: undefined,
102
214
  // The rejected attempt's diagnostics were returned in its receipt; the
103
215
  // retry has not submitted a new payload yet.
104
216
  normalizationDiagnostics: undefined,
@@ -107,6 +219,82 @@ export function prepareRejectedAttempt(rejected, promptArtifact) {
107
219
  updatedAt: Date.now(),
108
220
  };
109
221
  }
222
+ /** v4.0.5: re-bind a held RETRY prompt to a newly compiled artifact for the
223
+ * SAME logical round and attempt. This is not a new attempt: attempt, round,
224
+ * before evidence, retry facts, restore marker and branch stay byte-identical;
225
+ * only the prompt-derived identity (artifact, prompt hash, transaction id and
226
+ * the owning ids on checkpoints) changes.
227
+ *
228
+ * Context preservation is enforced here, not by convention:
229
+ * - the artifact identity must equal the held identity;
230
+ * - an invocation-recovery recompile must be
231
+ * `fresh_invocation / invocation_recovery`;
232
+ * - every other recompile must keep the held mode and reason exactly.
233
+ *
234
+ * Evaluation/result fields of the rejected attempt are cleared before the
235
+ * Agent receives the new prompt, exactly like `prepareRejectedAttempt`. */
236
+ export function rebindHeldPrompt(held, artifact, compiled, operatorAcceptance) {
237
+ if (held.phase !== "prompted" || !held.promptArtifact) {
238
+ throw new Error(`Cannot recompile a prompt in phase ${held.phase}`);
239
+ }
240
+ if (artifact.roundId !== held.roundId) {
241
+ throw new Error(`Held prompt identity mismatch: ${artifact.roundId} !== ${held.roundId}`);
242
+ }
243
+ if (artifact.round !== held.round || artifact.attempt !== held.attempt) {
244
+ throw new Error("Held prompt round/attempt mismatch: a recompile must keep the same " +
245
+ `round/attempt (${artifact.round}/${artifact.attempt} !== ` +
246
+ `${held.round}/${held.attempt})`);
247
+ }
248
+ const previous = held.promptArtifact;
249
+ if (artifact.contextReason === "invocation_recovery") {
250
+ if (artifact.contextMode !== "fresh_invocation") {
251
+ throw new Error("invocation_recovery requires a fresh_invocation recompile");
252
+ }
253
+ }
254
+ else if (artifact.contextMode !== previous.contextMode ||
255
+ artifact.contextReason !== previous.contextReason) {
256
+ throw new Error("A held-prompt recompile must preserve the context boundary: " +
257
+ `${previous.contextMode}/${previous.contextReason} !== ` +
258
+ `${artifact.contextMode}/${artifact.contextReason}`);
259
+ }
260
+ const transactionId = deriveTransactionId({
261
+ kind: held.kind,
262
+ branchId: held.branchId,
263
+ roundId: held.roundId,
264
+ attempt: artifact.attempt,
265
+ promptHash: artifact.promptHash,
266
+ });
267
+ const beforeCheckpoint = held.beforeCheckpoint
268
+ ? { ...held.beforeCheckpoint, transactionId }
269
+ : undefined;
270
+ const checkpoints = held.checkpoints
271
+ ? held.checkpoints.map((item) => ({ ...item, transactionId }))
272
+ : undefined;
273
+ return {
274
+ ...held,
275
+ transactionId,
276
+ ...(beforeCheckpoint ? { beforeCheckpoint } : {}),
277
+ ...(checkpoints ? { checkpoints } : {}),
278
+ ...(compiled !== undefined
279
+ ? { compiled }
280
+ : held.compiled
281
+ ? { compiled: held.compiled }
282
+ : {}),
283
+ ...(operatorAcceptance !== undefined
284
+ ? { operatorAcceptance }
285
+ : held.operatorAcceptance
286
+ ? { operatorAcceptance: held.operatorAcceptance }
287
+ : {}),
288
+ phase: "prompted",
289
+ afterEvidence: undefined,
290
+ evaluation: undefined,
291
+ result: undefined,
292
+ normalizationDiagnostics: undefined,
293
+ submittedFields: undefined,
294
+ promptArtifact: artifact,
295
+ updatedAt: Date.now(),
296
+ };
297
+ }
110
298
  /** v3.8: Shape check for a persisted contract binding. */
111
299
  function isContractBinding(value) {
112
300
  if (!isRecord(value))
@@ -117,10 +305,309 @@ function isContractBinding(value) {
117
305
  return false;
118
306
  return isRecord(value.config_hash_by_command);
119
307
  }
308
+ const EVIDENCE_POSTURE_STATUSES = [
309
+ "verified", "insufficient", "contradicted", "unknown", "not_applicable",
310
+ ];
311
+ const CONTRACT_ITEM_STATUSES = [
312
+ "pending", "insufficient", "contradicted", "verified",
313
+ ];
314
+ const CONTRACT_CLOSURES = [
315
+ "open", "verified", "blocked", "superseded",
316
+ ];
317
+ const FILE_RECONCILIATIONS = [
318
+ "match", "mismatch", "unknown", "not_applicable",
319
+ ];
320
+ function isNonNegativeInteger(value) {
321
+ return Number.isInteger(value) && value >= 0;
322
+ }
323
+ function isVerificationFlag(value) {
324
+ if (!isRecord(value))
325
+ return false;
326
+ // `severity` and `check` are the structural facts every consumer keys on.
327
+ // `field`/`detail` are present on every runtime-generated flag but are
328
+ // optional here so an older committed flag shape does not make the whole
329
+ // round unreadable.
330
+ return (value.severity === "info" || value.severity === "warn" ||
331
+ value.severity === "error") &&
332
+ typeof value.check === "string" &&
333
+ (value.field === undefined || typeof value.field === "string") &&
334
+ (value.detail === undefined || typeof value.detail === "string") &&
335
+ (value.ref === undefined || typeof value.ref === "string");
336
+ }
337
+ /** v4.0.3: schema 7 structural validation of the persisted evidence posture.
338
+ * A committed round without a well-formed posture is a HARD break — it is not
339
+ * committed history and is reported as legacy, never silently re-derived. */
340
+ export function isEvidencePosture(value) {
341
+ if (!isRecord(value))
342
+ return false;
343
+ if (!Number.isInteger(value.round) || value.round < 1)
344
+ return false;
345
+ if (value.kind !== "evaluation" && value.kind !== "rollback")
346
+ return false;
347
+ if (!EVIDENCE_POSTURE_STATUSES.includes(String(value.status))) {
348
+ return false;
349
+ }
350
+ if (typeof value.machineBackedSuccess !== "boolean" ||
351
+ typeof value.verificationConfigured !== "boolean")
352
+ return false;
353
+ const workspace = value.workspace;
354
+ if (!isRecord(workspace) ||
355
+ (workspace.status !== "observed" && workspace.status !== "unknown" &&
356
+ workspace.status !== "not_applicable") ||
357
+ !isNonNegativeInteger(workspace.changedPathCount))
358
+ return false;
359
+ if (value.files !== null) {
360
+ const files = value.files;
361
+ if (!isRecord(files) ||
362
+ !FILE_RECONCILIATIONS.includes(String(files.reconciliation)) ||
363
+ !isNonNegativeInteger(files.reported) ||
364
+ !isNonNegativeInteger(files.observed))
365
+ return false;
366
+ }
367
+ if (!Array.isArray(value.providers) ||
368
+ value.providers.length > EVIDENCE_POSTURE_CAPS.providers)
369
+ return false;
370
+ for (const provider of value.providers) {
371
+ if (!isRecord(provider) ||
372
+ typeof provider.providerId !== "string" ||
373
+ (provider.kind !== "git" && provider.kind !== "custom") ||
374
+ !OBSERVATION_STATUSES.includes(String(provider.status))) {
375
+ return false;
376
+ }
377
+ }
378
+ if (!Array.isArray(value.commands) ||
379
+ value.commands.length > EVIDENCE_POSTURE_CAPS.commands)
380
+ return false;
381
+ for (const command of value.commands) {
382
+ if (!isRecord(command) ||
383
+ typeof command.commandId !== "string" ||
384
+ !OBSERVATION_STATUSES.includes(String(command.status)) ||
385
+ typeof command.required !== "boolean")
386
+ return false;
387
+ }
388
+ if (value.contract !== null) {
389
+ const contract = value.contract;
390
+ if (!isRecord(contract) ||
391
+ typeof contract.id !== "string" ||
392
+ contract.id.length === 0 ||
393
+ !CONTRACT_CLOSURES.includes(String(contract.closure)) ||
394
+ !isNonNegativeInteger(contract.verifiedCount) ||
395
+ !isNonNegativeInteger(contract.insufficientCount) ||
396
+ !isNonNegativeInteger(contract.contradictedCount) ||
397
+ !isNonNegativeInteger(contract.pendingCount) ||
398
+ !Array.isArray(contract.items) ||
399
+ contract.items.length > EVIDENCE_POSTURE_CAPS.contractItems)
400
+ return false;
401
+ for (const item of contract.items) {
402
+ if (!isRecord(item) ||
403
+ typeof item.id !== "string" ||
404
+ !CONTRACT_ITEM_STATUSES.includes(String(item.status)) ||
405
+ !stringArray(item.criterionRefs) ||
406
+ !stringArray(item.reasons))
407
+ return false;
408
+ }
409
+ }
410
+ if (!Array.isArray(value.findings) ||
411
+ value.findings.length > EVIDENCE_POSTURE_CAPS.findings)
412
+ return false;
413
+ for (const finding of value.findings) {
414
+ if (!isRecord(finding) ||
415
+ typeof finding.id !== "string" ||
416
+ typeof finding.check !== "string" ||
417
+ (finding.severity !== "info" && finding.severity !== "warn" &&
418
+ finding.severity !== "error") ||
419
+ (finding.ref !== undefined && typeof finding.ref !== "string"))
420
+ return false;
421
+ }
422
+ const omitted = value.omitted;
423
+ if (!isRecord(omitted) ||
424
+ !isNonNegativeInteger(omitted.providers) ||
425
+ !isNonNegativeInteger(omitted.commands) ||
426
+ !isNonNegativeInteger(omitted.contractItems) ||
427
+ !isNonNegativeInteger(omitted.findings))
428
+ return false;
429
+ if (value.kind === "rollback") {
430
+ // A directive abandons work; it never carries evaluation evidence.
431
+ if (value.status !== "not_applicable" ||
432
+ value.machineBackedSuccess !== false ||
433
+ value.verificationConfigured !== false ||
434
+ workspace.status !== "not_applicable" ||
435
+ workspace.changedPathCount !== 0 ||
436
+ value.files !== null ||
437
+ value.providers.length > 0 ||
438
+ value.commands.length > 0 ||
439
+ value.contract !== null ||
440
+ value.findings.length > 0 ||
441
+ omitted.providers !== 0 ||
442
+ omitted.commands !== 0 ||
443
+ omitted.contractItems !== 0 ||
444
+ omitted.findings !== 0)
445
+ return false;
446
+ }
447
+ return true;
448
+ }
449
+ /** v4.0.3: structural validation of the transient retry context. It is only
450
+ * legal on a rejected attempt or its held retry prompt. */
451
+ export function isRetryContext(value) {
452
+ if (!isRecord(value))
453
+ return false;
454
+ if (!Number.isInteger(value.attempt) || value.attempt < 2)
455
+ return false;
456
+ if (!Number.isInteger(value.rejectedAttempt) ||
457
+ value.rejectedAttempt < 1)
458
+ return false;
459
+ if (value.attempt !== value.rejectedAttempt + 1)
460
+ return false;
461
+ if (typeof value.rejectionNotice !== "string")
462
+ return false;
463
+ if (!Array.isArray(value.verificationFlags) ||
464
+ !value.verificationFlags.every(isVerificationFlag))
465
+ return false;
466
+ if (!isEvidencePosture(value.evidencePosture))
467
+ return false;
468
+ if (value.evidencePosture.kind !== "evaluation")
469
+ return false;
470
+ if (typeof value.rejectedAt !== "number")
471
+ return false;
472
+ return true;
473
+ }
120
474
  export function isProcessResult(value) {
121
475
  if (!isRecord(value))
122
476
  return false;
123
- return ["continue", "stop", "reject", "terminate", "backtrack"].includes(String(value.action)) && Array.isArray(value.verificationFlags);
477
+ // v4.0.6: every committed decision carries a well-formed Agent outcome
478
+ // (null only for a rollback directive) and its derived success projection.
479
+ const outcome = value.agentOutcome;
480
+ const validOutcome = outcome === null || outcome === "success" ||
481
+ outcome === "partial" || outcome === "failed" || outcome === "blocked";
482
+ return ["continue", "stop", "reject", "terminate", "backtrack", "recovery"].includes(String(value.action)) && validOutcome &&
483
+ typeof value.agentSuccessClaim === "boolean" &&
484
+ Array.isArray(value.verificationFlags) &&
485
+ value.verificationFlags.every(isVerificationFlag) &&
486
+ isEvidencePosture(value.evidencePosture);
487
+ }
488
+ function stringArray(value) {
489
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
490
+ }
491
+ function isOperatorAcceptance(value) {
492
+ if (!isRecord(value))
493
+ return false;
494
+ return typeof value.task === "string" &&
495
+ stringArray(value.success_criteria) &&
496
+ stringArray(value.hard_constraints);
497
+ }
498
+ function isCheckpointRole(value) {
499
+ return value === "before" || value === "mid";
500
+ }
501
+ function isCompiledRoundFacts(value) {
502
+ if (!isRecord(value))
503
+ return false;
504
+ return typeof value.task === "string" &&
505
+ typeof value.goalId === "string" &&
506
+ stringArray(value.constraintsActive) &&
507
+ stringArray(value.constraintsRetired);
508
+ }
509
+ const CHECKPOINT_ID_RE = /^ck-[a-f0-9]{24}$/;
510
+ const CONTENT_HASH_RE = /^[a-f0-9]{64}$/;
511
+ function isCheckpointRef(value) {
512
+ if (!isRecord(value))
513
+ return false;
514
+ if (typeof value.checkpointId !== "string" ||
515
+ !CHECKPOINT_ID_RE.test(value.checkpointId))
516
+ return false;
517
+ if (typeof value.contentHash !== "string" ||
518
+ !CONTENT_HASH_RE.test(value.contentHash))
519
+ return false;
520
+ if (!isCheckpointRole(value.role))
521
+ return false;
522
+ if (!Number.isInteger(value.round) || value.round < 1)
523
+ return false;
524
+ if (typeof value.roundId !== "string" || value.roundId.length === 0)
525
+ return false;
526
+ if (typeof value.transactionId !== "string" || value.transactionId.length === 0)
527
+ return false;
528
+ if (typeof value.branchId !== "string" || value.branchId.length === 0)
529
+ return false;
530
+ if (!Number.isInteger(value.redoRound) || value.redoRound < 1)
531
+ return false;
532
+ if (typeof value.createdAt !== "number")
533
+ return false;
534
+ return true;
535
+ }
536
+ function isRecoveryTarget(value) {
537
+ if (!isRecord(value))
538
+ return false;
539
+ if (typeof value.checkpoint_id !== "string" ||
540
+ !CHECKPOINT_ID_RE.test(value.checkpoint_id))
541
+ return false;
542
+ if (typeof value.content_hash !== "string" ||
543
+ !CONTENT_HASH_RE.test(value.content_hash))
544
+ return false;
545
+ if (!isCheckpointRole(value.role))
546
+ return false;
547
+ if (!Number.isInteger(value.round) || value.round < 1)
548
+ return false;
549
+ if (typeof value.round_id !== "string" || value.round_id.length === 0)
550
+ return false;
551
+ if (typeof value.transaction_id !== "string" || value.transaction_id.length === 0)
552
+ return false;
553
+ if (!Number.isInteger(value.redo_round) || value.redo_round < 1)
554
+ return false;
555
+ return true;
556
+ }
557
+ function isRecoveryDirective(value) {
558
+ if (!isRecord(value))
559
+ return false;
560
+ if (value.origin !== "agent" && value.origin !== "machine")
561
+ return false;
562
+ if (typeof value.reason !== "string" ||
563
+ typeof value.transaction_id !== "string" ||
564
+ value.transaction_id.length === 0) {
565
+ return false;
566
+ }
567
+ if (!isRecoveryTarget(value.target))
568
+ return false;
569
+ if (value.trigger_rule !== undefined && typeof value.trigger_rule !== "string")
570
+ return false;
571
+ if (!stringArray(value.skipped_files) ||
572
+ !stringArray(value.skipped_discoveries) ||
573
+ !stringArray(value.approaches) ||
574
+ !stringArray(value.wrong_assumptions))
575
+ return false;
576
+ if (!Array.isArray(value.failed_rounds) ||
577
+ !value.failed_rounds.every((item) => Number.isInteger(item)))
578
+ return false;
579
+ if (value.observed_skipped_files !== undefined &&
580
+ !stringArray(value.observed_skipped_files))
581
+ return false;
582
+ if (value.reported_skipped_files !== undefined &&
583
+ !stringArray(value.reported_skipped_files))
584
+ return false;
585
+ if (value.abandoned_rounds !== undefined &&
586
+ (!Array.isArray(value.abandoned_rounds) ||
587
+ !value.abandoned_rounds.every((item) => Number.isInteger(item)))) {
588
+ return false;
589
+ }
590
+ if (value.trigger_findings !== undefined &&
591
+ (!Array.isArray(value.trigger_findings) ||
592
+ !value.trigger_findings.every(isVerificationFlag)))
593
+ return false;
594
+ return true;
595
+ }
596
+ function isBeforeCheckpointError(value) {
597
+ if (!isRecord(value))
598
+ return false;
599
+ return typeof value.code === "string" &&
600
+ typeof value.detail === "string" &&
601
+ typeof value.at === "number";
602
+ }
603
+ function isRestoreMarker(value) {
604
+ if (!isRecord(value))
605
+ return false;
606
+ return typeof value.directiveTransactionId === "string" &&
607
+ value.directiveTransactionId.length > 0 &&
608
+ Number.isInteger(value.redoRound) &&
609
+ value.redoRound >= 1 &&
610
+ typeof value.verifiedAt === "number";
124
611
  }
125
612
  function parseMachineObservations(value) {
126
613
  if (!Array.isArray(value))
@@ -140,14 +627,13 @@ function parseMachineObservations(value) {
140
627
  !isRecord(item.data))
141
628
  return null;
142
629
  // v3.9.1: Git observations moved to schema 2 (complete boundary
143
- // fingerprints); command/custom observations remain schema 1. A
144
- // mismatched Git observation is a hard break, reported by
145
- // legacyTransactionRounds as an "observation" envelope loss.
146
- const expectedVersion = item.kind === "git"
147
- ? GIT_OBSERVATION_SCHEMA_VERSION
148
- : 1;
149
- if (item.schemaVersion !== expectedVersion)
630
+ // fingerprints); command/custom observations remain schema 1. Custom
631
+ // observation data is opaque — it never carries a progress fact. ONE
632
+ // judgment is shared with legacyTransactionRounds, so the parser can never
633
+ // reject a round the reporter does not report (or the reverse).
634
+ if (observationVersionIssue(item.kind, item.schemaVersion, item.data, item.status)) {
150
635
  return null;
636
+ }
151
637
  if (item.kind === "git" && item.status === "observed") {
152
638
  const data = item.data;
153
639
  if (!Array.isArray(data.dirty) ||
@@ -169,6 +655,30 @@ export function parseRoundTransactionSnapshot(value) {
169
655
  return null;
170
656
  if (value.schemaVersion !== ROUND_TRANSACTION_SCHEMA_VERSION)
171
657
  return null;
658
+ if (value.kind !== "evaluation" && value.kind !== "rollback")
659
+ return null;
660
+ if (typeof value.branchId !== "string" || value.branchId.length === 0)
661
+ return null;
662
+ if (typeof value.transactionId !== "string" || value.transactionId.length === 0) {
663
+ return null;
664
+ }
665
+ if (value.operatorAcceptance !== undefined &&
666
+ !isOperatorAcceptance(value.operatorAcceptance))
667
+ return null;
668
+ if (value.compiled !== undefined && !isCompiledRoundFacts(value.compiled)) {
669
+ return null;
670
+ }
671
+ if (value.kind === "rollback") {
672
+ if (!isRecoveryDirective(value.recovery))
673
+ return null;
674
+ if (!isOperatorAcceptance(value.operatorAcceptance))
675
+ return null;
676
+ if (value.compiled !== undefined)
677
+ return null;
678
+ }
679
+ else if (value.recovery !== undefined) {
680
+ return null;
681
+ }
172
682
  if (typeof value.roundId !== "string" || typeof value.loopId !== "string") {
173
683
  return null;
174
684
  }
@@ -179,6 +689,17 @@ export function parseRoundTransactionSnapshot(value) {
179
689
  if (!["prepared", "prompted", "evaluated", "rejected", "committed", "terminated"]
180
690
  .includes(String(value.phase)))
181
691
  return null;
692
+ // Schema 6 structural invariant: a submitted evaluation always carries the
693
+ // compile facts and the immutable operator surface. A round without them
694
+ // cannot be re-derived from the same document, so it is not committed
695
+ // history.
696
+ if (value.kind === "evaluation" &&
697
+ (value.phase === "evaluated" || value.phase === "committed")) {
698
+ if (!isOperatorAcceptance(value.operatorAcceptance))
699
+ return null;
700
+ if (!isCompiledRoundFacts(value.compiled))
701
+ return null;
702
+ }
182
703
  const beforeEvidence = parseMachineObservations(value.beforeEvidence);
183
704
  if (!beforeEvidence)
184
705
  return null;
@@ -187,8 +708,83 @@ export function parseRoundTransactionSnapshot(value) {
187
708
  : parseMachineObservations(value.afterEvidence);
188
709
  if (value.afterEvidence !== undefined && !afterEvidence)
189
710
  return null;
711
+ const snapshotRound = value.round;
712
+ const snapshotRoundId = value.roundId;
713
+ const snapshotTransactionId = value.transactionId;
714
+ const snapshotBranchId = value.branchId;
715
+ if (value.kind === "rollback") {
716
+ // A rollback directive may carry the abandoned transaction's evidence,
717
+ // but never that transaction's savepoints or restore marker. The
718
+ // directive's `target` is the only checkpoint reference.
719
+ if (value.beforeCheckpoint !== undefined ||
720
+ value.beforeCheckpointError !== undefined ||
721
+ value.checkpoints !== undefined ||
722
+ value.restore !== undefined)
723
+ return null;
724
+ }
725
+ else {
726
+ if (value.beforeCheckpoint !== undefined) {
727
+ if (!isCheckpointRef(value.beforeCheckpoint))
728
+ return null;
729
+ const ref = value.beforeCheckpoint;
730
+ if (ref.role !== "before" ||
731
+ ref.round !== snapshotRound ||
732
+ ref.roundId !== snapshotRoundId ||
733
+ ref.transactionId !== snapshotTransactionId ||
734
+ ref.branchId !== snapshotBranchId ||
735
+ ref.redoRound !== snapshotRound)
736
+ return null;
737
+ }
738
+ if (value.beforeCheckpointError !== undefined &&
739
+ !isBeforeCheckpointError(value.beforeCheckpointError))
740
+ return null;
741
+ if ((value.beforeCheckpoint === undefined) ===
742
+ (value.beforeCheckpointError === undefined)) {
743
+ const observedGitBefore = beforeEvidence.some((observation) => observation.kind === "git" && observation.status === "observed");
744
+ // A Git-observed transaction owes a before-image or an explicit error.
745
+ if (observedGitBefore)
746
+ return null;
747
+ }
748
+ if (value.checkpoints !== undefined) {
749
+ if (!Array.isArray(value.checkpoints) ||
750
+ value.checkpoints.length > MAX_MID_CHECKPOINTS_PER_TRANSACTION)
751
+ return null;
752
+ const seen = new Set();
753
+ for (const item of value.checkpoints) {
754
+ if (!isCheckpointRef(item))
755
+ return null;
756
+ if (item.role !== "mid" ||
757
+ item.round !== snapshotRound ||
758
+ item.roundId !== snapshotRoundId ||
759
+ item.transactionId !== snapshotTransactionId ||
760
+ item.branchId !== snapshotBranchId ||
761
+ item.redoRound !== snapshotRound)
762
+ return null;
763
+ if (seen.has(item.checkpointId))
764
+ return null;
765
+ seen.add(item.checkpointId);
766
+ }
767
+ }
768
+ if (value.restore !== undefined) {
769
+ if (!isRestoreMarker(value.restore))
770
+ return null;
771
+ if (value.restore.redoRound !== snapshotRound)
772
+ return null;
773
+ if (value.restore.directiveTransactionId !== snapshotBranchId)
774
+ return null;
775
+ }
776
+ }
190
777
  if (value.result !== undefined && !isProcessResult(value.result))
191
778
  return null;
779
+ if (value.retryContext !== undefined) {
780
+ if (!isRetryContext(value.retryContext))
781
+ return null;
782
+ // Transient retry facts exist only between a rejection and the next
783
+ // attempt. A committed/terminated document that carries them would leak
784
+ // rejected-attempt evidence into history.
785
+ if (value.phase !== "rejected" && value.phase !== "prompted")
786
+ return null;
787
+ }
192
788
  if (value.contractBinding !== undefined && !isContractBinding(value.contractBinding))
193
789
  return null;
194
790
  if (value.normalizationDiagnostics !== undefined) {
@@ -214,9 +810,10 @@ export function parseRoundTransactionSnapshot(value) {
214
810
  return null;
215
811
  }
216
812
  }
217
- // A normalized/committed round always carries its attempted manifest. Its
218
- // absence is not a supported legacy shape.
219
- if ((value.phase === "evaluated" || value.phase === "committed") &&
813
+ // A normalized/committed evaluation always carries its attempted manifest.
814
+ // A rollback directive is not an evaluation and has no manifest.
815
+ if (value.kind === "evaluation" &&
816
+ (value.phase === "evaluated" || value.phase === "committed") &&
220
817
  value.submittedFields === undefined) {
221
818
  return null;
222
819
  }
@@ -229,12 +826,57 @@ export function parseRoundTransactionSnapshot(value) {
229
826
  // is not committed history, and `legacyTransactionRounds` reports the
230
827
  // loss explicitly instead of the loop looking complete while rounds are
231
828
  // missing.
829
+ // v3.9.4: schema 4 requires the ID-level coverage proof. A missing or
830
+ // malformed coverage object is a hard break, never an empty projection.
831
+ const coverage = artifact.coverage;
832
+ const coverageValid = isRecord(coverage) &&
833
+ Array.isArray(coverage.active) &&
834
+ coverage.active.every((entry) => isRecord(entry) &&
835
+ typeof entry.id === "string" &&
836
+ typeof entry.kind === "string") &&
837
+ Array.isArray(coverage.rendered) &&
838
+ coverage.rendered.every((id) => typeof id === "string") &&
839
+ Array.isArray(coverage.omitted) &&
840
+ coverage.omitted.every((entry) => isRecord(entry) &&
841
+ typeof entry.id === "string" &&
842
+ typeof entry.reason === "string") &&
843
+ typeof coverage.complete === "boolean";
844
+ // v4.0.2/v4.0.4: the context boundary is a REQUIRED artifact fact. An
845
+ // artifact without a legal pair is a hard break, never an implicit
846
+ // "fresh_invocation" default.
847
+ // v4.0.5: the shared runtime whitelists are the only legality source; the
848
+ // optional invocation instruction, when present, must be a non-empty
849
+ // string. A malformed value is reported as legacy, never silently
850
+ // accepted.
851
+ const contextModeValid = isPromptContextMode(artifact.contextMode);
852
+ const contextReasonValid = isPromptContextReason(artifact.contextReason);
853
+ const instructionValid = artifact.currentUserInstruction === undefined ||
854
+ (typeof artifact.currentUserInstruction === "string" &&
855
+ artifact.currentUserInstruction.trim().length > 0);
232
856
  if (artifact.schemaVersion !== PROMPT_ARTIFACT_SCHEMA_VERSION ||
233
857
  typeof artifact.roundId !== "string" ||
858
+ // v4.0.3: the artifact identity is part of the transaction identity.
859
+ artifact.roundId !== snapshotRoundId ||
860
+ artifact.round !== snapshotRound ||
861
+ artifact.attempt !== value.attempt ||
234
862
  typeof artifact.renderedPrompt !== "string" ||
235
863
  typeof artifact.promptHash !== "string" ||
236
864
  typeof artifact.stateHash !== "string" ||
237
- !["l0", "l1", "l2"].includes(String(artifact.level)))
865
+ !["l0", "l1", "l2"].includes(String(artifact.level)) ||
866
+ !contextModeValid ||
867
+ !contextReasonValid ||
868
+ !instructionValid ||
869
+ !coverageValid)
870
+ return null;
871
+ // v4.0.4: the SAME legality guard the assembler enforces. An illegal
872
+ // pair (for example a retry reason on attempt 1) is not committed history.
873
+ if (promptContextPairIssue({
874
+ level: artifact.level,
875
+ mode: artifact.contextMode,
876
+ reason: artifact.contextReason,
877
+ attempt: artifact.attempt,
878
+ round: artifact.round,
879
+ }) !== null)
238
880
  return null;
239
881
  }
240
882
  const snapshot = {
@@ -248,12 +890,10 @@ export function parseRoundTransactionSnapshot(value) {
248
890
  return snapshot;
249
891
  }
250
892
  export class RoundTransactionCoordinator {
251
- engine;
252
893
  store;
253
894
  commitStore;
254
- constructor(engine, store, commitStore) {
255
- this.engine = engine;
256
- this.store = store ?? engine.getStore();
895
+ constructor(store, commitStore) {
896
+ this.store = store;
257
897
  this.commitStore = commitStore ?? new VaultRoundCommitStore(this.store);
258
898
  }
259
899
  process(input) {
@@ -292,18 +932,20 @@ export class RoundTransactionCoordinator {
292
932
  task: input.task,
293
933
  currentRound: snapshot.round,
294
934
  maxRounds: input.maxRounds,
935
+ ...(input.history ? { history: input.history } : {}),
295
936
  selfEval: input.selfEval,
296
- lastSelfEval: input.lastSelfEval,
297
937
  consecutiveRejections: input.consecutiveRejections,
298
938
  lastRejectionCheck: input.lastRejectionCheck,
299
939
  evidenceDeltas: roundDeltas,
300
940
  beforeEvidence: snapshot.beforeEvidence,
301
- successTrajectory: input.successTrajectory,
302
- backtrackSkippedFiles: input.backtrackSkippedFiles,
303
- backtrackSkippedFingerprints: input.backtrackSkippedFingerprints,
304
- backtrackTargetGitHead: input.backtrackTargetGitHead,
305
- backtrackTargetDirty: input.backtrackTargetDirty,
306
941
  entrypointTrust: input.entrypointTrust,
942
+ // v3.9.4: the SAME manifest the receipt reads.
943
+ submittedFields: input.submittedFields ?? snapshot.submittedFields,
944
+ ...(input.beforeCheckpoint ? { beforeCheckpoint: input.beforeCheckpoint } : {}),
945
+ afterEvidence: input.actualEvidence,
946
+ ...(input.resolveCheckpointManifest
947
+ ? { resolveCheckpointManifest: input.resolveCheckpointManifest }
948
+ : {}),
307
949
  });
308
950
  const evaluated = {
309
951
  ...snapshot,
@@ -312,6 +954,8 @@ export class RoundTransactionCoordinator {
312
954
  afterEvidence: input.actualEvidence,
313
955
  evaluation: input.selfEval,
314
956
  result,
957
+ // A terminated attempt has no next retry; transient facts die with it.
958
+ ...(result.action === "terminate" ? { retryContext: undefined } : {}),
315
959
  ...(input.normalizationDiagnostics !== undefined
316
960
  ? { normalizationDiagnostics: input.normalizationDiagnostics }
317
961
  : {}),
@@ -330,9 +974,40 @@ export class RoundTransactionCoordinator {
330
974
  replayed: false,
331
975
  });
332
976
  }
977
+ if ((result.action === "recovery" || result.action === "backtrack") && result.recovery) {
978
+ const recovery = {
979
+ ...result.recovery,
980
+ transaction_id: deriveTransactionId({
981
+ kind: "rollback",
982
+ branchId: snapshot.branchId,
983
+ roundId: snapshot.roundId,
984
+ attempt: 1,
985
+ promptHash: snapshot.promptArtifact?.promptHash,
986
+ recovery: {
987
+ target: result.recovery.target,
988
+ reason: result.recovery.reason,
989
+ abandonedRounds: result.recovery.abandoned_rounds,
990
+ failedRounds: result.recovery.failed_rounds,
991
+ observedSkippedFiles: result.recovery.observed_skipped_files,
992
+ reportedSkippedFiles: result.recovery.reported_skipped_files,
993
+ triggerFindings: (result.recovery.trigger_findings ?? [])
994
+ .map((flag) => flag.check),
995
+ },
996
+ }),
997
+ };
998
+ return this.commitRecovery({
999
+ snapshot,
1000
+ loopId: snapshot.loopId,
1001
+ recovery,
1002
+ actualEvidence: input.actualEvidence,
1003
+ });
1004
+ }
333
1005
  const committedSnapshot = {
334
1006
  ...evaluated,
335
1007
  phase: "committed",
1008
+ // v4.0.3: transient retry facts die at the commit boundary. The retry
1009
+ // prompt artifact remains the record of what the Agent was asked.
1010
+ retryContext: undefined,
336
1011
  // v3.8: the contract binding is resolved HERE, at commit time, because
337
1012
  // the command configuration in force at declaration is not recoverable
338
1013
  // from the Vault afterwards. Machine-recomputed — never agent-supplied.
@@ -342,20 +1017,32 @@ export class RoundTransactionCoordinator {
342
1017
  })(),
343
1018
  updatedAt: Date.now(),
344
1019
  };
1020
+ if (!snapshot.compiled || !snapshot.operatorAcceptance) {
1021
+ throw new Error("An evaluation commit requires compile facts and the operator acceptance snapshot");
1022
+ }
345
1023
  const metadata = {
346
1024
  schema_version: ROUND_TRANSACTION_SCHEMA_VERSION,
347
1025
  round_id: snapshot.roundId,
348
1026
  snapshot: committedSnapshot,
349
1027
  result,
350
1028
  };
351
- this.engine.autoFeedback(input.selfEval, snapshot.loopId, snapshot.round, input.task, metadata);
1029
+ try {
1030
+ this.commitStore.appendFeedback(snapshot.loopId, snapshot.round, metadata);
1031
+ }
1032
+ catch (error) {
1033
+ policyMetrics.recordVaultWriteError("feedback_persist");
1034
+ logEvent("vault_write_error", { error: "feedback_persist" });
1035
+ throw error;
1036
+ }
352
1037
  // v3.7.1: a backtrack decision commits as the CURRENT round, which the
353
1038
  // incremental hydration cache never re-reads before the restore compile
354
1039
  // (it targets the same round). Drop the cache so the rollback — and its
355
1040
  // Recovery Brief facts — is visible to the very next compile.
356
- if (result.action === "backtrack") {
357
- this.engine.invalidateHydrationCache(snapshot.loopId);
358
- }
1041
+ logEvent("round_complete", {
1042
+ loopId: snapshot.loopId,
1043
+ round: snapshot.round,
1044
+ agentSuccessClaim: result.agentSuccessClaim,
1045
+ });
359
1046
  const persisted = this.readCommitted(committedSnapshot);
360
1047
  if (!persisted) {
361
1048
  throw new Error(`Round transaction commit failed: ${snapshot.roundId}`);
@@ -368,9 +1055,126 @@ export class RoundTransactionCoordinator {
368
1055
  });
369
1056
  return finish({ snapshot: committedSnapshot, result, replayed: false });
370
1057
  }
1058
+ /** v3.9.6: commit an Agent-initiated rollback directive. This path does not
1059
+ * run verification or enforcement: abandoning work is an action, not a
1060
+ * success claim. The directive is a typed event, not a `:feedback` round. */
1061
+ commitRecovery(input) {
1062
+ const { snapshot, recovery } = input;
1063
+ if (snapshot.kind !== "evaluation") {
1064
+ throw new Error(`Cannot recover from a ${snapshot.kind} transaction`);
1065
+ }
1066
+ const existing = this.readRecoveryCommitted(input.loopId, snapshot.round, recovery.transaction_id);
1067
+ if (existing) {
1068
+ policyMetrics.recordRound(input.loopId, existing.result, true);
1069
+ return existing;
1070
+ }
1071
+ const committedSnapshot = {
1072
+ ...snapshot,
1073
+ kind: "rollback",
1074
+ transactionId: recovery.transaction_id,
1075
+ phase: "committed",
1076
+ afterEvidence: input.actualEvidence,
1077
+ // A machine recovery keeps the triggering evaluation as the record of
1078
+ // what was abandoned. An agent recovery has no evaluation at all.
1079
+ evaluation: recovery.origin === "machine" ? snapshot.evaluation : undefined,
1080
+ result: undefined,
1081
+ // The operator surface is immutable and survives the rollback. The
1082
+ // directive carries the same byte-identical copy as the abandoned
1083
+ // evaluation so a cold process can still recover it.
1084
+ ...(snapshot.operatorAcceptance
1085
+ ? { operatorAcceptance: snapshot.operatorAcceptance }
1086
+ : {}),
1087
+ compiled: undefined,
1088
+ // The abandoned transaction's savepoints/restore marker belong to that
1089
+ // attempt, not to the child branch. The directive target is the only
1090
+ // recovery point recorded by this event.
1091
+ beforeCheckpoint: undefined,
1092
+ beforeCheckpointError: undefined,
1093
+ checkpoints: undefined,
1094
+ restore: undefined,
1095
+ retryContext: undefined,
1096
+ recovery,
1097
+ submittedFields: recovery.origin === "machine"
1098
+ ? (snapshot.submittedFields ?? [])
1099
+ : [],
1100
+ normalizationDiagnostics: recovery.origin === "machine"
1101
+ ? (snapshot.normalizationDiagnostics ?? [])
1102
+ : [],
1103
+ updatedAt: Date.now(),
1104
+ };
1105
+ const result = {
1106
+ action: recovery.origin === "machine" ? "backtrack" : "recovery",
1107
+ origin: recovery.origin,
1108
+ reason: recovery.reason,
1109
+ recovery,
1110
+ verificationFlags: [],
1111
+ agentSuccessClaim: false,
1112
+ agentOutcome: null,
1113
+ gateContradicted: false,
1114
+ evidencePosture: rollbackEvidencePosture(snapshot.round),
1115
+ newConsecutiveRejections: 0,
1116
+ shouldPushSuccessTrajectory: false,
1117
+ };
1118
+ if (!committedSnapshot.operatorAcceptance) {
1119
+ throw new Error("A rollback directive requires the immutable operator acceptance snapshot");
1120
+ }
1121
+ const envelope = {
1122
+ schema_version: ROUND_TRANSACTION_SCHEMA_VERSION,
1123
+ round_id: snapshot.roundId,
1124
+ snapshot: committedSnapshot,
1125
+ result,
1126
+ };
1127
+ try {
1128
+ this.commitStore.appendRollbackDirective(input.loopId, snapshot.round, recovery.transaction_id, envelope);
1129
+ }
1130
+ catch (error) {
1131
+ policyMetrics.recordVaultWriteError("rollback_persist");
1132
+ logEvent("vault_write_error", { error: "rollback_persist" });
1133
+ throw error;
1134
+ }
1135
+ const persisted = this.readRecoveryCommitted(input.loopId, snapshot.round, recovery.transaction_id);
1136
+ if (!persisted) {
1137
+ throw new Error(`Rollback directive read-back failed: ${recovery.transaction_id}`);
1138
+ }
1139
+ policyMetrics.recordRound(input.loopId, result, false);
1140
+ logEvent("rollback_directive_committed", {
1141
+ loopId: input.loopId,
1142
+ round: snapshot.round,
1143
+ transactionId: recovery.transaction_id,
1144
+ origin: recovery.origin,
1145
+ target: recovery.target,
1146
+ });
1147
+ return { snapshot: committedSnapshot, result, replayed: false };
1148
+ }
1149
+ readRecoveryCommitted(loopId, round, transactionId) {
1150
+ const entry = this.commitStore.findRollback(loopId, round, transactionId);
1151
+ if (!entry)
1152
+ return null;
1153
+ const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
1154
+ const transaction = lineage && isRecord(lineage.round_transaction)
1155
+ ? lineage.round_transaction
1156
+ : null;
1157
+ if (!transaction)
1158
+ return null;
1159
+ const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
1160
+ const result = transaction.result;
1161
+ if (!snapshot || snapshot.kind !== "rollback" || !isProcessResult(result)) {
1162
+ return null;
1163
+ }
1164
+ if (transactionIdentityIssue(entry, {
1165
+ loopId,
1166
+ round,
1167
+ schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
1168
+ transactionId,
1169
+ }))
1170
+ return null;
1171
+ return { snapshot, result, replayed: true };
1172
+ }
371
1173
  /** Recover an already committed decision without evaluating or writing. */
372
1174
  recover(snapshot) {
373
- const outcome = this.readCommitted(snapshot);
1175
+ const outcome = snapshot.kind === "rollback"
1176
+ ? this.readRecoveryCommitted(snapshot.loopId, snapshot.round, snapshot.transactionId)
1177
+ : this.readCommitted(snapshot);
374
1178
  if (outcome) {
375
1179
  policyMetrics.recordRound(snapshot.loopId, outcome.result, true);
376
1180
  }
@@ -382,28 +1186,41 @@ export class RoundTransactionCoordinator {
382
1186
  for (const entry of entries) {
383
1187
  if (entry.task_id !== taskId)
384
1188
  continue;
385
- const outcome = this.outcomeFromEntry(entry, expected.roundId, opts?.skipBacktrack === true);
1189
+ const outcome = this.outcomeFromEntry(entry, expected, opts?.skipBacktrack === true);
386
1190
  if (outcome)
387
1191
  return outcome;
388
1192
  }
389
1193
  return null;
390
1194
  }
391
- outcomeFromEntry(entry, expectedRoundId, skipBacktrack = false) {
1195
+ outcomeFromEntry(entry, expected, skipBacktrack = false) {
392
1196
  const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
393
1197
  const transaction = lineage && isRecord(lineage.round_transaction)
394
1198
  ? lineage.round_transaction
395
1199
  : null;
396
- if (!transaction || transaction.round_id !== expectedRoundId)
1200
+ if (!transaction || transaction.round_id !== expected.roundId)
1201
+ return null;
1202
+ if (transactionIdentityIssue(entry, {
1203
+ loopId: expected.loopId,
1204
+ round: expected.round,
1205
+ schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
1206
+ transactionId: expected.transactionId,
1207
+ }))
397
1208
  return null;
398
1209
  const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
399
1210
  const result = transaction.result;
400
1211
  if (!snapshot || !isProcessResult(result))
401
1212
  return null;
1213
+ // v3.9.7: the numeric round identity is NOT enough. A deep restore reuses
1214
+ // `target.redo_round`, where the abandoned branch already committed a
1215
+ // feedback round with the same roundId. Replaying it would silently discard
1216
+ // the redo submission. The transaction id is the branch-scoped identity.
1217
+ if (snapshot.transactionId !== expected.transactionId)
1218
+ return null;
402
1219
  // v3.2.1: a committed backtrack is a roll-back directive, not a terminal
403
1220
  // decision. The restored round reuses the same roundId, so replaying it
404
1221
  // would silently discard the agent's redo submission and re-emit the
405
1222
  // backtrack prompt forever (the redo's work is never evaluated, and the
406
- // R4/R9 terminate guards never run because evaluation is short-circuited).
1223
+ // the terminate guards never run because evaluation is short-circuited).
407
1224
  // Crash recovery still replays backtracks (recover() → reconcileCommittedRound
408
1225
  // resets the round counter from the committed decision); only the advance
409
1226
  // path skips them so the redo is evaluated normally.