loopforge 3.7.1 → 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 (144) hide show
  1. package/README.md +171 -169
  2. package/dist/audit.d.ts +53 -1
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +106 -22
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +52 -67
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +134 -147
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +103 -4
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-facts.d.ts +13 -0
  14. package/dist/cognitive-facts.d.ts.map +1 -1
  15. package/dist/cognitive-facts.js +28 -13
  16. package/dist/cognitive-facts.js.map +1 -1
  17. package/dist/committed-round.d.ts +103 -21
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +188 -45
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/contract-items.d.ts +55 -0
  22. package/dist/contract-items.d.ts.map +1 -0
  23. package/dist/contract-items.js +180 -0
  24. package/dist/contract-items.js.map +1 -0
  25. package/dist/enforcement-gate.d.ts +20 -2
  26. package/dist/enforcement-gate.d.ts.map +1 -1
  27. package/dist/enforcement-gate.js +197 -386
  28. package/dist/enforcement-gate.js.map +1 -1
  29. package/dist/engine.d.ts +1 -1
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +53 -38
  32. package/dist/engine.js.map +1 -1
  33. package/dist/evidence-claims.d.ts +4 -4
  34. package/dist/evidence-claims.d.ts.map +1 -1
  35. package/dist/evidence-claims.js +16 -14
  36. package/dist/evidence-claims.js.map +1 -1
  37. package/dist/evidence-provider.d.ts +85 -89
  38. package/dist/evidence-provider.d.ts.map +1 -1
  39. package/dist/evidence-provider.js +328 -184
  40. package/dist/evidence-provider.js.map +1 -1
  41. package/dist/explain.d.ts +76 -0
  42. package/dist/explain.d.ts.map +1 -0
  43. package/dist/explain.js +127 -0
  44. package/dist/explain.js.map +1 -0
  45. package/dist/generate-schema.js +152 -16
  46. package/dist/generate-schema.js.map +1 -1
  47. package/dist/index.d.ts +6 -6
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -5
  50. package/dist/index.js.map +1 -1
  51. package/dist/loop-compiler.d.ts +40 -53
  52. package/dist/loop-compiler.d.ts.map +1 -1
  53. package/dist/loop-compiler.js +229 -553
  54. package/dist/loop-compiler.js.map +1 -1
  55. package/dist/loop-extras-parser.d.ts +0 -1
  56. package/dist/loop-extras-parser.d.ts.map +1 -1
  57. package/dist/loop-extras-parser.js +0 -4
  58. package/dist/loop-extras-parser.js.map +1 -1
  59. package/dist/loop-projection.d.ts.map +1 -1
  60. package/dist/loop-projection.js +3 -1
  61. package/dist/loop-projection.js.map +1 -1
  62. package/dist/mcp/round-lifecycle.d.ts +30 -21
  63. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  64. package/dist/mcp/round-lifecycle.js +47 -103
  65. package/dist/mcp/round-lifecycle.js.map +1 -1
  66. package/dist/mcp/server.d.ts.map +1 -1
  67. package/dist/mcp/server.js +51 -7
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/mcp/session.d.ts +39 -23
  70. package/dist/mcp/session.d.ts.map +1 -1
  71. package/dist/mcp/session.js +148 -163
  72. package/dist/mcp/session.js.map +1 -1
  73. package/dist/mcp/submission-boundary.d.ts +89 -0
  74. package/dist/mcp/submission-boundary.d.ts.map +1 -0
  75. package/dist/mcp/submission-boundary.js +203 -0
  76. package/dist/mcp/submission-boundary.js.map +1 -0
  77. package/dist/mcp/tools.d.ts.map +1 -1
  78. package/dist/mcp/tools.js +249 -128
  79. package/dist/mcp/tools.js.map +1 -1
  80. package/dist/policy-metrics.js +2 -2
  81. package/dist/policy-metrics.js.map +1 -1
  82. package/dist/policy.d.ts +55 -93
  83. package/dist/policy.d.ts.map +1 -1
  84. package/dist/policy.js +149 -38
  85. package/dist/policy.js.map +1 -1
  86. package/dist/prompt-assembler.d.ts +1 -32
  87. package/dist/prompt-assembler.d.ts.map +1 -1
  88. package/dist/prompt-assembler.js +282 -435
  89. package/dist/prompt-assembler.js.map +1 -1
  90. package/dist/prompt-policy.d.ts +1 -4
  91. package/dist/prompt-policy.d.ts.map +1 -1
  92. package/dist/prompt-policy.js +6 -7
  93. package/dist/prompt-policy.js.map +1 -1
  94. package/dist/protocol.d.ts +365 -142
  95. package/dist/protocol.d.ts.map +1 -1
  96. package/dist/protocol.js +21 -66
  97. package/dist/protocol.js.map +1 -1
  98. package/dist/replay.js +2 -2
  99. package/dist/replay.js.map +1 -1
  100. package/dist/round-contract.d.ts +69 -57
  101. package/dist/round-contract.d.ts.map +1 -1
  102. package/dist/round-contract.js +104 -64
  103. package/dist/round-contract.js.map +1 -1
  104. package/dist/round-coordinator.d.ts +36 -16
  105. package/dist/round-coordinator.d.ts.map +1 -1
  106. package/dist/round-coordinator.js +166 -102
  107. package/dist/round-coordinator.js.map +1 -1
  108. package/dist/round-driver.d.ts +3 -5
  109. package/dist/round-driver.d.ts.map +1 -1
  110. package/dist/round-driver.js +0 -1
  111. package/dist/round-driver.js.map +1 -1
  112. package/dist/round-facts.d.ts +96 -0
  113. package/dist/round-facts.d.ts.map +1 -0
  114. package/dist/round-facts.js +216 -0
  115. package/dist/round-facts.js.map +1 -0
  116. package/dist/round-transaction.d.ts +19 -9
  117. package/dist/round-transaction.d.ts.map +1 -1
  118. package/dist/round-transaction.js +90 -22
  119. package/dist/round-transaction.js.map +1 -1
  120. package/dist/self-eval.d.ts +87 -9
  121. package/dist/self-eval.d.ts.map +1 -1
  122. package/dist/self-eval.js +396 -27
  123. package/dist/self-eval.js.map +1 -1
  124. package/dist/subgoal-state.d.ts +77 -0
  125. package/dist/subgoal-state.d.ts.map +1 -0
  126. package/dist/subgoal-state.js +198 -0
  127. package/dist/subgoal-state.js.map +1 -0
  128. package/dist/token-utils.d.ts +52 -19
  129. package/dist/token-utils.d.ts.map +1 -1
  130. package/dist/token-utils.js +83 -72
  131. package/dist/token-utils.js.map +1 -1
  132. package/dist/verification-gate.d.ts +14 -31
  133. package/dist/verification-gate.d.ts.map +1 -1
  134. package/dist/verification-gate.js +178 -496
  135. package/dist/verification-gate.js.map +1 -1
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/dist/workspace.d.ts +9 -0
  139. package/dist/workspace.d.ts.map +1 -1
  140. package/dist/workspace.js +17 -0
  141. package/dist/workspace.js.map +1 -1
  142. package/loop_policy.json +6 -30
  143. package/package.json +1 -1
  144. package/skills/perception/SKILL.md +243 -79
@@ -18,15 +18,15 @@
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, entrySubGoalUpdates, 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
30
  import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
31
31
  // ═══════════════════════════════════════════════════════════════════════════
32
32
  // v2.12: Check-name constants — single source of truth so the enforcement
@@ -64,8 +64,6 @@ export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_m
64
64
  /** v3.3: Test files changed in the same round a verification command passed.
65
65
  * Warn-only — the command result stays usable. */
66
66
  export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
67
- export const CHECK_INTENT_DRIFT = "intent_drift";
68
- export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
69
67
  export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
70
68
  export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
71
69
  // v3.3 — Round Contract checks. v3.4: split by target. The submission's own
@@ -84,30 +82,14 @@ export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
84
82
  // (completion truth: contract_completion_unverified; premature_boundary's
85
83
  // claimed-met-unverified arm). The framing is documentation only — no phase
86
84
  // field exists on the contract.
87
- /** Contract proposed with an empty done_when — nothing is promised, so
88
- * nothing can be verified at the boundary. Warn: the contract can be fixed
89
- * by re-declaring next round. */
90
- export const CHECK_ROUND_UNDERSPECIFIED = "round_underspecified";
91
- /** done_when items exist but verification_plan is empty or names commands
92
- * that are not configured AND enabled in policy.evidence.commands — the
93
- * completion claims could never be machine-checked. Warn. */
94
- 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";
95
89
  /** The round's actual git changes include files outside the ACTIVE
96
- * contract's declared scope. Warn + drift_clarification exemption
97
- * (R7-style). */
90
+ * contract's declared scope. Warn; v3.8: not waivable by explanation — the
91
+ * enforcement gate rejects, and repeated drift terminates. */
98
92
  export const CHECK_ROUND_SCOPE_DRIFT = "round_scope_drift";
99
- /** success claimed under the ACTIVE contract whose done_when items are
100
- * either claimed met without machine-verified evidence or silently dropped
101
- * (not in success_criteria_met NOR success_criteria_remaining). Error. */
102
- export const CHECK_PREMATURE_BOUNDARY = "premature_boundary";
103
- /** v3.5: Closing a Round Contract is a success-class claim and must be
104
- * machine-backed. The eval's met claims satisfy every done_when of the
105
- * ACTIVE contract but its verification_plan commands did not pass this
106
- * round. Error; warn under evidence.machine_backed_success "warn"; never
107
- * downgraded by no_change_reason (all done_when met contradicts "no
108
- * change"). Fail-open: plan names no longer configured+enabled are not
109
- * required (cannot observe). */
110
- export const CHECK_CONTRACT_COMPLETION_UNVERIFIED = "contract_completion_unverified";
111
93
  /** v3.5: The ACTIVE contract is still open (not completed, not blocked)
112
94
  * while a different contract was proposed — the proposal is ignored until
113
95
  * the active one closes. Warn: the walker still ignores it; this only
@@ -136,13 +118,8 @@ export const CHECK_DOMAIN = {
136
118
  [CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
137
119
  [CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
138
120
  // ── plan_contract ─────────────────────────────────────────────────────────
139
- [CHECK_INTENT_DRIFT]: "plan_contract",
140
- [CHECK_SUBGOAL_DRIFT]: "plan_contract",
141
- [CHECK_ROUND_UNDERSPECIFIED]: "plan_contract",
142
- [CHECK_ROUND_UNVERIFIABLE]: "plan_contract",
121
+ [CHECK_CONTRACT_ITEMS_UNVERIFIED]: "plan_contract",
143
122
  [CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
144
- [CHECK_PREMATURE_BOUNDARY]: "plan_contract",
145
- [CHECK_CONTRACT_COMPLETION_UNVERIFIED]: "plan_contract",
146
123
  [CHECK_CONTRACT_PREMATURE]: "plan_contract",
147
124
  // v3.7.1: cited-gate authorization conformance (opt-in blocking layer).
148
125
  [CHECK_USER_GATE_UNRESOLVED]: "plan_contract",
@@ -185,72 +162,46 @@ export function isFileInScope(file, scope) {
185
162
  export function collectOutOfScopeFiles(files, scope) {
186
163
  return files.filter((file) => !isFileInScope(file, scope));
187
164
  }
188
- /** v3.3: Detect verification-domain tampering for a command snapshot.
189
- * entrypointModified — a workspace file the command depends on
190
- * (snapshot.data.entrypointFiles) changed this round: the runtime executed
191
- * a script the agent just rewrote, so the "machine observation" has no
192
- * stable baseline. testFilesModified — the round changed test files
193
- * (isTestFile): normal in TDD, so warn-only; the command result stays
194
- * usable. Pure snapshot derivation — no fs access (this module is pure). */
195
- function commandTampered(snapshot, gitSnapshot) {
196
- 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)
197
173
  return { entrypointModified: false, testFilesModified: false };
198
- const gitFiles = new Set(gitSnapshot.files);
199
- const entrypoints = isRecord(snapshot.data) &&
200
- Array.isArray(snapshot.data.entrypointFiles)
201
- ? 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 ?? []
202
177
  : [];
203
178
  const entrypointModified = entrypoints.some((file) => gitFiles.has(file));
204
179
  const testFilesModified = [...gitFiles].some((file) => isTestFile(file));
205
180
  return { entrypointModified, testFilesModified };
206
181
  }
207
- function passedAfterCommand(snapshots) {
208
- const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
209
- return snapshots.find((snapshot) => isRecord(snapshot.data) &&
210
- snapshot.data.kind === "command" &&
211
- snapshot.data.phase === "after" &&
212
- snapshot.data.status === "passed" &&
213
- // v3.3: a command whose entrypoint changed this round is not machine
214
- // evidence — exclude it so providerStatus degrades to unavailable.
215
- !commandTampered(snapshot, git).entrypointModified) ?? null;
216
- }
217
- /** v3.5: Names of commands observed passing in the after-phase this round.
218
- * Tampered commands (entrypoint changed this round) are not machine
219
- * evidence and are excluded — the same rule passedAfterCommand applies. */
220
- function passedPlanCommandNames(snapshots) {
221
- const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
222
- const names = new Set();
223
- for (const snapshot of snapshots) {
224
- if (!isRecord(snapshot.data))
225
- continue;
226
- if (snapshot.data.kind !== "command")
227
- continue;
228
- if (snapshot.data.phase !== "after" || snapshot.data.status !== "passed")
229
- continue;
230
- if (commandTampered(snapshot, git).entrypointModified)
231
- continue;
232
- const name = snapshot.data.commandName;
233
- if (typeof name === "string" && name.length > 0)
234
- names.add(name);
235
- }
236
- 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;
237
188
  }
238
189
  /** v3.2: Derive the machine-verification status of a round. The verification
239
190
  * capability is modeled explicitly (verified / unavailable / absent) instead
240
191
  * of letting evidence-dependent checks silently disappear when snapshots are
241
192
  * missing — the fix for the "weakest when it matters most" gap. */
242
193
  export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
243
- const gitSnap = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
194
+ const gitSnap = evidenceSnapshots.find((item) => item.providerId === "git") ?? null;
244
195
  const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
245
196
  const command = passedAfterCommand(evidenceSnapshots);
246
197
  const commandVerified = command !== null;
247
- // testsMachineBacked: agent-reported test_results agree with a passed
198
+ // testsMachineBacked: agent-reported tests_reported agree with a passed
248
199
  // command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
249
200
  // performs — parseTestOutput lives in this module).
250
201
  let testsMachineBacked = false;
251
- const reported = selfEval.execution_evidence?.test_results;
252
- if (command && reported) {
253
- 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 ?? "");
254
205
  testsMachineBacked = parsed !== null &&
255
206
  parsed.passed === reported.passed &&
256
207
  parsed.failed === reported.failed &&
@@ -258,8 +209,8 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
258
209
  }
259
210
  // reportedFilesMatch: agent-reported files_changed equals the git diff set.
260
211
  let reportedFilesMatch = false;
261
- if (gitSnap && selfEval.execution_evidence) {
262
- const reportedSet = [...selfEval.execution_evidence.files_changed].sort();
212
+ if (gitSnap && selfEval.execution_report) {
213
+ const reportedSet = [...(selfEval.execution_report.files_changed ?? [])].sort();
263
214
  const actualSet = [...gitSnap.files].sort();
264
215
  reportedFilesMatch = reportedSet.length === actualSet.length &&
265
216
  reportedSet.every((file, index) => file === actualSet[index]);
@@ -285,7 +236,7 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
285
236
  }
286
237
  /** Per-round machine progress over decoded committed history. */
287
238
  export function machineProgressSeries(vaultEntries, currentRound, lookback) {
288
- return machineGitMotionSeries(committedRoundsFromEntries(vaultEntries, currentRound), currentRound, lookback);
239
+ return machineGitMotionSeries(derivationRounds(vaultEntries, currentRound), currentRound, lookback);
289
240
  }
290
241
  // ═══════════════════════════════════════════════════════════════════════════
291
242
  // Individual checks — each returns a VerificationFlag or null
@@ -293,8 +244,8 @@ export function machineProgressSeries(vaultEntries, currentRound, lookback) {
293
244
  function checkSuccessWithRemainingCriteria(selfEval) {
294
245
  if (!effectiveSuccess(selfEval))
295
246
  return null;
296
- const remaining = selfEval.execution_evidence?.success_criteria_remaining;
297
- if (!remaining || remaining.length === 0)
247
+ const remaining = claimedRemainingCriteria(selfEval.execution_report);
248
+ if (remaining.length === 0)
298
249
  return null;
299
250
  return makeVerificationFlag({
300
251
  severity: "error",
@@ -310,14 +261,14 @@ function checkSuccessWithRemainingCriteria(selfEval) {
310
261
  * next prompt tells the agent its claims are unverified. This is the
311
262
  * mild form of the 3.x criterion-specific completion rule. */
312
263
  function checkUnverifiedCriteriaClaims(selfEval, claimView) {
313
- const met = selfEval.execution_evidence?.success_criteria_met;
314
- if (!met || met.length === 0)
264
+ const met = claimedMetCriteria(selfEval.execution_report);
265
+ if (met.length === 0)
315
266
  return null;
316
267
  if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
317
268
  return null;
318
269
  return makeVerificationFlag({
319
270
  severity: "warn",
320
- field: "execution_evidence",
271
+ field: "execution_report",
321
272
  check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
322
273
  detail: `${met.length} criteria reported met but none machine-verified ` +
323
274
  "(no passing test evidence or command snapshot)",
@@ -410,13 +361,12 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
410
361
  /** v2.12/v3.6/v3.7: success without machine backing — the single
411
362
  * "success evidence" semantic. Two arms, evaluated in order:
412
363
  * 1. Empty/missing-evidence arm (v3.7: absorbed the enforcement-only
413
- * empty_success rule): success=true with NO execution_evidence at all, or
414
- * 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
415
366
  * unconditionally — BEFORE the providerStatus early-exit, so a passed
416
- * command never rescues it (execution_evidence stays mandatory), and
417
- * neither evidence.machine_backed_success "warn" nor a declared
418
- * no_change_reason downgrades it ("no change" with literally no evidence
419
- * 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).
420
370
  * 2. Claims arm (v3.6): success=true but the runtime observed no machine
421
371
  * evidence for it. The trigger is the runtime-derived providerStatus
422
372
  * (verified / unavailable / absent) — "verified" requires an UNTAMPERED
@@ -433,18 +383,18 @@ function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
433
383
  if (!effectiveSuccess(selfEval))
434
384
  return null;
435
385
  // v3.7: empty/missing-evidence arm (ex-R3 empty_success posture).
436
- const ev = selfEval.execution_evidence;
437
- 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)) {
438
388
  return makeVerificationFlag({
439
389
  severity: "error",
440
390
  field: "success",
441
391
  check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
442
392
  detail: ev
443
- ? "Agent claims success but execution_evidence shows no files changed " +
393
+ ? "Agent claims success but execution_report shows no files changed " +
444
394
  "and no tests were run. There is no verifiable evidence of work."
445
- : "Agent claims success but provided no execution_evidence. " +
446
- "Every successful round MUST include execution_evidence with " +
447
- "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.",
448
398
  });
449
399
  }
450
400
  // Claims arm (v3.6).
@@ -461,14 +411,15 @@ function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
461
411
  const noChange = !verificationConfigured &&
462
412
  typeof selfEval.no_change_reason === "string" &&
463
413
  selfEval.no_change_reason.trim().length > 0;
464
- // v3.3: severity follows evidence.machine_backed_success — "required"
465
- // rejects (error), "warn" tolerates with a warn (round commits, success
466
- // excluded from the trajectory, trust drops). no_change_reason stays info.
467
- const severity = noChange
468
- ? "info"
469
- : getPolicy().evidence.machine_backed_success === "warn"
470
- ? "warn"
471
- : "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";
472
423
  return makeVerificationFlag({
473
424
  severity,
474
425
  field: "success",
@@ -572,16 +523,16 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
572
523
  return null;
573
524
  }
574
525
  /** v1.18: Cross-validate agent-reported files_changed against git evidence
575
- * from the configured evidence providers (ProviderSnapshot array). */
526
+ * from the configured evidence providers (MachineObservation array). */
576
527
  function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
577
- if (!selfEval.execution_evidence)
528
+ if (!selfEval.execution_report)
578
529
  return null;
579
530
  if (evidenceSnapshots.length === 0)
580
531
  return null;
581
- const gitSnap = evidenceSnapshots.find((s) => s.provider === "git");
532
+ const gitSnap = evidenceSnapshots.find((s) => s.providerId === "git");
582
533
  if (!gitSnap)
583
534
  return null;
584
- const reported = [...selfEval.execution_evidence.files_changed].sort();
535
+ const reported = [...(selfEval.execution_report.files_changed ?? [])].sort();
585
536
  const actual = [...gitSnap.files].sort();
586
537
  if (reported.length === 0 && actual.length === 0)
587
538
  return null;
@@ -616,22 +567,20 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
616
567
  if (!effectiveSuccess(selfEval))
617
568
  return null;
618
569
  for (const snapshot of evidenceSnapshots) {
619
- if (snapshot.data.kind !== "command")
570
+ if (snapshot.kind !== "command")
620
571
  continue;
621
- if (snapshot.data.phase !== "after" || snapshot.data.required !== true)
572
+ if (snapshot.phase !== "after" || snapshot.data.required !== true)
622
573
  continue;
623
- const status = snapshot.data.status;
574
+ const status = snapshot.status;
624
575
  if (status === "passed")
625
576
  continue;
626
- const name = typeof snapshot.data.commandName === "string"
627
- ? snapshot.data.commandName
628
- : snapshot.provider;
577
+ const name = snapshot.data.commandId;
629
578
  const exitCode = typeof snapshot.data.exitCode === "number"
630
579
  ? ` (exit ${snapshot.data.exitCode})`
631
580
  : "";
632
581
  return makeVerificationFlag({
633
582
  severity: "error",
634
- field: "execution_evidence",
583
+ field: "execution_report",
635
584
  check: CHECK_REQUIRED_COMMAND_FAILED,
636
585
  detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
637
586
  });
@@ -639,88 +588,12 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
639
588
  return null;
640
589
  }
641
590
  // ── Test output parsing ────────────────────────────────────────────────────
642
- // jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
643
- /** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
644
- * Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
645
- function extractSubGoalIds(text) {
646
- return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
647
- }
648
591
  /** Whether a changed path looks like a test file. */
649
592
  function isTestFile(path) {
650
593
  return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
651
594
  /(^|[\\/])tests?[\\/]/i.test(path) ||
652
595
  /_test\.[a-z0-9]+$/i.test(path);
653
596
  }
654
- /** v2.1 (check 11): Compare the previous round's declared next_action with
655
- * the current round's actual output_summary. Detect when the agent says it
656
- * will do X but then does Y — silent drift without explanation.
657
- *
658
- * Only fires when both next_action and output_summary are non-empty.
659
- *
660
- * v2.14/v3.7.1: Structured-ID-first detection. Alignment is decided by a
661
- * waterfall of concrete signals before falling back to string similarity:
662
- * 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
663
- * round's declared done transitions (subgoal_updates). Description
664
- * matching is gone — transitions are strict ID references now.
665
- * 2. File paths named in next_action appearing in files_changed.
666
- * 3. Test evidence — next_action mentions tests and test files were
667
- * changed with tests actually run.
668
- * 4. Jaccard token similarity against the policy threshold (weak signal).
669
- * Any signal aligning is sufficient — text similarity is never the sole
670
- * authority when structured evidence exists. */
671
- function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
672
- if (!prevSelfEval?.next_action?.trim())
673
- return null;
674
- if (!selfEval.output_summary.trim())
675
- return null;
676
- const intent = prevSelfEval.next_action.trim();
677
- const actual = selfEval.output_summary.trim();
678
- const policy = getPolicy();
679
- const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
680
- // ── Signal 1: sub-goal IDs referenced by next_action → a done transition
681
- const intentIds = extractSubGoalIds(intent);
682
- if (intentIds.length > 0) {
683
- const doneIds = new Set((selfEval.subgoal_updates ?? [])
684
- .filter((u) => u.status === "done")
685
- .map((u) => u.id));
686
- const idMatched = intentIds.some((id) => doneIds.has(id));
687
- if (idMatched)
688
- return null;
689
- }
690
- // ── Signal 2: file paths named in next_action appear in files_changed
691
- const intentPaths = extractFilePathTokens(intent);
692
- if (intentPaths.length > 0 && filesChanged.length > 0) {
693
- const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
694
- if (pathMatched)
695
- return null;
696
- }
697
- // ── Signal 3: test evidence — intent mentions tests, test files changed,
698
- // and tests actually ran this round
699
- const intentTokens = tokenize(intent);
700
- const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
701
- const testResults = selfEval.execution_evidence?.test_results;
702
- const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
703
- const testFilesChanged = filesChanged.some((f) => isTestFile(f));
704
- if (mentionsTests && testsRan && testFilesChanged)
705
- return null;
706
- // ── Signal 4 (weak): Jaccard token similarity
707
- const score = jaccardSimilarity(intent, actual);
708
- const threshold = policy.evolution.intent_drift_threshold;
709
- if (score >= threshold)
710
- return null;
711
- const idNote = intentIds.length > 0
712
- ? `; ${intentIds.length} referenced sub-goal ID(s) did not match a declared done transition`
713
- : "";
714
- return makeVerificationFlag({
715
- severity: "warn",
716
- field: "output_summary",
717
- check: CHECK_INTENT_DRIFT,
718
- detail: `Declared intent was "${intent.slice(0, 120)}" ` +
719
- `but actual output "${actual.slice(0, 120)}" — ` +
720
- `similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
721
- idNote,
722
- });
723
- }
724
597
  /** Best-effort parse of test counts from common test-runner output formats.
725
598
  *
726
599
  * Scans the last 2000 characters of stdout (where summary lines typically
@@ -785,7 +658,7 @@ export function parseTestOutput(stdout) {
785
658
  }
786
659
  return null;
787
660
  }
788
- /** Cross-validate agent-reported test_results against command evidence output.
661
+ /** Cross-validate agent-reported tests_reported against command evidence output.
789
662
  *
790
663
  * For each command provider whose status is "passed" and output is not
791
664
  * truncated, attempts to parse test counts from stdout. Mismatched counts
@@ -795,17 +668,17 @@ export function parseTestOutput(stdout) {
795
668
  * Non-command evidence providers are skipped (no structural cross-check is
796
669
  * defined for them yet). */
797
670
  function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
798
- const reported = selfEval.execution_evidence?.test_results;
671
+ const reported = selfEval.execution_report?.tests_reported;
799
672
  if (!reported)
800
673
  return null;
801
674
  for (const snapshot of evidenceSnapshots) {
802
- if (snapshot.data.kind !== "command")
675
+ if (snapshot.kind !== "command")
803
676
  continue;
804
- if (snapshot.data.status !== "passed")
677
+ if (snapshot.status !== "passed")
805
678
  continue;
806
679
  if (snapshot.data.truncated === true)
807
680
  continue;
808
- const stdout = typeof snapshot.data.stdout === "string" ? snapshot.data.stdout : "";
681
+ const stdout = typeof snapshot.data.stdoutExcerpt === "string" ? snapshot.data.stdoutExcerpt : "";
809
682
  const parsed = parseTestOutput(stdout);
810
683
  if (!parsed)
811
684
  continue;
@@ -820,16 +693,14 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
820
693
  // Counts match exactly — evidence supports the agent's claim.
821
694
  continue;
822
695
  }
823
- const name = typeof snapshot.data.commandName === "string"
824
- ? snapshot.data.commandName
825
- : snapshot.provider;
696
+ const name = snapshot.data.commandId;
826
697
  // Agent hides failures → error. Other mismatch → warn.
827
698
  const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
828
699
  return makeVerificationFlag({
829
700
  severity,
830
- field: "test_results",
701
+ field: "tests_reported",
831
702
  check: CHECK_COMMAND_EVIDENCE_MISMATCH,
832
- detail: `Agent test_results don't match "${name}" output: ${mismatches.join("; ")}`,
703
+ detail: `Agent tests_reported don't match "${name}" output: ${mismatches.join("; ")}`,
833
704
  });
834
705
  }
835
706
  return null;
@@ -841,113 +712,47 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
841
712
  * confirm the tests still verify the task claims). Fail-open when the git
842
713
  * snapshot is missing or the command has no resolvable entrypoint. */
843
714
  function checkVerificationDomainIntegrity(evidenceSnapshots) {
844
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
715
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git") ?? null;
845
716
  if (!git)
846
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.
847
725
  for (const snapshot of evidenceSnapshots) {
848
- if (!isRecord(snapshot.data) || snapshot.data.kind !== "command")
726
+ if (!isRecord(snapshot.data) || snapshot.kind !== "command")
849
727
  continue;
850
- if (snapshot.data.phase !== "after")
728
+ if (snapshot.phase !== "after")
851
729
  continue;
852
- const { entrypointModified, testFilesModified } = commandTampered(snapshot, git);
853
- if (!entrypointModified && !testFilesModified)
730
+ if (!commandTampered(snapshot, git).entrypointModified)
854
731
  continue;
855
- const name = typeof snapshot.data.commandName === "string"
856
- ? snapshot.data.commandName
857
- : snapshot.provider;
858
- if (entrypointModified) {
859
- return makeVerificationFlag({
860
- severity: "error",
861
- field: "execution_evidence",
862
- check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
863
- detail: `Verification command "${name}" entrypoint changed this round — ` +
864
- "its result cannot be trusted as machine evidence. Keep the " +
865
- "verification command stable or resubmit the same round with the " +
866
- "entrypoint unchanged.",
867
- });
868
- }
869
732
  return makeVerificationFlag({
870
- severity: "warn",
871
- field: "execution_evidence",
872
- check: CHECK_TEST_FILES_MODIFIED,
873
- detail: `Test files changed in the same round command "${name}" passed — ` +
874
- "confirm the tests still verify the task claims.",
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.",
875
740
  });
876
741
  }
877
- return null;
878
- }
879
- /** v2.2: Check if the agent's declared next_action aligns with any pending
880
- * or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
881
- * plans work unrelated to any of them, it may indicate task drift.
882
- * This is a warn-level advisory — the agent always owns prioritization.
883
- *
884
- * v2.14: Exact sub-goal ID alignment first — next_action naming a pending
885
- * sub-goal by its stable ID (sg-XXXXXXXX) is aligned by definition.
886
- * Jaccard description similarity remains as the fallback. */
887
- function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
888
- const nextAction = selfEval.next_action?.trim();
889
- if (!nextAction)
890
- return null;
891
- // Reconstruct the pending set from vault entries and the current eval.
892
- // v3.7.1: emerged descriptions are the creation channel; done/canceled are
893
- // terminal ids reached through subgoal_updates (monotone — a terminal
894
- // transition can never be undone, so the terminal id set only grows).
895
- const pendingSubGoals = new Set();
896
- const terminalIds = new Set();
897
- for (const entry of vaultEntries) {
898
- const emerged = Array.isArray(entry.emerged_subtasks)
899
- ? entry.emerged_subtasks.filter((v) => typeof v === "string")
900
- : [];
901
- for (const desc of emerged) {
902
- pendingSubGoals.add(desc.trim());
903
- }
904
- for (const u of entrySubGoalUpdates(entry)) {
905
- if (u.status === "done" || u.status === "canceled")
906
- terminalIds.add(u.id);
907
- }
908
- }
909
- // Also process the current selfEval
910
- for (const desc of selfEval.emerged_subtasks ?? []) {
911
- pendingSubGoals.add(desc.trim());
912
- }
913
- for (const u of selfEval.subgoal_updates ?? []) {
914
- if (u.status === "done" || u.status === "canceled")
915
- terminalIds.add(u.id);
916
- }
917
- // Remove descriptions whose derived id reached a terminal state
918
- for (const desc of [...pendingSubGoals]) {
919
- if (terminalIds.has(deriveSubGoalId(desc)))
920
- pendingSubGoals.delete(desc);
921
- }
922
- // Need at least 3 pending sub-goals for the check to be meaningful
923
- if (pendingSubGoals.size < 3)
924
- return null;
925
- // v2.14: Stable ID match first — next_action naming a pending sub-goal
926
- // by ID is aligned by definition, regardless of description wording.
927
- const nextActionIds = extractSubGoalIds(nextAction);
928
- if (nextActionIds.length > 0) {
929
- const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
930
- if (nextActionIds.some((id) => pendingIds.has(id)))
931
- return null;
932
- }
933
- // Check if next_action aligns with any pending sub-goal
934
- let aligns = false;
935
- for (const sg of pendingSubGoals) {
936
- const score = jaccardSimilarity(nextAction, sg);
937
- if (score >= getPolicy().evolution.subgoal_drift_alignment_threshold) {
938
- aligns = true;
939
- break;
940
- }
941
- }
942
- if (!aligns) {
943
- const sample = [...pendingSubGoals].slice(0, 3).join(", ");
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)
749
+ continue;
944
750
  return makeVerificationFlag({
945
751
  severity: "warn",
946
- field: "next_action",
947
- check: CHECK_SUBGOAL_DRIFT,
948
- detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
949
- `Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
950
- `or cancel outdated sub-goals via subgoal_updates.`,
752
+ field: "execution_report",
753
+ check: CHECK_TEST_FILES_MODIFIED,
754
+ detail: `Test files changed in the same round command "${snapshot.data.commandId}" passed — ` +
755
+ "confirm the tests still verify the task claims.",
951
756
  });
952
757
  }
953
758
  return null;
@@ -971,9 +776,8 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtra
971
776
  // Machine arm — untouched-since-rollback proof, independent of claims.
972
777
  const failedEntries = Object.entries(backtrackSkippedFingerprints);
973
778
  if (failedEntries.length > 0) {
974
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
975
- isRecord(snapshot.data.fingerprints));
976
- const currentFingerprints = git
779
+ const git = evidenceSnapshots.find((snapshot) => snapshot.kind === "git" && isRecord(snapshot.data.fingerprints));
780
+ const currentFingerprints = git && git.kind === "git"
977
781
  ? git.data.fingerprints
978
782
  : null;
979
783
  if (currentFingerprints) {
@@ -994,11 +798,11 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtra
994
798
  }
995
799
  }
996
800
  }
997
- const ev = selfEval.execution_evidence;
998
- if (!ev || ev.files_changed.length === 0)
801
+ const ev = selfEval.execution_report;
802
+ if (!ev || (ev.files_changed ?? []).length === 0)
999
803
  return null;
1000
804
  // Claims arm — overlap as guidance only.
1001
- const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
805
+ const overlap = (ev.files_changed ?? []).filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
1002
806
  if (overlap.length === 0)
1003
807
  return null;
1004
808
  return makeVerificationFlag({
@@ -1015,7 +819,7 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtra
1015
819
  function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
1016
820
  if (!backtrackTargetGitHead)
1017
821
  return null;
1018
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
822
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git" && isRecord(snapshot.data) &&
1019
823
  typeof snapshot.data.head === "string");
1020
824
  if (!git)
1021
825
  return null; // git unavailable → cannot verify → skip (fail-open)
@@ -1034,67 +838,23 @@ function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead)
1034
838
  });
1035
839
  }
1036
840
  // ═══════════════════════════════════════════════════════════════════════════
1037
- // 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.
1038
848
  // ═══════════════════════════════════════════════════════════════════════════
1039
- /** A PROPOSAL that promises nothing is a wasted round's worth of trust:
1040
- * done_when empty means the boundary can never be machine-checked.
1041
- * v3.4: declaration-quality check on the submission's own proposal — never
1042
- * on the (derived) active contract. */
1043
- function checkRoundUnderspecified(selfEval) {
1044
- const contract = selfEval.round_contract;
1045
- if (!contract)
1046
- return null;
1047
- if (contract.done_when.length > 0)
1048
- return null;
1049
- return makeVerificationFlag({
1050
- severity: "warn",
1051
- field: "round_contract",
1052
- check: CHECK_ROUND_UNDERSPECIFIED,
1053
- detail: "Round Contract declared with an empty done_when — nothing is " +
1054
- "promised, so completion cannot be verified. Declare what must be " +
1055
- "true when the round is done.",
1056
- });
1057
- }
1058
- /** done_when items whose verification_plan cannot run (empty plan, or names
1059
- * outside the configured, enabled evidence.commands) can never be
1060
- * machine-checked at the boundary. v3.4: declaration-quality check on the
1061
- * submission's own proposal. */
1062
- function checkRoundUnverifiable(selfEval) {
1063
- const contract = selfEval.round_contract;
1064
- if (!contract)
1065
- return null;
1066
- if (contract.done_when.length === 0)
1067
- return null;
1068
- if (contract.verification_plan.length > 0 &&
1069
- contract.verification_plan.every((name) => isConfiguredCommand(name))) {
1070
- return null;
1071
- }
1072
- const unknown = contract.verification_plan.filter((name) => !isConfiguredCommand(name));
1073
- const detail = contract.verification_plan.length === 0
1074
- ? "done_when items declared but verification_plan is empty — no command " +
1075
- "can back the completion claims"
1076
- : `verification_plan names commands that are not configured and enabled ` +
1077
- `in evidence.commands: ${unknown.slice(0, 3).join(", ")}`;
1078
- return makeVerificationFlag({
1079
- severity: "warn",
1080
- field: "round_contract",
1081
- check: CHECK_ROUND_UNVERIFIABLE,
1082
- detail: detail + ". Name only configured, enabled evidence.commands, or drop " +
1083
- "done_when items that cannot be machine-verified.",
1084
- });
1085
- }
1086
849
  /** The round changed files outside the ACTIVE contract's declared scope.
1087
850
  * Git diff files are the machine-authoritative "what actually changed" set.
1088
- * v3.4: targets the contract the round actually executed under — derived
1089
- * from committed evals, never the submission's own proposal. Silent when
1090
- * no active contract exists (round 1 and whole-task rounds: the agent
1091
- * executed the original task, not a contract boundary). */
851
+ * Silent when no active contract exists or it declares no scope. */
1092
852
  function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
1093
853
  if (!activeContract)
1094
854
  return null;
1095
855
  if (activeContract.scope.length === 0)
1096
856
  return null;
1097
- const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git");
857
+ const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git");
1098
858
  if (!git)
1099
859
  return null; // git unavailable → cannot observe → fail open
1100
860
  const outOfScope = collectOutOfScopeFiles(git.files, activeContract.scope);
@@ -1108,142 +868,53 @@ function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
1108
868
  (outOfScope.length > 5
1109
869
  ? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
1110
870
  : `: ${outOfScope.join(", ")}`) +
1111
- `. Revert them or extend the contract scope and explain in drift_clarification.`,
871
+ `. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
1112
872
  });
1113
873
  }
1114
- /** success claimed under the ACTIVE contract whose done_when items were not
1115
- * honestly and verifiably completed: claimed met with no machine-verified
1116
- * evidence this round, or silently dropped (present in neither
1117
- * success_criteria_met nor success_criteria_remaining).
1118
- * v3.4: targets the derived ACTIVE contract (the round's real boundary) —
1119
- * a proposal declared on a round without an active contract is never
1120
- * checked for conformance against work the round did not do under it.
1121
- * v3.5.1: a fully-met posture (every done_when claimed satisfied) is owned
1122
- * by contract_completion_unverified — this check stays for mixed and
1123
- * silently-dropped success claims only.
1124
- * v3.6: no_change_reason never downgrades this check — the escape hatch
1125
- * belongs to the R8 success-claim family alone; a declared "no change"
1126
- * contradicts silently dropping contract done_when items. */
1127
- function checkPrematureBoundary(activeContract, selfEval, claimView) {
1128
- if (!activeContract || activeContract.done_when.length === 0)
1129
- return null;
1130
- if (!effectiveSuccess(selfEval))
1131
- return null;
1132
- const ev = selfEval.execution_evidence;
1133
- const met = ev?.success_criteria_met ?? [];
1134
- // v3.5.1: full-met → the completion check owns the posture (it also fires
1135
- // on success=false, so nothing is lost — and under machine_backed_success
1136
- // "warn" the same tolerance applies on both sides).
1137
- if (contractDoneWhenSatisfied(activeContract, met))
1138
- return null;
1139
- const remaining = ev?.success_criteria_remaining ?? [];
1140
- const problems = [];
1141
- for (const item of activeContract.done_when) {
1142
- // Shared matcher with the lifecycle walker (round-contract.ts) — the
1143
- // compile-side closure decision and this conformance check can never
1144
- // disagree on what counts as a satisfied item.
1145
- const isMet = met.some((m) => contractItemMatches(item, m));
1146
- const isRemaining = remaining.some((r) => contractItemMatches(item, r));
1147
- if (isMet) {
1148
- // Claimed met — needs round-level machine verification. The v3.3
1149
- // claim model is round-uniform: any verified claim backs the round,
1150
- // so zero verified claims = none of them are backed.
1151
- if (claimView.verifiedCount === 0) {
1152
- problems.push(`"${item.slice(0, 80)}" claimed met without machine-verified evidence`);
1153
- }
1154
- }
1155
- else if (!isRemaining) {
1156
- // Neither met nor remaining — silently dropped while success is
1157
- // claimed. Honest reporting would list it in remaining (R1's domain).
1158
- problems.push(`"${item.slice(0, 80)}" neither met nor listed as remaining`);
1159
- }
1160
- }
1161
- if (problems.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)
1162
879
  return null;
880
+ const unverified = statuses.items
881
+ .filter((item) => item.status === "insufficient")
882
+ .slice(0, 3)
883
+ .map((item) => item.itemId);
1163
884
  return makeVerificationFlag({
1164
- severity: "error",
1165
- field: "round_contract",
1166
- check: CHECK_PREMATURE_BOUNDARY,
1167
- detail: `Round Contract boundary claimed prematurely: ${problems.slice(0, 3).join("; ")}. ` +
1168
- `Run the contract's verification_plan commands and report real output, ` +
1169
- `or set success=false and list the items in success_criteria_remaining.`,
1170
- });
1171
- }
1172
- /** v3.5: Closing a Round Contract is a success-class claim and must be
1173
- * machine-backed. When the eval's met claims satisfy EVERY done_when of
1174
- * the ACTIVE contract (the walker's close condition), each verification_plan
1175
- * command must have been observed passing (after-phase command snapshot,
1176
- * untampered) in the same round. Fires regardless of the success flag —
1177
- * a claim-based closure that the machine cannot back must not advance the
1178
- * contract state machine. Severity follows evidence.machine_backed_success
1179
- * (the R8 tolerance switch). Deliberately no no_change_reason downgrade:
1180
- * all done_when met contradicts "no change". Fail-open: a plan name that
1181
- * is no longer a configured, enabled command cannot be observed and is
1182
- * not required. The check observes that the commands ran, not what they
1183
- * verified — R-C1's claim model remains the content bound. */
1184
- function checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots) {
1185
- if (!activeContract || activeContract.verification_plan.length === 0)
1186
- return null;
1187
- const met = selfEval.execution_evidence?.success_criteria_met ?? [];
1188
- if (!contractDoneWhenSatisfied(activeContract, met))
1189
- return null; // walker does not close
1190
- const required = activeContract.verification_plan
1191
- .filter((name) => isConfiguredCommand(name));
1192
- if (required.length === 0)
1193
- return null; // fail open — cannot observe
1194
- const passed = passedPlanCommandNames(evidenceSnapshots);
1195
- const missing = required.filter((name) => !passed.has(name));
1196
- if (missing.length === 0)
1197
- return null;
1198
- const severity = getPolicy().evidence.machine_backed_success === "warn"
1199
- ? "warn"
1200
- : "error";
1201
- return makeVerificationFlag({
1202
- severity,
885
+ severity: "warn",
1203
886
  field: "round_contract",
1204
- check: CHECK_CONTRACT_COMPLETION_UNVERIFIED,
1205
- detail: `Round Contract completion claimed (every done_when met), but verification_plan ` +
1206
- `command${missing.length > 1 ? "s" : ""} did not pass this round: ` +
1207
- `${missing.slice(0, 3).join(", ")}. Closing a contract is a success-class claim — ` +
1208
- `fix the underlying failure so the command${missing.length > 1 ? "s" : ""} pass and ` +
1209
- `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.`,
1210
890
  });
1211
891
  }
1212
- /** v3.5: A different contract proposed while the ACTIVE contract is still
1213
- * open. The walker ignores the premature proposal — this warn only makes
1214
- * the ignored state visible to the agent. Silent when the eval closes the
1215
- * active contract (all done_when met, or outcome=blocked — checked FIRST,
1216
- * mirroring the walker), when no proposal is submitted, or when the
1217
- * proposal equals the active contract (a restate). Equality is
1218
- * key-order-insensitive and normalized through parseRoundContract on both
1219
- * sides so committed-raw vs parsed-submission key-set drift cannot cause
1220
- * spurious warns. */
1221
- 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) {
1222
898
  if (!activeContract)
1223
899
  return null;
1224
- const met = selfEval.execution_evidence?.success_criteria_met ?? [];
1225
- if (contractDoneWhenSatisfied(activeContract, met))
900
+ if (statuses.closure !== "open")
1226
901
  return null; // closed
1227
902
  if (selfEval.outcome === "blocked")
1228
903
  return null; // closed
1229
904
  const proposal = selfEval.round_contract;
1230
905
  if (!proposal)
1231
906
  return null;
1232
- const same = stableStringify(parseRoundContract(activeContract)) ===
1233
- stableStringify(parseRoundContract(proposal));
1234
- if (same)
907
+ if (sameContract(proposal, activeContract))
1235
908
  return null; // restate → continue
1236
909
  return makeVerificationFlag({
1237
910
  severity: "warn",
1238
911
  field: "round_contract",
1239
912
  check: CHECK_CONTRACT_PREMATURE,
1240
- 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) " +
1241
914
  "while a different contract was proposed — it is ignored until the active " +
1242
- "contract is completed or blocked. Restate the active contract unchanged to " +
1243
- "continue it.",
915
+ "contract closes. Restate the active contract unchanged to continue it.",
1244
916
  });
1245
917
  }
1246
- // ═══════════════════════════════════════════════════════════════════════════
1247
918
  // Main entry point
1248
919
  // ═══════════════════════════════════════════════════════════════════════════
1249
920
  /** Verify a SelfEvaluation against the loop's cross-round lineage.
@@ -1345,7 +1016,18 @@ gateEntries = []) {
1345
1016
  // round_contract is a PROPOSAL for the next round and never participates
1346
1017
  // (the eval under verification is not yet committed, so it structurally
1347
1018
  // cannot influence the derivation).
1348
- 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
+ });
1349
1031
  // v2.12: Derived claim provenance — which reported criteria completions
1350
1032
  // are backed by machine evidence (pure derivation, not persisted).
1351
1033
  const claimView = deriveClaimView(selfEval, evidenceSnapshots);
@@ -1375,30 +1057,22 @@ gateEntries = []) {
1375
1057
  // v1.18: Cross-validate agent-reported files_changed against git evidence
1376
1058
  () => checkEvidenceIntegrity(selfEval, evidenceSnapshots),
1377
1059
  () => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
1378
- // v2.0: Cross-validate agent-reported test_results against command output
1060
+ // v2.0: Cross-validate agent-reported tests_reported against command output
1379
1061
  () => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
1380
1062
  // v3.3: Verification domain integrity — command entrypoint / test files
1381
1063
  // changed in the same round the command ran
1382
1064
  () => checkVerificationDomainIntegrity(evidenceSnapshots),
1383
- // v2.1: Detect intent-action drift — agent said X but did Y
1384
- () => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
1385
- // v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
1386
- () => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
1387
1065
  // v2.13: Post-backtrack workspace restore check
1388
1066
  () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceSnapshots),
1389
1067
  // v2.12: Post-backtrack git HEAD restore check
1390
1068
  () => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
1391
- // v3.3: Round Contract checks. v3.4: split by target — structural
1392
- // (warn) at proposal declaration, conformance against the derived
1393
- // ACTIVE contract (scope_drift warn / premature_boundary error).
1394
- // v3.5: + completion machine-backing (error) and premature-replacement
1395
- // visibility (warn).
1396
- () => checkRoundUnderspecified(selfEval),
1397
- () => 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.
1398
1073
  () => checkRoundScopeDrift(activeContract, evidenceSnapshots),
1399
- () => checkPrematureBoundary(activeContract, selfEval, claimView),
1400
- () => checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots),
1401
- () => checkContractPremature(activeContract, selfEval),
1074
+ () => checkContractItemsUnverified(contractStatuses),
1075
+ () => checkContractPremature(activeContract, contractStatuses, selfEval),
1402
1076
  // v3.7.1: opt-in gate-layer blocking (skipped when disabled)
1403
1077
  () => checkUserGateUnresolved(selfEval, gateEntries),
1404
1078
  ];
@@ -1422,6 +1096,14 @@ gateEntries = []) {
1422
1096
  else {
1423
1097
  verdict = "trusted";
1424
1098
  }
1425
- 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 });
1426
1108
  }
1427
1109
  //# sourceMappingURL=verification-gate.js.map