loopforge 3.9.4 → 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 (283) 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 +114 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +276 -182
  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 +97 -156
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +223 -419
  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 +26 -38
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +196 -319
  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 +27 -897
  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 +166 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +372 -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 +39 -23
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +368 -565
  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 +255 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1552 -539
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts.map +1 -1
  115. package/dist/mcp/server.js +42 -18
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/mcp/session.d.ts +81 -39
  118. package/dist/mcp/session.d.ts.map +1 -1
  119. package/dist/mcp/session.js +956 -282
  120. package/dist/mcp/session.js.map +1 -1
  121. package/dist/mcp/submission-boundary.d.ts +23 -31
  122. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  123. package/dist/mcp/submission-boundary.js +242 -92
  124. package/dist/mcp/submission-boundary.js.map +1 -1
  125. package/dist/mcp/submission-handle.d.ts +66 -0
  126. package/dist/mcp/submission-handle.d.ts.map +1 -0
  127. package/dist/mcp/submission-handle.js +173 -0
  128. package/dist/mcp/submission-handle.js.map +1 -0
  129. package/dist/mcp/tools.d.ts +17 -7
  130. package/dist/mcp/tools.d.ts.map +1 -1
  131. package/dist/mcp/tools.js +707 -244
  132. package/dist/mcp/tools.js.map +1 -1
  133. package/dist/open-question.d.ts +7 -0
  134. package/dist/open-question.d.ts.map +1 -0
  135. package/dist/open-question.js +14 -0
  136. package/dist/open-question.js.map +1 -0
  137. package/dist/policy-metrics.d.ts +31 -21
  138. package/dist/policy-metrics.d.ts.map +1 -1
  139. package/dist/policy-metrics.js +117 -64
  140. package/dist/policy-metrics.js.map +1 -1
  141. package/dist/policy.d.ts +11 -39
  142. package/dist/policy.d.ts.map +1 -1
  143. package/dist/policy.js +12 -12
  144. package/dist/policy.js.map +1 -1
  145. package/dist/progress.d.ts +45 -0
  146. package/dist/progress.d.ts.map +1 -0
  147. package/dist/progress.js +136 -0
  148. package/dist/progress.js.map +1 -0
  149. package/dist/prompt-assembler.d.ts +20 -6
  150. package/dist/prompt-assembler.d.ts.map +1 -1
  151. package/dist/prompt-assembler.js +475 -479
  152. package/dist/prompt-assembler.js.map +1 -1
  153. package/dist/prompt-coverage.d.ts +0 -6
  154. package/dist/prompt-coverage.d.ts.map +1 -1
  155. package/dist/prompt-coverage.js +4 -8
  156. package/dist/prompt-coverage.js.map +1 -1
  157. package/dist/prompt-freshness.d.ts +6 -17
  158. package/dist/prompt-freshness.d.ts.map +1 -1
  159. package/dist/prompt-freshness.js +27 -22
  160. package/dist/prompt-freshness.js.map +1 -1
  161. package/dist/prompt-policy.d.ts +14 -7
  162. package/dist/prompt-policy.d.ts.map +1 -1
  163. package/dist/prompt-policy.js +21 -5
  164. package/dist/prompt-policy.js.map +1 -1
  165. package/dist/protocol.d.ts +843 -424
  166. package/dist/protocol.d.ts.map +1 -1
  167. package/dist/protocol.js +73 -102
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/recovery-guidance.d.ts +13 -0
  170. package/dist/recovery-guidance.d.ts.map +1 -0
  171. package/dist/recovery-guidance.js +15 -0
  172. package/dist/recovery-guidance.js.map +1 -0
  173. package/dist/recovery.d.ts +68 -0
  174. package/dist/recovery.d.ts.map +1 -0
  175. package/dist/recovery.js +234 -0
  176. package/dist/recovery.js.map +1 -0
  177. package/dist/replay.d.ts +42 -8
  178. package/dist/replay.d.ts.map +1 -1
  179. package/dist/replay.js +311 -84
  180. package/dist/replay.js.map +1 -1
  181. package/dist/result-facts.d.ts +45 -0
  182. package/dist/result-facts.d.ts.map +1 -0
  183. package/dist/result-facts.js +86 -0
  184. package/dist/result-facts.js.map +1 -0
  185. package/dist/round-contract.d.ts +38 -8
  186. package/dist/round-contract.d.ts.map +1 -1
  187. package/dist/round-contract.js +71 -31
  188. package/dist/round-contract.js.map +1 -1
  189. package/dist/round-coordinator.d.ts +41 -89
  190. package/dist/round-coordinator.d.ts.map +1 -1
  191. package/dist/round-coordinator.js +250 -267
  192. package/dist/round-coordinator.js.map +1 -1
  193. package/dist/round-driver.d.ts +56 -16
  194. package/dist/round-driver.d.ts.map +1 -1
  195. package/dist/round-driver.js +225 -39
  196. package/dist/round-driver.js.map +1 -1
  197. package/dist/round-facts.d.ts +33 -52
  198. package/dist/round-facts.d.ts.map +1 -1
  199. package/dist/round-facts.js +51 -160
  200. package/dist/round-facts.js.map +1 -1
  201. package/dist/round-guidance.d.ts +56 -0
  202. package/dist/round-guidance.d.ts.map +1 -0
  203. package/dist/round-guidance.js +161 -0
  204. package/dist/round-guidance.js.map +1 -0
  205. package/dist/round-outcome.d.ts +21 -0
  206. package/dist/round-outcome.d.ts.map +1 -0
  207. package/dist/round-outcome.js +31 -0
  208. package/dist/round-outcome.js.map +1 -0
  209. package/dist/round-transaction.d.ts +168 -21
  210. package/dist/round-transaction.d.ts.map +1 -1
  211. package/dist/round-transaction.js +837 -38
  212. package/dist/round-transaction.js.map +1 -1
  213. package/dist/self-eval.d.ts +37 -61
  214. package/dist/self-eval.d.ts.map +1 -1
  215. package/dist/self-eval.js +413 -392
  216. package/dist/self-eval.js.map +1 -1
  217. package/dist/storage.d.ts +16 -0
  218. package/dist/storage.d.ts.map +1 -1
  219. package/dist/storage.js +54 -4
  220. package/dist/storage.js.map +1 -1
  221. package/dist/subgoal-state.d.ts +2 -3
  222. package/dist/subgoal-state.d.ts.map +1 -1
  223. package/dist/subgoal-state.js +2 -3
  224. package/dist/subgoal-state.js.map +1 -1
  225. package/dist/submission-guidance.d.ts +29 -21
  226. package/dist/submission-guidance.d.ts.map +1 -1
  227. package/dist/submission-guidance.js +308 -260
  228. package/dist/submission-guidance.js.map +1 -1
  229. package/dist/submission-receipt.d.ts +16 -8
  230. package/dist/submission-receipt.d.ts.map +1 -1
  231. package/dist/submission-receipt.js +145 -51
  232. package/dist/submission-receipt.js.map +1 -1
  233. package/dist/token-utils.d.ts +28 -7
  234. package/dist/token-utils.d.ts.map +1 -1
  235. package/dist/token-utils.js +27 -11
  236. package/dist/token-utils.js.map +1 -1
  237. package/dist/transaction-identity.d.ts +9 -0
  238. package/dist/transaction-identity.d.ts.map +1 -0
  239. package/dist/transaction-identity.js +69 -0
  240. package/dist/transaction-identity.js.map +1 -0
  241. package/dist/verification-actions.d.ts.map +1 -1
  242. package/dist/verification-actions.js +10 -11
  243. package/dist/verification-actions.js.map +1 -1
  244. package/dist/verification-gate.d.ts +23 -47
  245. package/dist/verification-gate.d.ts.map +1 -1
  246. package/dist/verification-gate.js +202 -423
  247. package/dist/verification-gate.js.map +1 -1
  248. package/dist/version.d.ts +1 -1
  249. package/dist/version.js +1 -1
  250. package/dist/working-memory.d.ts +187 -0
  251. package/dist/working-memory.d.ts.map +1 -0
  252. package/dist/working-memory.js +767 -0
  253. package/dist/working-memory.js.map +1 -0
  254. package/dist/workspace-checkpoint.d.ts +88 -0
  255. package/dist/workspace-checkpoint.d.ts.map +1 -0
  256. package/dist/workspace-checkpoint.js +326 -0
  257. package/dist/workspace-checkpoint.js.map +1 -0
  258. package/dist/workspace-restore.d.ts +68 -0
  259. package/dist/workspace-restore.d.ts.map +1 -0
  260. package/dist/workspace-restore.js +604 -0
  261. package/dist/workspace-restore.js.map +1 -0
  262. package/dist/workspace.d.ts +0 -9
  263. package/dist/workspace.d.ts.map +1 -1
  264. package/dist/workspace.js +0 -17
  265. package/dist/workspace.js.map +1 -1
  266. package/loop_policy.json +4 -18
  267. package/package.json +68 -68
  268. package/skills/forge/SKILL.md +212 -0
  269. package/skills/forge/references/evidence-and-contracts.md +204 -0
  270. package/skills/forge/references/recovery-and-views.md +188 -0
  271. package/dist/cognitive-facts.d.ts +0 -30
  272. package/dist/cognitive-facts.d.ts.map +0 -1
  273. package/dist/cognitive-facts.js +0 -113
  274. package/dist/cognitive-facts.js.map +0 -1
  275. package/dist/loop-extras-parser.d.ts +0 -81
  276. package/dist/loop-extras-parser.d.ts.map +0 -1
  277. package/dist/loop-extras-parser.js +0 -213
  278. package/dist/loop-extras-parser.js.map +0 -1
  279. package/dist/loop-projection.d.ts +0 -6
  280. package/dist/loop-projection.d.ts.map +0 -1
  281. package/dist/loop-projection.js +0 -28
  282. package/dist/loop-projection.js.map +0 -1
  283. package/skills/perception/SKILL.md +0 -389
@@ -21,14 +21,16 @@
21
21
  import { afterObservations, gitDeltaState } from "./evidence-provider.js";
22
22
  import { deriveFilesReconciliation } from "./files-reconciliation.js";
23
23
  import { isPassedAfterObservation } from "./evidence-provider.js";
24
- import { getPolicy } from "./policy.js";
24
+ import { deriveConfiguredCapability, getPolicy } from "./policy.js";
25
25
  import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
26
- import { isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
27
- import { derivationRounds, machineGitMotionSeries, entryViolations, } from "./committed-round.js";
28
- import { deriveActiveRoundContract, sameContract, } from "./round-contract.js";
29
- import { deriveContractItemStatuses, } from "./contract-items.js";
30
- import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
26
+ import { isRecord, extractFilePathTokens, deriveConstraintId, deriveCriterionId, isCriterionId, normalizeText, } from "./token-utils.js";
27
+ import { derivationRounds, } from "./committed-round.js";
28
+ import { acceptanceItemsDiffer, sameContract, } from "./round-contract.js";
29
+ import { deriveContractRequirement } from "./contract-requirement.js";
30
+ import { deriveRoundFacts } from "./round-facts.js";
31
+ import { machineVerifiedCriterionIds, resolveRoundFiles, } from "./evidence-claims.js";
31
32
  import { claimedMetCriteria, claimedRemainingCriteria, effectiveSuccess } from "./self-eval.js";
33
+ import { declaresBlocked } from "./round-outcome.js";
32
34
  import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
33
35
  // ═══════════════════════════════════════════════════════════════════════════
34
36
  // v2.12: Check-name constants — single source of truth so the enforcement
@@ -44,11 +46,6 @@ import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
44
46
  // ═══════════════════════════════════════════════════════════════════════════
45
47
  export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
46
48
  export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
47
- // v3.7: single id for the outcome-vs-success axis — error when a declared
48
- // success contradicts success=false, warn when success=true but the outcome
49
- // declares a non-success (the former success_claim_conflict direction).
50
- export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
51
- export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
52
49
  export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
53
50
  export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
54
51
  export const CHECK_DUPLICATE_CONSTRAINT_DISCOVERY = "duplicate_constraint_discovery";
@@ -56,7 +53,6 @@ export const CHECK_RECURRING_VIOLATION = "recurring_violation";
56
53
  export const CHECK_RETRACT_FRESH_CONSTRAINT = "retract_fresh_constraint";
57
54
  export const CHECK_EVIDENCE_INTEGRITY = "evidence_integrity";
58
55
  export const CHECK_REQUIRED_COMMAND_FAILED = "required_command_failed";
59
- export const CHECK_COMMAND_EVIDENCE_MISMATCH = "command_evidence_mismatch";
60
56
  /** v3.3: Verification domain integrity — a command entrypoint (run-tests.sh,
61
57
  * package.json, …) changed in the same round the command ran. The runtime
62
58
  * executed a script the agent just rewrote, so the observation has no
@@ -66,7 +62,6 @@ export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_m
66
62
  /** v3.3: Test files changed in the same round a verification command passed.
67
63
  * Warn-only — the command result stays usable. */
68
64
  export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
69
- export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
70
65
  export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
71
66
  // v3.3 — Round Contract checks. v3.4: split by target. The submission's own
72
67
  // round_contract is a PROPOSAL for the NEXT round and is checked only
@@ -92,11 +87,19 @@ export const CHECK_CONTRACT_ITEMS_UNVERIFIED = "contract_items_unverified";
92
87
  * contract's declared scope. Warn; v3.8: not waivable by explanation — the
93
88
  * enforcement gate rejects, and repeated drift terminates. */
94
89
  export const CHECK_ROUND_SCOPE_DRIFT = "round_scope_drift";
95
- /** v3.5: The ACTIVE contract is still open (not completed, not blocked)
96
- * while a different contract was proposed — the proposal is ignored until
97
- * the active one closes. Warn: the walker still ignores it; this only
98
- * surfaces the otherwise-silent state. */
90
+ /** v3.5/v4.0.6: The ACTIVE contract is still open (not completed, not
91
+ * blocked) while a different contract was proposed WITHOUT `supersedes`.
92
+ * Error: a different slice is never silently saved and ignored. To replace
93
+ * the active slice, add `round_contract.supersedes` naming it. */
99
94
  export const CHECK_CONTRACT_PREMATURE = "contract_premature";
95
+ /** v4.0.6: A continuing round with contract capability and a successor
96
+ * available would leave the next round without an active contract. The final
97
+ * configured round has no successor consumer and is exempt. */
98
+ export const CHECK_CONTRACT_REQUIRED = "contract_required";
99
+ /** v4.0.6: A successor proposal repeats the closed contract's ACCEPTANCE ITEMS
100
+ * verbatim (only work_item/scope changed). Error: a successor must bring
101
+ * genuinely new acceptance conditions, or it would re-verify the old slice. */
102
+ export const CHECK_CONTRACT_SUCCESSOR_UNCHANGED = "contract_successor_unchanged";
100
103
  /** v3.7.1: A cited gate (evaluation.gate_ids) is not approved. Opt-in:
101
104
  * checked ONLY when policy.gate.enabled — citations are meaningless when
102
105
  * the gate layer is off. Error → enforcement rejects the round. */
@@ -104,8 +107,6 @@ export const CHECK_USER_GATE_UNRESOLVED = "user_gate_unresolved";
104
107
  export const CHECK_DOMAIN = {
105
108
  // ── evaluation_consistency ────────────────────────────────────────────────
106
109
  [CHECK_SUCCESS_WITH_REMAINING_CRITERIA]: "evaluation_consistency",
107
- [CHECK_OUTCOME_SUCCESS_CONTRADICTION]: "evaluation_consistency",
108
- [CHECK_BLOCKED_WITHOUT_BLOCKER]: "evaluation_consistency",
109
110
  [CHECK_RETROACTIVE_CLAIM_BAD_ROUND]: "evaluation_consistency",
110
111
  [CHECK_RETROACTIVE_CLAIM_UNVERIFIED]: "evaluation_consistency",
111
112
  [CHECK_DUPLICATE_CONSTRAINT_DISCOVERY]: "evaluation_consistency",
@@ -116,17 +117,17 @@ export const CHECK_DOMAIN = {
116
117
  [CHECK_CRITERIA_CLAIMS_UNVERIFIED]: "evidence_integrity",
117
118
  [CHECK_EVIDENCE_INTEGRITY]: "evidence_integrity",
118
119
  [CHECK_REQUIRED_COMMAND_FAILED]: "evidence_integrity",
119
- [CHECK_COMMAND_EVIDENCE_MISMATCH]: "evidence_integrity",
120
120
  [CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
121
121
  [CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
122
122
  // ── plan_contract ─────────────────────────────────────────────────────────
123
123
  [CHECK_CONTRACT_ITEMS_UNVERIFIED]: "plan_contract",
124
124
  [CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
125
125
  [CHECK_CONTRACT_PREMATURE]: "plan_contract",
126
+ [CHECK_CONTRACT_REQUIRED]: "plan_contract",
127
+ [CHECK_CONTRACT_SUCCESSOR_UNCHANGED]: "plan_contract",
126
128
  // v3.7.1: cited-gate authorization conformance (opt-in blocking layer).
127
129
  [CHECK_USER_GATE_UNRESOLVED]: "plan_contract",
128
130
  // ── progress_recovery ─────────────────────────────────────────────────────
129
- [CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED]: "progress_recovery",
130
131
  };
131
132
  // ═══════════════════════════════════════════════════════════════════════════
132
133
  // Helpers
@@ -206,21 +207,8 @@ function passedAfterCommand(deltas) {
206
207
  * missing — the fix for the "weakest when it matters most" gap. */
207
208
  export function deriveEvidenceStatus(selfEval, evidenceDeltas) {
208
209
  const git = gitDeltaState(evidenceDeltas);
209
- const gitObserved = git.state === "observed";
210
210
  const command = passedAfterCommand(evidenceDeltas);
211
211
  const commandVerified = command !== null;
212
- // testsMachineBacked: agent-reported tests_reported agree with a passed
213
- // command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
214
- // performs — parseTestOutput lives in this module).
215
- let testsMachineBacked = false;
216
- const reported = selfEval.execution_report?.tests_reported;
217
- if (command && command.kind === "command" && reported) {
218
- const parsed = parseTestOutput(command.data.stdoutExcerpt ?? "");
219
- testsMachineBacked = parsed !== null &&
220
- parsed.passed === reported.passed &&
221
- parsed.failed === reported.failed &&
222
- parsed.skipped === reported.skipped;
223
- }
224
212
  // v3.3: git observation alone is no longer "verified" — the machine seeing
225
213
  // file changes is not the machine verifying success. Only a passed
226
214
  // after-phase command (observed by the runtime itself) upgrades a round.
@@ -234,15 +222,15 @@ export function deriveEvidenceStatus(selfEval, evidenceDeltas) {
234
222
  : "unavailable";
235
223
  return {
236
224
  providerStatus,
237
- gitObserved,
238
225
  gitChangeState: git.state,
239
226
  commandVerified,
240
- testsMachineBacked,
241
227
  };
242
228
  }
243
- /** Per-round machine progress over decoded committed history. */
244
- export function machineProgressSeries(vaultEntries, currentRound, lookback) {
245
- return machineGitMotionSeries(derivationRounds(vaultEntries, currentRound), currentRound, lookback);
229
+ /** Whether a changed path looks like a test file. */
230
+ function isTestFile(path) {
231
+ return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
232
+ /(^|[\/])tests?[\/]/i.test(path) ||
233
+ /_test\.[a-z0-9]+$/i.test(path);
246
234
  }
247
235
  // ═══════════════════════════════════════════════════════════════════════════
248
236
  // Individual checks — each returns a VerificationFlag or null
@@ -255,9 +243,9 @@ function checkSuccessWithRemainingCriteria(selfEval) {
255
243
  return null;
256
244
  return makeVerificationFlag({
257
245
  severity: "error",
258
- field: "success",
246
+ field: "outcome",
259
247
  check: CHECK_SUCCESS_WITH_REMAINING_CRITERIA,
260
- detail: `Agent claims success but ${remaining.length} criteria remain unmet: ` +
248
+ detail: `outcome=success but ${remaining.length} criteria remain unmet: ` +
261
249
  remaining.slice(0, 3).join("; "),
262
250
  });
263
251
  }
@@ -266,55 +254,27 @@ function checkSuccessWithRemainingCriteria(selfEval) {
266
254
  * command snapshot backs them. Warn — completion still counts, but the
267
255
  * next prompt tells the agent its claims are unverified. This is the
268
256
  * mild form of the 3.x criterion-specific completion rule. */
269
- function checkUnverifiedCriteriaClaims(selfEval, claimView) {
257
+ function checkUnverifiedCriteriaClaims(selfEval, machineVerifiedIds) {
270
258
  const met = claimedMetCriteria(selfEval.execution_report);
271
259
  if (met.length === 0)
272
260
  return null;
273
- if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
261
+ // v4.0.3: EVERY met claim is checked. A verified claim elsewhere in the
262
+ // same round does not excuse an unbacked one.
263
+ const unverified = met.filter((target) => {
264
+ const id = isCriterionId(target) ? target : deriveCriterionId(target);
265
+ return !machineVerifiedIds.has(id);
266
+ });
267
+ if (unverified.length === 0)
274
268
  return null;
275
269
  return makeVerificationFlag({
276
270
  severity: "warn",
277
271
  field: "execution_report",
278
272
  check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
279
- detail: `${met.length} criteria reported met but none machine-verified ` +
280
- "(no passing test evidence or command snapshot)",
273
+ detail: `${unverified.length} of ${met.length} criteria reported met are not ` +
274
+ "machine-verified: " + unverified.slice(0, 3).join("; ") +
275
+ (unverified.length > 3 ? " …" : ""),
281
276
  });
282
277
  }
283
- /** Declared outcome vs the core success flag — one check id for the whole
284
- * axis (v3.7: the former success_claim_conflict warn direction merged here).
285
- * A declared success with success=false is a self-contradiction (error);
286
- * success=true with a non-success outcome is an inconsistent claim (warn).
287
- * Also: outcome==="blocked" without a blocker description gets a warn so the
288
- * next prompt asks for it. */
289
- function checkOutcomeConsistency(selfEval) {
290
- const outcome = selfEval.outcome;
291
- if (outcome === "success" && selfEval.success === false) {
292
- return makeVerificationFlag({
293
- severity: "error",
294
- field: "outcome",
295
- check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
296
- detail: "outcome=success but success=false — self-contradictory",
297
- });
298
- }
299
- if (outcome !== undefined && outcome !== "success" && selfEval.success === true) {
300
- return makeVerificationFlag({
301
- severity: "warn",
302
- field: "success",
303
- check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
304
- detail: `outcome=${outcome} but success=true — the declared outcome wins`,
305
- });
306
- }
307
- if (outcome === "blocked" &&
308
- (typeof selfEval.blocker !== "string" || selfEval.blocker.trim().length === 0)) {
309
- return makeVerificationFlag({
310
- severity: "warn",
311
- field: "blocker",
312
- check: CHECK_BLOCKED_WITHOUT_BLOCKER,
313
- detail: "outcome=blocked but no blocker description was provided",
314
- });
315
- }
316
- return null;
317
- }
318
278
  /** v2.12: Retroactive claims reference PRIOR rounds. Each claim's file
319
279
  * paths are checked against the referenced round's committed git evidence
320
280
  * (P0 provenance layer). Unverifiable claims warn — they never upgrade
@@ -366,11 +326,10 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
366
326
  }
367
327
  /** v2.12/v3.6/v3.7/v3.9.1: success without machine backing — the single
368
328
  * "success evidence" semantic. Two arms, evaluated in order:
369
- * 1. Empty/missing-report arm: success=true with NO execution_report at all,
370
- * or with files_changed empty AND tests_reported null, is a WARN. It is a
371
- * reporting gap, not an acceptance gate: 3.9.1 does not reject a round
372
- * because an optional report field was not filled. The warn still keeps
373
- * success out of the success trajectory.
329
+ * 1. Missing-report arm: outcome=success with NO execution_report at all is
330
+ * a WARN. It is a reporting gap, not an acceptance gate: the runtime does
331
+ * not reject a round because an optional report field was not filled. An
332
+ * empty files_changed is a legitimate no-change report, not a gap.
374
333
  * 2. Claims arm (v3.6): success=true but the runtime observed no machine
375
334
  * evidence for it. The trigger is the runtime-derived providerStatus
376
335
  * (verified / unavailable / absent) — "verified" requires an UNTAMPERED
@@ -394,17 +353,14 @@ function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
394
353
  // remains the verification fact, and the warn still keeps success out of
395
354
  // the success trajectory (shouldPushSuccess keys on this check).
396
355
  const ev = selfEval.execution_report;
397
- if (!ev || ((ev.files_changed ?? []).length === 0 && ev.tests_reported === null)) {
356
+ if (!ev) {
398
357
  return makeVerificationFlag({
399
358
  severity: "warn",
400
- field: "success",
359
+ field: "outcome",
401
360
  check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
402
- detail: ev
403
- ? "Agent claims success but execution_report shows no files changed " +
404
- "and no tests were run. This is a reporting gap, not machine evidence."
405
- : "Agent claims success but provided no execution_report. The report is " +
406
- "expected but missing; this is a reporting gap, not by itself a " +
407
- "rejection. No machine evidence backs the claim.",
361
+ detail: "outcome=success but no execution_report was provided. The report " +
362
+ "is expected but missing; this is a reporting gap, not by itself a " +
363
+ "rejection. No machine evidence backs the claim.",
408
364
  });
409
365
  }
410
366
  // Claims arm (v3.6).
@@ -427,12 +383,12 @@ function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
427
383
  // this warn). The bounded verification-debt row is what eventually acts on
428
384
  // a persistent pattern. `no_change_reason` stays info, and only works when
429
385
  // machine verification was structurally impossible (no enabled command).
430
- // A success claim with NO execution_report at all is the other arm and
431
- // remains an unconditional error.
386
+ // A success claim with NO execution_report at all is the other arm and is
387
+ // also a warn: report coverage never decides acceptance.
432
388
  const severity = noChange ? "info" : "warn";
433
389
  return makeVerificationFlag({
434
390
  severity,
435
- field: "success",
391
+ field: "outcome",
436
392
  check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
437
393
  detail: noChange
438
394
  ? "success with zero verified claims (no_change_reason declared)"
@@ -446,18 +402,18 @@ function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolatio
446
402
  // Collect all previously-known constraints
447
403
  const known = new Set();
448
404
  for (const v of olderViolations)
449
- known.add(v.toLowerCase().trim());
405
+ known.add(normalizeText(v));
450
406
  if (prevSelfEval) {
451
407
  for (const d of prevSelfEval.discovered_constraints ?? []) {
452
- known.add(d.toLowerCase().trim());
408
+ known.add(normalizeText(d));
453
409
  }
454
410
  // Also treat previous violations as implicitly discovered
455
411
  for (const v of prevSelfEval.constraint_violations) {
456
- known.add(v.toLowerCase().trim());
412
+ known.add(normalizeText(v));
457
413
  }
458
414
  }
459
415
  for (const d of discovered) {
460
- if (known.has(d.toLowerCase().trim())) {
416
+ if (known.has(normalizeText(d))) {
461
417
  return makeVerificationFlag({
462
418
  severity: "warn",
463
419
  field: "discovered_constraints",
@@ -468,29 +424,27 @@ function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolatio
468
424
  }
469
425
  return null;
470
426
  }
471
- function checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound) {
427
+ function checkRecurringViolation(selfEval, prevSelfEval, committedRounds, currentRound) {
472
428
  const currViols = selfEval.constraint_violations;
473
429
  if (!currViols || currViols.length === 0)
474
430
  return null;
475
431
  // Build the violation history: [round N-2, round N-1, round N]
476
432
  const violationsByRound = [];
477
- // Round N-2 violations come from the vault entry for round N-1
478
- // (persistLoopLineage stores the PREVIOUS round's violations on each entry)
433
+ // Round N-2 violations come from the committed round's own evaluation.
479
434
  if (currentRound >= 3) {
480
- const entryNMinus1 = vaultEntries.find((e) => entryRound(e) === currentRound - 1);
481
- if (entryNMinus1) {
482
- const viols = entryViolations(entryNMinus1);
483
- if (viols.length)
484
- violationsByRound.push(viols.map((v) => v.toLowerCase().trim()));
435
+ const nMinus2 = committedRounds.find((view) => view.round === currentRound - 2);
436
+ const viols = nMinus2?.constraintViolations ?? [];
437
+ if (viols.length) {
438
+ violationsByRound.push(viols.map((v) => normalizeText(v)));
485
439
  }
486
440
  }
487
441
  // Round N-1 violations from prevSelfEval
488
442
  if (prevSelfEval) {
489
443
  const prevViols = prevSelfEval.constraint_violations;
490
- violationsByRound.push(prevViols.map((v) => v.toLowerCase().trim()));
444
+ violationsByRound.push(prevViols.map((v) => normalizeText(v)));
491
445
  }
492
446
  // Round N violations from current selfEval
493
- violationsByRound.push(currViols.map((v) => v.toLowerCase().trim()));
447
+ violationsByRound.push(currViols.map((v) => normalizeText(v)));
494
448
  // Need at least 3 rounds of data
495
449
  if (violationsByRound.length < 3)
496
450
  return null;
@@ -515,12 +469,11 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
515
469
  return null;
516
470
  if (!prevSelfEval)
517
471
  return null;
518
- const lastRoundDiscoveries = new Set();
519
- for (const d of prevSelfEval.discovered_constraints ?? []) {
520
- lastRoundDiscoveries.add(d.toLowerCase().trim());
521
- }
472
+ const discoveries = prevSelfEval.discovered_constraints ?? [];
473
+ const lastRoundDiscoveries = new Set(discoveries.map((discovery) => normalizeText(discovery)));
474
+ const discoveryIds = new Set(discoveries.map((discovery) => deriveConstraintId(discovery)));
522
475
  for (const r of retracted) {
523
- if (lastRoundDiscoveries.has(r.toLowerCase().trim())) {
476
+ if (lastRoundDiscoveries.has(normalizeText(r)) || discoveryIds.has(r)) {
524
477
  return makeVerificationFlag({
525
478
  severity: "warn",
526
479
  field: "retracted_constraints",
@@ -583,125 +536,6 @@ function checkRequiredCommandEvidence(selfEval, evidenceDeltas) {
583
536
  }
584
537
  return null;
585
538
  }
586
- // ── Test output parsing ────────────────────────────────────────────────────
587
- /** Whether a changed path looks like a test file. */
588
- function isTestFile(path) {
589
- return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
590
- /(^|[\\/])tests?[\\/]/i.test(path) ||
591
- /_test\.[a-z0-9]+$/i.test(path);
592
- }
593
- /** Best-effort parse of test counts from common test-runner output formats.
594
- *
595
- * Scans the last 2000 characters of stdout (where summary lines typically
596
- * appear) and tries patterns in descending specificity order. Returns null
597
- * when the output format is unrecognized, stdout is empty, or a confident
598
- * parse cannot be made.
599
- *
600
- * Recognized formats: Jest verbose/compact, Mocha, pytest/unittest, Go test,
601
- * and PHPUnit OK summaries. */
602
- export function parseTestOutput(stdout) {
603
- if (!stdout)
604
- return null;
605
- const tail = stdout.length > 2000 ? stdout.slice(-2000) : stdout;
606
- let match;
607
- // 1. Jest verbose: "Tests: 1 failed, 8 passed, 9 total"
608
- match = tail.match(/Tests:\s*(\d+)\s+failed,\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
609
- if (match) {
610
- return { failed: Number(match[1]), passed: Number(match[2]), total: Number(match[3]), skipped: 0 };
611
- }
612
- // 2. Jest compact: "Tests: 8 passed, 9 total"
613
- match = tail.match(/Tests:\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
614
- if (match) {
615
- const passed = Number(match[1]);
616
- const total = Number(match[2]);
617
- return { passed, failed: total - passed, total, skipped: 0 };
618
- }
619
- // 3. Mocha: "8 passing" + optional "2 failing"
620
- const mochaPass = tail.match(/(\d+)\s+passing/i);
621
- if (mochaPass) {
622
- const mochaFail = tail.match(/(\d+)\s+failing/i);
623
- const passed = Number(mochaPass[1]);
624
- const failed = mochaFail ? Number(mochaFail[1]) : 0;
625
- return { passed, failed, total: passed + failed, skipped: 0 };
626
- }
627
- // 4. Go test: count "--- PASS:" and "--- FAIL:" lines
628
- const goPassCount = (tail.match(/---\s+PASS:/g) || []).length;
629
- const goFailCount = (tail.match(/---\s+FAIL:/g) || []).length;
630
- if (goPassCount > 0 || goFailCount > 0) {
631
- return { passed: goPassCount, failed: goFailCount, total: goPassCount + goFailCount, skipped: 0 };
632
- }
633
- // 5. pytest / unittest: "8 passed, 1 failed" or "8 passed"
634
- match = tail.match(/(\d+)\s+passed[,;\s]+(\d+)\s+failed/i);
635
- if (match) {
636
- return { passed: Number(match[1]), failed: Number(match[2]), total: Number(match[1]) + Number(match[2]), skipped: 0 };
637
- }
638
- // pytest compact: "8 passed" (with a reasonable context hint)
639
- match = tail.match(/(?:=\s+test session|\b(?:passed|failed)\b)/i);
640
- if (match) {
641
- const pCount = tail.match(/(\d+)\s+passed/i);
642
- const fCount = tail.match(/(\d+)\s+failed/i);
643
- if (pCount || fCount) {
644
- const passed = pCount ? Number(pCount[1]) : 0;
645
- const failed = fCount ? Number(fCount[1]) : 0;
646
- return { passed, failed, total: passed + failed, skipped: 0 };
647
- }
648
- }
649
- // 6. PHPUnit: "OK (8 tests, 16 assertions)"
650
- match = tail.match(/OK\s*\((\d+)\s+tests?/i);
651
- if (match) {
652
- const total = Number(match[1]);
653
- return { passed: total, failed: 0, total, skipped: 0 };
654
- }
655
- return null;
656
- }
657
- /** Cross-validate agent-reported tests_reported against command evidence output.
658
- *
659
- * For each command provider whose status is "passed" and output is not
660
- * truncated, attempts to parse test counts from stdout. Mismatched counts
661
- * produce a warn-level flag; failures hidden by the agent (reported 0 failed
662
- * when the command shows >0) produce an error-level flag.
663
- *
664
- * Non-command evidence providers are skipped (no structural cross-check is
665
- * defined for them yet). */
666
- function checkCommandEvidenceIntegrity(selfEval, evidenceDeltas) {
667
- const reported = selfEval.execution_report?.tests_reported;
668
- if (!reported)
669
- return null;
670
- for (const snapshot of afterObservations(evidenceDeltas)) {
671
- if (snapshot.kind !== "command")
672
- continue;
673
- if (snapshot.status !== "passed")
674
- continue;
675
- if (snapshot.data.truncated === true)
676
- continue;
677
- const stdout = typeof snapshot.data.stdoutExcerpt === "string" ? snapshot.data.stdoutExcerpt : "";
678
- const parsed = parseTestOutput(stdout);
679
- if (!parsed)
680
- continue;
681
- const mismatches = [];
682
- if (parsed.passed !== reported.passed) {
683
- mismatches.push(`passed: reported ${reported.passed}, command shows ${parsed.passed}`);
684
- }
685
- if (parsed.failed !== reported.failed) {
686
- mismatches.push(`failed: reported ${reported.failed}, command shows ${parsed.failed}`);
687
- }
688
- if (mismatches.length === 0) {
689
- // Counts match exactly — evidence supports the agent's claim.
690
- continue;
691
- }
692
- const name = snapshot.data.commandId;
693
- // Agent hides failures → error. Other mismatch → warn.
694
- const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
695
- return makeVerificationFlag({
696
- severity,
697
- field: "tests_reported",
698
- check: CHECK_COMMAND_EVIDENCE_MISMATCH,
699
- detail: `Agent tests_reported don't match "${name}" output: ${mismatches.join("; ")}`,
700
- ref: name,
701
- });
702
- }
703
- return null;
704
- }
705
539
  /** v3.3: Verification domain integrity — command entrypoints and test files
706
540
  * changed in the same round the command ran. Entrypoint modification
707
541
  * invalidates the machine observation (error: the runtime executed a script
@@ -745,10 +579,8 @@ function checkVerificationDomainIntegrity(evidenceDeltas, drift) {
745
579
  (changed.length > 0
746
580
  ? "At round start: " + changed.map((entry) => entry.roundStart === "absent"
747
581
  ? `"${entry.file}" did not exist`
748
- : `"${entry.file}" was present`).join("; ") + ". "
749
- : "") +
750
- "Keep the verification command stable or resubmit the same round with the " +
751
- "entrypoint unchanged.",
582
+ : `"${entry.file}" was present`).join("; ") + "."
583
+ : ""),
752
584
  ref: snapshot.data.commandId,
753
585
  });
754
586
  }
@@ -771,132 +603,6 @@ function checkVerificationDomainIntegrity(evidenceDeltas, drift) {
771
603
  }
772
604
  return null;
773
605
  }
774
- /** v2.13: After a backtrack, check that the agent restored the workspace
775
- * before working. Two arms:
776
- *
777
- * 1. Machine arm (M3, error): a skipped file whose CURRENT git fingerprint
778
- * still equals the fingerprint recorded at its failed round was never
779
- * touched since the rollback — the workspace was provably not restored.
780
- * Restored or re-modified files change fingerprint and can never be
781
- * misjudged by this arm.
782
- *
783
- * 2. Claims arm (warn only): the agent's files_changed overlaps the skipped
784
- * list. A self-report alone cannot distinguish a restored-then-redone
785
- * file from a never-restored one — a legitimate multi-file redo would
786
- * otherwise be falsely terminated — so overlap is guidance, not a
787
- * verdict. (The old error tier for >= 3 overlapping files was removed
788
- * for exactly this reason: it fired on honest redos.) */
789
- function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints = {}, evidenceDeltas = []) {
790
- // Machine arm — untouched-since-rollback proof, independent of claims.
791
- const failedEntries = Object.entries(backtrackSkippedFingerprints);
792
- if (failedEntries.length > 0) {
793
- const afterGit = evidenceDeltas.find((delta) => delta.kind === "git")?.after;
794
- const currentFingerprints = afterGit && afterGit.kind === "git" &&
795
- isRecord(afterGit.data.fingerprints)
796
- ? afterGit.data.fingerprints
797
- : null;
798
- if (currentFingerprints) {
799
- const untouched = failedEntries
800
- .filter(([file, failedFp]) => currentFingerprints[file] === failedFp)
801
- .map(([file]) => file)
802
- .sort();
803
- if (untouched.length > 0) {
804
- return makeVerificationFlag({
805
- severity: "error",
806
- field: "workspace",
807
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
808
- detail: `File(s) from the failed rounds are byte-identical to their ` +
809
- `state at the rollback: ${untouched.slice(0, 5).join(", ")}. The ` +
810
- `workspace was never restored — restore it to the restore point ` +
811
- `(git checkout/reset) before redoing the work.`,
812
- });
813
- }
814
- }
815
- }
816
- const ev = selfEval.execution_report;
817
- if (!ev || (ev.files_changed ?? []).length === 0)
818
- return null;
819
- // Claims arm — overlap as guidance only.
820
- const overlap = (ev.files_changed ?? []).filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
821
- if (overlap.length === 0)
822
- return null;
823
- return makeVerificationFlag({
824
- severity: "warn",
825
- field: "files_changed",
826
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
827
- detail: `Agent modified files that overlap with skipped rounds: ` +
828
- `${overlap.join(", ")}. Verify the workspace was restored.`,
829
- });
830
- }
831
- /** v2.12: After a backtrack, the workspace must return to the restore point:
832
- * the same git HEAD that round committed, carrying no dirty file the restore
833
- * point did not already have. Compares HEAD's first 12 characters (short
834
- * hash), matching how git itself disambiguates.
835
- *
836
- * The dirty-set comparison is against the restore point's OWN baseline, not
837
- * an empty tree: unrelated work in progress in the same workspace is no
838
- * evidence of a failed restore, while a file the failed rounds left behind
839
- * is — it is dirty now and was not dirty then. An absent baseline means the
840
- * restore point's dirt was never observed, so only HEAD can be judged.
841
- *
842
- * A redo prepared before the agent restores the workspace has a stale before
843
- * snapshot, so its first submission is a recovery checkpoint; once the
844
- * restore is proven, later work may move HEAD freely. */
845
- function checkBacktrackGitHeadRestore(evidenceDeltas, backtrackTargetGitHead, beforeEvidence = [],
846
- /** The restore point's own dirty files — undefined when never observed. */
847
- backtrackTargetDirty) {
848
- if (!backtrackTargetGitHead)
849
- return null;
850
- const short = (hash) => hash.slice(0, 12);
851
- const readGit = (observations) => {
852
- const snapshot = observations.find((item) => item.providerId === "git" && item.status === "observed" && isRecord(item.data) &&
853
- typeof item.data.head === "string");
854
- if (!snapshot || !isRecord(snapshot.data))
855
- return null;
856
- const data = snapshot.data;
857
- // v3.9.1: the workspace's dirty set is one explicit list. "Tracked /
858
- // staged / untracked" were dirty-since-HEAD categories, not the complete
859
- // boundary, and are gone.
860
- const dirty = Array.isArray(data.dirty)
861
- ? data.dirty.filter((file) => typeof file === "string")
862
- : [];
863
- return { head: data.head, dirty };
864
- };
865
- const foreignDirty = (dirty) => Array.isArray(backtrackTargetDirty)
866
- ? dirty.filter((file) => !backtrackTargetDirty.includes(file))
867
- : [];
868
- const restored = (git) => git !== null && git.head.length > 0 &&
869
- short(git.head) === short(backtrackTargetGitHead) &&
870
- foreignDirty(git.dirty).length === 0;
871
- // When restoration happened before the redo was prepared, the before
872
- // snapshot already proves it. The redo may then commit new work.
873
- if (restored(readGit(beforeEvidence)))
874
- return null;
875
- const afterGit = readGit(afterObservations(evidenceDeltas));
876
- if (!afterGit)
877
- return null; // git unavailable → cannot verify → skip (fail-open)
878
- if (restored(afterGit))
879
- return null;
880
- // Normal lifecycle: prepare captured the pre-restore workspace. The first
881
- // submission must prove restoration before a later submission can commit.
882
- const headBack = afterGit.head.length > 0 &&
883
- short(afterGit.head) === short(backtrackTargetGitHead);
884
- const left = foreignDirty(afterGit.dirty);
885
- return makeVerificationFlag({
886
- severity: "error",
887
- field: "workspace",
888
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
889
- detail: headBack
890
- ? `Workspace HEAD is back at the backtrack restore commit ` +
891
- `(${short(backtrackTargetGitHead)}) but these files were not there at ` +
892
- `the restore point and are still changed: ${left.slice(0, 5).join(", ")}` +
893
- `${left.length > 5 ? ` (+${left.length - 5} more)` : ""}. Revert them ` +
894
- `before redoing the work.`
895
- : `Workspace HEAD (${short(afterGit.head)}) does not match the backtrack ` +
896
- `restore commit (${short(backtrackTargetGitHead)}). Submit one recovery ` +
897
- `evaluation at the restore point before redoing work.`,
898
- });
899
- }
900
606
  // ═══════════════════════════════════════════════════════════════════════════
901
607
  // v3.8 — Round Contract checks (all silent when no contract is declared)
902
608
  //
@@ -936,22 +642,37 @@ function checkRoundScopeDrift(activeContract, evidenceDeltas) {
936
642
  if (outOfScope === null || outOfScope.length === 0)
937
643
  return null;
938
644
  return makeVerificationFlag({
939
- severity: "warn",
645
+ // v3.9.9: report only. `scope` is the Agent's declared intent, not a
646
+ // permission, so drift is an INFO fact — it never rejects, terminates,
647
+ // backtracks, stalls, or counts toward a rejection streak.
648
+ severity: "info",
940
649
  field: "round_contract",
941
650
  check: CHECK_ROUND_SCOPE_DRIFT,
942
- detail: `Files changed outside the active contract's declared scope` +
651
+ detail: `Files changed outside the active contract's declared scope (report ` +
652
+ `only; this does not block the round)` +
943
653
  (outOfScope.length > 5
944
654
  ? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
945
- : `: ${outOfScope.join(", ")}`) +
946
- `. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
655
+ : `: ${outOfScope.join(", ")}`),
947
656
  });
948
657
  }
949
658
  /** v3.8: Claimed-but-unverified contract items. The round COMMITS — the debt
950
659
  * is surfaced (prompt, state file, handoff) and the enforcement gate's
951
660
  * verification-debt row handles a persistent pattern. */
952
- function checkContractItemsUnverified(statuses) {
661
+ function checkContractItemsUnverified(statuses, currentReport) {
953
662
  if (statuses.insufficientCount === 0)
954
663
  return null;
664
+ // v3.9.9: an honest abandonment retires the old debt finding. A superseded
665
+ // contract whose round does NOT claim any of its items `met` is the Agent
666
+ // saying "this slice was wrong" — not a completion claim to police. If the
667
+ // round still claims an old item `met`, the debt finding stands: a redirect
668
+ // cannot wash away an unverified completion claim.
669
+ if (statuses.closure === "superseded") {
670
+ const itemIds = new Set(statuses.items.map((item) => item.itemId));
671
+ const claimsMet = (currentReport?.claims ?? []).some((claim) => claim.id.startsWith("rci-") &&
672
+ claim.outcome === "met" && itemIds.has(claim.id));
673
+ if (!claimsMet)
674
+ return null;
675
+ }
955
676
  // v3.9.4: EVERY insufficient item is named. The previous 3-item cap hid the
956
677
  // rest of the debt from the one repair surface the agent reads.
957
678
  const unverified = statuses.items
@@ -962,33 +683,91 @@ function checkContractItemsUnverified(statuses) {
962
683
  field: "round_contract",
963
684
  check: CHECK_CONTRACT_ITEMS_UNVERIFIED,
964
685
  detail: `${statuses.insufficientCount} contract item(s) claimed met but not machine-verified: ` +
965
- `${unverified.join(", ")}. Run the bound command(s) or report the item as remaining.`,
686
+ `${unverified.join(", ")}.`,
966
687
  });
967
688
  }
968
- /** A different contract proposed while the ACTIVE contract is still open. The
969
- * walker ignores the premature proposal — this warn only makes the ignored
970
- * state visible. Silent when the active contract is closed (all items
971
- * verified, or outcome=blocked), when no proposal is submitted, or when the
972
- * proposal is a restate (content equality, never id equality). */
689
+ /** v4.0.6: A different contract proposed while the ACTIVE contract is still
690
+ * open and the proposal does not explicitly `supersedes` it. This is an
691
+ * ERROR: a different slice is never silently saved and ignored. Silent when
692
+ * the active contract is closed (all items verified, or outcome=blocked),
693
+ * when no proposal is submitted, or when the proposal is a restate (content
694
+ * equality, never id equality). A legal redirect makes the closure
695
+ * `superseded`, so it never reaches this check. */
973
696
  function checkContractPremature(activeContract, statuses, selfEval) {
974
697
  if (!activeContract)
975
698
  return null;
976
699
  if (statuses.closure !== "open")
977
700
  return null; // closed
978
- if (selfEval.outcome === "blocked")
701
+ if (declaresBlocked(selfEval))
979
702
  return null; // closed
980
703
  const proposal = selfEval.round_contract;
981
704
  if (!proposal)
982
705
  return null;
706
+ if (proposal.supersedes?.contract_id === activeContract.id)
707
+ return null; // explicit redirect
983
708
  if (sameContract(proposal, activeContract))
984
709
  return null; // restate → continue
985
710
  return makeVerificationFlag({
986
- severity: "warn",
711
+ severity: "error",
987
712
  field: "round_contract",
988
713
  check: CHECK_CONTRACT_PREMATURE,
989
714
  detail: "The ACTIVE Round Contract is still open (not every item verified, not blocked) " +
990
- "while a different contract was proposed — it is ignored until the active " +
991
- "contract closes. Restate the active contract unchanged to continue it.",
715
+ "while a different contract was proposed without supersedes — to replace it, " +
716
+ "add round_contract.supersedes with the active contract id and different " +
717
+ "acceptance items; otherwise omit round_contract to continue.",
718
+ });
719
+ }
720
+ /** v4.0.6: contract coverage. With an after-capable command configured, a
721
+ * continuing round must leave the next round with an active contract.
722
+ * `blocked` rounds are exempt (they close the contract and stop). The rule is
723
+ * the shared `deriveContractRequirement`, so the submission boundary and the
724
+ * gate cannot disagree. */
725
+ function checkContractRequired(activeContract, statuses, selfEval, contractCapability, successorRoundAvailable) {
726
+ const requirement = deriveContractRequirement({
727
+ shouldContinue: selfEval.should_continue === true,
728
+ blocked: declaresBlocked(selfEval),
729
+ proposalPresent: selfEval.round_contract !== undefined,
730
+ activeContract,
731
+ closure: statuses.closure,
732
+ contractCapability,
733
+ successorRoundAvailable,
734
+ });
735
+ if (!requirement.required)
736
+ return null;
737
+ return makeVerificationFlag({
738
+ severity: "error",
739
+ field: "round_contract",
740
+ check: CHECK_CONTRACT_REQUIRED,
741
+ detail: requirement.reason === "no_active_contract"
742
+ ? "A verification command is configured and this round continues, but no " +
743
+ "Round Contract is active for the next round and none was proposed. " +
744
+ "Submit round_contract with a work_item and at least one item bound to a " +
745
+ "configured after-capable command."
746
+ : "The active Round Contract closed this round and this round continues, " +
747
+ "but no successor round_contract was proposed. Submit the next slice's " +
748
+ "contract (a new work_item with acceptance items).",
749
+ });
750
+ }
751
+ /** v4.0.6: a successor for a closed contract must bring genuinely new
752
+ * acceptance conditions. Re-stating the same items would re-activate the old
753
+ * slice and re-verify it. Changing only work_item/scope is not a new slice. */
754
+ function checkContractSuccessorUnchanged(activeContract, statuses, selfEval) {
755
+ if (!activeContract)
756
+ return null;
757
+ if (statuses.closure === "open")
758
+ return null;
759
+ const proposal = selfEval.round_contract;
760
+ if (!proposal)
761
+ return null;
762
+ if (acceptanceItemsDiffer(proposal, activeContract))
763
+ return null;
764
+ return makeVerificationFlag({
765
+ severity: "error",
766
+ field: "round_contract",
767
+ check: CHECK_CONTRACT_SUCCESSOR_UNCHANGED,
768
+ detail: "The proposed successor repeats the closed contract's acceptance items " +
769
+ "verbatim. A successor must bring new acceptance conditions; changing only " +
770
+ "work_item or scope is not a new slice.",
992
771
  });
993
772
  }
994
773
  // Main entry point
@@ -1065,59 +844,63 @@ function checkUserGateUnresolved(selfEval, gateEntries) {
1065
844
  });
1066
845
  }
1067
846
  export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceDeltas = [],
1068
- /** v2.13: Files from skipped backtrack rounds. If the agent's
1069
- * files_changed overlaps significantly with these, the workspace
1070
- * was not properly restored before working. */
1071
- backtrackSkippedFiles = [],
1072
- /** M3 (v3.7.x): skipped-file git fingerprints at their failed rounds —
1073
- * machine proof of "untouched since the rollback" for the restore check. */
1074
- backtrackSkippedFingerprints = {},
1075
- /** v2.12: Git HEAD commit of the backtrack restore point. When set, the
1076
- * current git snapshot must sit at this commit — otherwise the workspace
1077
- * was not restored and the round cannot be accepted. */
1078
- backtrackTargetGitHead,
847
+ /** v3.7.1: gate records (task_type gate_opened / gate_decision) live
848
+ * outside the round prefix — the caller passes them in explicitly. */
1079
849
  /** v3.7.1: gate records (task_type gate_opened / gate_decision) live
1080
850
  * outside the round prefix — the caller passes them in explicitly. */
1081
851
  gateEntries = [],
1082
852
  /** v3.8.3: entrypoint files that no longer match the trusted round-start
1083
853
  * baseline. Derived by the caller (it needs the in-flight trust map and
1084
854
  * filesystem access); this function stays a pure function of its inputs. */
1085
- entrypointDrift = [], beforeEvidence = [],
1086
- /** v3.9: the restore point's own dirty files (undefined = never observed). */
1087
- backtrackTargetDirty,
855
+ entrypointDrift = [],
1088
856
  /** v3.9.4: whether the raw submission attempted
1089
857
  * `execution_report.files_changed` (from the persisted submittedFields
1090
858
  * manifest). Undefined falls back to "an execution report exists". */
1091
- filesReported) {
859
+ filesReported,
860
+ /** v4.0.3: the ONE `RoundFacts` bundle the coordinator derived before
861
+ * verification. When supplied, the gate consumes it instead of deriving
862
+ * the active contract / item statuses a second time. The fallback exists
863
+ * for direct library callers and unit tests; it calls the same derivation
864
+ * function, so there is still exactly one rule. */
865
+ facts, maxRounds = Number.POSITIVE_INFINITY) {
1092
866
  const flags = [];
1093
867
  const filesReportedForGate = filesReported ?? (selfEval.execution_report != null);
1094
- // Collect violations from all previous vault entries for duplicate-discovery
1095
- // and other checks that need deeper history.
1096
- const olderViolations = [];
1097
- for (const entry of vaultEntries) {
1098
- for (const v of entryViolations(entry))
1099
- olderViolations.push(v);
1100
- }
1101
868
  // v3.4: The ACTIVE contract this round executed under — derived from the
1102
869
  // committed :feedback evals of earlier rounds. The submission's own
1103
870
  // round_contract is a PROPOSAL for the next round and never participates
1104
871
  // (the eval under verification is not yet committed, so it structurally
1105
872
  // cannot influence the derivation).
1106
873
  const committedRounds = derivationRounds(vaultEntries, currentRound);
1107
- const activeContract = deriveActiveRoundContract(committedRounds);
1108
- // v3.8: The item-level status of the ACTIVE contract for THIS round — the
1109
- // same reducer the compile path, audit, and explain consume.
1110
- const contractStatuses = deriveContractItemStatuses({
1111
- contract: activeContract,
874
+ const olderViolations = [];
875
+ for (const view of committedRounds) {
876
+ for (const v of view.constraintViolations ?? [])
877
+ olderViolations.push(v);
878
+ }
879
+ // v4.0.3: the coordinator computes this bundle once and passes it in. A
880
+ // direct caller gets the SAME derivation — never a second rule.
881
+ const roundFacts = facts ?? deriveRoundFacts({
1112
882
  rounds: committedRounds,
1113
883
  currentRound,
1114
- currentReport: selfEval.execution_report ?? null,
1115
- currentDeltas: evidenceDeltas,
884
+ inFlight: {
885
+ currentReport: selfEval.execution_report ?? null,
886
+ currentDeltas: evidenceDeltas,
887
+ currentBlocked: declaresBlocked(selfEval),
888
+ // v3.9.9/v4.0.6: without this, debt/premature checks cannot see a
889
+ // superseded closure and would report the old contract as merely open.
890
+ currentRedirect: selfEval.round_contract?.supersedes ?? null,
891
+ },
1116
892
  commands: getPolicy().evidence.commands ?? [],
1117
893
  });
1118
- // v2.12: Derived claim provenance — which reported criteria completions
1119
- // are backed by machine evidence (pure derivation, not persisted).
1120
- const claimView = deriveClaimView(selfEval, evidenceDeltas);
894
+ const activeContract = roundFacts.activeContract;
895
+ const contractStatuses = roundFacts.itemStatuses;
896
+ // v4.0.3: the ONLY machine-verified criterion ids are contract-bound
897
+ // criterion facts and verified active-contract items. Agent claims and
898
+ // self-reported tests never enter this set.
899
+ const machineVerified = machineVerifiedCriterionIds({
900
+ criterionFacts: roundFacts.criterionFacts,
901
+ activeContract,
902
+ itemStatuses: contractStatuses,
903
+ });
1121
904
  // v3.2: Runtime machine-verification status (git observed / command passed /
1122
905
  // absent). v3.6: feeds the merged R8 check (providerStatus lens) that
1123
906
  // never self-skips.
@@ -1132,34 +915,30 @@ filesReported) {
1132
915
  // v2.12/v3.6/v3.7: the single success-evidence semantic — empty/missing-
1133
916
  // evidence arm (ex-R3) + claims arm (providerStatus lens, tamper-aware)
1134
917
  () => checkSuccessWithoutVerifiedEvidence(selfEval, evidenceStatus),
1135
- // v2.12: criteria met but none machine-verified (mild criterion-specific rule)
1136
- () => checkUnverifiedCriteriaClaims(selfEval, claimView),
1137
- // Declared outcome vs the required core success flag.
1138
- () => checkOutcomeConsistency(selfEval),
918
+ // v2.12/v4.0.3: per-claim machine verification
919
+ () => checkUnverifiedCriteriaClaims(selfEval, machineVerified),
1139
920
  // v2.12: retroactive claims against prior rounds
1140
921
  () => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
1141
922
  () => checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations),
1142
- () => checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound),
923
+ () => checkRecurringViolation(selfEval, prevSelfEval, committedRounds, currentRound),
1143
924
  () => checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound),
1144
925
  // v1.18: Cross-validate agent-reported files_changed against git evidence
1145
926
  () => checkEvidenceIntegrity(selfEval, evidenceDeltas, filesReportedForGate),
1146
927
  () => checkRequiredCommandEvidence(selfEval, evidenceDeltas),
1147
- // v2.0: Cross-validate agent-reported tests_reported against command output
1148
- () => checkCommandEvidenceIntegrity(selfEval, evidenceDeltas),
1149
928
  // v3.3: Verification domain integrity — command entrypoint / test files
1150
929
  // changed in the same round the command ran
1151
930
  () => checkVerificationDomainIntegrity(evidenceDeltas, entrypointDrift),
1152
- // v2.13: Post-backtrack workspace restore check
1153
- () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceDeltas),
1154
- // v2.12: Post-backtrack git HEAD restore check
1155
- () => checkBacktrackGitHeadRestore(evidenceDeltas, backtrackTargetGitHead, beforeEvidence, backtrackTargetDirty),
931
+ // v3.9.7: restore verification happens before the redo prompt, not on
932
+ // the redo submission.
1156
933
  // v3.8: Round Contract checks over the derived item model. Declaration
1157
934
  // quality is enforced structurally (contract_invalid) before the round
1158
935
  // advances, so the gate only reports execution state: scope drift,
1159
936
  // claimed-but-unverified items, and an ignored replacement proposal.
1160
937
  () => checkRoundScopeDrift(activeContract, evidenceDeltas),
1161
- () => checkContractItemsUnverified(contractStatuses),
938
+ () => checkContractItemsUnverified(contractStatuses, selfEval.execution_report ?? null),
1162
939
  () => checkContractPremature(activeContract, contractStatuses, selfEval),
940
+ () => checkContractRequired(activeContract, contractStatuses, selfEval, deriveConfiguredCapability(getPolicy()).contractVerificationAvailable, currentRound < maxRounds),
941
+ () => checkContractSuccessorUnchanged(activeContract, contractStatuses, selfEval),
1163
942
  // v3.7.1: opt-in gate-layer blocking (skipped when disabled)
1164
943
  () => checkUserGateUnresolved(selfEval, gateEntries),
1165
944
  ];