loopforge 3.7.1 → 3.8.2
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.
- package/README.md +84 -130
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +52 -67
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +134 -147
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -4
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +28 -13
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/committed-round.d.ts +103 -21
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +188 -45
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +20 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +197 -386
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +53 -38
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +85 -89
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +328 -184
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +152 -16
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +40 -53
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +229 -553
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +30 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +47 -103
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +51 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +39 -23
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +148 -163
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +249 -128
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +55 -93
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +149 -38
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +1 -32
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +282 -435
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +365 -142
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +21 -66
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +36 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +166 -102
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +3 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +0 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +19 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +90 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +87 -9
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +396 -27
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +14 -31
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +178 -496
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +59 -83
- package/package.json +67 -67
- package/skills/perception/SKILL.md +218 -204
|
@@ -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 {
|
|
21
|
+
import { isPassedAfterObservation } from "./evidence-provider.js";
|
|
22
|
+
import { getPolicy } from "./policy.js";
|
|
22
23
|
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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 {
|
|
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
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
export const
|
|
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
|
|
97
|
-
*
|
|
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
|
-
[
|
|
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
|
|
189
|
-
* entrypointModified — a workspace file the command depends on
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
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(
|
|
199
|
-
const entrypoints =
|
|
200
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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((
|
|
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
|
|
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.
|
|
252
|
-
if (command && reported) {
|
|
253
|
-
const parsed = parseTestOutput(
|
|
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.
|
|
262
|
-
const reportedSet = [...selfEval.
|
|
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(
|
|
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.
|
|
297
|
-
if (
|
|
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.
|
|
314
|
-
if (
|
|
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: "
|
|
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
|
|
414
|
-
* with files_changed empty AND
|
|
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 (
|
|
417
|
-
*
|
|
418
|
-
*
|
|
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.
|
|
437
|
-
if (!ev || (ev.files_changed.length === 0 && ev.
|
|
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
|
|
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
|
|
446
|
-
"Every successful round MUST include
|
|
447
|
-
"files_changed,
|
|
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.
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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 (
|
|
526
|
+
* from the configured evidence providers (MachineObservation array). */
|
|
576
527
|
function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
577
|
-
if (!selfEval.
|
|
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.
|
|
532
|
+
const gitSnap = evidenceSnapshots.find((s) => s.providerId === "git");
|
|
582
533
|
if (!gitSnap)
|
|
583
534
|
return null;
|
|
584
|
-
const reported = [...selfEval.
|
|
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.
|
|
570
|
+
if (snapshot.kind !== "command")
|
|
620
571
|
continue;
|
|
621
|
-
if (snapshot.
|
|
572
|
+
if (snapshot.phase !== "after" || snapshot.data.required !== true)
|
|
622
573
|
continue;
|
|
623
|
-
const status = snapshot.
|
|
574
|
+
const status = snapshot.status;
|
|
624
575
|
if (status === "passed")
|
|
625
576
|
continue;
|
|
626
|
-
const name =
|
|
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: "
|
|
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
|
|
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.
|
|
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.
|
|
675
|
+
if (snapshot.kind !== "command")
|
|
803
676
|
continue;
|
|
804
|
-
if (snapshot.
|
|
677
|
+
if (snapshot.status !== "passed")
|
|
805
678
|
continue;
|
|
806
679
|
if (snapshot.data.truncated === true)
|
|
807
680
|
continue;
|
|
808
|
-
const stdout = typeof snapshot.data.
|
|
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 =
|
|
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: "
|
|
701
|
+
field: "tests_reported",
|
|
831
702
|
check: CHECK_COMMAND_EVIDENCE_MISMATCH,
|
|
832
|
-
detail: `Agent
|
|
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.
|
|
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.
|
|
726
|
+
if (!isRecord(snapshot.data) || snapshot.kind !== "command")
|
|
849
727
|
continue;
|
|
850
|
-
if (snapshot.
|
|
728
|
+
if (snapshot.phase !== "after")
|
|
851
729
|
continue;
|
|
852
|
-
|
|
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: "
|
|
871
|
-
field: "
|
|
872
|
-
check:
|
|
873
|
-
detail: `
|
|
874
|
-
"
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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: "
|
|
947
|
-
check:
|
|
948
|
-
detail: `
|
|
949
|
-
|
|
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.
|
|
975
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
871
|
+
`. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
|
|
1112
872
|
});
|
|
1113
873
|
}
|
|
1114
|
-
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
|
|
1118
|
-
|
|
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: "
|
|
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:
|
|
1205
|
-
detail:
|
|
1206
|
-
|
|
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
|
-
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
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
|
-
() =>
|
|
1400
|
-
() =>
|
|
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
|
-
|
|
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
|