loopforge 3.7.0 → 3.8.1

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 (151) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +171 -157
  3. package/dist/audit.d.ts +53 -1
  4. package/dist/audit.d.ts.map +1 -1
  5. package/dist/audit.js +106 -22
  6. package/dist/audit.js.map +1 -1
  7. package/dist/canonical-state.d.ts +75 -69
  8. package/dist/canonical-state.d.ts.map +1 -1
  9. package/dist/canonical-state.js +276 -168
  10. package/dist/canonical-state.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +111 -12
  13. package/dist/cli.js.map +1 -1
  14. package/dist/cognitive-facts.d.ts +13 -0
  15. package/dist/cognitive-facts.d.ts.map +1 -1
  16. package/dist/cognitive-facts.js +30 -16
  17. package/dist/cognitive-facts.js.map +1 -1
  18. package/dist/cognitive-governance.d.ts +30 -6
  19. package/dist/cognitive-governance.d.ts.map +1 -1
  20. package/dist/cognitive-governance.js +74 -18
  21. package/dist/cognitive-governance.js.map +1 -1
  22. package/dist/committed-round.d.ts +109 -24
  23. package/dist/committed-round.d.ts.map +1 -1
  24. package/dist/committed-round.js +235 -52
  25. package/dist/committed-round.js.map +1 -1
  26. package/dist/contract-items.d.ts +55 -0
  27. package/dist/contract-items.d.ts.map +1 -0
  28. package/dist/contract-items.js +180 -0
  29. package/dist/contract-items.js.map +1 -0
  30. package/dist/enforcement-gate.d.ts +43 -20
  31. package/dist/enforcement-gate.d.ts.map +1 -1
  32. package/dist/enforcement-gate.js +269 -378
  33. package/dist/enforcement-gate.js.map +1 -1
  34. package/dist/engine.d.ts +11 -6
  35. package/dist/engine.d.ts.map +1 -1
  36. package/dist/engine.js +102 -67
  37. package/dist/engine.js.map +1 -1
  38. package/dist/evidence-claims.d.ts +4 -4
  39. package/dist/evidence-claims.d.ts.map +1 -1
  40. package/dist/evidence-claims.js +16 -14
  41. package/dist/evidence-claims.js.map +1 -1
  42. package/dist/evidence-provider.d.ts +91 -90
  43. package/dist/evidence-provider.d.ts.map +1 -1
  44. package/dist/evidence-provider.js +349 -160
  45. package/dist/evidence-provider.js.map +1 -1
  46. package/dist/explain.d.ts +76 -0
  47. package/dist/explain.d.ts.map +1 -0
  48. package/dist/explain.js +127 -0
  49. package/dist/explain.js.map +1 -0
  50. package/dist/generate-schema.js +250 -53
  51. package/dist/generate-schema.js.map +1 -1
  52. package/dist/index.d.ts +8 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +7 -6
  55. package/dist/index.js.map +1 -1
  56. package/dist/loop-compiler.d.ts +41 -40
  57. package/dist/loop-compiler.d.ts.map +1 -1
  58. package/dist/loop-compiler.js +275 -515
  59. package/dist/loop-compiler.js.map +1 -1
  60. package/dist/loop-extras-parser.d.ts +0 -1
  61. package/dist/loop-extras-parser.d.ts.map +1 -1
  62. package/dist/loop-extras-parser.js +0 -4
  63. package/dist/loop-extras-parser.js.map +1 -1
  64. package/dist/loop-projection.d.ts.map +1 -1
  65. package/dist/loop-projection.js +3 -1
  66. package/dist/loop-projection.js.map +1 -1
  67. package/dist/mcp/round-lifecycle.d.ts +40 -21
  68. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  69. package/dist/mcp/round-lifecycle.js +173 -120
  70. package/dist/mcp/round-lifecycle.js.map +1 -1
  71. package/dist/mcp/server.d.ts.map +1 -1
  72. package/dist/mcp/server.js +63 -10
  73. package/dist/mcp/server.js.map +1 -1
  74. package/dist/mcp/session.d.ts +53 -7
  75. package/dist/mcp/session.d.ts.map +1 -1
  76. package/dist/mcp/session.js +240 -139
  77. package/dist/mcp/session.js.map +1 -1
  78. package/dist/mcp/submission-boundary.d.ts +89 -0
  79. package/dist/mcp/submission-boundary.d.ts.map +1 -0
  80. package/dist/mcp/submission-boundary.js +203 -0
  81. package/dist/mcp/submission-boundary.js.map +1 -0
  82. package/dist/mcp/tools.d.ts.map +1 -1
  83. package/dist/mcp/tools.js +377 -125
  84. package/dist/mcp/tools.js.map +1 -1
  85. package/dist/policy-metrics.js +2 -2
  86. package/dist/policy-metrics.js.map +1 -1
  87. package/dist/policy.d.ts +65 -95
  88. package/dist/policy.d.ts.map +1 -1
  89. package/dist/policy.js +151 -39
  90. package/dist/policy.js.map +1 -1
  91. package/dist/prompt-assembler.d.ts +7 -33
  92. package/dist/prompt-assembler.d.ts.map +1 -1
  93. package/dist/prompt-assembler.js +313 -473
  94. package/dist/prompt-assembler.js.map +1 -1
  95. package/dist/prompt-policy.d.ts +1 -4
  96. package/dist/prompt-policy.d.ts.map +1 -1
  97. package/dist/prompt-policy.js +6 -7
  98. package/dist/prompt-policy.js.map +1 -1
  99. package/dist/protocol.d.ts +411 -168
  100. package/dist/protocol.d.ts.map +1 -1
  101. package/dist/protocol.js +23 -69
  102. package/dist/protocol.js.map +1 -1
  103. package/dist/replay.js +2 -2
  104. package/dist/replay.js.map +1 -1
  105. package/dist/round-contract.d.ts +69 -57
  106. package/dist/round-contract.d.ts.map +1 -1
  107. package/dist/round-contract.js +104 -64
  108. package/dist/round-contract.js.map +1 -1
  109. package/dist/round-coordinator.d.ts +55 -16
  110. package/dist/round-coordinator.d.ts.map +1 -1
  111. package/dist/round-coordinator.js +179 -57
  112. package/dist/round-coordinator.js.map +1 -1
  113. package/dist/round-driver.d.ts +7 -5
  114. package/dist/round-driver.d.ts.map +1 -1
  115. package/dist/round-driver.js +2 -1
  116. package/dist/round-driver.js.map +1 -1
  117. package/dist/round-facts.d.ts +96 -0
  118. package/dist/round-facts.d.ts.map +1 -0
  119. package/dist/round-facts.js +216 -0
  120. package/dist/round-facts.js.map +1 -0
  121. package/dist/round-transaction.d.ts +23 -9
  122. package/dist/round-transaction.d.ts.map +1 -1
  123. package/dist/round-transaction.js +99 -22
  124. package/dist/round-transaction.js.map +1 -1
  125. package/dist/self-eval.d.ts +127 -11
  126. package/dist/self-eval.d.ts.map +1 -1
  127. package/dist/self-eval.js +517 -36
  128. package/dist/self-eval.js.map +1 -1
  129. package/dist/subgoal-state.d.ts +77 -0
  130. package/dist/subgoal-state.d.ts.map +1 -0
  131. package/dist/subgoal-state.js +198 -0
  132. package/dist/subgoal-state.js.map +1 -0
  133. package/dist/token-utils.d.ts +52 -19
  134. package/dist/token-utils.d.ts.map +1 -1
  135. package/dist/token-utils.js +83 -72
  136. package/dist/token-utils.js.map +1 -1
  137. package/dist/verification-gate.d.ts +27 -46
  138. package/dist/verification-gate.d.ts.map +1 -1
  139. package/dist/verification-gate.js +308 -557
  140. package/dist/verification-gate.js.map +1 -1
  141. package/dist/version.d.ts +5 -0
  142. package/dist/version.d.ts.map +1 -0
  143. package/dist/version.js +5 -0
  144. package/dist/version.js.map +1 -0
  145. package/dist/workspace.d.ts +9 -0
  146. package/dist/workspace.d.ts.map +1 -1
  147. package/dist/workspace.js +17 -0
  148. package/dist/workspace.js.map +1 -1
  149. package/loop_policy.json +17 -25
  150. package/package.json +7 -7
  151. package/skills/perception/SKILL.md +266 -64
@@ -18,15 +18,16 @@
18
18
  * changes run order or verdict aggregation. Round Contract checks are further
19
19
  * framed as declaration / execution / closure stages in their doc comments.
20
20
  */
21
- import { getPolicy, isConfiguredCommand } from "./policy.js";
21
+ import { isPassedAfterObservation } from "./evidence-provider.js";
22
+ import { getPolicy } from "./policy.js";
22
23
  import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
23
- import { jaccardSimilarity, tokenize, isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
24
- import { deriveSubGoalId } from "./loop-compiler.js";
25
- import { committedRoundsFromEntries, machineGitMotionSeries, entryViolations } from "./committed-round.js";
26
- import { contractRoundEvaluations, contractDoneWhenSatisfied, contractItemMatches, deriveActiveRoundContract, } from "./round-contract.js";
27
- import { stableStringify } from "./canonical-state.js";
24
+ import { isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
25
+ import { derivationRounds, machineGitMotionSeries, entryViolations, } from "./committed-round.js";
26
+ import { deriveActiveRoundContract, sameContract, } from "./round-contract.js";
27
+ import { deriveContractItemStatuses, } from "./contract-items.js";
28
28
  import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
29
- import { effectiveSuccess, parseRoundContract } from "./self-eval.js";
29
+ import { claimedMetCriteria, claimedRemainingCriteria, effectiveSuccess } from "./self-eval.js";
30
+ import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
30
31
  // ═══════════════════════════════════════════════════════════════════════════
31
32
  // v2.12: Check-name constants — single source of truth so the enforcement
32
33
  // gate and audit can reference checks without string-literal drift.
@@ -63,8 +64,6 @@ export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_m
63
64
  /** v3.3: Test files changed in the same round a verification command passed.
64
65
  * Warn-only — the command result stays usable. */
65
66
  export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
66
- export const CHECK_INTENT_DRIFT = "intent_drift";
67
- export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
68
67
  export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
69
68
  export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
70
69
  // v3.3 — Round Contract checks. v3.4: split by target. The submission's own
@@ -83,35 +82,23 @@ export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
83
82
  // (completion truth: contract_completion_unverified; premature_boundary's
84
83
  // claimed-met-unverified arm). The framing is documentation only — no phase
85
84
  // field exists on the contract.
86
- /** Contract proposed with an empty done_when — nothing is promised, so
87
- * nothing can be verified at the boundary. Warn: the contract can be fixed
88
- * by re-declaring next round. */
89
- export const CHECK_ROUND_UNDERSPECIFIED = "round_underspecified";
90
- /** done_when items exist but verification_plan is empty or names commands
91
- * that are not configured AND enabled in policy.evidence.commands — the
92
- * completion claims could never be machine-checked. Warn. */
93
- export const CHECK_ROUND_UNVERIFIABLE = "round_unverifiable";
85
+ /** v3.8: Contract items claimed met but not machine-verified this round.
86
+ * The round commits; the debt is surfaced and the enforcement gate's
87
+ * verification-debt row handles a persistent pattern. */
88
+ export const CHECK_CONTRACT_ITEMS_UNVERIFIED = "contract_items_unverified";
94
89
  /** The round's actual git changes include files outside the ACTIVE
95
- * contract's declared scope. Warn + drift_clarification exemption
96
- * (R7-style). */
90
+ * contract's declared scope. Warn; v3.8: not waivable by explanation — the
91
+ * enforcement gate rejects, and repeated drift terminates. */
97
92
  export const CHECK_ROUND_SCOPE_DRIFT = "round_scope_drift";
98
- /** success claimed under the ACTIVE contract whose done_when items are
99
- * either claimed met without machine-verified evidence or silently dropped
100
- * (not in success_criteria_met NOR success_criteria_remaining). Error. */
101
- export const CHECK_PREMATURE_BOUNDARY = "premature_boundary";
102
- /** v3.5: Closing a Round Contract is a success-class claim and must be
103
- * machine-backed. The eval's met claims satisfy every done_when of the
104
- * ACTIVE contract but its verification_plan commands did not pass this
105
- * round. Error; warn under evidence.machine_backed_success "warn"; never
106
- * downgraded by no_change_reason (all done_when met contradicts "no
107
- * change"). Fail-open: plan names no longer configured+enabled are not
108
- * required (cannot observe). */
109
- export const CHECK_CONTRACT_COMPLETION_UNVERIFIED = "contract_completion_unverified";
110
93
  /** v3.5: The ACTIVE contract is still open (not completed, not blocked)
111
94
  * while a different contract was proposed — the proposal is ignored until
112
95
  * the active one closes. Warn: the walker still ignores it; this only
113
96
  * surfaces the otherwise-silent state. */
114
97
  export const CHECK_CONTRACT_PREMATURE = "contract_premature";
98
+ /** v3.7.1: A cited gate (evaluation.gate_ids) is not approved. Opt-in:
99
+ * checked ONLY when policy.gate.enabled — citations are meaningless when
100
+ * the gate layer is off. Error → enforcement rejects the round. */
101
+ export const CHECK_USER_GATE_UNRESOLVED = "user_gate_unresolved";
115
102
  export const CHECK_DOMAIN = {
116
103
  // ── evaluation_consistency ────────────────────────────────────────────────
117
104
  [CHECK_SUCCESS_WITH_REMAINING_CRITERIA]: "evaluation_consistency",
@@ -131,14 +118,11 @@ export const CHECK_DOMAIN = {
131
118
  [CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
132
119
  [CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
133
120
  // ── plan_contract ─────────────────────────────────────────────────────────
134
- [CHECK_INTENT_DRIFT]: "plan_contract",
135
- [CHECK_SUBGOAL_DRIFT]: "plan_contract",
136
- [CHECK_ROUND_UNDERSPECIFIED]: "plan_contract",
137
- [CHECK_ROUND_UNVERIFIABLE]: "plan_contract",
121
+ [CHECK_CONTRACT_ITEMS_UNVERIFIED]: "plan_contract",
138
122
  [CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
139
- [CHECK_PREMATURE_BOUNDARY]: "plan_contract",
140
- [CHECK_CONTRACT_COMPLETION_UNVERIFIED]: "plan_contract",
141
123
  [CHECK_CONTRACT_PREMATURE]: "plan_contract",
124
+ // v3.7.1: cited-gate authorization conformance (opt-in blocking layer).
125
+ [CHECK_USER_GATE_UNRESOLVED]: "plan_contract",
142
126
  // ── progress_recovery ─────────────────────────────────────────────────────
143
127
  [CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED]: "progress_recovery",
144
128
  };
@@ -178,72 +162,46 @@ export function isFileInScope(file, scope) {
178
162
  export function collectOutOfScopeFiles(files, scope) {
179
163
  return files.filter((file) => !isFileInScope(file, scope));
180
164
  }
181
- /** v3.3: Detect verification-domain tampering for a command snapshot.
182
- * entrypointModified — a workspace file the command depends on
183
- * (snapshot.data.entrypointFiles) changed this round: the runtime executed
184
- * a script the agent just rewrote, so the "machine observation" has no
185
- * stable baseline. testFilesModified — the round changed test files
186
- * (isTestFile): normal in TDD, so warn-only; the command result stays
187
- * usable. Pure snapshot derivation — no fs access (this module is pure). */
188
- function commandTampered(snapshot, gitSnapshot) {
189
- if (!gitSnapshot)
165
+ /** v3.3: Detect verification-domain tampering for a command observation.
166
+ * entrypointModified — a workspace file the command depends on changed this
167
+ * round: the runtime executed a script the agent just rewrote, so the
168
+ * "machine observation" has no stable baseline. testFilesModified — the
169
+ * round changed test files (isTestFile): normal in TDD, so warn-only; the
170
+ * command result stays usable. Pure derivation — no fs access. */
171
+ function commandTampered(observation, gitObservation) {
172
+ if (!gitObservation)
190
173
  return { entrypointModified: false, testFilesModified: false };
191
- const gitFiles = new Set(gitSnapshot.files);
192
- const entrypoints = isRecord(snapshot.data) &&
193
- Array.isArray(snapshot.data.entrypointFiles)
194
- ? snapshot.data.entrypointFiles.filter((f) => typeof f === "string")
174
+ const gitFiles = new Set(gitObservation.files);
175
+ const entrypoints = observation.kind === "command"
176
+ ? observation.data.entrypointFiles ?? []
195
177
  : [];
196
178
  const entrypointModified = entrypoints.some((file) => gitFiles.has(file));
197
179
  const testFilesModified = [...gitFiles].some((file) => isTestFile(file));
198
180
  return { entrypointModified, testFilesModified };
199
181
  }
200
- function passedAfterCommand(snapshots) {
201
- const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
202
- return snapshots.find((snapshot) => isRecord(snapshot.data) &&
203
- snapshot.data.kind === "command" &&
204
- snapshot.data.phase === "after" &&
205
- snapshot.data.status === "passed" &&
206
- // v3.3: a command whose entrypoint changed this round is not machine
207
- // evidence — exclude it so providerStatus degrades to unavailable.
208
- !commandTampered(snapshot, git).entrypointModified) ?? null;
209
- }
210
- /** v3.5: Names of commands observed passing in the after-phase this round.
211
- * Tampered commands (entrypoint changed this round) are not machine
212
- * evidence and are excluded — the same rule passedAfterCommand applies. */
213
- function passedPlanCommandNames(snapshots) {
214
- const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
215
- const names = new Set();
216
- for (const snapshot of snapshots) {
217
- if (!isRecord(snapshot.data))
218
- continue;
219
- if (snapshot.data.kind !== "command")
220
- continue;
221
- if (snapshot.data.phase !== "after" || snapshot.data.status !== "passed")
222
- continue;
223
- if (commandTampered(snapshot, git).entrypointModified)
224
- continue;
225
- const name = snapshot.data.commandName;
226
- if (typeof name === "string" && name.length > 0)
227
- names.add(name);
228
- }
229
- return names;
182
+ /** v3.8: The single machine-backed predicate lives in evidence-provider.ts.
183
+ * This wrapper resolves the round's git observation once for a collection. */
184
+ function passedAfterCommand(observations) {
185
+ const git = observations.find((item) => item.providerId === "git") ?? null;
186
+ const changed = git ? new Set(git.files) : null;
187
+ return observations.find((item) => isPassedAfterObservation(item, changed)) ?? null;
230
188
  }
231
189
  /** v3.2: Derive the machine-verification status of a round. The verification
232
190
  * capability is modeled explicitly (verified / unavailable / absent) instead
233
191
  * of letting evidence-dependent checks silently disappear when snapshots are
234
192
  * missing — the fix for the "weakest when it matters most" gap. */
235
193
  export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
236
- const gitSnap = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
194
+ const gitSnap = evidenceSnapshots.find((item) => item.providerId === "git") ?? null;
237
195
  const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
238
196
  const command = passedAfterCommand(evidenceSnapshots);
239
197
  const commandVerified = command !== null;
240
- // testsMachineBacked: agent-reported test_results agree with a passed
198
+ // testsMachineBacked: agent-reported tests_reported agree with a passed
241
199
  // command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
242
200
  // performs — parseTestOutput lives in this module).
243
201
  let testsMachineBacked = false;
244
- const reported = selfEval.execution_evidence?.test_results;
245
- if (command && reported) {
246
- const parsed = parseTestOutput(String(command.data.stdout ?? ""));
202
+ const reported = selfEval.execution_report?.tests_reported;
203
+ if (command && command.kind === "command" && reported) {
204
+ const parsed = parseTestOutput(command.data.stdoutExcerpt ?? "");
247
205
  testsMachineBacked = parsed !== null &&
248
206
  parsed.passed === reported.passed &&
249
207
  parsed.failed === reported.failed &&
@@ -251,8 +209,8 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
251
209
  }
252
210
  // reportedFilesMatch: agent-reported files_changed equals the git diff set.
253
211
  let reportedFilesMatch = false;
254
- if (gitSnap && selfEval.execution_evidence) {
255
- const reportedSet = [...selfEval.execution_evidence.files_changed].sort();
212
+ if (gitSnap && selfEval.execution_report) {
213
+ const reportedSet = [...(selfEval.execution_report.files_changed ?? [])].sort();
256
214
  const actualSet = [...gitSnap.files].sort();
257
215
  reportedFilesMatch = reportedSet.length === actualSet.length &&
258
216
  reportedSet.every((file, index) => file === actualSet[index]);
@@ -278,7 +236,7 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
278
236
  }
279
237
  /** Per-round machine progress over decoded committed history. */
280
238
  export function machineProgressSeries(vaultEntries, currentRound, lookback) {
281
- return machineGitMotionSeries(committedRoundsFromEntries(vaultEntries, currentRound), currentRound, lookback);
239
+ return machineGitMotionSeries(derivationRounds(vaultEntries, currentRound), currentRound, lookback);
282
240
  }
283
241
  // ═══════════════════════════════════════════════════════════════════════════
284
242
  // Individual checks — each returns a VerificationFlag or null
@@ -286,8 +244,8 @@ export function machineProgressSeries(vaultEntries, currentRound, lookback) {
286
244
  function checkSuccessWithRemainingCriteria(selfEval) {
287
245
  if (!effectiveSuccess(selfEval))
288
246
  return null;
289
- const remaining = selfEval.execution_evidence?.success_criteria_remaining;
290
- if (!remaining || remaining.length === 0)
247
+ const remaining = claimedRemainingCriteria(selfEval.execution_report);
248
+ if (remaining.length === 0)
291
249
  return null;
292
250
  return makeVerificationFlag({
293
251
  severity: "error",
@@ -303,14 +261,14 @@ function checkSuccessWithRemainingCriteria(selfEval) {
303
261
  * next prompt tells the agent its claims are unverified. This is the
304
262
  * mild form of the 3.x criterion-specific completion rule. */
305
263
  function checkUnverifiedCriteriaClaims(selfEval, claimView) {
306
- const met = selfEval.execution_evidence?.success_criteria_met;
307
- if (!met || met.length === 0)
264
+ const met = claimedMetCriteria(selfEval.execution_report);
265
+ if (met.length === 0)
308
266
  return null;
309
267
  if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
310
268
  return null;
311
269
  return makeVerificationFlag({
312
270
  severity: "warn",
313
- field: "execution_evidence",
271
+ field: "execution_report",
314
272
  check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
315
273
  detail: `${met.length} criteria reported met but none machine-verified ` +
316
274
  "(no passing test evidence or command snapshot)",
@@ -403,13 +361,12 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
403
361
  /** v2.12/v3.6/v3.7: success without machine backing — the single
404
362
  * "success evidence" semantic. Two arms, evaluated in order:
405
363
  * 1. Empty/missing-evidence arm (v3.7: absorbed the enforcement-only
406
- * empty_success rule): success=true with NO execution_evidence at all, or
407
- * with files_changed empty AND test_results null, is an error
364
+ * empty_success rule): success=true with NO execution_report at all, or
365
+ * with files_changed empty AND tests_reported null, is an error
408
366
  * unconditionally — BEFORE the providerStatus early-exit, so a passed
409
- * command never rescues it (execution_evidence stays mandatory), and
410
- * neither evidence.machine_backed_success "warn" nor a declared
411
- * no_change_reason downgrades it ("no change" with literally no evidence
412
- * recorded is still an unbacked claim).
367
+ * command never rescues it (execution_report stays mandatory), and
368
+ * no declared no_change_reason downgrades it ("no change" with literally
369
+ * no evidence recorded is still an unbacked claim).
413
370
  * 2. Claims arm (v3.6): success=true but the runtime observed no machine
414
371
  * evidence for it. The trigger is the runtime-derived providerStatus
415
372
  * (verified / unavailable / absent) — "verified" requires an UNTAMPERED
@@ -418,38 +375,51 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
418
375
  * success — "required" rejects (error), "warn" tolerates with a warn
419
376
  * (round commits, success excluded from the trajectory, trust drops). A
420
377
  * declared no_change_reason is the honest escape hatch: it downgrades to
421
- * info. no_change_reason is honored HERE ONLY — contract checks refuse it. */
378
+ * info — honored only while NO enabled verification command is
379
+ * configured (machine verification was structurally impossible); a
380
+ * configured command closes the escape. no_change_reason is honored HERE
381
+ * ONLY — contract checks refuse it. */
422
382
  function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
423
383
  if (!effectiveSuccess(selfEval))
424
384
  return null;
425
385
  // v3.7: empty/missing-evidence arm (ex-R3 empty_success posture).
426
- const ev = selfEval.execution_evidence;
427
- if (!ev || (ev.files_changed.length === 0 && ev.test_results === null)) {
386
+ const ev = selfEval.execution_report;
387
+ if (!ev || ((ev.files_changed ?? []).length === 0 && ev.tests_reported === null)) {
428
388
  return makeVerificationFlag({
429
389
  severity: "error",
430
390
  field: "success",
431
391
  check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
432
392
  detail: ev
433
- ? "Agent claims success but execution_evidence shows no files changed " +
393
+ ? "Agent claims success but execution_report shows no files changed " +
434
394
  "and no tests were run. There is no verifiable evidence of work."
435
- : "Agent claims success but provided no execution_evidence. " +
436
- "Every successful round MUST include execution_evidence with " +
437
- "files_changed, test_results, and progress_estimate.",
395
+ : "Agent claims success but provided no execution_report. " +
396
+ "Every successful round MUST include execution_report with " +
397
+ "files_changed, tests_reported, and progress_estimate.",
438
398
  });
439
399
  }
440
400
  // Claims arm (v3.6).
441
401
  if (status.providerStatus === "verified")
442
402
  return null;
443
- const noChange = typeof selfEval.no_change_reason === "string" &&
403
+ // M2 (v3.7.x): the no_change_reason escape is only honest when machine
404
+ // verification was STRUCTURALLY impossible. With an enabled verification
405
+ // command configured, the runtime had a machine-verifiable path for this
406
+ // round — a declared reason cannot waive it (that would let an agent
407
+ // dodge the configured verification by writing a sentence, even while git
408
+ // observed real file changes). Verification-less policies (the default:
409
+ // commands: []) keep the escape: there is genuinely nothing to run.
410
+ const verificationConfigured = getPolicy().evidence.commands.some((command) => command.enabled);
411
+ const noChange = !verificationConfigured &&
412
+ typeof selfEval.no_change_reason === "string" &&
444
413
  selfEval.no_change_reason.trim().length > 0;
445
- // v3.3: severity follows evidence.machine_backed_success — "required"
446
- // rejects (error), "warn" tolerates with a warn (round commits, success
447
- // excluded from the trajectory, trust drops). no_change_reason stays info.
448
- const severity = noChange
449
- ? "info"
450
- : getPolicy().evidence.machine_backed_success === "warn"
451
- ? "warn"
452
- : "error";
414
+ // v3.8: an unbacked success claim is "机器观察不足", not a contradiction —
415
+ // the round COMMITS with the claim recorded as insufficient, and the
416
+ // success stays out of the success trajectory (shouldPushSuccess keys on
417
+ // this warn). The bounded verification-debt row is what eventually acts on
418
+ // a persistent pattern. `no_change_reason` stays info, and only works when
419
+ // machine verification was structurally impossible (no enabled command).
420
+ // A success claim with NO execution_report at all is the other arm and
421
+ // remains an unconditional error.
422
+ const severity = noChange ? "info" : "warn";
453
423
  return makeVerificationFlag({
454
424
  severity,
455
425
  field: "success",
@@ -553,16 +523,16 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
553
523
  return null;
554
524
  }
555
525
  /** v1.18: Cross-validate agent-reported files_changed against git evidence
556
- * from the configured evidence providers (ProviderSnapshot array). */
526
+ * from the configured evidence providers (MachineObservation array). */
557
527
  function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
558
- if (!selfEval.execution_evidence)
528
+ if (!selfEval.execution_report)
559
529
  return null;
560
530
  if (evidenceSnapshots.length === 0)
561
531
  return null;
562
- const gitSnap = evidenceSnapshots.find((s) => s.provider === "git");
532
+ const gitSnap = evidenceSnapshots.find((s) => s.providerId === "git");
563
533
  if (!gitSnap)
564
534
  return null;
565
- const reported = [...selfEval.execution_evidence.files_changed].sort();
535
+ const reported = [...(selfEval.execution_report.files_changed ?? [])].sort();
566
536
  const actual = [...gitSnap.files].sort();
567
537
  if (reported.length === 0 && actual.length === 0)
568
538
  return null;
@@ -597,22 +567,20 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
597
567
  if (!effectiveSuccess(selfEval))
598
568
  return null;
599
569
  for (const snapshot of evidenceSnapshots) {
600
- if (snapshot.data.kind !== "command")
570
+ if (snapshot.kind !== "command")
601
571
  continue;
602
- if (snapshot.data.phase !== "after" || snapshot.data.required !== true)
572
+ if (snapshot.phase !== "after" || snapshot.data.required !== true)
603
573
  continue;
604
- const status = snapshot.data.status;
574
+ const status = snapshot.status;
605
575
  if (status === "passed")
606
576
  continue;
607
- const name = typeof snapshot.data.commandName === "string"
608
- ? snapshot.data.commandName
609
- : snapshot.provider;
577
+ const name = snapshot.data.commandId;
610
578
  const exitCode = typeof snapshot.data.exitCode === "number"
611
579
  ? ` (exit ${snapshot.data.exitCode})`
612
580
  : "";
613
581
  return makeVerificationFlag({
614
582
  severity: "error",
615
- field: "execution_evidence",
583
+ field: "execution_report",
616
584
  check: CHECK_REQUIRED_COMMAND_FAILED,
617
585
  detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
618
586
  });
@@ -620,122 +588,12 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
620
588
  return null;
621
589
  }
622
590
  // ── Test output parsing ────────────────────────────────────────────────────
623
- // jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
624
- /** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
625
- * Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
626
- function extractSubGoalIds(text) {
627
- return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
628
- }
629
591
  /** Whether a changed path looks like a test file. */
630
592
  function isTestFile(path) {
631
593
  return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
632
594
  /(^|[\\/])tests?[\\/]/i.test(path) ||
633
595
  /_test\.[a-z0-9]+$/i.test(path);
634
596
  }
635
- /** Map each referenced sub-goal ID to its known description(s), reconstructed
636
- * from emerged_subtasks in the vault lineage and the previous self-eval. */
637
- function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
638
- const map = new Map();
639
- for (const id of ids)
640
- map.set(id, []);
641
- const descriptions = [];
642
- for (const entry of vaultEntries) {
643
- const emerged = Array.isArray(entry.emerged_subtasks)
644
- ? entry.emerged_subtasks.filter((v) => typeof v === "string")
645
- : [];
646
- descriptions.push(...emerged);
647
- }
648
- descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
649
- for (const desc of descriptions) {
650
- const trimmed = desc.trim();
651
- if (!trimmed)
652
- continue;
653
- const id = deriveSubGoalId(trimmed);
654
- if (map.has(id))
655
- map.get(id).push(trimmed);
656
- }
657
- return map;
658
- }
659
- /** v2.1 (check 11): Compare the previous round's declared next_action with
660
- * the current round's actual output_summary. Detect when the agent says it
661
- * will do X but then does Y — silent drift without explanation.
662
- *
663
- * Only fires when both next_action and output_summary are non-empty.
664
- *
665
- * v2.14: Structured-ID-first detection. Alignment is decided by a waterfall
666
- * of concrete signals before falling back to string similarity:
667
- * 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
668
- * round's completed_subtasks (exact ID, ID derived from the
669
- * description text, or Jaccard against the referenced sub-goal's
670
- * known description).
671
- * 2. File paths named in next_action appearing in files_changed.
672
- * 3. Test evidence — next_action mentions tests and test files were
673
- * changed with tests actually run.
674
- * 4. Jaccard token similarity against the policy threshold (weak signal).
675
- * Any signal aligning is sufficient — text similarity is never the sole
676
- * authority when structured evidence exists. */
677
- function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
678
- if (!prevSelfEval?.next_action?.trim())
679
- return null;
680
- if (!selfEval.output_summary.trim())
681
- return null;
682
- const intent = prevSelfEval.next_action.trim();
683
- const actual = selfEval.output_summary.trim();
684
- const policy = getPolicy();
685
- const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
686
- // ── Signal 1: sub-goal IDs referenced by next_action → completed this round
687
- const intentIds = extractSubGoalIds(intent);
688
- if (intentIds.length > 0) {
689
- const completed = selfEval.completed_subtasks ?? [];
690
- const referenced = referencedSubGoalDescriptions(intentIds, vaultEntries, prevSelfEval);
691
- const idMatched = intentIds.some((id) => completed.some((entry) => {
692
- const text = entry.trim();
693
- if (!text)
694
- return false;
695
- if (text.toLowerCase() === id)
696
- return true;
697
- if (deriveSubGoalId(text) === id)
698
- return true;
699
- const descriptions = referenced.get(id) ?? [];
700
- return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
701
- }));
702
- if (idMatched)
703
- return null;
704
- }
705
- // ── Signal 2: file paths named in next_action appear in files_changed
706
- const intentPaths = extractFilePathTokens(intent);
707
- if (intentPaths.length > 0 && filesChanged.length > 0) {
708
- const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
709
- if (pathMatched)
710
- return null;
711
- }
712
- // ── Signal 3: test evidence — intent mentions tests, test files changed,
713
- // and tests actually ran this round
714
- const intentTokens = tokenize(intent);
715
- const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
716
- const testResults = selfEval.execution_evidence?.test_results;
717
- const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
718
- const testFilesChanged = filesChanged.some((f) => isTestFile(f));
719
- if (mentionsTests && testsRan && testFilesChanged)
720
- return null;
721
- // ── Signal 4 (weak): Jaccard token similarity
722
- const score = jaccardSimilarity(intent, actual);
723
- const threshold = policy.evolution.intent_drift_threshold;
724
- if (score >= threshold)
725
- return null;
726
- const idNote = intentIds.length > 0
727
- ? `; ${intentIds.length} referenced sub-goal ID(s) did not match completed_subtasks`
728
- : "";
729
- return makeVerificationFlag({
730
- severity: "warn",
731
- field: "output_summary",
732
- check: CHECK_INTENT_DRIFT,
733
- detail: `Declared intent was "${intent.slice(0, 120)}" ` +
734
- `but actual output "${actual.slice(0, 120)}" — ` +
735
- `similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
736
- idNote,
737
- });
738
- }
739
597
  /** Best-effort parse of test counts from common test-runner output formats.
740
598
  *
741
599
  * Scans the last 2000 characters of stdout (where summary lines typically
@@ -800,7 +658,7 @@ export function parseTestOutput(stdout) {
800
658
  }
801
659
  return null;
802
660
  }
803
- /** Cross-validate agent-reported test_results against command evidence output.
661
+ /** Cross-validate agent-reported tests_reported against command evidence output.
804
662
  *
805
663
  * For each command provider whose status is "passed" and output is not
806
664
  * truncated, attempts to parse test counts from stdout. Mismatched counts
@@ -810,17 +668,17 @@ export function parseTestOutput(stdout) {
810
668
  * Non-command evidence providers are skipped (no structural cross-check is
811
669
  * defined for them yet). */
812
670
  function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
813
- const reported = selfEval.execution_evidence?.test_results;
671
+ const reported = selfEval.execution_report?.tests_reported;
814
672
  if (!reported)
815
673
  return null;
816
674
  for (const snapshot of evidenceSnapshots) {
817
- if (snapshot.data.kind !== "command")
675
+ if (snapshot.kind !== "command")
818
676
  continue;
819
- if (snapshot.data.status !== "passed")
677
+ if (snapshot.status !== "passed")
820
678
  continue;
821
679
  if (snapshot.data.truncated === true)
822
680
  continue;
823
- const stdout = typeof snapshot.data.stdout === "string" ? snapshot.data.stdout : "";
681
+ const stdout = typeof snapshot.data.stdoutExcerpt === "string" ? snapshot.data.stdoutExcerpt : "";
824
682
  const parsed = parseTestOutput(stdout);
825
683
  if (!parsed)
826
684
  continue;
@@ -835,16 +693,14 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
835
693
  // Counts match exactly — evidence supports the agent's claim.
836
694
  continue;
837
695
  }
838
- const name = typeof snapshot.data.commandName === "string"
839
- ? snapshot.data.commandName
840
- : snapshot.provider;
696
+ const name = snapshot.data.commandId;
841
697
  // Agent hides failures → error. Other mismatch → warn.
842
698
  const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
843
699
  return makeVerificationFlag({
844
700
  severity,
845
- field: "test_results",
701
+ field: "tests_reported",
846
702
  check: CHECK_COMMAND_EVIDENCE_MISMATCH,
847
- detail: `Agent test_results don't match "${name}" output: ${mismatches.join("; ")}`,
703
+ detail: `Agent tests_reported don't match "${name}" output: ${mismatches.join("; ")}`,
848
704
  });
849
705
  }
850
706
  return null;
@@ -856,149 +712,99 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
856
712
  * confirm the tests still verify the task claims). Fail-open when the git
857
713
  * snapshot is missing or the command has no resolvable entrypoint. */
858
714
  function checkVerificationDomainIntegrity(evidenceSnapshots) {
859
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
715
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git") ?? null;
860
716
  if (!git)
861
717
  return null;
718
+ // v3.8.1: EVERY after-phase command is scanned for a tampered entrypoint
719
+ // before the test-file warn may answer for one of them. `testFilesModified`
720
+ // is derived from the git file set alone, so it is true for every command as
721
+ // soon as the round touched a test file; answering with that warn on the
722
+ // first command in array order meant a rewritten verification entrypoint
723
+ // produced no error flag at all, and the enforcement row that reacts to it
724
+ // never fired. The verdict depended on observation order.
862
725
  for (const snapshot of evidenceSnapshots) {
863
- if (!isRecord(snapshot.data) || snapshot.data.kind !== "command")
726
+ if (!isRecord(snapshot.data) || snapshot.kind !== "command")
864
727
  continue;
865
- if (snapshot.data.phase !== "after")
728
+ if (snapshot.phase !== "after")
866
729
  continue;
867
- const { entrypointModified, testFilesModified } = commandTampered(snapshot, git);
868
- if (!entrypointModified && !testFilesModified)
730
+ if (!commandTampered(snapshot, git).entrypointModified)
731
+ continue;
732
+ return makeVerificationFlag({
733
+ severity: "error",
734
+ field: "execution_report",
735
+ check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
736
+ detail: `Verification command "${snapshot.data.commandId}" entrypoint changed this round — ` +
737
+ "its result cannot be trusted as machine evidence. Keep the " +
738
+ "verification command stable or resubmit the same round with the " +
739
+ "entrypoint unchanged.",
740
+ });
741
+ }
742
+ // Only when NO entrypoint was tampered does the test-file warn stand in.
743
+ for (const snapshot of evidenceSnapshots) {
744
+ if (!isRecord(snapshot.data) || snapshot.kind !== "command")
745
+ continue;
746
+ if (snapshot.phase !== "after")
747
+ continue;
748
+ if (!commandTampered(snapshot, git).testFilesModified)
869
749
  continue;
870
- const name = typeof snapshot.data.commandName === "string"
871
- ? snapshot.data.commandName
872
- : snapshot.provider;
873
- if (entrypointModified) {
874
- return makeVerificationFlag({
875
- severity: "error",
876
- field: "execution_evidence",
877
- check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
878
- detail: `Verification command "${name}" entrypoint changed this round — ` +
879
- "its result cannot be trusted as machine evidence. Keep the " +
880
- "verification command stable or resubmit the same round with the " +
881
- "entrypoint unchanged.",
882
- });
883
- }
884
750
  return makeVerificationFlag({
885
751
  severity: "warn",
886
- field: "execution_evidence",
752
+ field: "execution_report",
887
753
  check: CHECK_TEST_FILES_MODIFIED,
888
- detail: `Test files changed in the same round command "${name}" passed — ` +
754
+ detail: `Test files changed in the same round command "${snapshot.data.commandId}" passed — ` +
889
755
  "confirm the tests still verify the task claims.",
890
756
  });
891
757
  }
892
758
  return null;
893
759
  }
894
- /** v2.2: Check if the agent's declared next_action aligns with any pending
895
- * or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
896
- * plans work unrelated to any of them, it may indicate task drift.
897
- * This is a warn-level advisory — the agent always owns prioritization.
760
+ /** v2.13: After a backtrack, check that the agent restored the workspace
761
+ * before working. Two arms:
898
762
  *
899
- * v2.14: Exact sub-goal ID alignment first — next_action naming a pending
900
- * sub-goal by its stable ID (sg-XXXXXXXX) is aligned by definition.
901
- * Jaccard description similarity remains as the fallback. */
902
- function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
903
- const nextAction = selfEval.next_action?.trim();
904
- if (!nextAction)
905
- return null;
906
- // Reconstruct pending sub-goal descriptions from vault entries
907
- const pendingSubGoals = new Set();
908
- const completedOrCanceled = new Set();
909
- for (const entry of vaultEntries) {
910
- const emerged = Array.isArray(entry.emerged_subtasks)
911
- ? entry.emerged_subtasks.filter((v) => typeof v === "string")
912
- : [];
913
- for (const desc of emerged) {
914
- pendingSubGoals.add(desc.trim());
915
- }
916
- const done = Array.isArray(entry.completed_subtasks)
917
- ? entry.completed_subtasks.filter((v) => typeof v === "string")
918
- : [];
919
- for (const desc of done) {
920
- completedOrCanceled.add(desc.trim());
921
- }
922
- const canceled = Array.isArray(entry.canceled_subtasks)
923
- ? entry.canceled_subtasks.filter((v) => typeof v === "string")
924
- : [];
925
- for (const desc of canceled) {
926
- completedOrCanceled.add(desc.trim());
763
+ * 1. Machine arm (M3, error): a skipped file whose CURRENT git fingerprint
764
+ * still equals the fingerprint recorded at its failed round was never
765
+ * touched since the rollback — the workspace was provably not restored.
766
+ * Restored or re-modified files change fingerprint and can never be
767
+ * misjudged by this arm.
768
+ *
769
+ * 2. Claims arm (warn only): the agent's files_changed overlaps the skipped
770
+ * list. A self-report alone cannot distinguish a restored-then-redone
771
+ * file from a never-restored one — a legitimate multi-file redo would
772
+ * otherwise be falsely terminated — so overlap is guidance, not a
773
+ * verdict. (The old error tier for >= 3 overlapping files was removed
774
+ * for exactly this reason: it fired on honest redos.) */
775
+ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints = {}, evidenceSnapshots = []) {
776
+ // Machine arm — untouched-since-rollback proof, independent of claims.
777
+ const failedEntries = Object.entries(backtrackSkippedFingerprints);
778
+ if (failedEntries.length > 0) {
779
+ const git = evidenceSnapshots.find((snapshot) => snapshot.kind === "git" && isRecord(snapshot.data.fingerprints));
780
+ const currentFingerprints = git && git.kind === "git"
781
+ ? git.data.fingerprints
782
+ : null;
783
+ if (currentFingerprints) {
784
+ const untouched = failedEntries
785
+ .filter(([file, failedFp]) => currentFingerprints[file] === failedFp)
786
+ .map(([file]) => file)
787
+ .sort();
788
+ if (untouched.length > 0) {
789
+ return makeVerificationFlag({
790
+ severity: "error",
791
+ field: "workspace",
792
+ check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
793
+ detail: `File(s) from the failed rounds are byte-identical to their ` +
794
+ `state at the rollback: ${untouched.slice(0, 5).join(", ")}. The ` +
795
+ `workspace was never restored — restore it to the restore point ` +
796
+ `(git checkout/reset) before redoing the work.`,
797
+ });
798
+ }
927
799
  }
928
800
  }
929
- // Also process current selfEval
930
- for (const desc of selfEval.emerged_subtasks ?? []) {
931
- pendingSubGoals.add(desc.trim());
932
- }
933
- for (const desc of selfEval.completed_subtasks ?? []) {
934
- completedOrCanceled.add(desc.trim());
935
- }
936
- for (const desc of selfEval.canceled_subtasks ?? []) {
937
- completedOrCanceled.add(desc.trim());
938
- }
939
- // Remove completed/canceled from pending
940
- for (const desc of completedOrCanceled) {
941
- pendingSubGoals.delete(desc);
942
- }
943
- // Need at least 3 pending sub-goals for the check to be meaningful
944
- if (pendingSubGoals.size < 3)
801
+ const ev = selfEval.execution_report;
802
+ if (!ev || (ev.files_changed ?? []).length === 0)
945
803
  return null;
946
- // v2.14: Stable ID match first — next_action naming a pending sub-goal
947
- // by ID is aligned by definition, regardless of description wording.
948
- const nextActionIds = extractSubGoalIds(nextAction);
949
- if (nextActionIds.length > 0) {
950
- const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
951
- if (nextActionIds.some((id) => pendingIds.has(id)))
952
- return null;
953
- }
954
- // Check if next_action aligns with any pending sub-goal
955
- let aligns = false;
956
- for (const sg of pendingSubGoals) {
957
- const score = jaccardSimilarity(nextAction, sg);
958
- if (score >= getPolicy().evolution.subgoal_drift_alignment_threshold) {
959
- aligns = true;
960
- break;
961
- }
962
- }
963
- if (!aligns) {
964
- const sample = [...pendingSubGoals].slice(0, 3).join(", ");
965
- return makeVerificationFlag({
966
- severity: "warn",
967
- field: "next_action",
968
- check: CHECK_SUBGOAL_DRIFT,
969
- detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
970
- `Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
971
- `or cancel outdated sub-goals via canceled_subtasks.`,
972
- });
973
- }
974
- return null;
975
- }
976
- /** v2.13: After a backtrack, check that the agent restored the workspace
977
- * before working. If the agent's files_changed overlaps with files from
978
- * skipped (failed) rounds, the workspace was likely not clean. */
979
- function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
980
- if (backtrackSkippedFiles.length === 0)
981
- return null;
982
- const ev = selfEval.execution_evidence;
983
- if (!ev || ev.files_changed.length === 0)
984
- return null;
985
- // Check overlap: files the agent changed vs. files from skipped rounds
986
- const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
804
+ // Claims arm — overlap as guidance only.
805
+ const overlap = (ev.files_changed ?? []).filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
987
806
  if (overlap.length === 0)
988
807
  return null;
989
- // Agent is modifying files that were part of failed rounds without
990
- // having properly restored the workspace first.
991
- if (overlap.length >= 3 || overlap.length === ev.files_changed.length) {
992
- return makeVerificationFlag({
993
- severity: "error",
994
- field: "files_changed",
995
- check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
996
- detail: `Agent modified ${overlap.length} file(s) that were part of ` +
997
- `the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
998
- `The workspace was likely not restored before working.`,
999
- });
1000
- }
1001
- // Minor overlap — warn only
1002
808
  return makeVerificationFlag({
1003
809
  severity: "warn",
1004
810
  field: "files_changed",
@@ -1013,7 +819,7 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
1013
819
  function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
1014
820
  if (!backtrackTargetGitHead)
1015
821
  return null;
1016
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
822
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git" && isRecord(snapshot.data) &&
1017
823
  typeof snapshot.data.head === "string");
1018
824
  if (!git)
1019
825
  return null; // git unavailable → cannot verify → skip (fail-open)
@@ -1032,67 +838,23 @@ function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead)
1032
838
  });
1033
839
  }
1034
840
  // ═══════════════════════════════════════════════════════════════════════════
1035
- // v3.3 — Round Contract checks (all silent when no contract is declared)
841
+ // v3.8 — Round Contract checks (all silent when no contract is declared)
842
+ //
843
+ // The item model moved closure to the runtime: an item is verified only when
844
+ // its bound commands were observed passing (untampered, same config) in the
845
+ // claiming round. A claimed-but-unbacked item is therefore `insufficient`
846
+ // (recorded, never a rejection), and a premature closure is structurally
847
+ // impossible. The checks below only surface state the agent must see.
1036
848
  // ═══════════════════════════════════════════════════════════════════════════
1037
- /** A PROPOSAL that promises nothing is a wasted round's worth of trust:
1038
- * done_when empty means the boundary can never be machine-checked.
1039
- * v3.4: declaration-quality check on the submission's own proposal — never
1040
- * on the (derived) active contract. */
1041
- function checkRoundUnderspecified(selfEval) {
1042
- const contract = selfEval.round_contract;
1043
- if (!contract)
1044
- return null;
1045
- if (contract.done_when.length > 0)
1046
- return null;
1047
- return makeVerificationFlag({
1048
- severity: "warn",
1049
- field: "round_contract",
1050
- check: CHECK_ROUND_UNDERSPECIFIED,
1051
- detail: "Round Contract declared with an empty done_when — nothing is " +
1052
- "promised, so completion cannot be verified. Declare what must be " +
1053
- "true when the round is done.",
1054
- });
1055
- }
1056
- /** done_when items whose verification_plan cannot run (empty plan, or names
1057
- * outside the configured, enabled evidence.commands) can never be
1058
- * machine-checked at the boundary. v3.4: declaration-quality check on the
1059
- * submission's own proposal. */
1060
- function checkRoundUnverifiable(selfEval) {
1061
- const contract = selfEval.round_contract;
1062
- if (!contract)
1063
- return null;
1064
- if (contract.done_when.length === 0)
1065
- return null;
1066
- if (contract.verification_plan.length > 0 &&
1067
- contract.verification_plan.every((name) => isConfiguredCommand(name))) {
1068
- return null;
1069
- }
1070
- const unknown = contract.verification_plan.filter((name) => !isConfiguredCommand(name));
1071
- const detail = contract.verification_plan.length === 0
1072
- ? "done_when items declared but verification_plan is empty — no command " +
1073
- "can back the completion claims"
1074
- : `verification_plan names commands that are not configured and enabled ` +
1075
- `in evidence.commands: ${unknown.slice(0, 3).join(", ")}`;
1076
- return makeVerificationFlag({
1077
- severity: "warn",
1078
- field: "round_contract",
1079
- check: CHECK_ROUND_UNVERIFIABLE,
1080
- detail: detail + ". Name only configured, enabled evidence.commands, or drop " +
1081
- "done_when items that cannot be machine-verified.",
1082
- });
1083
- }
1084
849
  /** The round changed files outside the ACTIVE contract's declared scope.
1085
850
  * Git diff files are the machine-authoritative "what actually changed" set.
1086
- * v3.4: targets the contract the round actually executed under — derived
1087
- * from committed evals, never the submission's own proposal. Silent when
1088
- * no active contract exists (round 1 and whole-task rounds: the agent
1089
- * executed the original task, not a contract boundary). */
851
+ * Silent when no active contract exists or it declares no scope. */
1090
852
  function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
1091
853
  if (!activeContract)
1092
854
  return null;
1093
855
  if (activeContract.scope.length === 0)
1094
856
  return null;
1095
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git");
857
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git");
1096
858
  if (!git)
1097
859
  return null; // git unavailable → cannot observe → fail open
1098
860
  const outOfScope = collectOutOfScopeFiles(git.files, activeContract.scope);
@@ -1106,142 +868,53 @@ function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
1106
868
  (outOfScope.length > 5
1107
869
  ? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
1108
870
  : `: ${outOfScope.join(", ")}`) +
1109
- `. Revert them or extend the contract scope and explain in drift_clarification.`,
1110
- });
1111
- }
1112
- /** success claimed under the ACTIVE contract whose done_when items were not
1113
- * honestly and verifiably completed: claimed met with no machine-verified
1114
- * evidence this round, or silently dropped (present in neither
1115
- * success_criteria_met nor success_criteria_remaining).
1116
- * v3.4: targets the derived ACTIVE contract (the round's real boundary) —
1117
- * a proposal declared on a round without an active contract is never
1118
- * checked for conformance against work the round did not do under it.
1119
- * v3.5.1: a fully-met posture (every done_when claimed satisfied) is owned
1120
- * by contract_completion_unverified — this check stays for mixed and
1121
- * silently-dropped success claims only.
1122
- * v3.6: no_change_reason never downgrades this check — the escape hatch
1123
- * belongs to the R8 success-claim family alone; a declared "no change"
1124
- * contradicts silently dropping contract done_when items. */
1125
- function checkPrematureBoundary(activeContract, selfEval, claimView) {
1126
- if (!activeContract || activeContract.done_when.length === 0)
1127
- return null;
1128
- if (!effectiveSuccess(selfEval))
1129
- return null;
1130
- const ev = selfEval.execution_evidence;
1131
- const met = ev?.success_criteria_met ?? [];
1132
- // v3.5.1: full-met → the completion check owns the posture (it also fires
1133
- // on success=false, so nothing is lost — and under machine_backed_success
1134
- // "warn" the same tolerance applies on both sides).
1135
- if (contractDoneWhenSatisfied(activeContract, met))
1136
- return null;
1137
- const remaining = ev?.success_criteria_remaining ?? [];
1138
- const problems = [];
1139
- for (const item of activeContract.done_when) {
1140
- // Shared matcher with the lifecycle walker (round-contract.ts) — the
1141
- // compile-side closure decision and this conformance check can never
1142
- // disagree on what counts as a satisfied item.
1143
- const isMet = met.some((m) => contractItemMatches(item, m));
1144
- const isRemaining = remaining.some((r) => contractItemMatches(item, r));
1145
- if (isMet) {
1146
- // Claimed met — needs round-level machine verification. The v3.3
1147
- // claim model is round-uniform: any verified claim backs the round,
1148
- // so zero verified claims = none of them are backed.
1149
- if (claimView.verifiedCount === 0) {
1150
- problems.push(`"${item.slice(0, 80)}" claimed met without machine-verified evidence`);
1151
- }
1152
- }
1153
- else if (!isRemaining) {
1154
- // Neither met nor remaining — silently dropped while success is
1155
- // claimed. Honest reporting would list it in remaining (R1's domain).
1156
- problems.push(`"${item.slice(0, 80)}" neither met nor listed as remaining`);
1157
- }
1158
- }
1159
- if (problems.length === 0)
1160
- return null;
1161
- return makeVerificationFlag({
1162
- severity: "error",
1163
- field: "round_contract",
1164
- check: CHECK_PREMATURE_BOUNDARY,
1165
- detail: `Round Contract boundary claimed prematurely: ${problems.slice(0, 3).join("; ")}. ` +
1166
- `Run the contract's verification_plan commands and report real output, ` +
1167
- `or set success=false and list the items in success_criteria_remaining.`,
871
+ `. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
1168
872
  });
1169
873
  }
1170
- /** v3.5: Closing a Round Contract is a success-class claim and must be
1171
- * machine-backed. When the eval's met claims satisfy EVERY done_when of
1172
- * the ACTIVE contract (the walker's close condition), each verification_plan
1173
- * command must have been observed passing (after-phase command snapshot,
1174
- * untampered) in the same round. Fires regardless of the success flag —
1175
- * a claim-based closure that the machine cannot back must not advance the
1176
- * contract state machine. Severity follows evidence.machine_backed_success
1177
- * (the R8 tolerance switch). Deliberately no no_change_reason downgrade:
1178
- * all done_when met contradicts "no change". Fail-open: a plan name that
1179
- * is no longer a configured, enabled command cannot be observed and is
1180
- * not required. The check observes that the commands ran, not what they
1181
- * verified — R-C1's claim model remains the content bound. */
1182
- function checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots) {
1183
- if (!activeContract || activeContract.verification_plan.length === 0)
1184
- return null;
1185
- const met = selfEval.execution_evidence?.success_criteria_met ?? [];
1186
- if (!contractDoneWhenSatisfied(activeContract, met))
1187
- return null; // walker does not close
1188
- const required = activeContract.verification_plan
1189
- .filter((name) => isConfiguredCommand(name));
1190
- if (required.length === 0)
1191
- return null; // fail open — cannot observe
1192
- const passed = passedPlanCommandNames(evidenceSnapshots);
1193
- const missing = required.filter((name) => !passed.has(name));
1194
- if (missing.length === 0)
874
+ /** v3.8: Claimed-but-unverified contract items. The round COMMITS — the debt
875
+ * is surfaced (prompt, state file, handoff) and the enforcement gate's
876
+ * verification-debt row handles a persistent pattern. */
877
+ function checkContractItemsUnverified(statuses) {
878
+ if (statuses.insufficientCount === 0)
1195
879
  return null;
1196
- const severity = getPolicy().evidence.machine_backed_success === "warn"
1197
- ? "warn"
1198
- : "error";
880
+ const unverified = statuses.items
881
+ .filter((item) => item.status === "insufficient")
882
+ .slice(0, 3)
883
+ .map((item) => item.itemId);
1199
884
  return makeVerificationFlag({
1200
- severity,
885
+ severity: "warn",
1201
886
  field: "round_contract",
1202
- check: CHECK_CONTRACT_COMPLETION_UNVERIFIED,
1203
- detail: `Round Contract completion claimed (every done_when met), but verification_plan ` +
1204
- `command${missing.length > 1 ? "s" : ""} did not pass this round: ` +
1205
- `${missing.slice(0, 3).join(", ")}. Closing a contract is a success-class claim — ` +
1206
- `fix the underlying failure so the command${missing.length > 1 ? "s" : ""} pass and ` +
1207
- `resubmit; no_change_reason does not apply to completion claims.`,
887
+ check: CHECK_CONTRACT_ITEMS_UNVERIFIED,
888
+ detail: `${statuses.insufficientCount} contract item(s) claimed met but not machine-verified: ` +
889
+ `${unverified.join(", ")}. Run the bound command(s) or report the item as remaining.`,
1208
890
  });
1209
891
  }
1210
- /** v3.5: A different contract proposed while the ACTIVE contract is still
1211
- * open. The walker ignores the premature proposal — this warn only makes
1212
- * the ignored state visible to the agent. Silent when the eval closes the
1213
- * active contract (all done_when met, or outcome=blocked — checked FIRST,
1214
- * mirroring the walker), when no proposal is submitted, or when the
1215
- * proposal equals the active contract (a restate). Equality is
1216
- * key-order-insensitive and normalized through parseRoundContract on both
1217
- * sides so committed-raw vs parsed-submission key-set drift cannot cause
1218
- * spurious warns. */
1219
- function checkContractPremature(activeContract, selfEval) {
892
+ /** A different contract proposed while the ACTIVE contract is still open. The
893
+ * walker ignores the premature proposal — this warn only makes the ignored
894
+ * state visible. Silent when the active contract is closed (all items
895
+ * verified, or outcome=blocked), when no proposal is submitted, or when the
896
+ * proposal is a restate (content equality, never id equality). */
897
+ function checkContractPremature(activeContract, statuses, selfEval) {
1220
898
  if (!activeContract)
1221
899
  return null;
1222
- const met = selfEval.execution_evidence?.success_criteria_met ?? [];
1223
- if (contractDoneWhenSatisfied(activeContract, met))
900
+ if (statuses.closure !== "open")
1224
901
  return null; // closed
1225
902
  if (selfEval.outcome === "blocked")
1226
903
  return null; // closed
1227
904
  const proposal = selfEval.round_contract;
1228
905
  if (!proposal)
1229
906
  return null;
1230
- const same = stableStringify(parseRoundContract(activeContract)) ===
1231
- stableStringify(parseRoundContract(proposal));
1232
- if (same)
907
+ if (sameContract(proposal, activeContract))
1233
908
  return null; // restate → continue
1234
909
  return makeVerificationFlag({
1235
910
  severity: "warn",
1236
911
  field: "round_contract",
1237
912
  check: CHECK_CONTRACT_PREMATURE,
1238
- detail: "The ACTIVE Round Contract is still open (not all done_when met, not blocked) " +
913
+ detail: "The ACTIVE Round Contract is still open (not every item verified, not blocked) " +
1239
914
  "while a different contract was proposed — it is ignored until the active " +
1240
- "contract is completed or blocked. Restate the active contract unchanged to " +
1241
- "continue it.",
915
+ "contract closes. Restate the active contract unchanged to continue it.",
1242
916
  });
1243
917
  }
1244
- // ═══════════════════════════════════════════════════════════════════════════
1245
918
  // Main entry point
1246
919
  // ═══════════════════════════════════════════════════════════════════════════
1247
920
  /** Verify a SelfEvaluation against the loop's cross-round lineage.
@@ -1256,15 +929,80 @@ function checkContractPremature(activeContract, selfEval) {
1256
929
  * @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
1257
930
  * Used by checkEvidenceIntegrity for multi-provider
1258
931
  * cross-validation. Defaults to empty array. */
1259
- export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
932
+ /** v3.7.1: Opt-in round blocking for the gate layer. For every gate id the
933
+ * round cites (gate_ids), the gate_opened record must exist, must be a
934
+ * USER gate, and must have an approved gate_decision. Error flags list
935
+ * every failing id with a reason. Entirely skipped when the feature is
936
+ * disabled — there is nothing to cite against. */
937
+ function checkUserGateUnresolved(selfEval, gateEntries) {
938
+ if (!getPolicy().gate.enabled)
939
+ return null;
940
+ const cited = (selfEval.gate_ids ?? []).filter((id) => typeof id === "string");
941
+ if (cited.length === 0)
942
+ return null;
943
+ const opened = new Set();
944
+ const userKind = new Set();
945
+ const approved = new Set();
946
+ for (const entry of gateEntries) {
947
+ if (entry.task_type === "gate_opened" && typeof entry.gate_id === "string") {
948
+ opened.add(entry.gate_id);
949
+ // v3.7.1: structured records declare gate_kind; legacy flat records
950
+ // (blocked-round auto-records) classify their stored action text.
951
+ if (entry.gate_kind === "user") {
952
+ userKind.add(entry.gate_id);
953
+ }
954
+ else if (typeof entry.gate_action === "string") {
955
+ let kind = null;
956
+ try {
957
+ kind = preflightStructuredGate(JSON.parse(entry.gate_action)).kind;
958
+ }
959
+ catch {
960
+ kind = deriveGate(entry.gate_action).gate.kind;
961
+ }
962
+ if (kind === "user")
963
+ userKind.add(entry.gate_id);
964
+ }
965
+ }
966
+ if (entry.task_type === "gate_decision" && entry.approved === true &&
967
+ typeof entry.gate_id === "string") {
968
+ approved.add(entry.gate_id);
969
+ }
970
+ }
971
+ const failures = [];
972
+ for (const gateId of cited) {
973
+ if (!opened.has(gateId))
974
+ failures.push(`${gateId} (not_found)`);
975
+ else if (!userKind.has(gateId))
976
+ failures.push(`${gateId} (not_user_gate)`);
977
+ else if (!approved.has(gateId))
978
+ failures.push(`${gateId} (not_approved)`);
979
+ }
980
+ if (failures.length === 0)
981
+ return null;
982
+ return makeVerificationFlag({
983
+ severity: "error",
984
+ field: "evaluation",
985
+ check: CHECK_USER_GATE_UNRESOLVED,
986
+ detail: `The round cites gates without an approved human decision: ` +
987
+ `${failures.join("; ")}. Obtain approval via loopforge_gate_check / ` +
988
+ `loopforge_gate_resolve before claiming the action is done.`,
989
+ });
990
+ }
991
+ export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
1260
992
  /** v2.13: Files from skipped backtrack rounds. If the agent's
1261
993
  * files_changed overlaps significantly with these, the workspace
1262
994
  * was not properly restored before working. */
1263
- backtrackSkippedFiles = [],
995
+ backtrackSkippedFiles = [],
996
+ /** M3 (v3.7.x): skipped-file git fingerprints at their failed rounds —
997
+ * machine proof of "untouched since the rollback" for the restore check. */
998
+ backtrackSkippedFingerprints = {},
1264
999
  /** v2.12: Git HEAD commit of the backtrack restore point. When set, the
1265
1000
  * current git snapshot must sit at this commit — otherwise the workspace
1266
1001
  * was not restored and the round cannot be accepted. */
1267
- backtrackTargetGitHead) {
1002
+ backtrackTargetGitHead,
1003
+ /** v3.7.1: gate records (task_type gate_opened / gate_decision) live
1004
+ * outside the round prefix — the caller passes them in explicitly. */
1005
+ gateEntries = []) {
1268
1006
  const flags = [];
1269
1007
  // Collect violations from all previous vault entries for duplicate-discovery
1270
1008
  // and other checks that need deeper history.
@@ -1278,7 +1016,18 @@ backtrackTargetGitHead) {
1278
1016
  // round_contract is a PROPOSAL for the next round and never participates
1279
1017
  // (the eval under verification is not yet committed, so it structurally
1280
1018
  // cannot influence the derivation).
1281
- const activeContract = deriveActiveRoundContract(contractRoundEvaluations(committedRoundsFromEntries(vaultEntries, currentRound)));
1019
+ const committedRounds = derivationRounds(vaultEntries, currentRound);
1020
+ const activeContract = deriveActiveRoundContract(committedRounds);
1021
+ // v3.8: The item-level status of the ACTIVE contract for THIS round — the
1022
+ // same reducer the compile path, audit, and explain consume.
1023
+ const contractStatuses = deriveContractItemStatuses({
1024
+ contract: activeContract,
1025
+ rounds: committedRounds,
1026
+ currentRound,
1027
+ currentReport: selfEval.execution_report ?? null,
1028
+ currentObservations: evidenceSnapshots,
1029
+ commands: getPolicy().evidence.commands ?? [],
1030
+ });
1282
1031
  // v2.12: Derived claim provenance — which reported criteria completions
1283
1032
  // are backed by machine evidence (pure derivation, not persisted).
1284
1033
  const claimView = deriveClaimView(selfEval, evidenceSnapshots);
@@ -1308,30 +1057,24 @@ backtrackTargetGitHead) {
1308
1057
  // v1.18: Cross-validate agent-reported files_changed against git evidence
1309
1058
  () => checkEvidenceIntegrity(selfEval, evidenceSnapshots),
1310
1059
  () => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
1311
- // v2.0: Cross-validate agent-reported test_results against command output
1060
+ // v2.0: Cross-validate agent-reported tests_reported against command output
1312
1061
  () => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
1313
1062
  // v3.3: Verification domain integrity — command entrypoint / test files
1314
1063
  // changed in the same round the command ran
1315
1064
  () => checkVerificationDomainIntegrity(evidenceSnapshots),
1316
- // v2.1: Detect intent-action drift — agent said X but did Y
1317
- () => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
1318
- // v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
1319
- () => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
1320
1065
  // v2.13: Post-backtrack workspace restore check
1321
- () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
1066
+ () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceSnapshots),
1322
1067
  // v2.12: Post-backtrack git HEAD restore check
1323
1068
  () => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
1324
- // v3.3: Round Contract checks. v3.4: split by target — structural
1325
- // (warn) at proposal declaration, conformance against the derived
1326
- // ACTIVE contract (scope_drift warn / premature_boundary error).
1327
- // v3.5: + completion machine-backing (error) and premature-replacement
1328
- // visibility (warn).
1329
- () => checkRoundUnderspecified(selfEval),
1330
- () => checkRoundUnverifiable(selfEval),
1069
+ // v3.8: Round Contract checks over the derived item model. Declaration
1070
+ // quality is enforced structurally (contract_invalid) before the round
1071
+ // advances, so the gate only reports execution state: scope drift,
1072
+ // claimed-but-unverified items, and an ignored replacement proposal.
1331
1073
  () => checkRoundScopeDrift(activeContract, evidenceSnapshots),
1332
- () => checkPrematureBoundary(activeContract, selfEval, claimView),
1333
- () => checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots),
1334
- () => checkContractPremature(activeContract, selfEval),
1074
+ () => checkContractItemsUnverified(contractStatuses),
1075
+ () => checkContractPremature(activeContract, contractStatuses, selfEval),
1076
+ // v3.7.1: opt-in gate-layer blocking (skipped when disabled)
1077
+ () => checkUserGateUnresolved(selfEval, gateEntries),
1335
1078
  ];
1336
1079
  for (const run of checks) {
1337
1080
  const result = run();
@@ -1353,6 +1096,14 @@ backtrackTargetGitHead) {
1353
1096
  else {
1354
1097
  verdict = "trusted";
1355
1098
  }
1356
- return makeVerificationResult({ verdict, flags });
1099
+ // v3.8.1: the machine-backed-success fact rides along with the result —
1100
+ // ONE derivation (`deriveEvidenceStatus`), so the progress evaluator reads
1101
+ // the same fact instead of inventing a second one. A round the machine
1102
+ // verified is the loop FINISHING; a motion window says nothing about it,
1103
+ // and the old self-reported `>= 0.95` guard said it with the agent's own
1104
+ // number.
1105
+ const machineBackedSuccess = effectiveSuccess(selfEval) &&
1106
+ deriveEvidenceStatus(selfEval, evidenceSnapshots).providerStatus === "verified";
1107
+ return makeVerificationResult({ verdict, flags, machineBackedSuccess });
1357
1108
  }
1358
1109
  //# sourceMappingURL=verification-gate.js.map