loopforge 3.2.1 → 3.7.0

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 (122) hide show
  1. package/README.md +74 -71
  2. package/dist/audit.d.ts.map +1 -1
  3. package/dist/audit.js +6 -38
  4. package/dist/audit.js.map +1 -1
  5. package/dist/canonical-state.d.ts +71 -6
  6. package/dist/canonical-state.d.ts.map +1 -1
  7. package/dist/canonical-state.js +176 -29
  8. package/dist/canonical-state.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +16 -26
  11. package/dist/cli.js.map +1 -1
  12. package/dist/cognitive-facts.d.ts +17 -0
  13. package/dist/cognitive-facts.d.ts.map +1 -0
  14. package/dist/cognitive-facts.js +99 -0
  15. package/dist/cognitive-facts.js.map +1 -0
  16. package/dist/cognitive-governance.d.ts +0 -1
  17. package/dist/cognitive-governance.d.ts.map +1 -1
  18. package/dist/cognitive-governance.js +1 -4
  19. package/dist/cognitive-governance.js.map +1 -1
  20. package/dist/committed-round.d.ts +100 -0
  21. package/dist/committed-round.d.ts.map +1 -0
  22. package/dist/committed-round.js +349 -0
  23. package/dist/committed-round.js.map +1 -0
  24. package/dist/enforcement-gate.d.ts +4 -2
  25. package/dist/enforcement-gate.d.ts.map +1 -1
  26. package/dist/enforcement-gate.js +385 -357
  27. package/dist/enforcement-gate.js.map +1 -1
  28. package/dist/engine.d.ts +16 -4
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +65 -16
  31. package/dist/engine.js.map +1 -1
  32. package/dist/evidence-claims.d.ts.map +1 -1
  33. package/dist/evidence-claims.js +15 -39
  34. package/dist/evidence-claims.js.map +1 -1
  35. package/dist/evidence-provider.d.ts +35 -17
  36. package/dist/evidence-provider.d.ts.map +1 -1
  37. package/dist/evidence-provider.js +105 -108
  38. package/dist/evidence-provider.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/loop-compiler.d.ts +21 -10
  44. package/dist/loop-compiler.d.ts.map +1 -1
  45. package/dist/loop-compiler.js +232 -276
  46. package/dist/loop-compiler.js.map +1 -1
  47. package/dist/loop-projection.d.ts +5 -25
  48. package/dist/loop-projection.d.ts.map +1 -1
  49. package/dist/loop-projection.js +18 -134
  50. package/dist/loop-projection.js.map +1 -1
  51. package/dist/loop-store.d.ts +16 -18
  52. package/dist/loop-store.d.ts.map +1 -1
  53. package/dist/loop-store.js +21 -63
  54. package/dist/loop-store.js.map +1 -1
  55. package/dist/mcp/round-lifecycle.d.ts +13 -10
  56. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  57. package/dist/mcp/round-lifecycle.js +95 -68
  58. package/dist/mcp/round-lifecycle.js.map +1 -1
  59. package/dist/mcp/server.js +3 -3
  60. package/dist/mcp/server.js.map +1 -1
  61. package/dist/mcp/session.d.ts +17 -5
  62. package/dist/mcp/session.d.ts.map +1 -1
  63. package/dist/mcp/session.js +177 -54
  64. package/dist/mcp/session.js.map +1 -1
  65. package/dist/mcp/tools.d.ts +12 -0
  66. package/dist/mcp/tools.d.ts.map +1 -1
  67. package/dist/mcp/tools.js +161 -53
  68. package/dist/mcp/tools.js.map +1 -1
  69. package/dist/policy-metrics.d.ts +0 -7
  70. package/dist/policy-metrics.d.ts.map +1 -1
  71. package/dist/policy-metrics.js +15 -55
  72. package/dist/policy-metrics.js.map +1 -1
  73. package/dist/policy.d.ts +22 -31
  74. package/dist/policy.d.ts.map +1 -1
  75. package/dist/policy.js +24 -42
  76. package/dist/policy.js.map +1 -1
  77. package/dist/prompt-assembler.d.ts +0 -3
  78. package/dist/prompt-assembler.d.ts.map +1 -1
  79. package/dist/prompt-assembler.js +124 -191
  80. package/dist/prompt-assembler.js.map +1 -1
  81. package/dist/protocol.d.ts +61 -46
  82. package/dist/protocol.d.ts.map +1 -1
  83. package/dist/protocol.js +2 -18
  84. package/dist/protocol.js.map +1 -1
  85. package/dist/replay.d.ts.map +1 -1
  86. package/dist/replay.js +21 -22
  87. package/dist/replay.js.map +1 -1
  88. package/dist/round-contract.d.ts +64 -0
  89. package/dist/round-contract.d.ts.map +1 -0
  90. package/dist/round-contract.js +80 -0
  91. package/dist/round-contract.js.map +1 -0
  92. package/dist/round-coordinator.d.ts +5 -3
  93. package/dist/round-coordinator.d.ts.map +1 -1
  94. package/dist/round-coordinator.js +34 -29
  95. package/dist/round-coordinator.js.map +1 -1
  96. package/dist/round-driver.d.ts +0 -4
  97. package/dist/round-driver.d.ts.map +1 -1
  98. package/dist/round-driver.js +1 -11
  99. package/dist/round-driver.js.map +1 -1
  100. package/dist/round-transaction.d.ts +0 -1
  101. package/dist/round-transaction.d.ts.map +1 -1
  102. package/dist/round-transaction.js +0 -1
  103. package/dist/round-transaction.js.map +1 -1
  104. package/dist/self-eval.d.ts +29 -34
  105. package/dist/self-eval.d.ts.map +1 -1
  106. package/dist/self-eval.js +112 -148
  107. package/dist/self-eval.js.map +1 -1
  108. package/dist/token-utils.d.ts +7 -3
  109. package/dist/token-utils.d.ts.map +1 -1
  110. package/dist/token-utils.js +18 -3
  111. package/dist/token-utils.js.map +1 -1
  112. package/dist/verification-gate.d.ts +72 -21
  113. package/dist/verification-gate.d.ts.map +1 -1
  114. package/dist/verification-gate.js +544 -157
  115. package/dist/verification-gate.js.map +1 -1
  116. package/dist/workspace.d.ts +29 -0
  117. package/dist/workspace.d.ts.map +1 -0
  118. package/dist/workspace.js +61 -0
  119. package/dist/workspace.js.map +1 -0
  120. package/loop_policy.json +5 -17
  121. package/package.json +2 -2
  122. package/skills/perception/SKILL.md +137 -136
@@ -11,23 +11,40 @@
11
11
  * this round is excluded from the success trend (NOT
12
12
  * modified). Flags become hard constraints — the agent
13
13
  * must respond in the next round.
14
+ *
15
+ * v3.7: every check belongs to one of four verification domains (CHECK_DOMAIN
16
+ * below): evaluation consistency / evidence integrity / plan & contract /
17
+ * progress & recovery. A domain describes a check's semantic job — it never
18
+ * changes run order or verdict aggregation. Round Contract checks are further
19
+ * framed as declaration / execution / closure stages in their doc comments.
14
20
  */
15
- import { getPolicy } from "./policy.js";
21
+ import { getPolicy, isConfiguredCommand } from "./policy.js";
16
22
  import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
17
- import { jaccardSimilarity, tokenize, entryRound as sharedEntryRound, isRecord } from "./token-utils.js";
23
+ import { jaccardSimilarity, tokenize, isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
18
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";
19
28
  import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
20
- import { effectiveSuccess } from "./self-eval.js";
29
+ import { effectiveSuccess, parseRoundContract } from "./self-eval.js";
21
30
  // ═══════════════════════════════════════════════════════════════════════════
22
31
  // v2.12: Check-name constants — single source of truth so the enforcement
23
32
  // gate and audit can reference checks without string-literal drift.
33
+ //
34
+ // v3.7: every check belongs to one of four verification domains (CHECK_DOMAIN
35
+ // below). The flag set was converged from 27 to 24:
36
+ // - progress_regression removed — self-report-only decoration; machine-side
37
+ // progress policing lives in the enforcement gate's progress stall row.
38
+ // - empty_change_with_passing removed — success without machine backing is
39
+ // already covered by the success-evidence arms below.
40
+ // - success_claim_conflict merged into outcome_success_contradiction.
24
41
  // ═══════════════════════════════════════════════════════════════════════════
25
- export const CHECK_PROGRESS_REGRESSION = "progress_regression";
26
- export const CHECK_EMPTY_CHANGE_WITH_PASSING = "empty_change_with_passing";
27
42
  export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
28
43
  export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
44
+ // v3.7: single id for the outcome-vs-success axis — error when a declared
45
+ // success contradicts success=false, warn when success=true but the outcome
46
+ // declares a non-success (the former success_claim_conflict direction).
29
47
  export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
30
- export const CHECK_SUCCESS_CLAIM_CONFLICT = "success_claim_conflict";
31
48
  export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
32
49
  export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
33
50
  export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
@@ -37,38 +54,179 @@ export const CHECK_RETRACT_FRESH_CONSTRAINT = "retract_fresh_constraint";
37
54
  export const CHECK_EVIDENCE_INTEGRITY = "evidence_integrity";
38
55
  export const CHECK_REQUIRED_COMMAND_FAILED = "required_command_failed";
39
56
  export const CHECK_COMMAND_EVIDENCE_MISMATCH = "command_evidence_mismatch";
57
+ /** v3.3: Verification domain integrity — a command entrypoint (run-tests.sh,
58
+ * package.json, …) changed in the same round the command ran. The runtime
59
+ * executed a script the agent just rewrote, so the observation has no
60
+ * stable baseline: error. Test-file changes are normal dev activity (TDD):
61
+ * warn-only, see CHECK_TEST_FILES_MODIFIED. */
62
+ export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_modified";
63
+ /** v3.3: Test files changed in the same round a verification command passed.
64
+ * Warn-only — the command result stays usable. */
65
+ export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
40
66
  export const CHECK_INTENT_DRIFT = "intent_drift";
41
67
  export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
42
68
  export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
43
69
  export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
44
- /** v3.2: success declared with no machine-verified observation this round
45
- * (providerStatus ≠ verified). Warn-level: the round still commits, but its
46
- * success never enters the trajectory and trust drops. */
47
- export const CHECK_SUCCESS_UNVERIFIED = "success_unverified";
70
+ // v3.3 — Round Contract checks. v3.4: split by target. The submission's own
71
+ // round_contract is a PROPOSAL for the NEXT round and is checked only
72
+ // structurally at declaration (underspecified / unverifiable). Execution
73
+ // conformance (scope drift / premature boundary) targets the ACTIVE
74
+ // contract — the contract the round actually executed under, derived from
75
+ // the committed evals of earlier rounds (round-contract.ts). All checks are
76
+ // silent when their target is absent: contract-less rounds behave exactly
77
+ // as before these checks existed, and round 1 (no committed rounds → no
78
+ // active contract) never produces declaration off-by-one noise.
79
+ // v3.7: the checks are framed as three contract stages — Declaration
80
+ // (proposal quality: round_underspecified / round_unverifiable /
81
+ // contract_premature), Execution (conformance under the ACTIVE contract:
82
+ // round_scope_drift; premature_boundary's silently-dropped arm), Closure
83
+ // (completion truth: contract_completion_unverified; premature_boundary's
84
+ // claimed-met-unverified arm). The framing is documentation only — no phase
85
+ // 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";
94
+ /** The round's actual git changes include files outside the ACTIVE
95
+ * contract's declared scope. Warn + drift_clarification exemption
96
+ * (R7-style). */
97
+ 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
+ /** v3.5: The ACTIVE contract is still open (not completed, not blocked)
111
+ * while a different contract was proposed — the proposal is ignored until
112
+ * the active one closes. Warn: the walker still ignores it; this only
113
+ * surfaces the otherwise-silent state. */
114
+ export const CHECK_CONTRACT_PREMATURE = "contract_premature";
115
+ export const CHECK_DOMAIN = {
116
+ // ── evaluation_consistency ────────────────────────────────────────────────
117
+ [CHECK_SUCCESS_WITH_REMAINING_CRITERIA]: "evaluation_consistency",
118
+ [CHECK_OUTCOME_SUCCESS_CONTRADICTION]: "evaluation_consistency",
119
+ [CHECK_BLOCKED_WITHOUT_BLOCKER]: "evaluation_consistency",
120
+ [CHECK_RETROACTIVE_CLAIM_BAD_ROUND]: "evaluation_consistency",
121
+ [CHECK_RETROACTIVE_CLAIM_UNVERIFIED]: "evaluation_consistency",
122
+ [CHECK_DUPLICATE_CONSTRAINT_DISCOVERY]: "evaluation_consistency",
123
+ [CHECK_RECURRING_VIOLATION]: "evaluation_consistency",
124
+ [CHECK_RETRACT_FRESH_CONSTRAINT]: "evaluation_consistency",
125
+ // ── evidence_integrity ────────────────────────────────────────────────────
126
+ [CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE]: "evidence_integrity",
127
+ [CHECK_CRITERIA_CLAIMS_UNVERIFIED]: "evidence_integrity",
128
+ [CHECK_EVIDENCE_INTEGRITY]: "evidence_integrity",
129
+ [CHECK_REQUIRED_COMMAND_FAILED]: "evidence_integrity",
130
+ [CHECK_COMMAND_EVIDENCE_MISMATCH]: "evidence_integrity",
131
+ [CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
132
+ [CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
133
+ // ── 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",
138
+ [CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
139
+ [CHECK_PREMATURE_BOUNDARY]: "plan_contract",
140
+ [CHECK_CONTRACT_COMPLETION_UNVERIFIED]: "plan_contract",
141
+ [CHECK_CONTRACT_PREMATURE]: "plan_contract",
142
+ // ── progress_recovery ─────────────────────────────────────────────────────
143
+ [CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED]: "progress_recovery",
144
+ };
48
145
  // ═══════════════════════════════════════════════════════════════════════════
49
146
  // Helpers
50
147
  // ═══════════════════════════════════════════════════════════════════════════
51
- /** Extract the round number from a vault entry's loop_lineage.
52
- * Returns 0 if the entry has no lineage or no round field.
53
- * In practice, persistLoopLineage always writes round ≥ 1, so 0
54
- * unambiguously means "not a valid round entry" in this context.
55
- * Exported for reuse by enforcement-gate.ts. */
56
- export function entryRound(entry) {
57
- return sharedEntryRound(entry);
58
- }
59
- /** Read constraint_violations from a vault entry (entry-level, stored from
60
- * the previous round's last_round_result at persist time). */
61
- function entryViolations(entry) {
62
- const viols = entry.constraint_violations;
63
- if (Array.isArray(viols))
64
- return viols.filter((v) => typeof v === "string");
65
- return [];
148
+ // ── v3.3: Round Contract scope matching ─────────────────────────────────────
149
+ /** Normalize a contract scope entry: backslashes → forward slashes, strip
150
+ * leading "./", trim, strip trailing slashes. "." / "./" / "" normalize to
151
+ * "" = the repository root (everything is in scope). */
152
+ export function normalizeScopeEntry(path) {
153
+ let s = path.replace(/\\/g, "/").trim();
154
+ while (s.startsWith("./"))
155
+ s = s.slice(2);
156
+ if (s === "." || s === "")
157
+ return "";
158
+ while (s.endsWith("/"))
159
+ s = s.slice(0, -1);
160
+ return s;
161
+ }
162
+ /** Whether a git-diff file path falls inside any declared scope entry:
163
+ * exact file match, or the file lives under a declared directory. The ""
164
+ * entry (repo root) matches everything. Git paths are always workspace-
165
+ * relative with forward slashes. */
166
+ export function isFileInScope(file, scope) {
167
+ const normalizedFile = normalizeScopeEntry(file);
168
+ for (const entry of scope) {
169
+ const normalized = normalizeScopeEntry(entry);
170
+ if (normalized === "" || normalizedFile === normalized ||
171
+ normalizedFile.startsWith(normalized + "/")) {
172
+ return true;
173
+ }
174
+ }
175
+ return false;
176
+ }
177
+ /** Git-diff files outside the declared scope (the round_scope_drift signal). */
178
+ export function collectOutOfScopeFiles(files, scope) {
179
+ return files.filter((file) => !isFileInScope(file, scope));
180
+ }
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)
190
+ 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")
195
+ : [];
196
+ const entrypointModified = entrypoints.some((file) => gitFiles.has(file));
197
+ const testFilesModified = [...gitFiles].some((file) => isTestFile(file));
198
+ return { entrypointModified, testFilesModified };
66
199
  }
67
200
  function passedAfterCommand(snapshots) {
201
+ const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
68
202
  return snapshots.find((snapshot) => isRecord(snapshot.data) &&
69
203
  snapshot.data.kind === "command" &&
70
204
  snapshot.data.phase === "after" &&
71
- snapshot.data.status === "passed") ?? null;
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;
72
230
  }
73
231
  /** v3.2: Derive the machine-verification status of a round. The verification
74
232
  * capability is modeled explicitly (verified / unavailable / absent) instead
@@ -99,9 +257,17 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
99
257
  reportedFilesMatch = reportedSet.length === actualSet.length &&
100
258
  reportedSet.every((file, index) => file === actualSet[index]);
101
259
  }
260
+ // v3.3: git observation alone is no longer "verified" — the machine seeing
261
+ // file changes is not the machine verifying success. Only a passed
262
+ // after-phase command (observed by the runtime itself) upgrades a round.
263
+ // Any other snapshot presence is "unavailable": the machine saw activity
264
+ // (or nothing at all this round) but could not verify the success claim,
265
+ // so R8 warns and trust drops (v3.6: merged success_unverified semantics).
102
266
  const providerStatus = evidenceSnapshots.length === 0
103
267
  ? "absent"
104
- : gitObserved || commandVerified ? "verified" : "unavailable";
268
+ : commandVerified
269
+ ? "verified"
270
+ : "unavailable";
105
271
  return {
106
272
  providerStatus,
107
273
  gitObserved,
@@ -110,85 +276,13 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
110
276
  reportedFilesMatch,
111
277
  };
112
278
  }
113
- /** v3.2: Per-round machine progress — whether git observed file changes in
114
- * each of the last `lookback` committed rounds, rebuilt from the feedback
115
- * entries' roundEvidence snapshots (already persisted at commit). Returns
116
- * null when fewer than `lookback` rounds carry git snapshots — the machine
117
- * signal is unavailable and callers (R4/R5 on the heuristic path) keep
118
- * their legacy skip. The snapshot shape is parsed defensively here because
119
- * this module must not import round-transaction (import cycle). */
279
+ /** Per-round machine progress over decoded committed history. */
120
280
  export function machineProgressSeries(vaultEntries, currentRound, lookback) {
121
- const byRound = new Map();
122
- for (const entry of vaultEntries) {
123
- const tid = String(entry.task_id ?? "");
124
- if (!tid.endsWith(":feedback"))
125
- continue;
126
- const rnd = entryRound(entry);
127
- if (rnd <= 0 || rnd >= currentRound)
128
- continue;
129
- const rt = isRecord(entry.loop_lineage)
130
- ? entry.loop_lineage.round_transaction
131
- : null;
132
- const snapshot = isRecord(rt) ? rt.snapshot : null;
133
- const roundEvidence = isRecord(snapshot) ? snapshot.roundEvidence : null;
134
- if (!Array.isArray(roundEvidence))
135
- continue;
136
- const git = roundEvidence.find((value) => isRecord(value) && value.provider === "git");
137
- if (!git)
138
- continue;
139
- const gitFiles = git.files;
140
- byRound.set(rnd, Array.isArray(gitFiles) && gitFiles.length > 0);
141
- }
142
- if (byRound.size < lookback)
143
- return null;
144
- const sorted = [...byRound.keys()].sort((a, b) => a - b);
145
- const recent = sorted.slice(-lookback);
146
- // Continuity: the recent rounds must be the actual last `lookback` rounds.
147
- if (recent[0] < currentRound - lookback)
148
- return null;
149
- return recent.map((round) => byRound.get(round));
281
+ return machineGitMotionSeries(committedRoundsFromEntries(vaultEntries, currentRound), currentRound, lookback);
150
282
  }
151
283
  // ═══════════════════════════════════════════════════════════════════════════
152
284
  // Individual checks — each returns a VerificationFlag or null
153
285
  // ═══════════════════════════════════════════════════════════════════════════
154
- function checkProgressRegression(selfEval, prevSelfEval) {
155
- if (!prevSelfEval?.execution_evidence)
156
- return null;
157
- if (!selfEval.execution_evidence)
158
- return null;
159
- const prevProgress = prevSelfEval.execution_evidence.progress_estimate;
160
- const currProgress = selfEval.execution_evidence.progress_estimate;
161
- if (typeof prevProgress !== "number" || typeof currProgress !== "number")
162
- return null;
163
- // Use delta with epsilon to avoid IEEE 754 rounding issues (0.8 - 0.2 > 0.6 in float)
164
- if (prevProgress - currProgress <= 0.2 + 1e-10)
165
- return null;
166
- return makeVerificationFlag({
167
- severity: "warn",
168
- field: "progress_estimate",
169
- check: CHECK_PROGRESS_REGRESSION,
170
- detail: `Progress dropped from ${prevProgress.toFixed(2)} to ` +
171
- `${currProgress.toFixed(2)} (delta: ${(currProgress - prevProgress).toFixed(2)})`,
172
- });
173
- }
174
- function checkEmptyChangeWithPassing(selfEval) {
175
- const ev = selfEval.execution_evidence;
176
- if (!ev)
177
- return null;
178
- const filesEmpty = ev.files_changed.length === 0;
179
- const testsAllPass = ev.test_results !== null &&
180
- ev.test_results.failed === 0 &&
181
- ev.test_results.passed > 0;
182
- if (!filesEmpty || !testsAllPass || !effectiveSuccess(selfEval))
183
- return null;
184
- return makeVerificationFlag({
185
- severity: "warn",
186
- field: "execution_evidence",
187
- check: CHECK_EMPTY_CHANGE_WITH_PASSING,
188
- detail: "Agent claims success with no files changed and all tests passing — " +
189
- "verify that work was actually performed",
190
- });
191
- }
192
286
  function checkSuccessWithRemainingCriteria(selfEval) {
193
287
  if (!effectiveSuccess(selfEval))
194
288
  return null;
@@ -222,10 +316,12 @@ function checkUnverifiedCriteriaClaims(selfEval, claimView) {
222
316
  "(no passing test evidence or command snapshot)",
223
317
  });
224
318
  }
225
- /** v2.12: Declared outcome vs legacy boolean consistency. A declared success
226
- * with success=false is a self-contradiction (error); a declared non-success
227
- * with success=true is a compat conflict (warn). Also: outcome==="blocked"
228
- * without a blocker description gets a warn so the next prompt asks for it. */
319
+ /** Declared outcome vs the core success flag — one check id for the whole
320
+ * axis (v3.7: the former success_claim_conflict warn direction merged here).
321
+ * A declared success with success=false is a self-contradiction (error);
322
+ * success=true with a non-success outcome is an inconsistent claim (warn).
323
+ * Also: outcome==="blocked" without a blocker description gets a warn so the
324
+ * next prompt asks for it. */
229
325
  function checkOutcomeConsistency(selfEval) {
230
326
  const outcome = selfEval.outcome;
231
327
  if (outcome === "success" && selfEval.success === false) {
@@ -240,7 +336,7 @@ function checkOutcomeConsistency(selfEval) {
240
336
  return makeVerificationFlag({
241
337
  severity: "warn",
242
338
  field: "success",
243
- check: CHECK_SUCCESS_CLAIM_CONFLICT,
339
+ check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
244
340
  detail: `outcome=${outcome} but success=true — the declared outcome wins`,
245
341
  });
246
342
  }
@@ -288,7 +384,7 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
288
384
  }));
289
385
  continue;
290
386
  }
291
- const paths = extractFilePaths(claim.claim);
387
+ const paths = extractFilePathTokens(claim.claim);
292
388
  const unverified = paths.filter((path) => !observed.some((file) => file === path || file.endsWith(path) || path.endsWith(file)));
293
389
  if (paths.length > 0 && unverified.length > 0) {
294
390
  flags.push(makeVerificationFlag({
@@ -300,50 +396,67 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
300
396
  }));
301
397
  }
302
398
  }
303
- return flags.length > 0 ? flags[0] : null;
399
+ // v3.3.1: return every offending claim's flag — a single truncated flag
400
+ // hid all but the first problem until the next round.
401
+ return flags.length > 0 ? flags : null;
304
402
  }
305
- /** v2.12: success with zero machine-verifiable evidence. The agent claims
306
- * completion but nothing the runtime observed backs it — no verified claims
307
- * (test evidence), no test results, no required commands. A declared
308
- * no_change_reason is the honest escape hatch: it downgrades to info. */
309
- function checkSuccessWithoutVerifiedEvidence(selfEval, claimView) {
403
+ /** v2.12/v3.6/v3.7: success without machine backing — the single
404
+ * "success evidence" semantic. Two arms, evaluated in order:
405
+ * 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
408
+ * 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).
413
+ * 2. Claims arm (v3.6): success=true but the runtime observed no machine
414
+ * evidence for it. The trigger is the runtime-derived providerStatus
415
+ * (verified / unavailable / absent) — "verified" requires an UNTAMPERED
416
+ * passed after-command, so an entrypoint-tampered command no longer
417
+ * counts as machine evidence. Severity follows evidence.machine_backed_
418
+ * success — "required" rejects (error), "warn" tolerates with a warn
419
+ * (round commits, success excluded from the trajectory, trust drops). A
420
+ * 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. */
422
+ function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
310
423
  if (!effectiveSuccess(selfEval))
311
424
  return null;
312
- if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
425
+ // 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)) {
428
+ return makeVerificationFlag({
429
+ severity: "error",
430
+ field: "success",
431
+ check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
432
+ detail: ev
433
+ ? "Agent claims success but execution_evidence shows no files changed " +
434
+ "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.",
438
+ });
439
+ }
440
+ // Claims arm (v3.6).
441
+ if (status.providerStatus === "verified")
313
442
  return null;
314
443
  const noChange = typeof selfEval.no_change_reason === "string" &&
315
444
  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";
316
453
  return makeVerificationFlag({
317
- severity: noChange ? "info" : "error",
454
+ severity,
318
455
  field: "success",
319
456
  check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
320
457
  detail: noChange
321
458
  ? "success with zero verified claims (no_change_reason declared)"
322
- : "success with zero verified claims and no test evidence",
323
- });
324
- }
325
- /** v3.2: success declared but no machine-verified observation this round.
326
- * Unlike R8 (which keys off the agent's SELF-REPORTED test_results), this
327
- * keys off the runtime-derived providerStatus: git diff observed, or a
328
- * passed after-command. Warn-level by design — the round commits, but its
329
- * success never enters the trajectory (and trust drops). A declared
330
- * no_change_reason is the honest escape hatch, mirroring R8.
331
- * Crucially this check does NOT self-skip on the heuristic-extraction path:
332
- * an unstructured success claim without machine observation is flagged
333
- * exactly the same — the check degrades, it never disappears. */
334
- function checkSuccessUnverified(selfEval, status) {
335
- if (!effectiveSuccess(selfEval))
336
- return null;
337
- if (typeof selfEval.no_change_reason === "string" &&
338
- selfEval.no_change_reason.trim().length > 0)
339
- return null;
340
- if (status.providerStatus === "verified")
341
- return null;
342
- return makeVerificationFlag({
343
- severity: "warn",
344
- field: "success",
345
- check: CHECK_SUCCESS_UNVERIFIED,
346
- detail: `success declared but no machine-verified observation this round (providerStatus: ${status.providerStatus})`,
459
+ : `success declared but no machine-verified observation this round (providerStatus: ${status.providerStatus})`,
347
460
  });
348
461
  }
349
462
  function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations) {
@@ -513,10 +626,6 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
513
626
  function extractSubGoalIds(text) {
514
627
  return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
515
628
  }
516
- /** Extract file-path-like tokens (e.g. "src/auth/login.ts") from text. */
517
- function extractFilePaths(text) {
518
- return [...new Set(text.match(/[\w./-]+\.[a-z]{2,6}\b/gi) ?? [])];
519
- }
520
629
  /** Whether a changed path looks like a test file. */
521
630
  function isTestFile(path) {
522
631
  return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
@@ -594,7 +703,7 @@ function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
594
703
  return null;
595
704
  }
596
705
  // ── Signal 2: file paths named in next_action appear in files_changed
597
- const intentPaths = extractFilePaths(intent);
706
+ const intentPaths = extractFilePathTokens(intent);
598
707
  if (intentPaths.length > 0 && filesChanged.length > 0) {
599
708
  const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
600
709
  if (pathMatched)
@@ -740,6 +849,48 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
740
849
  }
741
850
  return null;
742
851
  }
852
+ /** v3.3: Verification domain integrity — command entrypoints and test files
853
+ * changed in the same round the command ran. Entrypoint modification
854
+ * invalidates the machine observation (error: the runtime executed a script
855
+ * the agent just rewrote). Test-file changes are normal dev activity (warn:
856
+ * confirm the tests still verify the task claims). Fail-open when the git
857
+ * snapshot is missing or the command has no resolvable entrypoint. */
858
+ function checkVerificationDomainIntegrity(evidenceSnapshots) {
859
+ const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
860
+ if (!git)
861
+ return null;
862
+ for (const snapshot of evidenceSnapshots) {
863
+ if (!isRecord(snapshot.data) || snapshot.data.kind !== "command")
864
+ continue;
865
+ if (snapshot.data.phase !== "after")
866
+ continue;
867
+ const { entrypointModified, testFilesModified } = commandTampered(snapshot, git);
868
+ if (!entrypointModified && !testFilesModified)
869
+ 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
+ return makeVerificationFlag({
885
+ severity: "warn",
886
+ field: "execution_evidence",
887
+ check: CHECK_TEST_FILES_MODIFIED,
888
+ detail: `Test files changed in the same round command "${name}" passed — ` +
889
+ "confirm the tests still verify the task claims.",
890
+ });
891
+ }
892
+ return null;
893
+ }
743
894
  /** v2.2: Check if the agent's declared next_action aligns with any pending
744
895
  * or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
745
896
  * plans work unrelated to any of them, it may indicate task drift.
@@ -881,13 +1032,225 @@ function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead)
881
1032
  });
882
1033
  }
883
1034
  // ═══════════════════════════════════════════════════════════════════════════
1035
+ // v3.3 — Round Contract checks (all silent when no contract is declared)
1036
+ // ═══════════════════════════════════════════════════════════════════════════
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
+ /** The round changed files outside the ACTIVE contract's declared scope.
1085
+ * 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). */
1090
+ function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
1091
+ if (!activeContract)
1092
+ return null;
1093
+ if (activeContract.scope.length === 0)
1094
+ return null;
1095
+ const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git");
1096
+ if (!git)
1097
+ return null; // git unavailable → cannot observe → fail open
1098
+ const outOfScope = collectOutOfScopeFiles(git.files, activeContract.scope);
1099
+ if (outOfScope.length === 0)
1100
+ return null;
1101
+ return makeVerificationFlag({
1102
+ severity: "warn",
1103
+ field: "round_contract",
1104
+ check: CHECK_ROUND_SCOPE_DRIFT,
1105
+ detail: `Files changed outside the active contract's declared scope` +
1106
+ (outOfScope.length > 5
1107
+ ? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
1108
+ : `: ${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.`,
1168
+ });
1169
+ }
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)
1195
+ return null;
1196
+ const severity = getPolicy().evidence.machine_backed_success === "warn"
1197
+ ? "warn"
1198
+ : "error";
1199
+ return makeVerificationFlag({
1200
+ severity,
1201
+ 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.`,
1208
+ });
1209
+ }
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) {
1220
+ if (!activeContract)
1221
+ return null;
1222
+ const met = selfEval.execution_evidence?.success_criteria_met ?? [];
1223
+ if (contractDoneWhenSatisfied(activeContract, met))
1224
+ return null; // closed
1225
+ if (selfEval.outcome === "blocked")
1226
+ return null; // closed
1227
+ const proposal = selfEval.round_contract;
1228
+ if (!proposal)
1229
+ return null;
1230
+ const same = stableStringify(parseRoundContract(activeContract)) ===
1231
+ stableStringify(parseRoundContract(proposal));
1232
+ if (same)
1233
+ return null; // restate → continue
1234
+ return makeVerificationFlag({
1235
+ severity: "warn",
1236
+ field: "round_contract",
1237
+ check: CHECK_CONTRACT_PREMATURE,
1238
+ detail: "The ACTIVE Round Contract is still open (not all done_when met, not blocked) " +
1239
+ "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.",
1242
+ });
1243
+ }
1244
+ // ═══════════════════════════════════════════════════════════════════════════
884
1245
  // Main entry point
885
1246
  // ═══════════════════════════════════════════════════════════════════════════
886
1247
  /** Verify a SelfEvaluation against the loop's cross-round lineage.
887
1248
  *
888
1249
  * @param selfEval The agent's self-evaluation for the current round.
889
1250
  * @param currentRound The current round number (1-based).
890
- * @param vaultEntries Vault entries for this loop (non-feedback only).
1251
+ * @param vaultEntries Vault entries for this loop (committed lineage
1252
+ * entries AND :feedback entries — the feedback
1253
+ * entries feed the ACTIVE-contract derivation).
891
1254
  * @param prevSelfEval The agent's self-evaluation from the previous round
892
1255
  * (null for round 1).
893
1256
  * @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
@@ -910,24 +1273,32 @@ backtrackTargetGitHead) {
910
1273
  for (const v of entryViolations(entry))
911
1274
  olderViolations.push(v);
912
1275
  }
1276
+ // v3.4: The ACTIVE contract this round executed under — derived from the
1277
+ // committed :feedback evals of earlier rounds. The submission's own
1278
+ // round_contract is a PROPOSAL for the next round and never participates
1279
+ // (the eval under verification is not yet committed, so it structurally
1280
+ // cannot influence the derivation).
1281
+ const activeContract = deriveActiveRoundContract(contractRoundEvaluations(committedRoundsFromEntries(vaultEntries, currentRound)));
913
1282
  // v2.12: Derived claim provenance — which reported criteria completions
914
1283
  // are backed by machine evidence (pure derivation, not persisted).
915
1284
  const claimView = deriveClaimView(selfEval, evidenceSnapshots);
916
1285
  // v3.2: Runtime machine-verification status (git observed / command passed /
917
- // absent). Feeds the success_unverified check that never self-skips.
1286
+ // absent). v3.6: feeds the merged R8 check (providerStatus lens) that
1287
+ // never self-skips.
918
1288
  const evidenceStatus = deriveEvidenceStatus(selfEval, evidenceSnapshots);
919
1289
  // Run all checks
1290
+ // v3.3.1: a check may return several flags (checkRetroactiveClaims raises
1291
+ // one per offending claim) — the previous single-flag contract truncated
1292
+ // the list at the first problem, hiding every other bad retroactive claim
1293
+ // from the agent until the next round.
920
1294
  const checks = [
921
- () => checkProgressRegression(selfEval, prevSelfEval),
922
- () => checkEmptyChangeWithPassing(selfEval),
923
1295
  () => checkSuccessWithRemainingCriteria(selfEval),
924
- // v2.12: success with zero verified claims and no test evidence
925
- () => checkSuccessWithoutVerifiedEvidence(selfEval, claimView),
926
- // v3.2: success without a machine-verified observation (never self-skips)
927
- () => checkSuccessUnverified(selfEval, evidenceStatus),
1296
+ // v2.12/v3.6/v3.7: the single success-evidence semantic — empty/missing-
1297
+ // evidence arm (ex-R3) + claims arm (providerStatus lens, tamper-aware)
1298
+ () => checkSuccessWithoutVerifiedEvidence(selfEval, evidenceStatus),
928
1299
  // v2.12: criteria met but none machine-verified (mild criterion-specific rule)
929
1300
  () => checkUnverifiedCriteriaClaims(selfEval, claimView),
930
- // v2.12: declared outcome vs legacy boolean consistency
1301
+ // Declared outcome vs the required core success flag.
931
1302
  () => checkOutcomeConsistency(selfEval),
932
1303
  // v2.12: retroactive claims against prior rounds
933
1304
  () => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
@@ -939,6 +1310,9 @@ backtrackTargetGitHead) {
939
1310
  () => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
940
1311
  // v2.0: Cross-validate agent-reported test_results against command output
941
1312
  () => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
1313
+ // v3.3: Verification domain integrity — command entrypoint / test files
1314
+ // changed in the same round the command ran
1315
+ () => checkVerificationDomainIntegrity(evidenceSnapshots),
942
1316
  // v2.1: Detect intent-action drift — agent said X but did Y
943
1317
  () => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
944
1318
  // v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
@@ -947,11 +1321,24 @@ backtrackTargetGitHead) {
947
1321
  () => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
948
1322
  // v2.12: Post-backtrack git HEAD restore check
949
1323
  () => 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),
1331
+ () => checkRoundScopeDrift(activeContract, evidenceSnapshots),
1332
+ () => checkPrematureBoundary(activeContract, selfEval, claimView),
1333
+ () => checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots),
1334
+ () => checkContractPremature(activeContract, selfEval),
950
1335
  ];
951
1336
  for (const run of checks) {
952
- const flag = run();
953
- if (flag)
954
- flags.push(flag);
1337
+ const result = run();
1338
+ if (Array.isArray(result))
1339
+ flags.push(...result);
1340
+ else if (result)
1341
+ flags.push(result);
955
1342
  }
956
1343
  // Determine verdict from the most severe flag present
957
1344
  const hasError = flags.some((f) => f.severity === "error");