loopforge 3.0.0 → 3.6.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.
- package/README.md +126 -143
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +131 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +98 -23
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +257 -181
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -33
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +59 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +222 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +78 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1109 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +111 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +887 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +116 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +50 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +167 -50
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +58 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +841 -304
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +6 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +26 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +115 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +214 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +935 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -43
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +64 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +433 -1110
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +20 -11
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +587 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -24
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +78 -184
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +61 -50
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +48 -146
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +487 -278
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +398 -236
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +147 -17
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +32 -69
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +23 -25
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +103 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +13 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +25 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +14 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +37 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +48 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +248 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +149 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +1343 -0
- package/dist/verification-gate.js.map +1 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +37 -16
- package/package.json +5 -10
- package/skills/perception/SKILL.md +184 -202
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
/** Verification Gate — Layer 1 cross-round consistency checks.
|
|
2
|
+
*
|
|
3
|
+
* Pure-function module. Validates an agent's SelfEvaluation against the
|
|
4
|
+
* loop's own lineage before it enters the compiler.
|
|
5
|
+
*
|
|
6
|
+
* Verdict semantics:
|
|
7
|
+
* - trusted: all checks passed; flags are informational only.
|
|
8
|
+
* - suspect: one or more warn-level flags; flags become warnings in
|
|
9
|
+
* the next prompt so the agent can clarify.
|
|
10
|
+
* - contradicted: one or more error-level flags; the success flag for
|
|
11
|
+
* this round is excluded from the success trend (NOT
|
|
12
|
+
* modified). Flags become hard constraints — the agent
|
|
13
|
+
* must respond in the next round.
|
|
14
|
+
*/
|
|
15
|
+
import { getPolicy, isConfiguredCommand } from "./policy.js";
|
|
16
|
+
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
17
|
+
import { jaccardSimilarity, tokenize, entryRound as sharedEntryRound, isRecord } from "./token-utils.js";
|
|
18
|
+
import { deriveSubGoalId } from "./loop-compiler.js";
|
|
19
|
+
import { committedRoundsFromEntries, machineGitMotionSeries } from "./committed-round.js";
|
|
20
|
+
import { contractRoundEvaluations, contractDoneWhenSatisfied, contractItemMatches, deriveActiveRoundContract, } from "./round-contract.js";
|
|
21
|
+
import { stableStringify } from "./canonical-state.js";
|
|
22
|
+
import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
|
|
23
|
+
import { effectiveSuccess, parseRoundContract } from "./self-eval.js";
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// v2.12: Check-name constants — single source of truth so the enforcement
|
|
26
|
+
// gate and audit can reference checks without string-literal drift.
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
export const CHECK_PROGRESS_REGRESSION = "progress_regression";
|
|
29
|
+
export const CHECK_EMPTY_CHANGE_WITH_PASSING = "empty_change_with_passing";
|
|
30
|
+
export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
|
|
31
|
+
export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
|
|
32
|
+
export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
|
|
33
|
+
export const CHECK_SUCCESS_CLAIM_CONFLICT = "success_claim_conflict";
|
|
34
|
+
export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
|
|
35
|
+
export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
|
|
36
|
+
export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
|
|
37
|
+
export const CHECK_DUPLICATE_CONSTRAINT_DISCOVERY = "duplicate_constraint_discovery";
|
|
38
|
+
export const CHECK_RECURRING_VIOLATION = "recurring_violation";
|
|
39
|
+
export const CHECK_RETRACT_FRESH_CONSTRAINT = "retract_fresh_constraint";
|
|
40
|
+
export const CHECK_EVIDENCE_INTEGRITY = "evidence_integrity";
|
|
41
|
+
export const CHECK_REQUIRED_COMMAND_FAILED = "required_command_failed";
|
|
42
|
+
export const CHECK_COMMAND_EVIDENCE_MISMATCH = "command_evidence_mismatch";
|
|
43
|
+
/** v3.3: Verification domain integrity — a command entrypoint (run-tests.sh,
|
|
44
|
+
* package.json, …) changed in the same round the command ran. The runtime
|
|
45
|
+
* executed a script the agent just rewrote, so the observation has no
|
|
46
|
+
* stable baseline: error. Test-file changes are normal dev activity (TDD):
|
|
47
|
+
* warn-only, see CHECK_TEST_FILES_MODIFIED. */
|
|
48
|
+
export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_modified";
|
|
49
|
+
/** v3.3: Test files changed in the same round a verification command passed.
|
|
50
|
+
* Warn-only — the command result stays usable. */
|
|
51
|
+
export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
|
|
52
|
+
export const CHECK_INTENT_DRIFT = "intent_drift";
|
|
53
|
+
export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
|
|
54
|
+
export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
|
|
55
|
+
export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
56
|
+
// v3.3 — Round Contract checks. v3.4: split by target. The submission's own
|
|
57
|
+
// round_contract is a PROPOSAL for the NEXT round and is checked only
|
|
58
|
+
// structurally at declaration (underspecified / unverifiable). Execution
|
|
59
|
+
// conformance (scope drift / premature boundary) targets the ACTIVE
|
|
60
|
+
// contract — the contract the round actually executed under, derived from
|
|
61
|
+
// the committed evals of earlier rounds (round-contract.ts). All checks are
|
|
62
|
+
// silent when their target is absent: contract-less rounds behave exactly
|
|
63
|
+
// as before these checks existed, and round 1 (no committed rounds → no
|
|
64
|
+
// active contract) never produces declaration off-by-one noise.
|
|
65
|
+
/** Contract proposed with an empty done_when — nothing is promised, so
|
|
66
|
+
* nothing can be verified at the boundary. Warn: the contract can be fixed
|
|
67
|
+
* by re-declaring next round. */
|
|
68
|
+
export const CHECK_ROUND_UNDERSPECIFIED = "round_underspecified";
|
|
69
|
+
/** done_when items exist but verification_plan is empty or names commands
|
|
70
|
+
* that are not configured AND enabled in policy.evidence.commands — the
|
|
71
|
+
* completion claims could never be machine-checked. Warn. */
|
|
72
|
+
export const CHECK_ROUND_UNVERIFIABLE = "round_unverifiable";
|
|
73
|
+
/** The round's actual git changes include files outside the ACTIVE
|
|
74
|
+
* contract's declared scope. Warn + drift_clarification exemption
|
|
75
|
+
* (R7-style). */
|
|
76
|
+
export const CHECK_ROUND_SCOPE_DRIFT = "round_scope_drift";
|
|
77
|
+
/** success claimed under the ACTIVE contract whose done_when items are
|
|
78
|
+
* either claimed met without machine-verified evidence or silently dropped
|
|
79
|
+
* (not in success_criteria_met NOR success_criteria_remaining). Error. */
|
|
80
|
+
export const CHECK_PREMATURE_BOUNDARY = "premature_boundary";
|
|
81
|
+
/** v3.5: Closing a Round Contract is a success-class claim and must be
|
|
82
|
+
* machine-backed. The eval's met claims satisfy every done_when of the
|
|
83
|
+
* ACTIVE contract but its verification_plan commands did not pass this
|
|
84
|
+
* round. Error; warn under evidence.machine_backed_success "warn"; never
|
|
85
|
+
* downgraded by no_change_reason (all done_when met contradicts "no
|
|
86
|
+
* change"). Fail-open: plan names no longer configured+enabled are not
|
|
87
|
+
* required (cannot observe). */
|
|
88
|
+
export const CHECK_CONTRACT_COMPLETION_UNVERIFIED = "contract_completion_unverified";
|
|
89
|
+
/** v3.5: The ACTIVE contract is still open (not completed, not blocked)
|
|
90
|
+
* while a different contract was proposed — the proposal is ignored until
|
|
91
|
+
* the active one closes. Warn: the walker still ignores it; this only
|
|
92
|
+
* surfaces the otherwise-silent state. */
|
|
93
|
+
export const CHECK_CONTRACT_PREMATURE = "contract_premature";
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
// Helpers
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
/** Extract the round number from a vault entry's loop_lineage.
|
|
98
|
+
* Returns 0 if the entry has no lineage or no round field.
|
|
99
|
+
* In practice, persistLoopLineage always writes round ≥ 1, so 0
|
|
100
|
+
* unambiguously means "not a valid round entry" in this context.
|
|
101
|
+
* Exported for reuse by enforcement-gate.ts. */
|
|
102
|
+
export function entryRound(entry) {
|
|
103
|
+
return sharedEntryRound(entry);
|
|
104
|
+
}
|
|
105
|
+
/** Read constraint_violations from a vault entry (entry-level, stored from
|
|
106
|
+
* the previous round's last_round_result at persist time). */
|
|
107
|
+
function entryViolations(entry) {
|
|
108
|
+
const viols = entry.constraint_violations;
|
|
109
|
+
if (Array.isArray(viols))
|
|
110
|
+
return viols.filter((v) => typeof v === "string");
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
// ── v3.3: Round Contract scope matching ─────────────────────────────────────
|
|
114
|
+
/** Normalize a contract scope entry: backslashes → forward slashes, strip
|
|
115
|
+
* leading "./", trim, strip trailing slashes. "." / "./" / "" normalize to
|
|
116
|
+
* "" = the repository root (everything is in scope). */
|
|
117
|
+
export function normalizeScopeEntry(path) {
|
|
118
|
+
let s = path.replace(/\\/g, "/").trim();
|
|
119
|
+
while (s.startsWith("./"))
|
|
120
|
+
s = s.slice(2);
|
|
121
|
+
if (s === "." || s === "")
|
|
122
|
+
return "";
|
|
123
|
+
while (s.endsWith("/"))
|
|
124
|
+
s = s.slice(0, -1);
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
127
|
+
/** Whether a git-diff file path falls inside any declared scope entry:
|
|
128
|
+
* exact file match, or the file lives under a declared directory. The ""
|
|
129
|
+
* entry (repo root) matches everything. Git paths are always workspace-
|
|
130
|
+
* relative with forward slashes. */
|
|
131
|
+
export function isFileInScope(file, scope) {
|
|
132
|
+
const normalizedFile = normalizeScopeEntry(file);
|
|
133
|
+
for (const entry of scope) {
|
|
134
|
+
const normalized = normalizeScopeEntry(entry);
|
|
135
|
+
if (normalized === "" || normalizedFile === normalized ||
|
|
136
|
+
normalizedFile.startsWith(normalized + "/")) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/** Git-diff files outside the declared scope (the round_scope_drift signal). */
|
|
143
|
+
export function collectOutOfScopeFiles(files, scope) {
|
|
144
|
+
return files.filter((file) => !isFileInScope(file, scope));
|
|
145
|
+
}
|
|
146
|
+
/** v3.3: Detect verification-domain tampering for a command snapshot.
|
|
147
|
+
* entrypointModified — a workspace file the command depends on
|
|
148
|
+
* (snapshot.data.entrypointFiles) changed this round: the runtime executed
|
|
149
|
+
* a script the agent just rewrote, so the "machine observation" has no
|
|
150
|
+
* stable baseline. testFilesModified — the round changed test files
|
|
151
|
+
* (isTestFile): normal in TDD, so warn-only; the command result stays
|
|
152
|
+
* usable. Pure snapshot derivation — no fs access (this module is pure). */
|
|
153
|
+
function commandTampered(snapshot, gitSnapshot) {
|
|
154
|
+
if (!gitSnapshot)
|
|
155
|
+
return { entrypointModified: false, testFilesModified: false };
|
|
156
|
+
const gitFiles = new Set(gitSnapshot.files);
|
|
157
|
+
const entrypoints = isRecord(snapshot.data) &&
|
|
158
|
+
Array.isArray(snapshot.data.entrypointFiles)
|
|
159
|
+
? snapshot.data.entrypointFiles.filter((f) => typeof f === "string")
|
|
160
|
+
: [];
|
|
161
|
+
const entrypointModified = entrypoints.some((file) => gitFiles.has(file));
|
|
162
|
+
const testFilesModified = [...gitFiles].some((file) => isTestFile(file));
|
|
163
|
+
return { entrypointModified, testFilesModified };
|
|
164
|
+
}
|
|
165
|
+
function passedAfterCommand(snapshots) {
|
|
166
|
+
const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
167
|
+
return snapshots.find((snapshot) => isRecord(snapshot.data) &&
|
|
168
|
+
snapshot.data.kind === "command" &&
|
|
169
|
+
snapshot.data.phase === "after" &&
|
|
170
|
+
snapshot.data.status === "passed" &&
|
|
171
|
+
// v3.3: a command whose entrypoint changed this round is not machine
|
|
172
|
+
// evidence — exclude it so providerStatus degrades to unavailable.
|
|
173
|
+
!commandTampered(snapshot, git).entrypointModified) ?? null;
|
|
174
|
+
}
|
|
175
|
+
/** v3.5: Names of commands observed passing in the after-phase this round.
|
|
176
|
+
* Tampered commands (entrypoint changed this round) are not machine
|
|
177
|
+
* evidence and are excluded — the same rule passedAfterCommand applies. */
|
|
178
|
+
function passedPlanCommandNames(snapshots) {
|
|
179
|
+
const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
180
|
+
const names = new Set();
|
|
181
|
+
for (const snapshot of snapshots) {
|
|
182
|
+
if (!isRecord(snapshot.data))
|
|
183
|
+
continue;
|
|
184
|
+
if (snapshot.data.kind !== "command")
|
|
185
|
+
continue;
|
|
186
|
+
if (snapshot.data.phase !== "after" || snapshot.data.status !== "passed")
|
|
187
|
+
continue;
|
|
188
|
+
if (commandTampered(snapshot, git).entrypointModified)
|
|
189
|
+
continue;
|
|
190
|
+
const name = snapshot.data.commandName;
|
|
191
|
+
if (typeof name === "string" && name.length > 0)
|
|
192
|
+
names.add(name);
|
|
193
|
+
}
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
/** v3.2: Derive the machine-verification status of a round. The verification
|
|
197
|
+
* capability is modeled explicitly (verified / unavailable / absent) instead
|
|
198
|
+
* of letting evidence-dependent checks silently disappear when snapshots are
|
|
199
|
+
* missing — the fix for the "weakest when it matters most" gap. */
|
|
200
|
+
export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
201
|
+
const gitSnap = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
202
|
+
const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
|
|
203
|
+
const command = passedAfterCommand(evidenceSnapshots);
|
|
204
|
+
const commandVerified = command !== null;
|
|
205
|
+
// testsMachineBacked: agent-reported test_results agree with a passed
|
|
206
|
+
// command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
|
|
207
|
+
// performs — parseTestOutput lives in this module).
|
|
208
|
+
let testsMachineBacked = false;
|
|
209
|
+
const reported = selfEval.execution_evidence?.test_results;
|
|
210
|
+
if (command && reported) {
|
|
211
|
+
const parsed = parseTestOutput(String(command.data.stdout ?? ""));
|
|
212
|
+
testsMachineBacked = parsed !== null &&
|
|
213
|
+
parsed.passed === reported.passed &&
|
|
214
|
+
parsed.failed === reported.failed &&
|
|
215
|
+
parsed.skipped === reported.skipped;
|
|
216
|
+
}
|
|
217
|
+
// reportedFilesMatch: agent-reported files_changed equals the git diff set.
|
|
218
|
+
let reportedFilesMatch = false;
|
|
219
|
+
if (gitSnap && selfEval.execution_evidence) {
|
|
220
|
+
const reportedSet = [...selfEval.execution_evidence.files_changed].sort();
|
|
221
|
+
const actualSet = [...gitSnap.files].sort();
|
|
222
|
+
reportedFilesMatch = reportedSet.length === actualSet.length &&
|
|
223
|
+
reportedSet.every((file, index) => file === actualSet[index]);
|
|
224
|
+
}
|
|
225
|
+
// v3.3: git observation alone is no longer "verified" — the machine seeing
|
|
226
|
+
// file changes is not the machine verifying success. Only a passed
|
|
227
|
+
// after-phase command (observed by the runtime itself) upgrades a round.
|
|
228
|
+
// Any other snapshot presence is "unavailable": the machine saw activity
|
|
229
|
+
// (or nothing at all this round) but could not verify the success claim,
|
|
230
|
+
// so R8 warns and trust drops (v3.6: merged success_unverified semantics).
|
|
231
|
+
const providerStatus = evidenceSnapshots.length === 0
|
|
232
|
+
? "absent"
|
|
233
|
+
: commandVerified
|
|
234
|
+
? "verified"
|
|
235
|
+
: "unavailable";
|
|
236
|
+
return {
|
|
237
|
+
providerStatus,
|
|
238
|
+
gitObserved,
|
|
239
|
+
commandVerified,
|
|
240
|
+
testsMachineBacked,
|
|
241
|
+
reportedFilesMatch,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Per-round machine progress over decoded committed history. */
|
|
245
|
+
export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
246
|
+
return machineGitMotionSeries(committedRoundsFromEntries(vaultEntries, currentRound), currentRound, lookback);
|
|
247
|
+
}
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
|
+
// Individual checks — each returns a VerificationFlag or null
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
+
function checkProgressRegression(selfEval, prevSelfEval) {
|
|
252
|
+
if (!prevSelfEval?.execution_evidence)
|
|
253
|
+
return null;
|
|
254
|
+
if (!selfEval.execution_evidence)
|
|
255
|
+
return null;
|
|
256
|
+
const prevProgress = prevSelfEval.execution_evidence.progress_estimate;
|
|
257
|
+
const currProgress = selfEval.execution_evidence.progress_estimate;
|
|
258
|
+
if (typeof prevProgress !== "number" || typeof currProgress !== "number")
|
|
259
|
+
return null;
|
|
260
|
+
// Use delta with epsilon to avoid IEEE 754 rounding issues (0.8 - 0.2 > 0.6 in float)
|
|
261
|
+
if (prevProgress - currProgress <= 0.2 + 1e-10)
|
|
262
|
+
return null;
|
|
263
|
+
return makeVerificationFlag({
|
|
264
|
+
severity: "warn",
|
|
265
|
+
field: "progress_estimate",
|
|
266
|
+
check: CHECK_PROGRESS_REGRESSION,
|
|
267
|
+
detail: `Progress dropped from ${prevProgress.toFixed(2)} to ` +
|
|
268
|
+
`${currProgress.toFixed(2)} (delta: ${(currProgress - prevProgress).toFixed(2)})`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function checkEmptyChangeWithPassing(selfEval) {
|
|
272
|
+
const ev = selfEval.execution_evidence;
|
|
273
|
+
if (!ev)
|
|
274
|
+
return null;
|
|
275
|
+
const filesEmpty = ev.files_changed.length === 0;
|
|
276
|
+
const testsAllPass = ev.test_results !== null &&
|
|
277
|
+
ev.test_results.failed === 0 &&
|
|
278
|
+
ev.test_results.passed > 0;
|
|
279
|
+
if (!filesEmpty || !testsAllPass || !effectiveSuccess(selfEval))
|
|
280
|
+
return null;
|
|
281
|
+
return makeVerificationFlag({
|
|
282
|
+
severity: "warn",
|
|
283
|
+
field: "execution_evidence",
|
|
284
|
+
check: CHECK_EMPTY_CHANGE_WITH_PASSING,
|
|
285
|
+
detail: "Agent claims success with no files changed and all tests passing — " +
|
|
286
|
+
"verify that work was actually performed",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function checkSuccessWithRemainingCriteria(selfEval) {
|
|
290
|
+
if (!effectiveSuccess(selfEval))
|
|
291
|
+
return null;
|
|
292
|
+
const remaining = selfEval.execution_evidence?.success_criteria_remaining;
|
|
293
|
+
if (!remaining || remaining.length === 0)
|
|
294
|
+
return null;
|
|
295
|
+
return makeVerificationFlag({
|
|
296
|
+
severity: "error",
|
|
297
|
+
field: "success",
|
|
298
|
+
check: CHECK_SUCCESS_WITH_REMAINING_CRITERIA,
|
|
299
|
+
detail: `Agent claims success but ${remaining.length} criteria remain unmet: ` +
|
|
300
|
+
remaining.slice(0, 3).join("; "),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** v2.12: Criteria reported met but none machine-verified. The agent puts
|
|
304
|
+
* completed criteria into the ledger, but no passing test evidence or
|
|
305
|
+
* command snapshot backs them. Warn — completion still counts, but the
|
|
306
|
+
* next prompt tells the agent its claims are unverified. This is the
|
|
307
|
+
* mild form of the 3.x criterion-specific completion rule. */
|
|
308
|
+
function checkUnverifiedCriteriaClaims(selfEval, claimView) {
|
|
309
|
+
const met = selfEval.execution_evidence?.success_criteria_met;
|
|
310
|
+
if (!met || met.length === 0)
|
|
311
|
+
return null;
|
|
312
|
+
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
313
|
+
return null;
|
|
314
|
+
return makeVerificationFlag({
|
|
315
|
+
severity: "warn",
|
|
316
|
+
field: "execution_evidence",
|
|
317
|
+
check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
|
|
318
|
+
detail: `${met.length} criteria reported met but none machine-verified ` +
|
|
319
|
+
"(no passing test evidence or command snapshot)",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** Declared outcome vs the core success flag. A declared success with
|
|
323
|
+
* success=false is a self-contradiction (error); a declared non-success with
|
|
324
|
+
* success=true is an inconsistent claim (warn). Also: outcome==="blocked"
|
|
325
|
+
* without a blocker description gets a warn so the next prompt asks for it. */
|
|
326
|
+
function checkOutcomeConsistency(selfEval) {
|
|
327
|
+
const outcome = selfEval.outcome;
|
|
328
|
+
if (outcome === "success" && selfEval.success === false) {
|
|
329
|
+
return makeVerificationFlag({
|
|
330
|
+
severity: "error",
|
|
331
|
+
field: "outcome",
|
|
332
|
+
check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
|
|
333
|
+
detail: "outcome=success but success=false — self-contradictory",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (outcome !== undefined && outcome !== "success" && selfEval.success === true) {
|
|
337
|
+
return makeVerificationFlag({
|
|
338
|
+
severity: "warn",
|
|
339
|
+
field: "success",
|
|
340
|
+
check: CHECK_SUCCESS_CLAIM_CONFLICT,
|
|
341
|
+
detail: `outcome=${outcome} but success=true — the declared outcome wins`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (outcome === "blocked" &&
|
|
345
|
+
(typeof selfEval.blocker !== "string" || selfEval.blocker.trim().length === 0)) {
|
|
346
|
+
return makeVerificationFlag({
|
|
347
|
+
severity: "warn",
|
|
348
|
+
field: "blocker",
|
|
349
|
+
check: CHECK_BLOCKED_WITHOUT_BLOCKER,
|
|
350
|
+
detail: "outcome=blocked but no blocker description was provided",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/** v2.12: Retroactive claims reference PRIOR rounds. Each claim's file
|
|
356
|
+
* paths are checked against the referenced round's committed git evidence
|
|
357
|
+
* (P0 provenance layer). Unverifiable claims warn — they never upgrade
|
|
358
|
+
* criteria status. */
|
|
359
|
+
function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
360
|
+
const claims = selfEval.retroactiveClaims;
|
|
361
|
+
if (!claims || claims.length === 0)
|
|
362
|
+
return null;
|
|
363
|
+
const loopId = vaultEntries.find((entry) => typeof entry.loop_id === "string")
|
|
364
|
+
?.loop_id;
|
|
365
|
+
const flags = [];
|
|
366
|
+
for (const claim of claims) {
|
|
367
|
+
if (claim.round < 1 || claim.round >= currentRound) {
|
|
368
|
+
flags.push(makeVerificationFlag({
|
|
369
|
+
severity: "warn",
|
|
370
|
+
field: "retroactiveClaims",
|
|
371
|
+
check: CHECK_RETROACTIVE_CLAIM_BAD_ROUND,
|
|
372
|
+
detail: `retroactive claim targets round ${claim.round} (must be a prior round)`,
|
|
373
|
+
}));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!loopId)
|
|
377
|
+
continue; // no vault context → cannot verify (fail-open)
|
|
378
|
+
const observed = resolveRoundFiles(vaultEntries, loopId, claim.round);
|
|
379
|
+
if (observed === null) {
|
|
380
|
+
flags.push(makeVerificationFlag({
|
|
381
|
+
severity: "warn",
|
|
382
|
+
field: "retroactiveClaims",
|
|
383
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
384
|
+
detail: `retroactive claim for round ${claim.round} has no observable git evidence`,
|
|
385
|
+
}));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const paths = extractFilePaths(claim.claim);
|
|
389
|
+
const unverified = paths.filter((path) => !observed.some((file) => file === path || file.endsWith(path) || path.endsWith(file)));
|
|
390
|
+
if (paths.length > 0 && unverified.length > 0) {
|
|
391
|
+
flags.push(makeVerificationFlag({
|
|
392
|
+
severity: "warn",
|
|
393
|
+
field: "retroactiveClaims",
|
|
394
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
395
|
+
detail: `retroactive claim references files not observed in round ${claim.round}: ` +
|
|
396
|
+
unverified.slice(0, 3).join(", "),
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// v3.3.1: return every offending claim's flag — a single truncated flag
|
|
401
|
+
// hid all but the first problem until the next round.
|
|
402
|
+
return flags.length > 0 ? flags : null;
|
|
403
|
+
}
|
|
404
|
+
/** v2.12/v3.6: success without a machine-verified observation. The agent
|
|
405
|
+
* claims completion but the runtime observed no machine evidence for it.
|
|
406
|
+
* v3.6: the trigger is the runtime-derived providerStatus (verified /
|
|
407
|
+
* unavailable / absent) — the v3.2 success_unverified check merged here.
|
|
408
|
+
* ProviderStatus "verified" requires an UNTAMPERED passed after-command, so
|
|
409
|
+
* an entrypoint-tampered command no longer counts as machine evidence
|
|
410
|
+
* (that coverage previously lived in success_unverified alone); the check
|
|
411
|
+
* never self-skips on the heuristic-extraction path. Severity follows
|
|
412
|
+
* evidence.machine_backed_success — "required" rejects (error), "warn"
|
|
413
|
+
* tolerates with a warn (round commits, success excluded from the
|
|
414
|
+
* trajectory, trust drops). A declared no_change_reason is the honest
|
|
415
|
+
* escape hatch: it downgrades to info. no_change_reason is honored HERE
|
|
416
|
+
* ONLY — contract checks refuse it (v3.6). */
|
|
417
|
+
function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
|
|
418
|
+
if (!effectiveSuccess(selfEval))
|
|
419
|
+
return null;
|
|
420
|
+
if (status.providerStatus === "verified")
|
|
421
|
+
return null;
|
|
422
|
+
const noChange = typeof selfEval.no_change_reason === "string" &&
|
|
423
|
+
selfEval.no_change_reason.trim().length > 0;
|
|
424
|
+
// v3.3: severity follows evidence.machine_backed_success — "required"
|
|
425
|
+
// rejects (error), "warn" tolerates with a warn (round commits, success
|
|
426
|
+
// excluded from the trajectory, trust drops). no_change_reason stays info.
|
|
427
|
+
const severity = noChange
|
|
428
|
+
? "info"
|
|
429
|
+
: getPolicy().evidence.machine_backed_success === "warn"
|
|
430
|
+
? "warn"
|
|
431
|
+
: "error";
|
|
432
|
+
return makeVerificationFlag({
|
|
433
|
+
severity,
|
|
434
|
+
field: "success",
|
|
435
|
+
check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
|
|
436
|
+
detail: noChange
|
|
437
|
+
? "success with zero verified claims (no_change_reason declared)"
|
|
438
|
+
: `success declared but no machine-verified observation this round (providerStatus: ${status.providerStatus})`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations) {
|
|
442
|
+
const discovered = selfEval.discovered_constraints;
|
|
443
|
+
if (!discovered || discovered.length === 0)
|
|
444
|
+
return null;
|
|
445
|
+
// Collect all previously-known constraints
|
|
446
|
+
const known = new Set();
|
|
447
|
+
for (const v of olderViolations)
|
|
448
|
+
known.add(v.toLowerCase().trim());
|
|
449
|
+
if (prevSelfEval) {
|
|
450
|
+
for (const d of prevSelfEval.discovered_constraints ?? []) {
|
|
451
|
+
known.add(d.toLowerCase().trim());
|
|
452
|
+
}
|
|
453
|
+
// Also treat previous violations as implicitly discovered
|
|
454
|
+
for (const v of prevSelfEval.constraint_violations) {
|
|
455
|
+
known.add(v.toLowerCase().trim());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const d of discovered) {
|
|
459
|
+
if (known.has(d.toLowerCase().trim())) {
|
|
460
|
+
return makeVerificationFlag({
|
|
461
|
+
severity: "warn",
|
|
462
|
+
field: "discovered_constraints",
|
|
463
|
+
check: CHECK_DUPLICATE_CONSTRAINT_DISCOVERY,
|
|
464
|
+
detail: `Constraint "${d}" was already known from a previous round`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
471
|
+
const currViols = selfEval.constraint_violations;
|
|
472
|
+
if (!currViols || currViols.length === 0)
|
|
473
|
+
return null;
|
|
474
|
+
// Build the violation history: [round N-2, round N-1, round N]
|
|
475
|
+
const violationsByRound = [];
|
|
476
|
+
// Round N-2 violations come from the vault entry for round N-1
|
|
477
|
+
// (persistLoopLineage stores the PREVIOUS round's violations on each entry)
|
|
478
|
+
if (currentRound >= 3) {
|
|
479
|
+
const entryNMinus1 = vaultEntries.find((e) => entryRound(e) === currentRound - 1);
|
|
480
|
+
if (entryNMinus1) {
|
|
481
|
+
const viols = entryViolations(entryNMinus1);
|
|
482
|
+
if (viols.length)
|
|
483
|
+
violationsByRound.push(viols.map((v) => v.toLowerCase().trim()));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Round N-1 violations from prevSelfEval
|
|
487
|
+
if (prevSelfEval) {
|
|
488
|
+
const prevViols = prevSelfEval.constraint_violations;
|
|
489
|
+
violationsByRound.push(prevViols.map((v) => v.toLowerCase().trim()));
|
|
490
|
+
}
|
|
491
|
+
// Round N violations from current selfEval
|
|
492
|
+
violationsByRound.push(currViols.map((v) => v.toLowerCase().trim()));
|
|
493
|
+
// Need at least 3 rounds of data
|
|
494
|
+
if (violationsByRound.length < 3)
|
|
495
|
+
return null;
|
|
496
|
+
// Check each current violation against the previous 2 rounds
|
|
497
|
+
const [rNMinus2, rNMinus1, rN] = violationsByRound.slice(-3);
|
|
498
|
+
for (const v of rN) {
|
|
499
|
+
if (rNMinus1.includes(v) && rNMinus2.includes(v)) {
|
|
500
|
+
return makeVerificationFlag({
|
|
501
|
+
severity: "error",
|
|
502
|
+
field: "constraint_violations",
|
|
503
|
+
check: CHECK_RECURRING_VIOLATION,
|
|
504
|
+
detail: `Constraint violation "${v}" has appeared in 3 consecutive rounds ` +
|
|
505
|
+
`(rounds ${currentRound - 2}–${currentRound}) without resolution`,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
|
|
512
|
+
const retracted = selfEval.retracted_constraints;
|
|
513
|
+
if (!retracted || retracted.length === 0)
|
|
514
|
+
return null;
|
|
515
|
+
if (!prevSelfEval)
|
|
516
|
+
return null;
|
|
517
|
+
const lastRoundDiscoveries = new Set();
|
|
518
|
+
for (const d of prevSelfEval.discovered_constraints ?? []) {
|
|
519
|
+
lastRoundDiscoveries.add(d.toLowerCase().trim());
|
|
520
|
+
}
|
|
521
|
+
for (const r of retracted) {
|
|
522
|
+
if (lastRoundDiscoveries.has(r.toLowerCase().trim())) {
|
|
523
|
+
return makeVerificationFlag({
|
|
524
|
+
severity: "warn",
|
|
525
|
+
field: "retracted_constraints",
|
|
526
|
+
check: CHECK_RETRACT_FRESH_CONSTRAINT,
|
|
527
|
+
detail: `Retracting constraint "${r}" that was just discovered in round ` +
|
|
528
|
+
`${currentRound - 1} — may indicate rapid flip-flopping`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
/** v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
535
|
+
* from the configured evidence providers (ProviderSnapshot array). */
|
|
536
|
+
function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
537
|
+
if (!selfEval.execution_evidence)
|
|
538
|
+
return null;
|
|
539
|
+
if (evidenceSnapshots.length === 0)
|
|
540
|
+
return null;
|
|
541
|
+
const gitSnap = evidenceSnapshots.find((s) => s.provider === "git");
|
|
542
|
+
if (!gitSnap)
|
|
543
|
+
return null;
|
|
544
|
+
const reported = [...selfEval.execution_evidence.files_changed].sort();
|
|
545
|
+
const actual = [...gitSnap.files].sort();
|
|
546
|
+
if (reported.length === 0 && actual.length === 0)
|
|
547
|
+
return null;
|
|
548
|
+
if (reported.length === 0 && actual.length > 0) {
|
|
549
|
+
return makeVerificationFlag({
|
|
550
|
+
severity: "warn",
|
|
551
|
+
field: "files_changed",
|
|
552
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
553
|
+
detail: `Agent reported no files changed but evidence shows: ${actual.join(", ")}`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const ghostFiles = reported.filter((f) => !actual.includes(f));
|
|
557
|
+
const missedFiles = actual.filter((f) => !reported.includes(f));
|
|
558
|
+
if (ghostFiles.length > 0 || missedFiles.length > 0) {
|
|
559
|
+
const parts = [];
|
|
560
|
+
if (ghostFiles.length > 0)
|
|
561
|
+
parts.push(`unconfirmed: [${ghostFiles.join(", ")}]`);
|
|
562
|
+
if (missedFiles.length > 0)
|
|
563
|
+
parts.push(`unreported: [${missedFiles.join(", ")}]`);
|
|
564
|
+
return makeVerificationFlag({
|
|
565
|
+
severity: "warn",
|
|
566
|
+
field: "files_changed",
|
|
567
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
568
|
+
detail: `Agent files_changed doesn't match evidence: ${parts.join("; ")}`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
/** A required, explicitly configured verification command is authoritative
|
|
574
|
+
* when the Agent claims success. Optional commands remain observational. */
|
|
575
|
+
function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
576
|
+
if (!effectiveSuccess(selfEval))
|
|
577
|
+
return null;
|
|
578
|
+
for (const snapshot of evidenceSnapshots) {
|
|
579
|
+
if (snapshot.data.kind !== "command")
|
|
580
|
+
continue;
|
|
581
|
+
if (snapshot.data.phase !== "after" || snapshot.data.required !== true)
|
|
582
|
+
continue;
|
|
583
|
+
const status = snapshot.data.status;
|
|
584
|
+
if (status === "passed")
|
|
585
|
+
continue;
|
|
586
|
+
const name = typeof snapshot.data.commandName === "string"
|
|
587
|
+
? snapshot.data.commandName
|
|
588
|
+
: snapshot.provider;
|
|
589
|
+
const exitCode = typeof snapshot.data.exitCode === "number"
|
|
590
|
+
? ` (exit ${snapshot.data.exitCode})`
|
|
591
|
+
: "";
|
|
592
|
+
return makeVerificationFlag({
|
|
593
|
+
severity: "error",
|
|
594
|
+
field: "execution_evidence",
|
|
595
|
+
check: CHECK_REQUIRED_COMMAND_FAILED,
|
|
596
|
+
detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
// ── Test output parsing ────────────────────────────────────────────────────
|
|
602
|
+
// jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
|
|
603
|
+
/** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
|
|
604
|
+
* Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
|
|
605
|
+
function extractSubGoalIds(text) {
|
|
606
|
+
return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
|
|
607
|
+
}
|
|
608
|
+
/** Extract file-path-like tokens (e.g. "src/auth/login.ts") from text. */
|
|
609
|
+
function extractFilePaths(text) {
|
|
610
|
+
return [...new Set(text.match(/[\w./-]+\.[a-z]{2,6}\b/gi) ?? [])];
|
|
611
|
+
}
|
|
612
|
+
/** Whether a changed path looks like a test file. */
|
|
613
|
+
function isTestFile(path) {
|
|
614
|
+
return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
|
|
615
|
+
/(^|[\\/])tests?[\\/]/i.test(path) ||
|
|
616
|
+
/_test\.[a-z0-9]+$/i.test(path);
|
|
617
|
+
}
|
|
618
|
+
/** Map each referenced sub-goal ID to its known description(s), reconstructed
|
|
619
|
+
* from emerged_subtasks in the vault lineage and the previous self-eval. */
|
|
620
|
+
function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
|
|
621
|
+
const map = new Map();
|
|
622
|
+
for (const id of ids)
|
|
623
|
+
map.set(id, []);
|
|
624
|
+
const descriptions = [];
|
|
625
|
+
for (const entry of vaultEntries) {
|
|
626
|
+
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
627
|
+
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
628
|
+
: [];
|
|
629
|
+
descriptions.push(...emerged);
|
|
630
|
+
}
|
|
631
|
+
descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
|
|
632
|
+
for (const desc of descriptions) {
|
|
633
|
+
const trimmed = desc.trim();
|
|
634
|
+
if (!trimmed)
|
|
635
|
+
continue;
|
|
636
|
+
const id = deriveSubGoalId(trimmed);
|
|
637
|
+
if (map.has(id))
|
|
638
|
+
map.get(id).push(trimmed);
|
|
639
|
+
}
|
|
640
|
+
return map;
|
|
641
|
+
}
|
|
642
|
+
/** v2.1 (check 11): Compare the previous round's declared next_action with
|
|
643
|
+
* the current round's actual output_summary. Detect when the agent says it
|
|
644
|
+
* will do X but then does Y — silent drift without explanation.
|
|
645
|
+
*
|
|
646
|
+
* Only fires when both next_action and output_summary are non-empty.
|
|
647
|
+
*
|
|
648
|
+
* v2.14: Structured-ID-first detection. Alignment is decided by a waterfall
|
|
649
|
+
* of concrete signals before falling back to string similarity:
|
|
650
|
+
* 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
|
|
651
|
+
* round's completed_subtasks (exact ID, ID derived from the
|
|
652
|
+
* description text, or Jaccard against the referenced sub-goal's
|
|
653
|
+
* known description).
|
|
654
|
+
* 2. File paths named in next_action appearing in files_changed.
|
|
655
|
+
* 3. Test evidence — next_action mentions tests and test files were
|
|
656
|
+
* changed with tests actually run.
|
|
657
|
+
* 4. Jaccard token similarity against the policy threshold (weak signal).
|
|
658
|
+
* Any signal aligning is sufficient — text similarity is never the sole
|
|
659
|
+
* authority when structured evidence exists. */
|
|
660
|
+
function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
661
|
+
if (!prevSelfEval?.next_action?.trim())
|
|
662
|
+
return null;
|
|
663
|
+
if (!selfEval.output_summary.trim())
|
|
664
|
+
return null;
|
|
665
|
+
const intent = prevSelfEval.next_action.trim();
|
|
666
|
+
const actual = selfEval.output_summary.trim();
|
|
667
|
+
const policy = getPolicy();
|
|
668
|
+
const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
|
|
669
|
+
// ── Signal 1: sub-goal IDs referenced by next_action → completed this round
|
|
670
|
+
const intentIds = extractSubGoalIds(intent);
|
|
671
|
+
if (intentIds.length > 0) {
|
|
672
|
+
const completed = selfEval.completed_subtasks ?? [];
|
|
673
|
+
const referenced = referencedSubGoalDescriptions(intentIds, vaultEntries, prevSelfEval);
|
|
674
|
+
const idMatched = intentIds.some((id) => completed.some((entry) => {
|
|
675
|
+
const text = entry.trim();
|
|
676
|
+
if (!text)
|
|
677
|
+
return false;
|
|
678
|
+
if (text.toLowerCase() === id)
|
|
679
|
+
return true;
|
|
680
|
+
if (deriveSubGoalId(text) === id)
|
|
681
|
+
return true;
|
|
682
|
+
const descriptions = referenced.get(id) ?? [];
|
|
683
|
+
return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
|
|
684
|
+
}));
|
|
685
|
+
if (idMatched)
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
// ── Signal 2: file paths named in next_action appear in files_changed
|
|
689
|
+
const intentPaths = extractFilePaths(intent);
|
|
690
|
+
if (intentPaths.length > 0 && filesChanged.length > 0) {
|
|
691
|
+
const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
|
|
692
|
+
if (pathMatched)
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
// ── Signal 3: test evidence — intent mentions tests, test files changed,
|
|
696
|
+
// and tests actually ran this round
|
|
697
|
+
const intentTokens = tokenize(intent);
|
|
698
|
+
const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
|
|
699
|
+
const testResults = selfEval.execution_evidence?.test_results;
|
|
700
|
+
const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
|
|
701
|
+
const testFilesChanged = filesChanged.some((f) => isTestFile(f));
|
|
702
|
+
if (mentionsTests && testsRan && testFilesChanged)
|
|
703
|
+
return null;
|
|
704
|
+
// ── Signal 4 (weak): Jaccard token similarity
|
|
705
|
+
const score = jaccardSimilarity(intent, actual);
|
|
706
|
+
const threshold = policy.evolution.intent_drift_threshold;
|
|
707
|
+
if (score >= threshold)
|
|
708
|
+
return null;
|
|
709
|
+
const idNote = intentIds.length > 0
|
|
710
|
+
? `; ${intentIds.length} referenced sub-goal ID(s) did not match completed_subtasks`
|
|
711
|
+
: "";
|
|
712
|
+
return makeVerificationFlag({
|
|
713
|
+
severity: "warn",
|
|
714
|
+
field: "output_summary",
|
|
715
|
+
check: CHECK_INTENT_DRIFT,
|
|
716
|
+
detail: `Declared intent was "${intent.slice(0, 120)}" ` +
|
|
717
|
+
`but actual output "${actual.slice(0, 120)}" — ` +
|
|
718
|
+
`similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
|
|
719
|
+
idNote,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/** Best-effort parse of test counts from common test-runner output formats.
|
|
723
|
+
*
|
|
724
|
+
* Scans the last 2000 characters of stdout (where summary lines typically
|
|
725
|
+
* appear) and tries patterns in descending specificity order. Returns null
|
|
726
|
+
* when the output format is unrecognized, stdout is empty, or a confident
|
|
727
|
+
* parse cannot be made.
|
|
728
|
+
*
|
|
729
|
+
* Recognized formats: Jest verbose/compact, Mocha, pytest/unittest, Go test,
|
|
730
|
+
* and PHPUnit OK summaries. */
|
|
731
|
+
export function parseTestOutput(stdout) {
|
|
732
|
+
if (!stdout)
|
|
733
|
+
return null;
|
|
734
|
+
const tail = stdout.length > 2000 ? stdout.slice(-2000) : stdout;
|
|
735
|
+
let match;
|
|
736
|
+
// 1. Jest verbose: "Tests: 1 failed, 8 passed, 9 total"
|
|
737
|
+
match = tail.match(/Tests:\s*(\d+)\s+failed,\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
|
|
738
|
+
if (match) {
|
|
739
|
+
return { failed: Number(match[1]), passed: Number(match[2]), total: Number(match[3]), skipped: 0 };
|
|
740
|
+
}
|
|
741
|
+
// 2. Jest compact: "Tests: 8 passed, 9 total"
|
|
742
|
+
match = tail.match(/Tests:\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
|
|
743
|
+
if (match) {
|
|
744
|
+
const passed = Number(match[1]);
|
|
745
|
+
const total = Number(match[2]);
|
|
746
|
+
return { passed, failed: total - passed, total, skipped: 0 };
|
|
747
|
+
}
|
|
748
|
+
// 3. Mocha: "8 passing" + optional "2 failing"
|
|
749
|
+
const mochaPass = tail.match(/(\d+)\s+passing/i);
|
|
750
|
+
if (mochaPass) {
|
|
751
|
+
const mochaFail = tail.match(/(\d+)\s+failing/i);
|
|
752
|
+
const passed = Number(mochaPass[1]);
|
|
753
|
+
const failed = mochaFail ? Number(mochaFail[1]) : 0;
|
|
754
|
+
return { passed, failed, total: passed + failed, skipped: 0 };
|
|
755
|
+
}
|
|
756
|
+
// 4. Go test: count "--- PASS:" and "--- FAIL:" lines
|
|
757
|
+
const goPassCount = (tail.match(/---\s+PASS:/g) || []).length;
|
|
758
|
+
const goFailCount = (tail.match(/---\s+FAIL:/g) || []).length;
|
|
759
|
+
if (goPassCount > 0 || goFailCount > 0) {
|
|
760
|
+
return { passed: goPassCount, failed: goFailCount, total: goPassCount + goFailCount, skipped: 0 };
|
|
761
|
+
}
|
|
762
|
+
// 5. pytest / unittest: "8 passed, 1 failed" or "8 passed"
|
|
763
|
+
match = tail.match(/(\d+)\s+passed[,;\s]+(\d+)\s+failed/i);
|
|
764
|
+
if (match) {
|
|
765
|
+
return { passed: Number(match[1]), failed: Number(match[2]), total: Number(match[1]) + Number(match[2]), skipped: 0 };
|
|
766
|
+
}
|
|
767
|
+
// pytest compact: "8 passed" (with a reasonable context hint)
|
|
768
|
+
match = tail.match(/(?:=\s+test session|\b(?:passed|failed)\b)/i);
|
|
769
|
+
if (match) {
|
|
770
|
+
const pCount = tail.match(/(\d+)\s+passed/i);
|
|
771
|
+
const fCount = tail.match(/(\d+)\s+failed/i);
|
|
772
|
+
if (pCount || fCount) {
|
|
773
|
+
const passed = pCount ? Number(pCount[1]) : 0;
|
|
774
|
+
const failed = fCount ? Number(fCount[1]) : 0;
|
|
775
|
+
return { passed, failed, total: passed + failed, skipped: 0 };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// 6. PHPUnit: "OK (8 tests, 16 assertions)"
|
|
779
|
+
match = tail.match(/OK\s*\((\d+)\s+tests?/i);
|
|
780
|
+
if (match) {
|
|
781
|
+
const total = Number(match[1]);
|
|
782
|
+
return { passed: total, failed: 0, total, skipped: 0 };
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
/** Cross-validate agent-reported test_results against command evidence output.
|
|
787
|
+
*
|
|
788
|
+
* For each command provider whose status is "passed" and output is not
|
|
789
|
+
* truncated, attempts to parse test counts from stdout. Mismatched counts
|
|
790
|
+
* produce a warn-level flag; failures hidden by the agent (reported 0 failed
|
|
791
|
+
* when the command shows >0) produce an error-level flag.
|
|
792
|
+
*
|
|
793
|
+
* Non-command evidence providers are skipped (no structural cross-check is
|
|
794
|
+
* defined for them yet). */
|
|
795
|
+
function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
796
|
+
const reported = selfEval.execution_evidence?.test_results;
|
|
797
|
+
if (!reported)
|
|
798
|
+
return null;
|
|
799
|
+
for (const snapshot of evidenceSnapshots) {
|
|
800
|
+
if (snapshot.data.kind !== "command")
|
|
801
|
+
continue;
|
|
802
|
+
if (snapshot.data.status !== "passed")
|
|
803
|
+
continue;
|
|
804
|
+
if (snapshot.data.truncated === true)
|
|
805
|
+
continue;
|
|
806
|
+
const stdout = typeof snapshot.data.stdout === "string" ? snapshot.data.stdout : "";
|
|
807
|
+
const parsed = parseTestOutput(stdout);
|
|
808
|
+
if (!parsed)
|
|
809
|
+
continue;
|
|
810
|
+
const mismatches = [];
|
|
811
|
+
if (parsed.passed !== reported.passed) {
|
|
812
|
+
mismatches.push(`passed: reported ${reported.passed}, command shows ${parsed.passed}`);
|
|
813
|
+
}
|
|
814
|
+
if (parsed.failed !== reported.failed) {
|
|
815
|
+
mismatches.push(`failed: reported ${reported.failed}, command shows ${parsed.failed}`);
|
|
816
|
+
}
|
|
817
|
+
if (mismatches.length === 0) {
|
|
818
|
+
// Counts match exactly — evidence supports the agent's claim.
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const name = typeof snapshot.data.commandName === "string"
|
|
822
|
+
? snapshot.data.commandName
|
|
823
|
+
: snapshot.provider;
|
|
824
|
+
// Agent hides failures → error. Other mismatch → warn.
|
|
825
|
+
const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
|
|
826
|
+
return makeVerificationFlag({
|
|
827
|
+
severity,
|
|
828
|
+
field: "test_results",
|
|
829
|
+
check: CHECK_COMMAND_EVIDENCE_MISMATCH,
|
|
830
|
+
detail: `Agent test_results don't match "${name}" output: ${mismatches.join("; ")}`,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
/** v3.3: Verification domain integrity — command entrypoints and test files
|
|
836
|
+
* changed in the same round the command ran. Entrypoint modification
|
|
837
|
+
* invalidates the machine observation (error: the runtime executed a script
|
|
838
|
+
* the agent just rewrote). Test-file changes are normal dev activity (warn:
|
|
839
|
+
* confirm the tests still verify the task claims). Fail-open when the git
|
|
840
|
+
* snapshot is missing or the command has no resolvable entrypoint. */
|
|
841
|
+
function checkVerificationDomainIntegrity(evidenceSnapshots) {
|
|
842
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
843
|
+
if (!git)
|
|
844
|
+
return null;
|
|
845
|
+
for (const snapshot of evidenceSnapshots) {
|
|
846
|
+
if (!isRecord(snapshot.data) || snapshot.data.kind !== "command")
|
|
847
|
+
continue;
|
|
848
|
+
if (snapshot.data.phase !== "after")
|
|
849
|
+
continue;
|
|
850
|
+
const { entrypointModified, testFilesModified } = commandTampered(snapshot, git);
|
|
851
|
+
if (!entrypointModified && !testFilesModified)
|
|
852
|
+
continue;
|
|
853
|
+
const name = typeof snapshot.data.commandName === "string"
|
|
854
|
+
? snapshot.data.commandName
|
|
855
|
+
: snapshot.provider;
|
|
856
|
+
if (entrypointModified) {
|
|
857
|
+
return makeVerificationFlag({
|
|
858
|
+
severity: "error",
|
|
859
|
+
field: "execution_evidence",
|
|
860
|
+
check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
|
|
861
|
+
detail: `Verification command "${name}" entrypoint changed this round — ` +
|
|
862
|
+
"its result cannot be trusted as machine evidence. Keep the " +
|
|
863
|
+
"verification command stable or resubmit the same round with the " +
|
|
864
|
+
"entrypoint unchanged.",
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return makeVerificationFlag({
|
|
868
|
+
severity: "warn",
|
|
869
|
+
field: "execution_evidence",
|
|
870
|
+
check: CHECK_TEST_FILES_MODIFIED,
|
|
871
|
+
detail: `Test files changed in the same round command "${name}" passed — ` +
|
|
872
|
+
"confirm the tests still verify the task claims.",
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
/** v2.2: Check if the agent's declared next_action aligns with any pending
|
|
878
|
+
* or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
|
|
879
|
+
* plans work unrelated to any of them, it may indicate task drift.
|
|
880
|
+
* This is a warn-level advisory — the agent always owns prioritization.
|
|
881
|
+
*
|
|
882
|
+
* v2.14: Exact sub-goal ID alignment first — next_action naming a pending
|
|
883
|
+
* sub-goal by its stable ID (sg-XXXXXXXX) is aligned by definition.
|
|
884
|
+
* Jaccard description similarity remains as the fallback. */
|
|
885
|
+
function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
886
|
+
const nextAction = selfEval.next_action?.trim();
|
|
887
|
+
if (!nextAction)
|
|
888
|
+
return null;
|
|
889
|
+
// Reconstruct pending sub-goal descriptions from vault entries
|
|
890
|
+
const pendingSubGoals = new Set();
|
|
891
|
+
const completedOrCanceled = new Set();
|
|
892
|
+
for (const entry of vaultEntries) {
|
|
893
|
+
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
894
|
+
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
895
|
+
: [];
|
|
896
|
+
for (const desc of emerged) {
|
|
897
|
+
pendingSubGoals.add(desc.trim());
|
|
898
|
+
}
|
|
899
|
+
const done = Array.isArray(entry.completed_subtasks)
|
|
900
|
+
? entry.completed_subtasks.filter((v) => typeof v === "string")
|
|
901
|
+
: [];
|
|
902
|
+
for (const desc of done) {
|
|
903
|
+
completedOrCanceled.add(desc.trim());
|
|
904
|
+
}
|
|
905
|
+
const canceled = Array.isArray(entry.canceled_subtasks)
|
|
906
|
+
? entry.canceled_subtasks.filter((v) => typeof v === "string")
|
|
907
|
+
: [];
|
|
908
|
+
for (const desc of canceled) {
|
|
909
|
+
completedOrCanceled.add(desc.trim());
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Also process current selfEval
|
|
913
|
+
for (const desc of selfEval.emerged_subtasks ?? []) {
|
|
914
|
+
pendingSubGoals.add(desc.trim());
|
|
915
|
+
}
|
|
916
|
+
for (const desc of selfEval.completed_subtasks ?? []) {
|
|
917
|
+
completedOrCanceled.add(desc.trim());
|
|
918
|
+
}
|
|
919
|
+
for (const desc of selfEval.canceled_subtasks ?? []) {
|
|
920
|
+
completedOrCanceled.add(desc.trim());
|
|
921
|
+
}
|
|
922
|
+
// Remove completed/canceled from pending
|
|
923
|
+
for (const desc of completedOrCanceled) {
|
|
924
|
+
pendingSubGoals.delete(desc);
|
|
925
|
+
}
|
|
926
|
+
// Need at least 3 pending sub-goals for the check to be meaningful
|
|
927
|
+
if (pendingSubGoals.size < 3)
|
|
928
|
+
return null;
|
|
929
|
+
// v2.14: Stable ID match first — next_action naming a pending sub-goal
|
|
930
|
+
// by ID is aligned by definition, regardless of description wording.
|
|
931
|
+
const nextActionIds = extractSubGoalIds(nextAction);
|
|
932
|
+
if (nextActionIds.length > 0) {
|
|
933
|
+
const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
|
|
934
|
+
if (nextActionIds.some((id) => pendingIds.has(id)))
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
// Check if next_action aligns with any pending sub-goal
|
|
938
|
+
let aligns = false;
|
|
939
|
+
for (const sg of pendingSubGoals) {
|
|
940
|
+
const score = jaccardSimilarity(nextAction, sg);
|
|
941
|
+
if (score >= getPolicy().evolution.subgoal_drift_alignment_threshold) {
|
|
942
|
+
aligns = true;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!aligns) {
|
|
947
|
+
const sample = [...pendingSubGoals].slice(0, 3).join(", ");
|
|
948
|
+
return makeVerificationFlag({
|
|
949
|
+
severity: "warn",
|
|
950
|
+
field: "next_action",
|
|
951
|
+
check: CHECK_SUBGOAL_DRIFT,
|
|
952
|
+
detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
|
|
953
|
+
`Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
|
|
954
|
+
`or cancel outdated sub-goals via canceled_subtasks.`,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
/** v2.13: After a backtrack, check that the agent restored the workspace
|
|
960
|
+
* before working. If the agent's files_changed overlaps with files from
|
|
961
|
+
* skipped (failed) rounds, the workspace was likely not clean. */
|
|
962
|
+
function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
963
|
+
if (backtrackSkippedFiles.length === 0)
|
|
964
|
+
return null;
|
|
965
|
+
const ev = selfEval.execution_evidence;
|
|
966
|
+
if (!ev || ev.files_changed.length === 0)
|
|
967
|
+
return null;
|
|
968
|
+
// Check overlap: files the agent changed vs. files from skipped rounds
|
|
969
|
+
const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
|
|
970
|
+
if (overlap.length === 0)
|
|
971
|
+
return null;
|
|
972
|
+
// Agent is modifying files that were part of failed rounds without
|
|
973
|
+
// having properly restored the workspace first.
|
|
974
|
+
if (overlap.length >= 3 || overlap.length === ev.files_changed.length) {
|
|
975
|
+
return makeVerificationFlag({
|
|
976
|
+
severity: "error",
|
|
977
|
+
field: "files_changed",
|
|
978
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
979
|
+
detail: `Agent modified ${overlap.length} file(s) that were part of ` +
|
|
980
|
+
`the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
|
|
981
|
+
`The workspace was likely not restored before working.`,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
// Minor overlap — warn only
|
|
985
|
+
return makeVerificationFlag({
|
|
986
|
+
severity: "warn",
|
|
987
|
+
field: "files_changed",
|
|
988
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
989
|
+
detail: `Agent modified files that overlap with skipped rounds: ` +
|
|
990
|
+
`${overlap.join(", ")}. Verify the workspace was restored.`,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/** v2.12: After a backtrack, the working tree must return to the restore
|
|
994
|
+
* point's git HEAD. Compares the first 12 characters (short hash), matching
|
|
995
|
+
* how git itself disambiguates. */
|
|
996
|
+
function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
|
|
997
|
+
if (!backtrackTargetGitHead)
|
|
998
|
+
return null;
|
|
999
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
|
|
1000
|
+
typeof snapshot.data.head === "string");
|
|
1001
|
+
if (!git)
|
|
1002
|
+
return null; // git unavailable → cannot verify → skip (fail-open)
|
|
1003
|
+
const currentHead = git.data.head;
|
|
1004
|
+
if (currentHead.length === 0)
|
|
1005
|
+
return null;
|
|
1006
|
+
if (currentHead.slice(0, 12) === backtrackTargetGitHead.slice(0, 12))
|
|
1007
|
+
return null;
|
|
1008
|
+
return makeVerificationFlag({
|
|
1009
|
+
severity: "error",
|
|
1010
|
+
field: "workspace",
|
|
1011
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
1012
|
+
detail: `Workspace HEAD (${currentHead.slice(0, 12)}) does not match the ` +
|
|
1013
|
+
`backtrack restore commit (${backtrackTargetGitHead.slice(0, 12)}). ` +
|
|
1014
|
+
`Restore the working tree before continuing.`,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1018
|
+
// v3.3 — Round Contract checks (all silent when no contract is declared)
|
|
1019
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1020
|
+
/** A PROPOSAL that promises nothing is a wasted round's worth of trust:
|
|
1021
|
+
* done_when empty means the boundary can never be machine-checked.
|
|
1022
|
+
* v3.4: declaration-quality check on the submission's own proposal — never
|
|
1023
|
+
* on the (derived) active contract. */
|
|
1024
|
+
function checkRoundUnderspecified(selfEval) {
|
|
1025
|
+
const contract = selfEval.round_contract;
|
|
1026
|
+
if (!contract)
|
|
1027
|
+
return null;
|
|
1028
|
+
if (contract.done_when.length > 0)
|
|
1029
|
+
return null;
|
|
1030
|
+
return makeVerificationFlag({
|
|
1031
|
+
severity: "warn",
|
|
1032
|
+
field: "round_contract",
|
|
1033
|
+
check: CHECK_ROUND_UNDERSPECIFIED,
|
|
1034
|
+
detail: "Round Contract declared with an empty done_when — nothing is " +
|
|
1035
|
+
"promised, so completion cannot be verified. Declare what must be " +
|
|
1036
|
+
"true when the round is done.",
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
/** done_when items whose verification_plan cannot run (empty plan, or names
|
|
1040
|
+
* outside the configured, enabled evidence.commands) can never be
|
|
1041
|
+
* machine-checked at the boundary. v3.4: declaration-quality check on the
|
|
1042
|
+
* submission's own proposal. */
|
|
1043
|
+
function checkRoundUnverifiable(selfEval) {
|
|
1044
|
+
const contract = selfEval.round_contract;
|
|
1045
|
+
if (!contract)
|
|
1046
|
+
return null;
|
|
1047
|
+
if (contract.done_when.length === 0)
|
|
1048
|
+
return null;
|
|
1049
|
+
if (contract.verification_plan.length > 0 &&
|
|
1050
|
+
contract.verification_plan.every((name) => isConfiguredCommand(name))) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
const unknown = contract.verification_plan.filter((name) => !isConfiguredCommand(name));
|
|
1054
|
+
const detail = contract.verification_plan.length === 0
|
|
1055
|
+
? "done_when items declared but verification_plan is empty — no command " +
|
|
1056
|
+
"can back the completion claims"
|
|
1057
|
+
: `verification_plan names commands that are not configured and enabled ` +
|
|
1058
|
+
`in evidence.commands: ${unknown.slice(0, 3).join(", ")}`;
|
|
1059
|
+
return makeVerificationFlag({
|
|
1060
|
+
severity: "warn",
|
|
1061
|
+
field: "round_contract",
|
|
1062
|
+
check: CHECK_ROUND_UNVERIFIABLE,
|
|
1063
|
+
detail: detail + ". Name only configured, enabled evidence.commands, or drop " +
|
|
1064
|
+
"done_when items that cannot be machine-verified.",
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
/** The round changed files outside the ACTIVE contract's declared scope.
|
|
1068
|
+
* Git diff files are the machine-authoritative "what actually changed" set.
|
|
1069
|
+
* v3.4: targets the contract the round actually executed under — derived
|
|
1070
|
+
* from committed evals, never the submission's own proposal. Silent when
|
|
1071
|
+
* no active contract exists (round 1 and whole-task rounds: the agent
|
|
1072
|
+
* executed the original task, not a contract boundary). */
|
|
1073
|
+
function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
|
|
1074
|
+
if (!activeContract)
|
|
1075
|
+
return null;
|
|
1076
|
+
if (activeContract.scope.length === 0)
|
|
1077
|
+
return null;
|
|
1078
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git");
|
|
1079
|
+
if (!git)
|
|
1080
|
+
return null; // git unavailable → cannot observe → fail open
|
|
1081
|
+
const outOfScope = collectOutOfScopeFiles(git.files, activeContract.scope);
|
|
1082
|
+
if (outOfScope.length === 0)
|
|
1083
|
+
return null;
|
|
1084
|
+
return makeVerificationFlag({
|
|
1085
|
+
severity: "warn",
|
|
1086
|
+
field: "round_contract",
|
|
1087
|
+
check: CHECK_ROUND_SCOPE_DRIFT,
|
|
1088
|
+
detail: `Files changed outside the active contract's declared scope` +
|
|
1089
|
+
(outOfScope.length > 5
|
|
1090
|
+
? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
|
|
1091
|
+
: `: ${outOfScope.join(", ")}`) +
|
|
1092
|
+
`. Revert them or extend the contract scope and explain in drift_clarification.`,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
/** success claimed under the ACTIVE contract whose done_when items were not
|
|
1096
|
+
* honestly and verifiably completed: claimed met with no machine-verified
|
|
1097
|
+
* evidence this round, or silently dropped (present in neither
|
|
1098
|
+
* success_criteria_met nor success_criteria_remaining).
|
|
1099
|
+
* v3.4: targets the derived ACTIVE contract (the round's real boundary) —
|
|
1100
|
+
* a proposal declared on a round without an active contract is never
|
|
1101
|
+
* checked for conformance against work the round did not do under it.
|
|
1102
|
+
* v3.5.1: a fully-met posture (every done_when claimed satisfied) is owned
|
|
1103
|
+
* by contract_completion_unverified — this check stays for mixed and
|
|
1104
|
+
* silently-dropped success claims only.
|
|
1105
|
+
* v3.6: no_change_reason never downgrades this check — the escape hatch
|
|
1106
|
+
* belongs to the R8 success-claim family alone; a declared "no change"
|
|
1107
|
+
* contradicts silently dropping contract done_when items. */
|
|
1108
|
+
function checkPrematureBoundary(activeContract, selfEval, claimView) {
|
|
1109
|
+
if (!activeContract || activeContract.done_when.length === 0)
|
|
1110
|
+
return null;
|
|
1111
|
+
if (!effectiveSuccess(selfEval))
|
|
1112
|
+
return null;
|
|
1113
|
+
const ev = selfEval.execution_evidence;
|
|
1114
|
+
const met = ev?.success_criteria_met ?? [];
|
|
1115
|
+
// v3.5.1: full-met → the completion check owns the posture (it also fires
|
|
1116
|
+
// on success=false, so nothing is lost — and under machine_backed_success
|
|
1117
|
+
// "warn" the same tolerance applies on both sides).
|
|
1118
|
+
if (contractDoneWhenSatisfied(activeContract, met))
|
|
1119
|
+
return null;
|
|
1120
|
+
const remaining = ev?.success_criteria_remaining ?? [];
|
|
1121
|
+
const problems = [];
|
|
1122
|
+
for (const item of activeContract.done_when) {
|
|
1123
|
+
// Shared matcher with the lifecycle walker (round-contract.ts) — the
|
|
1124
|
+
// compile-side closure decision and this conformance check can never
|
|
1125
|
+
// disagree on what counts as a satisfied item.
|
|
1126
|
+
const isMet = met.some((m) => contractItemMatches(item, m));
|
|
1127
|
+
const isRemaining = remaining.some((r) => contractItemMatches(item, r));
|
|
1128
|
+
if (isMet) {
|
|
1129
|
+
// Claimed met — needs round-level machine verification. The v3.3
|
|
1130
|
+
// claim model is round-uniform: any verified claim backs the round,
|
|
1131
|
+
// so zero verified claims = none of them are backed.
|
|
1132
|
+
if (claimView.verifiedCount === 0) {
|
|
1133
|
+
problems.push(`"${item.slice(0, 80)}" claimed met without machine-verified evidence`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else if (!isRemaining) {
|
|
1137
|
+
// Neither met nor remaining — silently dropped while success is
|
|
1138
|
+
// claimed. Honest reporting would list it in remaining (R1's domain).
|
|
1139
|
+
problems.push(`"${item.slice(0, 80)}" neither met nor listed as remaining`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (problems.length === 0)
|
|
1143
|
+
return null;
|
|
1144
|
+
return makeVerificationFlag({
|
|
1145
|
+
severity: "error",
|
|
1146
|
+
field: "round_contract",
|
|
1147
|
+
check: CHECK_PREMATURE_BOUNDARY,
|
|
1148
|
+
detail: `Round Contract boundary claimed prematurely: ${problems.slice(0, 3).join("; ")}. ` +
|
|
1149
|
+
`Run the contract's verification_plan commands and report real output, ` +
|
|
1150
|
+
`or set success=false and list the items in success_criteria_remaining.`,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
/** v3.5: Closing a Round Contract is a success-class claim and must be
|
|
1154
|
+
* machine-backed. When the eval's met claims satisfy EVERY done_when of
|
|
1155
|
+
* the ACTIVE contract (the walker's close condition), each verification_plan
|
|
1156
|
+
* command must have been observed passing (after-phase command snapshot,
|
|
1157
|
+
* untampered) in the same round. Fires regardless of the success flag —
|
|
1158
|
+
* a claim-based closure that the machine cannot back must not advance the
|
|
1159
|
+
* contract state machine. Severity follows evidence.machine_backed_success
|
|
1160
|
+
* (the R8 tolerance switch). Deliberately no no_change_reason downgrade:
|
|
1161
|
+
* all done_when met contradicts "no change". Fail-open: a plan name that
|
|
1162
|
+
* is no longer a configured, enabled command cannot be observed and is
|
|
1163
|
+
* not required. The check observes that the commands ran, not what they
|
|
1164
|
+
* verified — R-C1's claim model remains the content bound. */
|
|
1165
|
+
function checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots) {
|
|
1166
|
+
if (!activeContract || activeContract.verification_plan.length === 0)
|
|
1167
|
+
return null;
|
|
1168
|
+
const met = selfEval.execution_evidence?.success_criteria_met ?? [];
|
|
1169
|
+
if (!contractDoneWhenSatisfied(activeContract, met))
|
|
1170
|
+
return null; // walker does not close
|
|
1171
|
+
const required = activeContract.verification_plan
|
|
1172
|
+
.filter((name) => isConfiguredCommand(name));
|
|
1173
|
+
if (required.length === 0)
|
|
1174
|
+
return null; // fail open — cannot observe
|
|
1175
|
+
const passed = passedPlanCommandNames(evidenceSnapshots);
|
|
1176
|
+
const missing = required.filter((name) => !passed.has(name));
|
|
1177
|
+
if (missing.length === 0)
|
|
1178
|
+
return null;
|
|
1179
|
+
const severity = getPolicy().evidence.machine_backed_success === "warn"
|
|
1180
|
+
? "warn"
|
|
1181
|
+
: "error";
|
|
1182
|
+
return makeVerificationFlag({
|
|
1183
|
+
severity,
|
|
1184
|
+
field: "round_contract",
|
|
1185
|
+
check: CHECK_CONTRACT_COMPLETION_UNVERIFIED,
|
|
1186
|
+
detail: `Round Contract completion claimed (every done_when met), but verification_plan ` +
|
|
1187
|
+
`command${missing.length > 1 ? "s" : ""} did not pass this round: ` +
|
|
1188
|
+
`${missing.slice(0, 3).join(", ")}. Closing a contract is a success-class claim — ` +
|
|
1189
|
+
`fix the underlying failure so the command${missing.length > 1 ? "s" : ""} pass and ` +
|
|
1190
|
+
`resubmit; no_change_reason does not apply to completion claims.`,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
/** v3.5: A different contract proposed while the ACTIVE contract is still
|
|
1194
|
+
* open. The walker ignores the premature proposal — this warn only makes
|
|
1195
|
+
* the ignored state visible to the agent. Silent when the eval closes the
|
|
1196
|
+
* active contract (all done_when met, or outcome=blocked — checked FIRST,
|
|
1197
|
+
* mirroring the walker), when no proposal is submitted, or when the
|
|
1198
|
+
* proposal equals the active contract (a restate). Equality is
|
|
1199
|
+
* key-order-insensitive and normalized through parseRoundContract on both
|
|
1200
|
+
* sides so committed-raw vs parsed-submission key-set drift cannot cause
|
|
1201
|
+
* spurious warns. */
|
|
1202
|
+
function checkContractPremature(activeContract, selfEval) {
|
|
1203
|
+
if (!activeContract)
|
|
1204
|
+
return null;
|
|
1205
|
+
const met = selfEval.execution_evidence?.success_criteria_met ?? [];
|
|
1206
|
+
if (contractDoneWhenSatisfied(activeContract, met))
|
|
1207
|
+
return null; // closed
|
|
1208
|
+
if (selfEval.outcome === "blocked")
|
|
1209
|
+
return null; // closed
|
|
1210
|
+
const proposal = selfEval.round_contract;
|
|
1211
|
+
if (!proposal)
|
|
1212
|
+
return null;
|
|
1213
|
+
const same = stableStringify(parseRoundContract(activeContract)) ===
|
|
1214
|
+
stableStringify(parseRoundContract(proposal));
|
|
1215
|
+
if (same)
|
|
1216
|
+
return null; // restate → continue
|
|
1217
|
+
return makeVerificationFlag({
|
|
1218
|
+
severity: "warn",
|
|
1219
|
+
field: "round_contract",
|
|
1220
|
+
check: CHECK_CONTRACT_PREMATURE,
|
|
1221
|
+
detail: "The ACTIVE Round Contract is still open (not all done_when met, not blocked) " +
|
|
1222
|
+
"while a different contract was proposed — it is ignored until the active " +
|
|
1223
|
+
"contract is completed or blocked. Restate the active contract unchanged to " +
|
|
1224
|
+
"continue it.",
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1228
|
+
// Main entry point
|
|
1229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1230
|
+
/** Verify a SelfEvaluation against the loop's cross-round lineage.
|
|
1231
|
+
*
|
|
1232
|
+
* @param selfEval The agent's self-evaluation for the current round.
|
|
1233
|
+
* @param currentRound The current round number (1-based).
|
|
1234
|
+
* @param vaultEntries Vault entries for this loop (committed lineage
|
|
1235
|
+
* entries AND :feedback entries — the feedback
|
|
1236
|
+
* entries feed the ACTIVE-contract derivation).
|
|
1237
|
+
* @param prevSelfEval The agent's self-evaluation from the previous round
|
|
1238
|
+
* (null for round 1).
|
|
1239
|
+
* @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
|
|
1240
|
+
* Used by checkEvidenceIntegrity for multi-provider
|
|
1241
|
+
* cross-validation. Defaults to empty array. */
|
|
1242
|
+
export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
|
|
1243
|
+
/** v2.13: Files from skipped backtrack rounds. If the agent's
|
|
1244
|
+
* files_changed overlaps significantly with these, the workspace
|
|
1245
|
+
* was not properly restored before working. */
|
|
1246
|
+
backtrackSkippedFiles = [],
|
|
1247
|
+
/** v2.12: Git HEAD commit of the backtrack restore point. When set, the
|
|
1248
|
+
* current git snapshot must sit at this commit — otherwise the workspace
|
|
1249
|
+
* was not restored and the round cannot be accepted. */
|
|
1250
|
+
backtrackTargetGitHead) {
|
|
1251
|
+
const flags = [];
|
|
1252
|
+
// Collect violations from all previous vault entries for duplicate-discovery
|
|
1253
|
+
// and other checks that need deeper history.
|
|
1254
|
+
const olderViolations = [];
|
|
1255
|
+
for (const entry of vaultEntries) {
|
|
1256
|
+
for (const v of entryViolations(entry))
|
|
1257
|
+
olderViolations.push(v);
|
|
1258
|
+
}
|
|
1259
|
+
// v3.4: The ACTIVE contract this round executed under — derived from the
|
|
1260
|
+
// committed :feedback evals of earlier rounds. The submission's own
|
|
1261
|
+
// round_contract is a PROPOSAL for the next round and never participates
|
|
1262
|
+
// (the eval under verification is not yet committed, so it structurally
|
|
1263
|
+
// cannot influence the derivation).
|
|
1264
|
+
const activeContract = deriveActiveRoundContract(contractRoundEvaluations(committedRoundsFromEntries(vaultEntries, currentRound)));
|
|
1265
|
+
// v2.12: Derived claim provenance — which reported criteria completions
|
|
1266
|
+
// are backed by machine evidence (pure derivation, not persisted).
|
|
1267
|
+
const claimView = deriveClaimView(selfEval, evidenceSnapshots);
|
|
1268
|
+
// v3.2: Runtime machine-verification status (git observed / command passed /
|
|
1269
|
+
// absent). v3.6: feeds the merged R8 check (providerStatus lens) that
|
|
1270
|
+
// never self-skips.
|
|
1271
|
+
const evidenceStatus = deriveEvidenceStatus(selfEval, evidenceSnapshots);
|
|
1272
|
+
// Run all checks
|
|
1273
|
+
// v3.3.1: a check may return several flags (checkRetroactiveClaims raises
|
|
1274
|
+
// one per offending claim) — the previous single-flag contract truncated
|
|
1275
|
+
// the list at the first problem, hiding every other bad retroactive claim
|
|
1276
|
+
// from the agent until the next round.
|
|
1277
|
+
const checks = [
|
|
1278
|
+
() => checkProgressRegression(selfEval, prevSelfEval),
|
|
1279
|
+
() => checkEmptyChangeWithPassing(selfEval),
|
|
1280
|
+
() => checkSuccessWithRemainingCriteria(selfEval),
|
|
1281
|
+
// v2.12/v3.6: success without a machine-verified observation — the merged
|
|
1282
|
+
// success_unverified semantics (runtime providerStatus lens, tamper-aware)
|
|
1283
|
+
() => checkSuccessWithoutVerifiedEvidence(selfEval, evidenceStatus),
|
|
1284
|
+
// v2.12: criteria met but none machine-verified (mild criterion-specific rule)
|
|
1285
|
+
() => checkUnverifiedCriteriaClaims(selfEval, claimView),
|
|
1286
|
+
// Declared outcome vs the required core success flag.
|
|
1287
|
+
() => checkOutcomeConsistency(selfEval),
|
|
1288
|
+
// v2.12: retroactive claims against prior rounds
|
|
1289
|
+
() => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
|
|
1290
|
+
() => checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations),
|
|
1291
|
+
() => checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
1292
|
+
() => checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound),
|
|
1293
|
+
// v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
1294
|
+
() => checkEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
1295
|
+
() => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
|
|
1296
|
+
// v2.0: Cross-validate agent-reported test_results against command output
|
|
1297
|
+
() => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
1298
|
+
// v3.3: Verification domain integrity — command entrypoint / test files
|
|
1299
|
+
// changed in the same round the command ran
|
|
1300
|
+
() => checkVerificationDomainIntegrity(evidenceSnapshots),
|
|
1301
|
+
// v2.1: Detect intent-action drift — agent said X but did Y
|
|
1302
|
+
() => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
|
|
1303
|
+
// v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
|
|
1304
|
+
() => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
1305
|
+
// v2.13: Post-backtrack workspace restore check
|
|
1306
|
+
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
|
|
1307
|
+
// v2.12: Post-backtrack git HEAD restore check
|
|
1308
|
+
() => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
|
|
1309
|
+
// v3.3: Round Contract checks. v3.4: split by target — structural
|
|
1310
|
+
// (warn) at proposal declaration, conformance against the derived
|
|
1311
|
+
// ACTIVE contract (scope_drift warn / premature_boundary error).
|
|
1312
|
+
// v3.5: + completion machine-backing (error) and premature-replacement
|
|
1313
|
+
// visibility (warn).
|
|
1314
|
+
() => checkRoundUnderspecified(selfEval),
|
|
1315
|
+
() => checkRoundUnverifiable(selfEval),
|
|
1316
|
+
() => checkRoundScopeDrift(activeContract, evidenceSnapshots),
|
|
1317
|
+
() => checkPrematureBoundary(activeContract, selfEval, claimView),
|
|
1318
|
+
() => checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots),
|
|
1319
|
+
() => checkContractPremature(activeContract, selfEval),
|
|
1320
|
+
];
|
|
1321
|
+
for (const run of checks) {
|
|
1322
|
+
const result = run();
|
|
1323
|
+
if (Array.isArray(result))
|
|
1324
|
+
flags.push(...result);
|
|
1325
|
+
else if (result)
|
|
1326
|
+
flags.push(result);
|
|
1327
|
+
}
|
|
1328
|
+
// Determine verdict from the most severe flag present
|
|
1329
|
+
const hasError = flags.some((f) => f.severity === "error");
|
|
1330
|
+
const hasWarn = flags.some((f) => f.severity === "warn");
|
|
1331
|
+
let verdict;
|
|
1332
|
+
if (hasError) {
|
|
1333
|
+
verdict = "contradicted";
|
|
1334
|
+
}
|
|
1335
|
+
else if (hasWarn) {
|
|
1336
|
+
verdict = "suspect";
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
verdict = "trusted";
|
|
1340
|
+
}
|
|
1341
|
+
return makeVerificationResult({ verdict, flags });
|
|
1342
|
+
}
|
|
1343
|
+
//# sourceMappingURL=verification-gate.js.map
|