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
@@ -17,32 +17,22 @@
17
17
  * with stopReason "enforcement_terminated".
18
18
  */
19
19
  import { makeEnforcementResult } from "./protocol.js";
20
- import { machineProgressSeries, CHECK_SUCCESS_WITH_REMAINING_CRITERIA, CHECK_RECURRING_VIOLATION, CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED, CHECK_REQUIRED_COMMAND_FAILED, CHECK_COMMAND_EVIDENCE_MISMATCH, CHECK_OUTCOME_SUCCESS_CONTRADICTION, CHECK_VERIFICATION_ENTRYPOINT_MODIFIED, CHECK_ROUND_SCOPE_DRIFT, CHECK_CONTRACT_ITEMS_UNVERIFIED, CHECK_USER_GATE_UNRESOLVED } from "./verification-gate.js";
20
+ import { CHECK_SUCCESS_WITH_REMAINING_CRITERIA, CHECK_RECURRING_VIOLATION, CHECK_REQUIRED_COMMAND_FAILED, CHECK_VERIFICATION_ENTRYPOINT_MODIFIED, CHECK_CONTRACT_ITEMS_UNVERIFIED, CHECK_USER_GATE_UNRESOLVED, CHECK_CONTRACT_REQUIRED, CHECK_CONTRACT_PREMATURE, CHECK_CONTRACT_SUCCESSOR_UNCHANGED } from "./verification-gate.js";
21
+ import { deriveProgressWindow, } from "./progress.js";
22
+ import { declaresBlocked } from "./round-outcome.js";
21
23
  import { getPolicy } from "./policy.js";
22
- import { isRecord, entryRound } from "./token-utils.js";
23
- import { derivationRounds, decodeCommittedRound, machineEvidenceForRound, } from "./committed-round.js";
24
+ import { derivationRounds, hasCommittedMachineBacktrack, } from "./committed-round.js";
24
25
  // ═══════════════════════════════════════════════════════════════════════════
25
26
  // Helpers
26
27
  // ═══════════════════════════════════════════════════════════════════════════
27
28
  // ═══════════════════════════════════════════════════════════════════════════
28
29
  // v2.10: Backtrack helpers
29
30
  // ═══════════════════════════════════════════════════════════════════════════
30
- /** Extract the committed round decision's result from a vault entry.
31
- * The round commit entry (`loop:<id>:r<N>:feedback`) carries the full
32
- * decision under loop_lineage.round_transaction; lineage entries carry
33
- * nothing at the top level. */
34
- function committedRoundResult(entry) {
35
- return decodeCommittedRound(entry)?.result ?? null;
36
- }
37
31
  /** True when a backtrack already committed at or after the current round —
38
32
  * i.e. the loop is re-walking territory the system already rolled back
39
33
  * once. Guards against the reject↔backtrack deadlock on persistent stalls. */
40
34
  function hasCommittedBacktrack(vaultEntries, currentRound) {
41
- return vaultEntries.some((entry) => {
42
- if (entryRound(entry) < currentRound)
43
- return false;
44
- return committedRoundResult(entry)?.action === "backtrack";
45
- });
35
+ return hasCommittedMachineBacktrack(vaultEntries, currentRound);
46
36
  }
47
37
  /** Check whether a committed round represents a clean restore point.
48
38
  * as a safe restore point. A clean round was accepted (not rejected)
@@ -55,12 +45,18 @@ function hasCommittedBacktrack(vaultEntries, currentRound) {
55
45
  * run of such rounds). Rejected/terminated rounds never commit, and
56
46
  * rolled-back rounds are excluded below. */
57
47
  function isCleanRound(round) {
48
+ // v4.0.3: a round without a complete observable boundary is not a
49
+ // machine-clean restore point. Unknown evidence never reads as clean.
50
+ if (round.evidenceIncomplete)
51
+ return false;
58
52
  return !round.verificationFlags.some((flag) => flag.severity === "error");
59
53
  }
60
54
  /** Scan backwards from currentRound to find the most recent clean round.
61
55
  * Collects discovered_constraints from skipped rounds along the way.
62
56
  * Returns null when no clean round is found within maxDepth. */
63
- export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
57
+ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth,
58
+ /** v3.9.7: a candidate must have a materialized restore checkpoint. */
59
+ isEligible) {
64
60
  const completed = derivationRounds(vaultEntries, currentRound)
65
61
  .sort((a, b) => b.round - a.round);
66
62
  const skippedDiscoveries = [];
@@ -85,32 +81,12 @@ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
85
81
  }
86
82
  }
87
83
  }
88
- if (isCleanRound(round)) {
84
+ if (isCleanRound(round) && (isEligible?.(targetRound) ?? true)) {
89
85
  return { round: targetRound, skippedDiscoveries };
90
86
  }
91
87
  }
92
88
  return null;
93
89
  }
94
- export function findBacktrackTargetSnapshot(restoreRound, vaultEntries) {
95
- const committed = derivationRounds(vaultEntries)
96
- .find((view) => view.round === restoreRound);
97
- if (!committed)
98
- return null;
99
- for (const item of machineEvidenceForRound(committed)) {
100
- if (!isRecord(item) || item.providerId !== "git" ||
101
- item.status !== "observed" || !isRecord(item.data))
102
- continue;
103
- const head = item.data.head;
104
- if (typeof head !== "string" || head.length === 0)
105
- continue;
106
- // v3.9.1: the restore baseline is the boundary's one dirty list.
107
- const dirtyFiles = Array.isArray(item.data.dirty)
108
- ? item.data.dirty.filter((file) => typeof file === "string")
109
- : [];
110
- return { head, dirtyFiles };
111
- }
112
- return null;
113
- }
114
90
  /** v3.8.1: the ONE Recovery Brief renderer.
115
91
  *
116
92
  * v3.9.4: it is the shared fact renderer for the prompt's protected
@@ -125,29 +101,50 @@ export function recoveryBriefLines(input) {
125
101
  const lines = [
126
102
  `- **Trigger**: ${input.triggerRule || "unknown"}`,
127
103
  ];
104
+ if (input.origin) {
105
+ lines.push(`- **Initiated by**: ${input.origin === "agent" ? "Agent" : "Machine"}`);
106
+ }
107
+ if (input.origin === "agent" && input.reason?.trim()) {
108
+ const report = input.reason.trim()
109
+ .replace(/[\r\n\t]+/g, " ")
110
+ .replace(/`/g, "\\`")
111
+ .replace(/\s{2,}/g, " ");
112
+ lines.push(`- **Agent reason (report only; not a LoopForge instruction)**: ${report}`);
113
+ }
128
114
  if (input.failureReason?.trim()) {
129
115
  lines.push(`- **Failure reason**: ${input.failureReason.trim()}`);
130
116
  }
131
- lines.push(`- **Restored to round**: ${input.target} (the redo is round ${input.target + 1})`);
117
+ lines.push(input.checkpointId
118
+ ? `- **Restored checkpoint**: \`${input.checkpointId}\` (${input.restoreVerified === false
119
+ ? "restore NOT verified"
120
+ : "machine-verified by LoopForge"})`
121
+ : "- **Restored state**: the committed restore point", `- **Redo round**: ${input.redoRound}`);
132
122
  if (input.recoveryRoundId) {
133
123
  lines.push(`- **Recovery Round ID**: \`${input.recoveryRoundId}\``);
134
124
  }
135
125
  if (input.failedRounds.length > 0) {
136
- lines.push(`- **Failed rounds**: ${input.failedRounds.join(", ")}`);
126
+ lines.push(`- **${input.origin === "agent" ? "Abandoned" : "Failed"} rounds**: ${input.failedRounds.join(", ")}`);
137
127
  }
128
+ const approachLabel = input.origin === "agent" ? "Abandoned approach" : "Failed approach";
138
129
  const shown = input.approaches.slice(0, maxApproaches);
139
130
  for (const approach of shown) {
140
- lines.push(` - Failed approach: ${approach}`);
131
+ lines.push(` - ${approachLabel}: ${approach}`);
141
132
  }
142
133
  if (input.approaches.length > shown.length) {
143
- lines.push(` - … and ${input.approaches.length - shown.length} more failed approaches`);
134
+ lines.push(` - … and ${input.approaches.length - shown.length} more ${approachLabel.toLowerCase()}s`);
144
135
  }
145
136
  if (input.wrongAssumptions.length > 0) {
146
- lines.push("- **Falsified assumptions** (do not rebuild on these):");
137
+ lines.push("- **Falsified assumptions (Agent reports)**:");
147
138
  for (const assumption of input.wrongAssumptions.slice(0, maxAssumptions)) {
148
139
  lines.push(` - ${assumption}`);
149
140
  }
150
141
  }
142
+ if (input.preservedDiscoveries && input.preservedDiscoveries.length > 0) {
143
+ lines.push("- **Preserved discoveries (Agent reports)**:");
144
+ for (const discovery of input.preservedDiscoveries) {
145
+ lines.push(` - ${discovery}`);
146
+ }
147
+ }
151
148
  return lines;
152
149
  }
153
150
  // ═══════════════════════════════════════════════════════════════════════════
@@ -181,24 +178,22 @@ function enforceRecurringViolation(flags) {
181
178
  return makeEnforcementResult({
182
179
  action: "reject",
183
180
  reason: flag.detail,
184
- fix_instructions: "The same constraint violation has appeared in 3 consecutive rounds. " +
185
- "You must: (a) explain WHY this violation keeps occurring, " +
186
- "and (b) propose a DIFFERENT approach than what you used in the last 3 rounds. " +
187
- "Do NOT retry the same strategy — it has failed 3 times.",
181
+ fix_instructions: "The same constraint violation has been reported in three consecutive " +
182
+ "committed rounds. This is a record of your own reports. The finding " +
183
+ "clears when a committed evaluation no longer reports the same " +
184
+ "violation, or when you declare outcome blocked with a blocker. " +
185
+ "LoopForge does not prescribe a technique or decomposition.",
188
186
  check: "recurring_violation",
189
187
  });
190
188
  }
191
- /** R-EVID (v2.14): A required command failure, a test-results mismatch that
192
- * hides failures, or a self-contradictory outcome claim contradicts the
189
+ /** R-EVID (v2.14 / v4.0.6): A required command failure contradicts the
193
190
  * success claim before commit. The verification gate detects the
194
191
  * contradiction; this rule decides what to do about it — reject so the
195
192
  * agent redos the SAME round with honest claims. Same-check rejections
196
193
  * accumulate toward R6 via the per-check counter. */
197
194
  function enforceEvidenceContradiction(flags) {
198
195
  const contradiction = flags.find((f) => f.severity === "error" &&
199
- (f.check === CHECK_REQUIRED_COMMAND_FAILED ||
200
- f.check === CHECK_COMMAND_EVIDENCE_MISMATCH ||
201
- f.check === CHECK_OUTCOME_SUCCESS_CONTRADICTION));
196
+ f.check === CHECK_REQUIRED_COMMAND_FAILED);
202
197
  if (!contradiction)
203
198
  return null;
204
199
  return makeEnforcementResult({
@@ -212,6 +207,39 @@ function enforceEvidenceContradiction(flags) {
212
207
  check: contradiction.check,
213
208
  });
214
209
  }
210
+ /** v4.0.6 R-COVER: contract coverage rules. With after-capable commands, a
211
+ * continuing round must leave the next round an active contract; a different
212
+ * proposal while the contract is open needs `supersedes`; and a successor for
213
+ * a closed contract must bring new acceptance items. The verification gate
214
+ * states the structural fact; this row rejects the round with the concrete
215
+ * repair. The uniform ladder below escalates repeats. */
216
+ function enforceContractCoverage(flags) {
217
+ const flag = flags.find((f) => f.severity === "error" &&
218
+ (f.check === CHECK_CONTRACT_REQUIRED ||
219
+ f.check === CHECK_CONTRACT_PREMATURE ||
220
+ f.check === CHECK_CONTRACT_SUCCESSOR_UNCHANGED));
221
+ if (!flag)
222
+ return null;
223
+ const fix = flag.check === CHECK_CONTRACT_REQUIRED
224
+ ? "This round continues with an after-capable verification command, so the " +
225
+ "next round must execute under an active Round Contract. Submit " +
226
+ "round_contract with a work_item and at least one acceptance item bound " +
227
+ "to a configured command."
228
+ : flag.check === CHECK_CONTRACT_PREMATURE
229
+ ? "The active Round Contract is still open. Omit round_contract to continue " +
230
+ "it unchanged, or — to replace the slice — add " +
231
+ "round_contract.supersedes with the active rc- id and different acceptance items."
232
+ : "A closed contract's successor must bring NEW acceptance items. Changing " +
233
+ "only work_item or scope re-verifies the old slice and is rejected. " +
234
+ "Declare different item descriptions/commands; if this round is " +
235
+ "stopping, omit round_contract instead.";
236
+ return makeEnforcementResult({
237
+ action: "reject",
238
+ reason: flag.detail,
239
+ fix_instructions: fix,
240
+ check: flag.check,
241
+ });
242
+ }
215
243
  /** v3.3: R-EVID-VERIFY — the verification command's entrypoint changed in
216
244
  * the same round the command ran. The runtime executed a script the agent
217
245
  * just rewrote, so the "machine observation" has no stable baseline and
@@ -260,7 +288,8 @@ function enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound
260
288
  return makeEnforcementResult({
261
289
  action: "terminate",
262
290
  reason: "The verification command's entrypoint was changed in the round it " +
263
- "verified and was not restored after the rollback.",
291
+ "verified and was not restored after being asked once. Backtrack is " +
292
+ "disabled or already committed for this round.",
264
293
  check,
265
294
  });
266
295
  }
@@ -294,8 +323,8 @@ function enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound
294
323
  * occurrence, which is exactly what the L4 rule forbids.
295
324
  *
296
325
  * The streak is derived from COMMITTED round flags, so it survives restart
297
- * and cannot be moved by an agent self-report. Git motion does not excuse
298
- * it: churning code without verifying is exactly what this row catches. */
326
+ * and cannot be moved by an agent self-report. Machine progress does not
327
+ * excuse it: churning code without verifying is exactly what this row catches. */
299
328
  function enforceContractItemsUnverified(flags, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck) {
300
329
  const limit = getPolicy().engine.unverified_claim_streak_limit;
301
330
  if (!Number.isFinite(limit) || limit <= 0)
@@ -304,9 +333,9 @@ function enforceContractItemsUnverified(flags, currentRound, vaultEntries, conse
304
333
  if (!current)
305
334
  return null;
306
335
  let streak = 1;
336
+ const window = derivationRounds(vaultEntries, currentRound);
307
337
  for (let round = currentRound - 1; round >= 1; round--) {
308
- const entry = vaultEntries.find((item) => entryRound(item) === round && String(item.task_id ?? "").endsWith(":feedback"));
309
- const committed = entry ? decodeCommittedRound(entry) : null;
338
+ const committed = window.find((view) => view.round === round);
310
339
  const carried = committed?.verificationFlags.some((flag) => flag.check === CHECK_CONTRACT_ITEMS_UNVERIFIED);
311
340
  if (!carried)
312
341
  break;
@@ -344,165 +373,52 @@ function enforceContractItemsUnverified(flags, currentRound, vaultEntries, conse
344
373
  check: "contract_items_unverified",
345
374
  });
346
375
  }
347
- /** R-C2 (v3.3): Round Contract scope drift. The gate produced
348
- * round_scope_drift (warn): files changed outside the contract's declared
349
- * scope. v3.8: scope drift is a MACHINE fact and is no longer waivable by an
350
- * agent explanation (the drift_clarification channel was deleted) — the
351
- * agent must revert the out-of-scope changes or close the active contract
352
- * and declare an extended scope.
353
- *
354
- * v3.8.3: that second route is now actually reachable. The v3.7 wording
355
- * ("extend the scope in your re-declared round_contract") was structurally
356
- * dead — a replacement proposal is IGNORED while the active contract is open
357
- * — so the fix text was rewritten to the only legal route (blocked + the
358
- * extended scope as the next proposal) without the matching escape being
359
- * implemented. `scopeDriftWaiver` is that missing half: the coordinator has
360
- * verified that the round declares blocked AND that its proposal covers every
361
- * drifted file. What is waived is the REJECTION, never the fact. */
362
- function enforceScopeDrift(flags, consecutiveRejections, lastRejectionCheck, scopeDriftWaiver) {
363
- const flag = flags.find((f) => f.check === CHECK_ROUND_SCOPE_DRIFT);
364
- if (!flag)
365
- return null;
366
- // v3.8.3: the legal exit. A round that declares itself blocked may switch
367
- // contracts, but only by declaring a proposal that covers every file it
368
- // changed out of scope — the coverage test is the coordinator's, because
369
- // only there are the git files and the active contract both in reach.
370
- //
371
- // Checked ABOVE the terminate arm on purpose: the counter accumulates while
372
- // the agent tries to get the declaration right, so an escape that only
373
- // works at streak 0 or 1 is not an escape — it is a race the agent loses.
374
- // The flag is NOT suppressed; it stays on the committed round, so
375
- // audit/explain still report the drift as a machine fact. */
376
- if (scopeDriftWaiver)
377
- return null;
378
- const ownStreak = lastRejectionCheck === CHECK_ROUND_SCOPE_DRIFT
379
- ? consecutiveRejections
380
- : 0;
381
- if (ownStreak >= 2) {
382
- return makeEnforcementResult({
383
- action: "terminate",
384
- reason: `Repeated scope drift — the agent keeps changing files outside its ` +
385
- `declared contract scope.`,
386
- check: "round_scope_drift",
387
- });
388
- }
389
- const escalation = getPolicy().engine.enforcement_escalation_enabled &&
390
- ownStreak >= 1
391
- ? buildEscalationNotice()
392
- : "";
393
- return makeEnforcementResult({
394
- action: "reject",
395
- reason: flag.detail,
396
- // L3 (v3.7.x): option (b) previously told the agent to "extend the scope
397
- // in your re-declared round_contract" — structurally unreachable: while a
398
- // contract is ACTIVE, a replacement proposal is ignored as premature, so
399
- // the guidance dead-ended into repeated rejects/termination. The only
400
- // legal scope change is to CLOSE the active contract first: resubmit with
401
- // outcome="blocked" (blocker naming the too-narrow scope) and the
402
- // extended scope as the NEXT round's contract in the same submission.
403
- // v3.8.3: that route now works, and its one condition is stated here —
404
- // the new scope must cover EVERY drifted file, not just the listed ones
405
- // (the detail above truncates its list).
406
- fix_instructions: "Files changed outside the contract's declared scope. You must either: " +
407
- "(a) revert the out-of-scope changes, or (b) legally extend the scope: " +
408
- "a replacement contract is IGNORED while the current one is active, so " +
409
- "resubmit this work with outcome: \"blocked\" (blocker naming the " +
410
- "too-narrow scope) plus a round_contract whose scope covers EVERY file " +
411
- "listed above (and any other file this round changed outside the current " +
412
- "scope). Scope drift is a machine fact — no explanation waives " +
413
- "it. Repeated scope drift terminates the loop." +
414
- escalation,
415
- check: "round_scope_drift",
416
- });
417
- }
418
- /** R9 (v2.12): Post-backtrack workspace not restored. The verification gate
419
- * produced backtrack_workspace_not_restored (error): either skipped-round
420
- * files are still dirty, or the working tree is not at the restore point's
421
- * git HEAD. The agent must restore the workspace, not redo the round. */
422
- function enforceBacktrackNotRestored(flags, vaultEntries = [], currentRound = 0) {
423
- const flag = flags.find((f) => f.check === CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED && f.severity === "error");
424
- if (!flag)
425
- return null;
426
- if (!getPolicy().engine.backtrack_enabled)
427
- return null;
428
- // v2.14: same deadlock guard as R4/R5 — a backtrack that already fired
429
- // for this round means a non-restoring agent would otherwise cycle
430
- // R9-backtrack forever (backtracks do not increment the rejection
431
- // counter). One rollback chance, then terminate.
432
- if (currentRound > 0 && hasCommittedBacktrack(vaultEntries, currentRound)) {
433
- return makeEnforcementResult({
434
- action: "terminate",
435
- reason: "The workspace is still not restored after the previous backtrack: " +
436
- flag.detail,
437
- check: "backtrack_workspace_not_restored",
438
- });
439
- }
440
- return makeEnforcementResult({
441
- action: "backtrack",
442
- reason: "The workspace was not restored after backtrack: " + flag.detail,
443
- // v3.8: the requirement, not the recipe — LoopForge never mutates the
444
- // working tree and does not prescribe how the agent restores it.
445
- fix_instructions: "Return the working directory to the clean round's state before " +
446
- "submitting the next round: the skipped-round files must be reverted " +
447
- "and HEAD must be back at the restore point the backtrack prompt named.",
448
- check: "backtrack_workspace_not_restored",
449
- });
450
- }
451
- /** Shared observation pipeline for the progress evaluator (stall window,
452
- * flatline tier). Only observable machine history can establish a stall. */
453
- function evaluateStallWindow(tier, currentRound, vaultEntries,
376
+ /** The ONE progress-stall predicate. A stall requires machine proof that both
377
+ * the committed window AND the submitted round are unchanged; unknown and
378
+ * not_applicable never produce a verdict. */
379
+ function evaluateStallWindow(currentRound, vaultEntries,
454
380
  /** v3.8.1: the round's success is machine-verified (`deriveEvidenceStatus`).
455
- * A verified round is finishing work; a motion window cannot outvote it. */
381
+ * A verified round is finishing work; a progress window cannot outvote it. */
456
382
  machineBackedSuccess,
457
- /** v3.9: the submitted round's own Git motion (`inFlightGitMotion`).
458
- * null = no Git observation, so no excuse. */
459
- inFlightMotion) {
460
- // A round the machine verified is not churn, whatever the window says. This
461
- // is the machine-backed replacement for the old self-reported `>= 0.95`
462
- // near-completion guard: it lets a loop FINISH (a closing round that only
463
- // runs the verification and declares success changes no files).
383
+ /** The submitted round's own provider-neutral progress. */
384
+ inFlightProgress) {
464
385
  if (machineBackedSuccess)
465
386
  return null;
466
- // The round being submitted is part of the recent work: when IT shows
467
- // machine motion, no window of older rounds can call the loop stalled.
468
- // Without this the abandoned window that triggered a rollback judges every
469
- // redo forever — the redo's work is in flight and cannot be in that window.
470
- // This is the machine replacement for the removed self-reported progress
471
- // injection; a round that moves nothing still reaches the verdict and its
472
- // deadlock guard.
473
- if (inFlightMotion === true)
387
+ if (inFlightProgress.state === "changed")
388
+ return null;
389
+ // "Unknown never stalls": a submitted round whose progress cannot be
390
+ // proven unchanged gets no verdict.
391
+ if (inFlightProgress.state !== "unchanged")
474
392
  return null;
475
- const window = tier === "progress_stall"
476
- ? 3
477
- : Math.max(1, getPolicy().engine.stall_lookback_rounds);
393
+ const window = Math.max(1, getPolicy().engine.stall_lookback_rounds);
478
394
  if (currentRound < window)
479
395
  return null;
480
- const machine = machineProgressSeries(vaultEntries, currentRound, window);
481
- if (machine === null || machine.some((value) => value))
396
+ const progress = deriveProgressWindow(derivationRounds(vaultEntries, currentRound), currentRound, window);
397
+ if (progress.state !== "unchanged")
482
398
  return null;
483
- const detail = tier === "progress_stall"
484
- ? `no machine-observed changes in the last ${window} rounds`
485
- : `no machine-observed changes for ${window} consecutive rounds`;
486
- return { tier, detail, window };
399
+ // The verdict is about the Git workspace coverage ONLY. A custom provider's
400
+ // fact — changed, unchanged or unknown — is never part of this sentence.
401
+ const providers = progress.providers.length > 0
402
+ ? ` (providers: ${progress.providers.join(", ")})`
403
+ : "";
404
+ return {
405
+ detail: `no machine-observed progress within the Git workspace coverage in the ` +
406
+ `last ${window} committed rounds${providers}`,
407
+ window,
408
+ };
487
409
  }
488
- /** Shared progress escalation and backtrack deadlock guard (v3.7: one
489
- * evaluator). The flatline tier keeps its steeper ladder while
490
- * using the same mechanics — the tier is an internal diagnostic only. */
410
+ /** One progress disposition ladder. The reason states the machine fact; the
411
+ * fix states the legal protocol paths, never a semantic strategy. */
491
412
  function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck) {
492
- const { tier, detail, window } = verdict;
493
- const flatline = tier === "progress_flatline";
413
+ const { detail } = verdict;
494
414
  const escalation = getPolicy().engine.enforcement_escalation_enabled;
495
- const stalledReason = flatline
496
- ? `Progress has been ${detail} for ${window} consecutive rounds.`
497
- : `Progress has stalled: ${detail}.`;
498
- // v3.7: both tiers emit the single progress_stall check id — the tier
499
- // survives only in the reason/fix wording (former progress_flatline id).
415
+ const stalledReason = `Machine-observed progress is flat: ${detail}.`;
500
416
  const check = "progress_stall";
501
417
  // v3.8.3: the stall escalates on its OWN streak. The persisted counter is
502
- // one scalar shared by every check, so two rejections of any other kind used
503
- // to turn a first-time stall straight into termination.
418
+ // one scalar shared by every check, so two rejections of any other kind must
419
+ // not turn a first-time stall straight into termination.
504
420
  const ownStreak = lastRejectionCheck === check ? consecutiveRejections : 0;
505
- if ((flatline && ownStreak >= 1) || ownStreak >= 2) {
421
+ if (ownStreak >= 2) {
506
422
  return makeEnforcementResult({
507
423
  action: "terminate",
508
424
  reason: `${stalledReason} The stall persisted after escalation. Terminating loop.`,
@@ -516,7 +432,7 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
516
432
  check,
517
433
  });
518
434
  }
519
- const escalated = flatline ? escalation : ownStreak >= 1 && escalation;
435
+ const escalated = ownStreak >= 1 && escalation;
520
436
  if (escalated && getPolicy().engine.backtrack_enabled) {
521
437
  if (hasCommittedBacktrack(vaultEntries, currentRound)) {
522
438
  return makeEnforcementResult({
@@ -528,34 +444,26 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
528
444
  return makeEnforcementResult({
529
445
  action: "backtrack",
530
446
  reason: stalledReason,
531
- fix_instructions: flatline
532
- ? "Progress is exactly flat. After rollback, choose a radically different approach."
533
- : "After rollback, do not repeat the stalled approach; choose a different technique or task decomposition.",
534
- check,
535
- });
536
- }
537
- if (flatline && !escalation) {
538
- return makeEnforcementResult({
539
- action: "terminate",
540
- reason: `${stalledReason} The agent is making zero forward motion and cannot recover.`,
447
+ fix_instructions: "This is a machine liveness fact, not a judgment about your plan. " +
448
+ "The loop will restore the last clean round. After the restore you own " +
449
+ "the next action: continue with a new evaluation, request recovery to " +
450
+ "an earlier committed point, or declare blocked/stop.",
541
451
  check,
542
452
  });
543
453
  }
544
454
  return makeEnforcementResult({
545
455
  action: "reject",
546
456
  reason: stalledReason,
547
- fix_instructions: (flatline
548
- ? "Explain the fundamental blocker, choose a radically different approach, and set a concrete verifiable goal."
549
- : "Explain the blocker, choose a different technique or task decomposition, and set a concrete verifiable goal for this retry.") +
457
+ fix_instructions: "This is a machine liveness fact, not a judgment about your plan. " +
458
+ "You own the next action: submit a new evaluation, request recovery to " +
459
+ "an earlier committed point, or declare outcome blocked with a blocker. " +
460
+ "LoopForge does not prescribe a technique or decomposition." +
550
461
  (escalated ? buildEscalationNotice() : ""),
551
462
  check,
552
463
  });
553
464
  }
554
- /** The stall-tier window is consulted first. A shorter configured flatline
555
- * lookback retains its steeper disposition ladder. */
556
- function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRejections, machineBackedSuccess, lastRejectionCheck, inFlightMotion) {
557
- const verdict = evaluateStallWindow("progress_stall", currentRound, vaultEntries, machineBackedSuccess, inFlightMotion)
558
- ?? evaluateStallWindow("progress_flatline", currentRound, vaultEntries, machineBackedSuccess, inFlightMotion);
465
+ function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRejections, machineBackedSuccess, lastRejectionCheck, inFlightProgress) {
466
+ const verdict = evaluateStallWindow(currentRound, vaultEntries, machineBackedSuccess, inFlightProgress);
559
467
  return verdict
560
468
  ? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck)
561
469
  : null;
@@ -567,15 +475,7 @@ function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRej
567
475
  * instead of terminating immediately. Termination occurs on the third
568
476
  * consecutive rejection. When the flag is disabled, behavior is unchanged
569
477
  * (terminate at ≥2 rejections). */
570
- function enforceMaxRejections(consecutiveRejections, driftWaived) {
571
- // v3.8.3: this row is the catch-all for a round that LOOKS clean while the
572
- // persisted counter says otherwise. A round waived past scope drift is not
573
- // that shape: it declares blocked and its new proposal covers every file it
574
- // drifted into, so the counter it carries is the drift streak it just
575
- // resolved — not unresolved persistence. Without this, the legal exit would
576
- // still dead-end here at a counter of 2 or more.
577
- if (driftWaived)
578
- return null;
478
+ function enforceMaxRejections(consecutiveRejections) {
579
479
  const escalation = getPolicy().engine.enforcement_escalation_enabled;
580
480
  if (escalation) {
581
481
  if (consecutiveRejections >= 3) {
@@ -593,9 +493,9 @@ function enforceMaxRejections(consecutiveRejections, driftWaived) {
593
493
  reason: `${consecutiveRejections} consecutive enforcement rejections for the ` +
594
494
  `same issue without resolution.`,
595
495
  fix_instructions: "You have been rejected multiple times for the same issue. " +
596
- "You must address ALL items in the Evidence Gap section above. " +
597
- "If you do not understand why you are being rejected, explain " +
598
- "your confusion explicitly and ask for clarification." +
496
+ "Address every concrete finding in the mandatory Verification Gate. " +
497
+ "If you need human input, stop with needs_human_input; there is no " +
498
+ "semantic waiver for the findings." +
599
499
  buildEscalationNotice(),
600
500
  check: "max_rejections",
601
501
  });
@@ -619,64 +519,41 @@ function enforceMaxRejections(consecutiveRejections, driftWaived) {
619
519
  function buildEscalationNotice() {
620
520
  return [
621
521
  "",
622
- "### 🆘 Escalation — Seek Human Guidance",
522
+ "### 🆘 Escalation — Human Decision Required",
623
523
  "",
624
- "You have been rejected multiple times for the same issue. " +
625
- "Before retrying, pause and ask a human for help:",
524
+ "This check has rejected the same round more than once. Before retrying, " +
525
+ "ask the human whether to continue the current scope, change the " +
526
+ "operator-owned scope, or stop.",
626
527
  "",
627
- "1. Explain what you've tried so far and why each approach failed.",
628
- "2. Ask the human for specific guidance on what to try next.",
629
- "3. Incorporate their feedback before re-submitting your self-evaluation.",
528
+ "1. State the machine fact and the attempt already made.",
529
+ "2. Ask the human for a decision (continue / change scope / stop).",
530
+ "3. Record the decision in the next evaluation, or stop.",
630
531
  "",
631
- "⚠️ If this round is rejected again, the loop will be **terminated**. " +
632
- "Do NOT resubmit without meaningful changes.",
532
+ "⚠️ If the same check rejects again, the loop will be **terminated**.",
633
533
  ].join("\n");
634
534
  }
635
- // ═══════════════════════════════════════════════════════════════════════════
636
- // Diagnostic gap builder (v2.7)
637
- // ═══════════════════════════════════════════════════════════════════════════
638
- /** Build a diagnostic gap section from verification flags.
639
- *
640
- * Translates each error/warn-level VerificationFlag into a concrete
641
- * claim-vs-evidence mismatch statement. Info-level flags are excluded
642
- * because they are purely informational and do not represent gaps.
643
- *
644
- * Returns an empty string when there are no diagnostic flags to report. */
645
- function buildDiagnosticGap(flags) {
646
- const diagnosticFlags = flags.filter((f) => f.severity === "error" || f.severity === "warn");
647
- if (diagnosticFlags.length === 0)
648
- return "";
649
- const lines = [
650
- "### Evidence Gap",
651
- "",
652
- "The enforcement gate detected mismatches between your claims and the evidence. " +
653
- "Each item below shows a specific discrepancy — address every one before re-submitting.",
654
- "",
655
- ];
656
- for (const flag of diagnosticFlags) {
657
- const icon = flag.severity === "error" ? "🚫" : "⚠️";
658
- lines.push(`- ${icon} **[${flag.check}]** \`${flag.field}\` — ${flag.detail}`);
659
- }
660
- lines.push("");
661
- return lines.join("\n");
662
- }
663
535
  const RULE_TABLE = [
664
536
  { checks: ["success_with_remaining_criteria"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
665
537
  { checks: ["recurring_violation"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
666
- { checks: ["required_command_failed", "command_evidence_mismatch", "outcome_success_contradiction"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
538
+ { checks: ["required_command_failed"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
539
+ // v4.0.6: contract coverage — a continuing round must leave an active
540
+ // contract; a different proposal needs an explicit supersede; a successor
541
+ // must change the acceptance items. Reject and let the uniform ladder
542
+ // escalate repeats (three same-check rejections terminate).
543
+ { checks: ["contract_required", "contract_premature", "contract_successor_unchanged"], ladder: "uniform", terminalAfter: 3, noticeOnRepeat: true },
667
544
  // v3.8.3: own ladder — reject → backtrack → terminate. A rewritten
668
545
  // entrypoint is not fixed by asking again (the round's baseline is frozen on
669
546
  // purpose), so the second occurrence rolls the work back instead of killing
670
547
  // the loop. See enforceVerificationEntrypointTampered.
671
548
  { checks: ["verification_entrypoint_modified"], ladder: "internal" },
672
- { checks: ["round_scope_drift"], ladder: "internal" },
549
+ // v3.9.9: round_scope_drift is REPORT-ONLY. It has no enforcement row, no
550
+ // waiver, no rejection counter, and no stall/backtrack interaction.
673
551
  // v3.8.1: "internal" — the handler owns this row's ladder (it must terminate
674
552
  // as `incomplete`, with its own reason). Declaring it uniform as well ran the
675
553
  // escalation notice twice and left the uniform terminate branch dead.
676
554
  { checks: ["contract_items_unverified"], ladder: "internal" },
677
555
  { checks: ["progress_stall"], ladder: "internal" },
678
556
  { checks: ["max_rejections"], ladder: "counter" },
679
- { checks: ["backtrack_workspace_not_restored"], ladder: "internal" },
680
557
  // v3.7.1: cited gates without an approved human decision
681
558
  { checks: ["user_gate_unresolved"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
682
559
  ];
@@ -729,16 +606,24 @@ export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries,
729
606
  * check and lets uniform rows escalate on THEIR OWN streak instead of
730
607
  * inheriting an unrelated history. */
731
608
  lastRejectionCheck = "",
732
- /** v3.8.3: the coordinator has verified that this round declares itself
733
- * blocked AND that its new contract proposal's scope covers every file the
734
- * round changed outside the active contract's scope. Computed there, not
735
- * here: neither the observations nor the active contract reach this
736
- * function. See `enforceScopeDrift`. */
737
- scopeDriftWaiver = false,
738
- /** v3.9: the submitted round's own Git motion — see `inFlightGitMotion`.
739
- * null when no Git observation was made. */
740
- inFlightMotion = null) {
609
+ /** v3.9.7: the submitted round's provider-neutral progress. */
610
+ inFlightProgress = {
611
+ state: "unknown",
612
+ providers: [],
613
+ coverages: [],
614
+ }) {
741
615
  const { flags } = verifyResult;
616
+ // v3.9.9: `round_scope_drift` is a retired enforcement check. A session
617
+ // restored from an older runtime may still name it; it is not a current
618
+ // check, and its persisted count must not reach the max_rejections
619
+ // catch-all as unresolved persistence.
620
+ const retiredScopeCheck = lastRejectionCheck === "round_scope_drift";
621
+ const effectiveRejections = retiredScopeCheck ? 0 : consecutiveRejections;
622
+ const effectiveLastCheck = retiredScopeCheck ? "" : lastRejectionCheck;
623
+ // A valid blocked stop is the Agent's protocol action. Liveness/retry rows
624
+ // must not convert it into a same-round rejection; machine-integrity rows
625
+ // still apply below because their facts make the submission untrustworthy.
626
+ const blockedStop = declaresBlocked(selfEval) && selfEval.should_continue === false;
742
627
  // Run enforcement rules in priority order — the array order IS the
743
628
  // priority; the R-labels in the comments are historical (RULE_TABLE is
744
629
  // the semantic description). The first rule that fires wins.
@@ -748,9 +633,11 @@ inFlightMotion = null) {
748
633
  // v2.14: R-EVID — a required command failure, hidden test failures, or
749
634
  // a self-contradictory outcome claim contradicts the success claim
750
635
  () => enforceEvidenceContradiction(flags),
636
+ // v4.0.6: contract coverage (required / premature / successor unchanged)
637
+ () => enforceContractCoverage(flags),
751
638
  // v3.3: R-EVID-VERIFY — the verification command entrypoint changed in
752
639
  // the same round it ran.
753
- () => enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound, consecutiveRejections, lastRejectionCheck),
640
+ () => enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound, effectiveRejections, effectiveLastCheck),
754
641
  // v3.5: contract completion claimed without its verification_plan
755
642
  // commands passing — runs before R-C1: the completion-truth question
756
643
  // outranks boundary nuance.
@@ -760,23 +647,26 @@ inFlightMotion = null) {
760
647
  // success_without_verified_evidence, which is now a warn: report coverage
761
648
  // no longer decides acceptance, and the machine-evidence warn still keeps
762
649
  // an unbacked success out of the trajectory.
763
- // v3.3: R-C2 — files changed outside the contract's declared scope
764
- () => enforceScopeDrift(flags, consecutiveRejections, lastRejectionCheck, scopeDriftWaiver),
650
+ // v3.9.9: scope drift is an INFO fact only; it never appears here.
765
651
  // v3.8: verification debt — contract items claimed met without machine
766
652
  // verification (fires only after the configured streak)
767
- () => enforceContractItemsUnverified(flags, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck),
768
- // v3.7: single progress evaluator (former R4/R5 slots merged) —
653
+ () => blockedStop
654
+ ? null
655
+ : enforceContractItemsUnverified(flags, currentRound, vaultEntries, effectiveRejections, effectiveLastCheck),
656
+ // v3.9.7: one provider-neutral progress evaluator —
769
657
  // reject → backtrack → terminate with the deadlock guard
770
- () => enforceProgressStall(flags, currentRound, vaultEntries, consecutiveRejections, verifyResult.machineBackedSuccess, lastRejectionCheck, inFlightMotion),
658
+ () => blockedStop
659
+ ? null
660
+ : enforceProgressStall(flags, currentRound, vaultEntries, effectiveRejections, verifyResult.machineBackedSuccess, effectiveLastCheck, inFlightProgress),
771
661
  // R6: rejection-counter catch-all (evaluated only when no row above
772
662
  // fired — a clean-looking round carrying a high persisted counter)
773
663
  // v3.7.1: cited gate without an approved human decision
774
664
  () => enforceUserGateUnresolved(flags),
775
- // v3.8.3: the counter is shared, so this row must know that a scope-drift
776
- // waiver already excused the streak it carries (see enforceMaxRejections).
777
- () => enforceMaxRejections(consecutiveRejections, scopeDriftWaiver && flags.some((f) => f.check === CHECK_ROUND_SCOPE_DRIFT)),
778
- // v2.12: R9 — post-backtrack workspace not restored
779
- () => enforceBacktrackNotRestored(flags, vaultEntries, currentRound),
665
+ // v3.9.9: round_scope_drift is not a rejection source, so this catch-all
666
+ // has no drift-waiver special case anymore.
667
+ () => blockedStop
668
+ ? null
669
+ : enforceMaxRejections(effectiveRejections),
780
670
  ];
781
671
  // v3.7: uniform-ladder escalation. Rows without their own escalation
782
672
  // ladder (R1/R2/R-EVID/R-EVID-VERIFY) escalate on the session's
@@ -802,8 +692,8 @@ inFlightMotion = null) {
802
692
  // occurrence into termination. The user_gate row is the sharp case:
803
693
  // its recovery is human approval, so a gate cited after unrelated
804
694
  // rejections must reject with instructions, not kill the loop.
805
- const ownStreak = lastRejectionCheck === result.check
806
- ? consecutiveRejections
695
+ const ownStreak = effectiveLastCheck === result.check
696
+ ? effectiveRejections
807
697
  : 0;
808
698
  if (ownStreak >= (row.terminalAfter ?? 2)) {
809
699
  return makeEnforcementResult({
@@ -831,54 +721,41 @@ inFlightMotion = null) {
831
721
  fix_instructions: "",
832
722
  });
833
723
  }
834
- /** Build a rejection prompt for the agent.
724
+ /** Build the protected retry notice for the agent.
835
725
  *
836
- * The prompt clearly states the round was rejected, why, what the agent
837
- * must fix, and that the agent must redo the SAME round (not advance).
838
- *
839
- * v2.7: Accepts verificationFlags to render a diagnostic "Evidence Gap"
840
- * section. Each error/warn flag becomes a concrete claim-vs-evidence
841
- * mismatch statement so the agent knows exactly what to correct rather
842
- * than retrying blindly.
726
+ * v3.9.7: this is a protocol fact, not a second task narrative. It carries
727
+ * the enforcement reason, the required fix and the same-round instruction.
728
+ * Findings are rendered exactly once by the mandatory Verification Gate; the
729
+ * original task/contract is rendered by Objective/Current Task. The notice
730
+ * never promises that an explanation will re-evaluate a machine fact.
843
731
  *
844
732
  * @param currentRound The round number that was rejected (NOT incremented).
845
- * @param task The original loop task description.
733
+ * @param _task Kept for the public signature; not rendered here.
846
734
  * @param enforceResult The enforcement decision with reason and fix instructions.
847
- * @param verificationFlags The verification gate's findings for this round.
848
- * Used to build the Evidence Gap section. */
849
- export function buildRejectionPrompt(currentRound, task, enforceResult, verificationFlags = []) {
735
+ * @param _verificationFlags Kept for the public signature; findings render in
736
+ * the mandatory Verification Gate. */
737
+ export function buildRejectionPrompt(currentRound, _task, enforceResult, _verificationFlags = []) {
850
738
  const lines = [
851
739
  "## ⛔ Round " + currentRound + " — REJECTED",
852
740
  "",
853
- "Your self-evaluation for Round " + currentRound +
854
- " was **rejected** by the enforcement gate.",
741
+ "The enforcement gate rejected this round. This is a machine/protocol",
742
+ "decision, not a judgment about your reasoning strategy.",
855
743
  "",
856
744
  "### Reason",
857
745
  "",
858
746
  enforceResult.reason,
859
747
  "",
860
748
  ];
861
- // v2.7: Diagnostic gap — show the agent exactly what doesn't match.
862
- const gap = buildDiagnosticGap(verificationFlags);
863
- if (gap) {
864
- lines.push(gap);
865
- }
866
749
  if (enforceResult.fix_instructions) {
867
750
  lines.push("### Required Fix");
868
751
  lines.push("");
869
752
  lines.push(enforceResult.fix_instructions);
870
753
  lines.push("");
871
754
  }
872
- lines.push("### Your Task (Round " + currentRound + " — Retry)");
873
- lines.push("");
874
- lines.push(task);
875
- lines.push("");
876
755
  lines.push("### Instructions");
877
756
  lines.push("");
878
757
  lines.push("1. Read and address each issue in **Required Fix** above.", "2. Re-execute **Round " + currentRound +
879
- "** — do NOT advance to the next round.", "3. Submit a corrected self-evaluation via `loopforge_next`.", "4. Be honest in your self-evaluation — " +
880
- "claiming success when criteria are unmet will be rejected again.", "5. If you believe this rejection is incorrect, " +
881
- "explain why in your output_summary and the enforcement gate will re-evaluate.");
758
+ "** — do NOT advance to the next round.", "3. Submit a corrected evaluation via `loopforge_next` using the latest", " `roundId` returned by LoopForge.", "4. If you believe a finding is factually wrong, identify the specific", " machine fact you dispute and submit a corrected evaluation. The gate", " re-evaluates facts, not explanations.");
882
759
  return lines.join("\n");
883
760
  }
884
761
  //# sourceMappingURL=enforcement-gate.js.map