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
@@ -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",
@@ -534,10 +487,12 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
534
487
  }
535
488
  /** v1.18: Cross-validate agent-reported files_changed against git evidence
536
489
  * from the configured evidence providers (MachineObservation array). */
537
- function checkEvidenceIntegrity(selfEval, evidenceDeltas) {
538
- if (!selfEval.execution_report)
539
- return null;
540
- const reconciliation = deriveFilesReconciliation(selfEval.execution_report, evidenceDeltas);
490
+ function checkEvidenceIntegrity(selfEval, evidenceDeltas,
491
+ /** v3.9.4: whether the raw submission named
492
+ * `execution_report.files_changed`. The receipt uses the SAME manifest, so
493
+ * the two projections can never disagree about an omitted report. */
494
+ filesReported) {
495
+ const reconciliation = deriveFilesReconciliation(selfEval.execution_report ?? null, evidenceDeltas, { attempted: filesReported });
541
496
  if (reconciliation.status !== "mismatch")
542
497
  return null;
543
498
  const parts = [];
@@ -576,124 +531,7 @@ function checkRequiredCommandEvidence(selfEval, evidenceDeltas) {
576
531
  field: "execution_report",
577
532
  check: CHECK_REQUIRED_COMMAND_FAILED,
578
533
  detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
579
- });
580
- }
581
- return null;
582
- }
583
- // ── Test output parsing ────────────────────────────────────────────────────
584
- /** Whether a changed path looks like a test file. */
585
- function isTestFile(path) {
586
- return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
587
- /(^|[\\/])tests?[\\/]/i.test(path) ||
588
- /_test\.[a-z0-9]+$/i.test(path);
589
- }
590
- /** Best-effort parse of test counts from common test-runner output formats.
591
- *
592
- * Scans the last 2000 characters of stdout (where summary lines typically
593
- * appear) and tries patterns in descending specificity order. Returns null
594
- * when the output format is unrecognized, stdout is empty, or a confident
595
- * parse cannot be made.
596
- *
597
- * Recognized formats: Jest verbose/compact, Mocha, pytest/unittest, Go test,
598
- * and PHPUnit OK summaries. */
599
- export function parseTestOutput(stdout) {
600
- if (!stdout)
601
- return null;
602
- const tail = stdout.length > 2000 ? stdout.slice(-2000) : stdout;
603
- let match;
604
- // 1. Jest verbose: "Tests: 1 failed, 8 passed, 9 total"
605
- match = tail.match(/Tests:\s*(\d+)\s+failed,\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
606
- if (match) {
607
- return { failed: Number(match[1]), passed: Number(match[2]), total: Number(match[3]), skipped: 0 };
608
- }
609
- // 2. Jest compact: "Tests: 8 passed, 9 total"
610
- match = tail.match(/Tests:\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
611
- if (match) {
612
- const passed = Number(match[1]);
613
- const total = Number(match[2]);
614
- return { passed, failed: total - passed, total, skipped: 0 };
615
- }
616
- // 3. Mocha: "8 passing" + optional "2 failing"
617
- const mochaPass = tail.match(/(\d+)\s+passing/i);
618
- if (mochaPass) {
619
- const mochaFail = tail.match(/(\d+)\s+failing/i);
620
- const passed = Number(mochaPass[1]);
621
- const failed = mochaFail ? Number(mochaFail[1]) : 0;
622
- return { passed, failed, total: passed + failed, skipped: 0 };
623
- }
624
- // 4. Go test: count "--- PASS:" and "--- FAIL:" lines
625
- const goPassCount = (tail.match(/---\s+PASS:/g) || []).length;
626
- const goFailCount = (tail.match(/---\s+FAIL:/g) || []).length;
627
- if (goPassCount > 0 || goFailCount > 0) {
628
- return { passed: goPassCount, failed: goFailCount, total: goPassCount + goFailCount, skipped: 0 };
629
- }
630
- // 5. pytest / unittest: "8 passed, 1 failed" or "8 passed"
631
- match = tail.match(/(\d+)\s+passed[,;\s]+(\d+)\s+failed/i);
632
- if (match) {
633
- return { passed: Number(match[1]), failed: Number(match[2]), total: Number(match[1]) + Number(match[2]), skipped: 0 };
634
- }
635
- // pytest compact: "8 passed" (with a reasonable context hint)
636
- match = tail.match(/(?:=\s+test session|\b(?:passed|failed)\b)/i);
637
- if (match) {
638
- const pCount = tail.match(/(\d+)\s+passed/i);
639
- const fCount = tail.match(/(\d+)\s+failed/i);
640
- if (pCount || fCount) {
641
- const passed = pCount ? Number(pCount[1]) : 0;
642
- const failed = fCount ? Number(fCount[1]) : 0;
643
- return { passed, failed, total: passed + failed, skipped: 0 };
644
- }
645
- }
646
- // 6. PHPUnit: "OK (8 tests, 16 assertions)"
647
- match = tail.match(/OK\s*\((\d+)\s+tests?/i);
648
- if (match) {
649
- const total = Number(match[1]);
650
- return { passed: total, failed: 0, total, skipped: 0 };
651
- }
652
- return null;
653
- }
654
- /** Cross-validate agent-reported tests_reported against command evidence output.
655
- *
656
- * For each command provider whose status is "passed" and output is not
657
- * truncated, attempts to parse test counts from stdout. Mismatched counts
658
- * produce a warn-level flag; failures hidden by the agent (reported 0 failed
659
- * when the command shows >0) produce an error-level flag.
660
- *
661
- * Non-command evidence providers are skipped (no structural cross-check is
662
- * defined for them yet). */
663
- function checkCommandEvidenceIntegrity(selfEval, evidenceDeltas) {
664
- const reported = selfEval.execution_report?.tests_reported;
665
- if (!reported)
666
- return null;
667
- for (const snapshot of afterObservations(evidenceDeltas)) {
668
- if (snapshot.kind !== "command")
669
- continue;
670
- if (snapshot.status !== "passed")
671
- continue;
672
- if (snapshot.data.truncated === true)
673
- continue;
674
- const stdout = typeof snapshot.data.stdoutExcerpt === "string" ? snapshot.data.stdoutExcerpt : "";
675
- const parsed = parseTestOutput(stdout);
676
- if (!parsed)
677
- continue;
678
- const mismatches = [];
679
- if (parsed.passed !== reported.passed) {
680
- mismatches.push(`passed: reported ${reported.passed}, command shows ${parsed.passed}`);
681
- }
682
- if (parsed.failed !== reported.failed) {
683
- mismatches.push(`failed: reported ${reported.failed}, command shows ${parsed.failed}`);
684
- }
685
- if (mismatches.length === 0) {
686
- // Counts match exactly — evidence supports the agent's claim.
687
- continue;
688
- }
689
- const name = snapshot.data.commandId;
690
- // Agent hides failures → error. Other mismatch → warn.
691
- const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
692
- return makeVerificationFlag({
693
- severity,
694
- field: "tests_reported",
695
- check: CHECK_COMMAND_EVIDENCE_MISMATCH,
696
- detail: `Agent tests_reported don't match "${name}" output: ${mismatches.join("; ")}`,
534
+ ref: name,
697
535
  });
698
536
  }
699
537
  return null;
@@ -741,10 +579,9 @@ function checkVerificationDomainIntegrity(evidenceDeltas, drift) {
741
579
  (changed.length > 0
742
580
  ? "At round start: " + changed.map((entry) => entry.roundStart === "absent"
743
581
  ? `"${entry.file}" did not exist`
744
- : `"${entry.file}" was present`).join("; ") + ". "
745
- : "") +
746
- "Keep the verification command stable or resubmit the same round with the " +
747
- "entrypoint unchanged.",
582
+ : `"${entry.file}" was present`).join("; ") + "."
583
+ : ""),
584
+ ref: snapshot.data.commandId,
748
585
  });
749
586
  }
750
587
  // Only when NO entrypoint was tampered does the test-file warn stand in.
@@ -761,136 +598,11 @@ function checkVerificationDomainIntegrity(evidenceDeltas, drift) {
761
598
  check: CHECK_TEST_FILES_MODIFIED,
762
599
  detail: `Test files changed in the same round command "${snapshot.data.commandId}" passed — ` +
763
600
  "confirm the tests still verify the task claims.",
601
+ ref: snapshot.data.commandId,
764
602
  });
765
603
  }
766
604
  return null;
767
605
  }
768
- /** v2.13: After a backtrack, check that the agent restored the workspace
769
- * before working. Two arms:
770
- *
771
- * 1. Machine arm (M3, error): a skipped file whose CURRENT git fingerprint
772
- * still equals the fingerprint recorded at its failed round was never
773
- * touched since the rollback — the workspace was provably not restored.
774
- * Restored or re-modified files change fingerprint and can never be
775
- * misjudged by this arm.
776
- *
777
- * 2. Claims arm (warn only): the agent's files_changed overlaps the skipped
778
- * list. A self-report alone cannot distinguish a restored-then-redone
779
- * file from a never-restored one — a legitimate multi-file redo would
780
- * otherwise be falsely terminated — so overlap is guidance, not a
781
- * verdict. (The old error tier for >= 3 overlapping files was removed
782
- * for exactly this reason: it fired on honest redos.) */
783
- function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints = {}, evidenceDeltas = []) {
784
- // Machine arm — untouched-since-rollback proof, independent of claims.
785
- const failedEntries = Object.entries(backtrackSkippedFingerprints);
786
- if (failedEntries.length > 0) {
787
- const afterGit = evidenceDeltas.find((delta) => delta.kind === "git")?.after;
788
- const currentFingerprints = afterGit && afterGit.kind === "git" &&
789
- isRecord(afterGit.data.fingerprints)
790
- ? afterGit.data.fingerprints
791
- : null;
792
- if (currentFingerprints) {
793
- const untouched = failedEntries
794
- .filter(([file, failedFp]) => currentFingerprints[file] === failedFp)
795
- .map(([file]) => file)
796
- .sort();
797
- if (untouched.length > 0) {
798
- return makeVerificationFlag({
799
- severity: "error",
800
- field: "workspace",
801
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
802
- detail: `File(s) from the failed rounds are byte-identical to their ` +
803
- `state at the rollback: ${untouched.slice(0, 5).join(", ")}. The ` +
804
- `workspace was never restored — restore it to the restore point ` +
805
- `(git checkout/reset) before redoing the work.`,
806
- });
807
- }
808
- }
809
- }
810
- const ev = selfEval.execution_report;
811
- if (!ev || (ev.files_changed ?? []).length === 0)
812
- return null;
813
- // Claims arm — overlap as guidance only.
814
- const overlap = (ev.files_changed ?? []).filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
815
- if (overlap.length === 0)
816
- return null;
817
- return makeVerificationFlag({
818
- severity: "warn",
819
- field: "files_changed",
820
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
821
- detail: `Agent modified files that overlap with skipped rounds: ` +
822
- `${overlap.join(", ")}. Verify the workspace was restored.`,
823
- });
824
- }
825
- /** v2.12: After a backtrack, the workspace must return to the restore point:
826
- * the same git HEAD that round committed, carrying no dirty file the restore
827
- * point did not already have. Compares HEAD's first 12 characters (short
828
- * hash), matching how git itself disambiguates.
829
- *
830
- * The dirty-set comparison is against the restore point's OWN baseline, not
831
- * an empty tree: unrelated work in progress in the same workspace is no
832
- * evidence of a failed restore, while a file the failed rounds left behind
833
- * is — it is dirty now and was not dirty then. An absent baseline means the
834
- * restore point's dirt was never observed, so only HEAD can be judged.
835
- *
836
- * A redo prepared before the agent restores the workspace has a stale before
837
- * snapshot, so its first submission is a recovery checkpoint; once the
838
- * restore is proven, later work may move HEAD freely. */
839
- function checkBacktrackGitHeadRestore(evidenceDeltas, backtrackTargetGitHead, beforeEvidence = [],
840
- /** The restore point's own dirty files — undefined when never observed. */
841
- backtrackTargetDirty) {
842
- if (!backtrackTargetGitHead)
843
- return null;
844
- const short = (hash) => hash.slice(0, 12);
845
- const readGit = (observations) => {
846
- const snapshot = observations.find((item) => item.providerId === "git" && item.status === "observed" && isRecord(item.data) &&
847
- typeof item.data.head === "string");
848
- if (!snapshot || !isRecord(snapshot.data))
849
- return null;
850
- const data = snapshot.data;
851
- // v3.9.1: the workspace's dirty set is one explicit list. "Tracked /
852
- // staged / untracked" were dirty-since-HEAD categories, not the complete
853
- // boundary, and are gone.
854
- const dirty = Array.isArray(data.dirty)
855
- ? data.dirty.filter((file) => typeof file === "string")
856
- : [];
857
- return { head: data.head, dirty };
858
- };
859
- const foreignDirty = (dirty) => Array.isArray(backtrackTargetDirty)
860
- ? dirty.filter((file) => !backtrackTargetDirty.includes(file))
861
- : [];
862
- const restored = (git) => git !== null && git.head.length > 0 &&
863
- short(git.head) === short(backtrackTargetGitHead) &&
864
- foreignDirty(git.dirty).length === 0;
865
- // When restoration happened before the redo was prepared, the before
866
- // snapshot already proves it. The redo may then commit new work.
867
- if (restored(readGit(beforeEvidence)))
868
- return null;
869
- const afterGit = readGit(afterObservations(evidenceDeltas));
870
- if (!afterGit)
871
- return null; // git unavailable → cannot verify → skip (fail-open)
872
- if (restored(afterGit))
873
- return null;
874
- // Normal lifecycle: prepare captured the pre-restore workspace. The first
875
- // submission must prove restoration before a later submission can commit.
876
- const headBack = afterGit.head.length > 0 &&
877
- short(afterGit.head) === short(backtrackTargetGitHead);
878
- const left = foreignDirty(afterGit.dirty);
879
- return makeVerificationFlag({
880
- severity: "error",
881
- field: "workspace",
882
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
883
- detail: headBack
884
- ? `Workspace HEAD is back at the backtrack restore commit ` +
885
- `(${short(backtrackTargetGitHead)}) but these files were not there at ` +
886
- `the restore point and are still changed: ${left.slice(0, 5).join(", ")}` +
887
- `${left.length > 5 ? ` (+${left.length - 5} more)` : ""}. Revert them ` +
888
- `before redoing the work.`
889
- : `Workspace HEAD (${short(afterGit.head)}) does not match the backtrack ` +
890
- `restore commit (${short(backtrackTargetGitHead)}). Submit one recovery ` +
891
- `evaluation at the restore point before redoing work.`,
892
- });
893
- }
894
606
  // ═══════════════════════════════════════════════════════════════════════════
895
607
  // v3.8 — Round Contract checks (all silent when no contract is declared)
896
608
  //
@@ -930,58 +642,132 @@ function checkRoundScopeDrift(activeContract, evidenceDeltas) {
930
642
  if (outOfScope === null || outOfScope.length === 0)
931
643
  return null;
932
644
  return makeVerificationFlag({
933
- 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",
934
649
  field: "round_contract",
935
650
  check: CHECK_ROUND_SCOPE_DRIFT,
936
- 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)` +
937
653
  (outOfScope.length > 5
938
654
  ? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
939
- : `: ${outOfScope.join(", ")}`) +
940
- `. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
655
+ : `: ${outOfScope.join(", ")}`),
941
656
  });
942
657
  }
943
658
  /** v3.8: Claimed-but-unverified contract items. The round COMMITS — the debt
944
659
  * is surfaced (prompt, state file, handoff) and the enforcement gate's
945
660
  * verification-debt row handles a persistent pattern. */
946
- function checkContractItemsUnverified(statuses) {
661
+ function checkContractItemsUnverified(statuses, currentReport) {
947
662
  if (statuses.insufficientCount === 0)
948
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
+ }
676
+ // v3.9.4: EVERY insufficient item is named. The previous 3-item cap hid the
677
+ // rest of the debt from the one repair surface the agent reads.
949
678
  const unverified = statuses.items
950
679
  .filter((item) => item.status === "insufficient")
951
- .slice(0, 3)
952
680
  .map((item) => item.itemId);
953
681
  return makeVerificationFlag({
954
682
  severity: "warn",
955
683
  field: "round_contract",
956
684
  check: CHECK_CONTRACT_ITEMS_UNVERIFIED,
957
685
  detail: `${statuses.insufficientCount} contract item(s) claimed met but not machine-verified: ` +
958
- `${unverified.join(", ")}. Run the bound command(s) or report the item as remaining.`,
686
+ `${unverified.join(", ")}.`,
959
687
  });
960
688
  }
961
- /** A different contract proposed while the ACTIVE contract is still open. The
962
- * walker ignores the premature proposal — this warn only makes the ignored
963
- * state visible. Silent when the active contract is closed (all items
964
- * verified, or outcome=blocked), when no proposal is submitted, or when the
965
- * 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. */
966
696
  function checkContractPremature(activeContract, statuses, selfEval) {
967
697
  if (!activeContract)
968
698
  return null;
969
699
  if (statuses.closure !== "open")
970
700
  return null; // closed
971
- if (selfEval.outcome === "blocked")
701
+ if (declaresBlocked(selfEval))
972
702
  return null; // closed
973
703
  const proposal = selfEval.round_contract;
974
704
  if (!proposal)
975
705
  return null;
706
+ if (proposal.supersedes?.contract_id === activeContract.id)
707
+ return null; // explicit redirect
976
708
  if (sameContract(proposal, activeContract))
977
709
  return null; // restate → continue
978
710
  return makeVerificationFlag({
979
- severity: "warn",
711
+ severity: "error",
980
712
  field: "round_contract",
981
713
  check: CHECK_CONTRACT_PREMATURE,
982
714
  detail: "The ACTIVE Round Contract is still open (not every item verified, not blocked) " +
983
- "while a different contract was proposed — it is ignored until the active " +
984
- "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.",
985
771
  });
986
772
  }
987
773
  // Main entry point
@@ -1058,54 +844,63 @@ function checkUserGateUnresolved(selfEval, gateEntries) {
1058
844
  });
1059
845
  }
1060
846
  export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceDeltas = [],
1061
- /** v2.13: Files from skipped backtrack rounds. If the agent's
1062
- * files_changed overlaps significantly with these, the workspace
1063
- * was not properly restored before working. */
1064
- backtrackSkippedFiles = [],
1065
- /** M3 (v3.7.x): skipped-file git fingerprints at their failed rounds —
1066
- * machine proof of "untouched since the rollback" for the restore check. */
1067
- backtrackSkippedFingerprints = {},
1068
- /** v2.12: Git HEAD commit of the backtrack restore point. When set, the
1069
- * current git snapshot must sit at this commit — otherwise the workspace
1070
- * was not restored and the round cannot be accepted. */
1071
- 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. */
1072
849
  /** v3.7.1: gate records (task_type gate_opened / gate_decision) live
1073
850
  * outside the round prefix — the caller passes them in explicitly. */
1074
851
  gateEntries = [],
1075
852
  /** v3.8.3: entrypoint files that no longer match the trusted round-start
1076
853
  * baseline. Derived by the caller (it needs the in-flight trust map and
1077
854
  * filesystem access); this function stays a pure function of its inputs. */
1078
- entrypointDrift = [], beforeEvidence = [],
1079
- /** v3.9: the restore point's own dirty files (undefined = never observed). */
1080
- backtrackTargetDirty) {
855
+ entrypointDrift = [],
856
+ /** v3.9.4: whether the raw submission attempted
857
+ * `execution_report.files_changed` (from the persisted submittedFields
858
+ * manifest). Undefined falls back to "an execution report exists". */
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) {
1081
866
  const flags = [];
1082
- // Collect violations from all previous vault entries for duplicate-discovery
1083
- // and other checks that need deeper history.
1084
- const olderViolations = [];
1085
- for (const entry of vaultEntries) {
1086
- for (const v of entryViolations(entry))
1087
- olderViolations.push(v);
1088
- }
867
+ const filesReportedForGate = filesReported ?? (selfEval.execution_report != null);
1089
868
  // v3.4: The ACTIVE contract this round executed under — derived from the
1090
869
  // committed :feedback evals of earlier rounds. The submission's own
1091
870
  // round_contract is a PROPOSAL for the next round and never participates
1092
871
  // (the eval under verification is not yet committed, so it structurally
1093
872
  // cannot influence the derivation).
1094
873
  const committedRounds = derivationRounds(vaultEntries, currentRound);
1095
- const activeContract = deriveActiveRoundContract(committedRounds);
1096
- // v3.8: The item-level status of the ACTIVE contract for THIS round — the
1097
- // same reducer the compile path, audit, and explain consume.
1098
- const contractStatuses = deriveContractItemStatuses({
1099
- 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({
1100
882
  rounds: committedRounds,
1101
883
  currentRound,
1102
- currentReport: selfEval.execution_report ?? null,
1103
- 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
+ },
1104
892
  commands: getPolicy().evidence.commands ?? [],
1105
893
  });
1106
- // v2.12: Derived claim provenance — which reported criteria completions
1107
- // are backed by machine evidence (pure derivation, not persisted).
1108
- 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
+ });
1109
904
  // v3.2: Runtime machine-verification status (git observed / command passed /
1110
905
  // absent). v3.6: feeds the merged R8 check (providerStatus lens) that
1111
906
  // never self-skips.
@@ -1120,34 +915,30 @@ backtrackTargetDirty) {
1120
915
  // v2.12/v3.6/v3.7: the single success-evidence semantic — empty/missing-
1121
916
  // evidence arm (ex-R3) + claims arm (providerStatus lens, tamper-aware)
1122
917
  () => checkSuccessWithoutVerifiedEvidence(selfEval, evidenceStatus),
1123
- // v2.12: criteria met but none machine-verified (mild criterion-specific rule)
1124
- () => checkUnverifiedCriteriaClaims(selfEval, claimView),
1125
- // Declared outcome vs the required core success flag.
1126
- () => checkOutcomeConsistency(selfEval),
918
+ // v2.12/v4.0.3: per-claim machine verification
919
+ () => checkUnverifiedCriteriaClaims(selfEval, machineVerified),
1127
920
  // v2.12: retroactive claims against prior rounds
1128
921
  () => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
1129
922
  () => checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations),
1130
- () => checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound),
923
+ () => checkRecurringViolation(selfEval, prevSelfEval, committedRounds, currentRound),
1131
924
  () => checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound),
1132
925
  // v1.18: Cross-validate agent-reported files_changed against git evidence
1133
- () => checkEvidenceIntegrity(selfEval, evidenceDeltas),
926
+ () => checkEvidenceIntegrity(selfEval, evidenceDeltas, filesReportedForGate),
1134
927
  () => checkRequiredCommandEvidence(selfEval, evidenceDeltas),
1135
- // v2.0: Cross-validate agent-reported tests_reported against command output
1136
- () => checkCommandEvidenceIntegrity(selfEval, evidenceDeltas),
1137
928
  // v3.3: Verification domain integrity — command entrypoint / test files
1138
929
  // changed in the same round the command ran
1139
930
  () => checkVerificationDomainIntegrity(evidenceDeltas, entrypointDrift),
1140
- // v2.13: Post-backtrack workspace restore check
1141
- () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceDeltas),
1142
- // v2.12: Post-backtrack git HEAD restore check
1143
- () => checkBacktrackGitHeadRestore(evidenceDeltas, backtrackTargetGitHead, beforeEvidence, backtrackTargetDirty),
931
+ // v3.9.7: restore verification happens before the redo prompt, not on
932
+ // the redo submission.
1144
933
  // v3.8: Round Contract checks over the derived item model. Declaration
1145
934
  // quality is enforced structurally (contract_invalid) before the round
1146
935
  // advances, so the gate only reports execution state: scope drift,
1147
936
  // claimed-but-unverified items, and an ignored replacement proposal.
1148
937
  () => checkRoundScopeDrift(activeContract, evidenceDeltas),
1149
- () => checkContractItemsUnverified(contractStatuses),
938
+ () => checkContractItemsUnverified(contractStatuses, selfEval.execution_report ?? null),
1150
939
  () => checkContractPremature(activeContract, contractStatuses, selfEval),
940
+ () => checkContractRequired(activeContract, contractStatuses, selfEval, deriveConfiguredCapability(getPolicy()).contractVerificationAvailable, currentRound < maxRounds),
941
+ () => checkContractSuccessorUnchanged(activeContract, contractStatuses, selfEval),
1151
942
  // v3.7.1: opt-in gate-layer blocking (skipped when disabled)
1152
943
  () => checkUserGateUnresolved(selfEval, gateEntries),
1153
944
  ];