@cassiomc1/forgeloop 0.1.10 → 0.1.12
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/.cursor/rules/project-loop.mdc +16 -18
- package/.github/copilot-instructions.md +16 -19
- package/AGENTS.md +18 -24
- package/AGENT_COMPATIBILITY.md +4 -211
- package/CLAUDE.md +16 -18
- package/LOOP_ENGINEERING.md +73 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +4 -0
- package/PROTOCOL_INTEGRATION.md +171 -0
- package/QUALITY_SCORECARD.md +1 -1
- package/README.md +123 -103
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +4 -0
- package/package.json +5 -4
- package/schemas/current-contract.schema.json +48 -2
- package/schemas/evidence.schema.json +1 -0
- package/schemas/execution-receipt.schema.json +2 -0
- package/schemas/work-state.schema.json +44 -1
- package/src/cli.js +48 -4
- package/src/commands/record-terminal-result.js +18 -0
- package/src/commands/validate-protocol.js +14 -2
- package/src/core/checks.js +20 -0
- package/src/core/completion-artifacts.js +266 -11
- package/src/core/completion-recovery.js +15 -0
- package/src/core/completion-relationships.js +23 -9
- package/src/core/completion.js +130 -2
- package/src/core/contract.js +82 -4
- package/src/core/coverage.js +17 -12
- package/src/core/discovery-surfaces.js +22 -0
- package/src/core/events.js +183 -2
- package/src/core/evidence-readiness.js +333 -0
- package/src/core/evidence.js +22 -4
- package/src/core/execution-prerequisites.js +6 -1
- package/src/core/inspect.js +23 -12
- package/src/core/native-adapters.js +30 -2
- package/src/core/next-action.js +103 -27
- package/src/core/phase.js +68 -5
- package/src/core/protocol.js +28 -1
- package/src/core/receipt.js +5 -0
- package/src/core/schema-validation.js +28 -6
- package/src/core/templates.js +1 -0
- package/src/core/work-state.js +27 -0
- package/conformance/README.md +0 -148
- package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
- package/conformance/backend-auth/REQUEST.md +0 -4
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/blind-premium-website/REQUEST.md +0 -6
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
- package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
- package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/complete-website/REQUEST.md +0 -6
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/complete-website/REQUIRED_GATES.json +0 -3
- package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
- package/conformance/docs-only/REQUEST.md +0 -4
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/docs-only/REQUIRED_GATES.json +0 -3
- package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
- package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
- package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
- package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
- package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
- package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
- package/conformance/simple-bug/REQUEST.md +0 -4
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
- package/src/core/agent-support.js +0 -89
package/src/core/completion.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
2
|
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
3
|
-
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
|
|
3
|
+
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
6
6
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
@@ -9,6 +9,8 @@ import { createReceipt, validateReceipt } from "./receipt.js";
|
|
|
9
9
|
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
10
10
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
11
11
|
import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
12
|
+
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
13
|
+
import { classifyRequirement, evaluateTerminalRequirements } from "./evidence-readiness.js";
|
|
12
14
|
|
|
13
15
|
function issue(code, message, artifacts = [], details = {}) {
|
|
14
16
|
return { code, message, artifacts, ...details };
|
|
@@ -27,6 +29,7 @@ function repairNext(error) {
|
|
|
27
29
|
case "E_RECEIPT_CONTRACT_MISMATCH":
|
|
28
30
|
case "E_RECEIPT_ROUTE_MISMATCH":
|
|
29
31
|
case "E_RECEIPT_STATE_MISMATCH":
|
|
32
|
+
case "E_RECEIPT_CYCLE_MISMATCH":
|
|
30
33
|
return "Run forgeloop prepare-completion, inspect the receipt, and validate it before retrying completion.";
|
|
31
34
|
case "E_PHASE_PREREQUISITE_MISSING":
|
|
32
35
|
return "Advance from EXECUTING through VERIFYING and REVIEWING, resolving the named prerequisite before completion.";
|
|
@@ -39,6 +42,36 @@ function repairNext(error) {
|
|
|
39
42
|
case "E_CHECK_INVALID":
|
|
40
43
|
case "E_CHECK_STATUS_CONTRADICTION":
|
|
41
44
|
return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
|
|
45
|
+
case "E_EVIDENCE_PARTIAL":
|
|
46
|
+
return "Run or finish the missing component checks and record observed evidence.";
|
|
47
|
+
case "E_EVIDENCE_INVALID":
|
|
48
|
+
return "Resolve the invalid evidence format or failure status with forgeloop record-check.";
|
|
49
|
+
case "E_FUTURE_LIFECYCLE_EVIDENCE":
|
|
50
|
+
return "Remove the premature terminal claim; the lifecycle event must satisfy it.";
|
|
51
|
+
case "E_CIRCULAR_COMPLETION_REQUIREMENT":
|
|
52
|
+
case "E_MIXED_TERMINAL_REQUIREMENT":
|
|
53
|
+
return "Split ordinary verification from terminal lifecycle criteria.";
|
|
54
|
+
case "E_PUBLICATION_REQUIREMENT_PENDING":
|
|
55
|
+
return "The contract explicitly requires publication. Record authoritative publication evidence or revise the contract if publication is not in scope.";
|
|
56
|
+
case "E_PRODUCTION_REQUIREMENT_PENDING":
|
|
57
|
+
return "The contract explicitly requires production readiness. Record authoritative deployment/readiness evidence before completion.";
|
|
58
|
+
case "E_TERMINAL_REQUIREMENT_PENDING":
|
|
59
|
+
return "The contract contains an unresolved terminal requirement. Ensure all terminal lifecycle criteria are satisfied.";
|
|
60
|
+
case "E_TERMINAL_REQUIREMENT_UNKNOWN":
|
|
61
|
+
return "Terminal results must reference an existing canonical requirement declared in the contract.";
|
|
62
|
+
case "E_TERMINAL_REQUIREMENT_NOT_TERMINAL":
|
|
63
|
+
return "Use forgeloop record-check for ordinary verification requirements.";
|
|
64
|
+
case "E_TERMINAL_REQUIREMENT_TYPE_MISMATCH":
|
|
65
|
+
return "Ensure the supplied terminal type matches the canonical requirement type.";
|
|
66
|
+
case "E_TERMINAL_STATUS_REGRESSION":
|
|
67
|
+
return "Terminal publication status cannot regress to a weaker state.";
|
|
68
|
+
case "E_STATE_LEDGER_DIVERGENCE":
|
|
69
|
+
return "Do not edit work-state or receipt manually; recover through supported lifecycle commands.";
|
|
70
|
+
case "E_COMPLETION_RECOVERY_UNAUTHORIZED":
|
|
71
|
+
case "E_COMPLETION_REJECTION_LEDGER_MISMATCH":
|
|
72
|
+
case "E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH":
|
|
73
|
+
case "E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH":
|
|
74
|
+
return "Ensure a matching completion rejection exists in the protocol ledger and artifacts remain unmodified before recovery.";
|
|
42
75
|
case "E_GATE_UNVERIFIED":
|
|
43
76
|
case "E_GATE_STALE":
|
|
44
77
|
return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
|
|
@@ -99,6 +132,9 @@ async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
|
99
132
|
}
|
|
100
133
|
const ledger = await validateEventLedger(target, packageRoot);
|
|
101
134
|
for (const error of ledger.errors) errors.push({ ...error, artifacts: [ARTIFACT_PATHS.events] });
|
|
135
|
+
for (const error of validateStateLedgerCoherence(state, ledger.events)) {
|
|
136
|
+
errors.push({ ...error, artifacts: [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events] });
|
|
137
|
+
}
|
|
102
138
|
const requiredEvents = LIFECYCLE_MILESTONES.slice(0, state?.phase === "COMPLETE" ? undefined : -1);
|
|
103
139
|
const observed = new Set(ledger.events.filter((event) => event.taskId === taskId).map((event) => event.event));
|
|
104
140
|
for (const event of requiredEvents) {
|
|
@@ -213,9 +249,29 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
213
249
|
}
|
|
214
250
|
}
|
|
215
251
|
|
|
216
|
-
const sortedErrors = sortIssues(errors);
|
|
217
252
|
const receiptValue = receipt?.value;
|
|
218
253
|
const publication = receiptValue ? publicationStatus(receiptValue) : "not-published";
|
|
254
|
+
|
|
255
|
+
if (contract) {
|
|
256
|
+
const allContractReqs = [
|
|
257
|
+
...(contract.value.verification ?? []),
|
|
258
|
+
...(contract.value.successCriteria ?? []),
|
|
259
|
+
];
|
|
260
|
+
const terminalEval = evaluateTerminalRequirements({
|
|
261
|
+
requirements: allContractReqs,
|
|
262
|
+
receipt: receiptValue,
|
|
263
|
+
});
|
|
264
|
+
for (const termErr of terminalEval.errors) {
|
|
265
|
+
errors.push(issue(
|
|
266
|
+
termErr.code,
|
|
267
|
+
termErr.message,
|
|
268
|
+
[ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt],
|
|
269
|
+
{ requirementId: termErr.requirementId },
|
|
270
|
+
));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const sortedErrors = sortIssues(errors);
|
|
219
275
|
const valid = sortedErrors.length === 0;
|
|
220
276
|
return {
|
|
221
277
|
status: valid ? "VALID" : "REJECTED",
|
|
@@ -236,6 +292,77 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
236
292
|
|
|
237
293
|
export async function runComplete({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
238
294
|
const result = await evaluateCompletion({ target, packageRoot, strict });
|
|
295
|
+
const rejectionCodes = [...new Set(result.errors.map((error) => error.code))].sort();
|
|
296
|
+
const evidenceOnlyRejection = rejectionCodes.length > 0
|
|
297
|
+
&& rejectionCodes.every(isRecoverableCompletionEvidenceCode);
|
|
298
|
+
if (persist && result.status === "REJECTED" && evidenceOnlyRejection) {
|
|
299
|
+
const state = await readWorkState(target, packageRoot);
|
|
300
|
+
if (state?.phase === "REVIEWING") {
|
|
301
|
+
const reasonCodes = rejectionCodes;
|
|
302
|
+
const missingRequirementIds = [...new Set(result.errors.map((error) => error.requirementId).filter(Boolean))].sort();
|
|
303
|
+
const currentCycle = state.verificationCycle ?? 1;
|
|
304
|
+
const alreadyRejected = state.lastCompletionAttempt?.status === "REJECTED"
|
|
305
|
+
&& (state.lastCompletionAttempt.verificationCycle ?? 1) === currentCycle
|
|
306
|
+
&& JSON.stringify([...(state.lastCompletionAttempt.reasonCodes ?? [])].sort()) === JSON.stringify(reasonCodes)
|
|
307
|
+
&& JSON.stringify([...(state.lastCompletionAttempt.missingRequirementIds ?? [])].sort()) === JSON.stringify(missingRequirementIds);
|
|
308
|
+
|
|
309
|
+
if (alreadyRejected) {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const now = new Date().toISOString();
|
|
314
|
+
const next = {
|
|
315
|
+
...state,
|
|
316
|
+
lastCompletionAttempt: {
|
|
317
|
+
status: "REJECTED",
|
|
318
|
+
reasonCodes,
|
|
319
|
+
missingRequirementIds,
|
|
320
|
+
verificationCycle: currentCycle,
|
|
321
|
+
timestamp: now,
|
|
322
|
+
},
|
|
323
|
+
lastUpdated: now,
|
|
324
|
+
};
|
|
325
|
+
let receipt = null;
|
|
326
|
+
try {
|
|
327
|
+
receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
328
|
+
} catch {
|
|
329
|
+
// The evaluator already reports a missing or invalid receipt; evidence-only rejection can persist without one.
|
|
330
|
+
}
|
|
331
|
+
await writeWorkState(target, next, { packageRoot });
|
|
332
|
+
let nextReceipt = null;
|
|
333
|
+
if (receipt) {
|
|
334
|
+
nextReceipt = await createReceipt({
|
|
335
|
+
...receipt.value,
|
|
336
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
337
|
+
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
338
|
+
}, packageRoot);
|
|
339
|
+
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
340
|
+
}
|
|
341
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
342
|
+
const lastRejection = ledger.events.filter((e) => e.taskId === state.taskId && e.event === "COMPLETION_REJECTED").at(-1);
|
|
343
|
+
const lastDetails = lastRejection?.details ?? {};
|
|
344
|
+
const isIdentical = lastRejection
|
|
345
|
+
&& (lastDetails.verificationCycle ?? 1) === currentCycle
|
|
346
|
+
&& JSON.stringify([...(lastDetails.reasonCodes ?? [])].sort()) === JSON.stringify(reasonCodes)
|
|
347
|
+
&& JSON.stringify([...(lastDetails.missingRequirementIds ?? [])].sort()) === JSON.stringify(missingRequirementIds)
|
|
348
|
+
&& lastDetails.stateFingerprint === canonicalFingerprint(next)
|
|
349
|
+
&& lastDetails.receiptFingerprint === (nextReceipt ? canonicalFingerprint(nextReceipt) : undefined);
|
|
350
|
+
|
|
351
|
+
if (!isIdentical) {
|
|
352
|
+
await appendProtocolEvent(target, {
|
|
353
|
+
taskId: state.taskId,
|
|
354
|
+
event: "COMPLETION_REJECTED",
|
|
355
|
+
details: {
|
|
356
|
+
verificationCycle: currentCycle,
|
|
357
|
+
reasonCodes,
|
|
358
|
+
missingRequirementIds,
|
|
359
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
360
|
+
...(nextReceipt ? { receiptFingerprint: canonicalFingerprint(nextReceipt) } : {}),
|
|
361
|
+
},
|
|
362
|
+
}, packageRoot);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
239
366
|
if (persist && result.status === "VALID") {
|
|
240
367
|
const state = await readWorkState(target, packageRoot);
|
|
241
368
|
if (state && state.phase !== "COMPLETE") {
|
|
@@ -252,6 +379,7 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
252
379
|
const nextReceipt = await createReceipt({
|
|
253
380
|
...receipt.value,
|
|
254
381
|
stateFingerprint: canonicalFingerprint(next),
|
|
382
|
+
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
255
383
|
}, packageRoot);
|
|
256
384
|
await writeWorkState(target, next, { packageRoot });
|
|
257
385
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
package/src/core/contract.js
CHANGED
|
@@ -2,26 +2,91 @@ import { PROTOCOL_VERSION } from "./protocol.js";
|
|
|
2
2
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
3
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
4
4
|
import { assertSecretFree } from "./receipt.js";
|
|
5
|
+
import { REQUIREMENT_TYPES, isMixedTerminalRequirement } from "./evidence-readiness.js";
|
|
5
6
|
|
|
6
7
|
export const CONTRACT_SCHEMA_VERSION = 1;
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
+
const CONTRACT_STRING_ARRAY_FIELDS = Object.freeze([
|
|
9
10
|
"deliverables",
|
|
10
11
|
"constraints",
|
|
11
12
|
"risks",
|
|
12
|
-
"verification",
|
|
13
|
-
"successCriteria",
|
|
14
13
|
"stopConditions",
|
|
15
14
|
"unresolvedDecisions",
|
|
16
15
|
"sourceRefs",
|
|
17
16
|
]);
|
|
18
17
|
|
|
18
|
+
const CONTRACT_REQUIREMENT_FIELDS = Object.freeze([
|
|
19
|
+
"verification",
|
|
20
|
+
"successCriteria",
|
|
21
|
+
]);
|
|
22
|
+
|
|
19
23
|
function assertStringArray(value, label) {
|
|
20
24
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
21
25
|
throw new Error(`${label} must be an array of non-empty strings`);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
function assertRequirementItem(item, label, seenIds) {
|
|
30
|
+
if (typeof item === "string") {
|
|
31
|
+
if (item.trim() === "") {
|
|
32
|
+
throw new Error(`${label} must be a non-empty string or requirement object`);
|
|
33
|
+
}
|
|
34
|
+
if (isMixedTerminalRequirement(item)) {
|
|
35
|
+
const err = new Error(`Mixed verification and lifecycle requirement detected in ${label}: ${item}`);
|
|
36
|
+
err.code = "E_CIRCULAR_COMPLETION_REQUIREMENT";
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
42
|
+
if (typeof item.text !== "string" || item.text.trim() === "") {
|
|
43
|
+
throw new Error(`${label}.text must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
if (isMixedTerminalRequirement(item.text)) {
|
|
46
|
+
const err = new Error(`Mixed verification and lifecycle requirement detected in ${label}: ${item.text}`);
|
|
47
|
+
err.code = "E_CIRCULAR_COMPLETION_REQUIREMENT";
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
if (item.id !== undefined) {
|
|
51
|
+
if (typeof item.id !== "string" || item.id.trim() === "") {
|
|
52
|
+
throw new Error(`${label}.id must be a non-empty string`);
|
|
53
|
+
}
|
|
54
|
+
if (seenIds.has(item.id)) {
|
|
55
|
+
throw new Error(`Duplicate requirement ID in contract: ${item.id}`);
|
|
56
|
+
}
|
|
57
|
+
seenIds.add(item.id);
|
|
58
|
+
}
|
|
59
|
+
if (item.type !== undefined && !REQUIREMENT_TYPES.includes(item.type)) {
|
|
60
|
+
throw new Error(`${label}.type must be a valid requirement type`);
|
|
61
|
+
}
|
|
62
|
+
if (item.operator !== undefined && !["SINGLE", "ALL"].includes(item.operator)) {
|
|
63
|
+
throw new Error(`${label}.operator must be SINGLE or ALL`);
|
|
64
|
+
}
|
|
65
|
+
if (item.requiredEvidenceKind !== undefined && !["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED", "HYPOTHESIS"].includes(item.requiredEvidenceKind)) {
|
|
66
|
+
throw new Error(`${label}.requiredEvidenceKind must be a valid evidence kind`);
|
|
67
|
+
}
|
|
68
|
+
if (item.requirements !== undefined) {
|
|
69
|
+
if (!Array.isArray(item.requirements)) {
|
|
70
|
+
throw new Error(`${label}.requirements must be an array`);
|
|
71
|
+
}
|
|
72
|
+
item.requirements.forEach((child, index) => {
|
|
73
|
+
assertRequirementItem(child, `${label}.requirements[${index}]`, seenIds);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`${label} must be a non-empty string or requirement object`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertRequirementArray(value, label, seenIds) {
|
|
82
|
+
if (!Array.isArray(value)) {
|
|
83
|
+
throw new Error(`${label} must be an array`);
|
|
84
|
+
}
|
|
85
|
+
value.forEach((item, index) => {
|
|
86
|
+
assertRequirementItem(item, `${label}[${index}]`, seenIds);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
25
90
|
const ASSUMPTION_FIELDS = Object.freeze([
|
|
26
91
|
"value",
|
|
27
92
|
"reason",
|
|
@@ -93,11 +158,17 @@ export function createContract(input = {}) {
|
|
|
93
158
|
assumptions: [],
|
|
94
159
|
};
|
|
95
160
|
assertAssumptions(input.assumptions ?? []);
|
|
96
|
-
|
|
161
|
+
const seenIds = new Set();
|
|
162
|
+
for (const field of CONTRACT_STRING_ARRAY_FIELDS) {
|
|
97
163
|
const value = input[field] ?? [];
|
|
98
164
|
assertStringArray(value, `Contract ${field}`);
|
|
99
165
|
contract[field] = [...value];
|
|
100
166
|
}
|
|
167
|
+
for (const field of CONTRACT_REQUIREMENT_FIELDS) {
|
|
168
|
+
const value = input[field] ?? [];
|
|
169
|
+
assertRequirementArray(value, `Contract ${field}`, seenIds);
|
|
170
|
+
contract[field] = structuredClone(value);
|
|
171
|
+
}
|
|
101
172
|
contract.assumptions = (input.assumptions ?? []).map((assumption) => ({
|
|
102
173
|
value: assumption.value,
|
|
103
174
|
reason: assumption.reason,
|
|
@@ -116,6 +187,13 @@ export function contractFingerprint(contract) {
|
|
|
116
187
|
export async function validateContract(contract, packageRoot) {
|
|
117
188
|
assertSecretFree(contract);
|
|
118
189
|
assertAssumptions(contract.assumptions ?? []);
|
|
190
|
+
const seenIds = new Set();
|
|
191
|
+
for (const field of CONTRACT_STRING_ARRAY_FIELDS) {
|
|
192
|
+
assertStringArray(contract[field] ?? [], `Contract ${field}`);
|
|
193
|
+
}
|
|
194
|
+
for (const field of CONTRACT_REQUIREMENT_FIELDS) {
|
|
195
|
+
assertRequirementArray(contract[field] ?? [], `Contract ${field}`, seenIds);
|
|
196
|
+
}
|
|
119
197
|
const schema = await readSchema("current-contract", packageRoot);
|
|
120
198
|
assertSchema(contract, schema, "current contract");
|
|
121
199
|
return contract;
|
package/src/core/coverage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
import { evaluateRequiredEvidence, normalizeRequirements } from "./evidence-readiness.js";
|
|
2
3
|
|
|
3
4
|
export const COVERAGE_SCHEMA_VERSION = 1;
|
|
4
5
|
export const COVERAGE_STATUSES = Object.freeze(["COVERED", "PARTIAL", "NOT_VERIFIED", "BLOCKED"]);
|
|
@@ -59,7 +60,8 @@ export function assertCoverage(value, label = "evidence coverage") {
|
|
|
59
60
|
const expected = evaluateCoverage(requiredEvidence, observedEvidence, {
|
|
60
61
|
blocked: value.status === "BLOCKED",
|
|
61
62
|
});
|
|
62
|
-
|
|
63
|
+
const semanticPartial = value.status === "PARTIAL" && value.details?.readinessStatus === "PARTIAL";
|
|
64
|
+
if (value.status !== expected && !semanticPartial && !(value.status === "BLOCKED" && expected === "BLOCKED")) {
|
|
63
65
|
throw coverageError("E_EVIDENCE_COVERAGE_PARTIAL", `${label}.status does not match its observed evidence`);
|
|
64
66
|
}
|
|
65
67
|
return value;
|
|
@@ -72,18 +74,21 @@ export function assertCoverageList(value, label = "evidenceCoverage") {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export function coverageForRequirements(requirements, checks, { blockedIds = [] } = {}) {
|
|
75
|
-
const normalizedRequirements =
|
|
77
|
+
const normalizedRequirements = normalizeRequirements(requirements ?? []);
|
|
76
78
|
const normalizedChecks = Array.isArray(checks) ? checks : [];
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
79
|
+
const readiness = evaluateRequiredEvidence({ requirements: normalizedRequirements, checks: normalizedChecks });
|
|
80
|
+
const covered = new Set(readiness.covered.map((item) => item.id));
|
|
81
|
+
const partial = new Set(readiness.partial.map((item) => item.id));
|
|
82
|
+
const invalid = new Set(readiness.invalid.map((item) => item.id));
|
|
82
83
|
const blocked = new Set(blockedIds);
|
|
83
|
-
return normalizedRequirements.map((requirement) => createCoverage({
|
|
84
|
-
requirement,
|
|
85
|
-
requiredEvidence: [requirement],
|
|
86
|
-
observedEvidence:
|
|
87
|
-
blocked: blocked.has(requirement),
|
|
84
|
+
return normalizedRequirements.filter((requirement) => !requirement.terminalOwned).map((requirement) => createCoverage({
|
|
85
|
+
requirement: requirement.text,
|
|
86
|
+
requiredEvidence: [requirement.text],
|
|
87
|
+
observedEvidence: covered.has(requirement.id) ? [requirement.text] : [],
|
|
88
|
+
blocked: blocked.has(requirement.text) || blocked.has(requirement.id),
|
|
89
|
+
...(partial.has(requirement.id) || invalid.has(requirement.id) ? {
|
|
90
|
+
status: "PARTIAL",
|
|
91
|
+
details: { requirementId: requirement.id, readinessStatus: "PARTIAL" },
|
|
92
|
+
} : {}),
|
|
88
93
|
}));
|
|
89
94
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const DISCOVERY_SURFACES = Object.freeze([
|
|
2
|
+
Object.freeze({
|
|
3
|
+
id: "agents-md",
|
|
4
|
+
path: "AGENTS.md",
|
|
5
|
+
kind: "project-instructions",
|
|
6
|
+
}),
|
|
7
|
+
Object.freeze({
|
|
8
|
+
id: "claude-md",
|
|
9
|
+
path: "CLAUDE.md",
|
|
10
|
+
kind: "project-instructions",
|
|
11
|
+
}),
|
|
12
|
+
Object.freeze({
|
|
13
|
+
id: "cursor-rule",
|
|
14
|
+
path: ".cursor/rules/project-loop.mdc",
|
|
15
|
+
kind: "project-instructions",
|
|
16
|
+
}),
|
|
17
|
+
Object.freeze({
|
|
18
|
+
id: "github-repository-instructions",
|
|
19
|
+
path: ".github/copilot-instructions.md",
|
|
20
|
+
kind: "project-instructions",
|
|
21
|
+
}),
|
|
22
|
+
]);
|
package/src/core/events.js
CHANGED
|
@@ -8,6 +8,7 @@ import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
|
8
8
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
9
9
|
import { assertSecretFree } from "./receipt.js";
|
|
10
10
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
11
|
+
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
11
12
|
|
|
12
13
|
const EVENT_SCHEMA_VERSION = 1;
|
|
13
14
|
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
@@ -27,7 +28,12 @@ export const ACTIVATION_EVENT_MATRIX = Object.freeze([
|
|
|
27
28
|
Object.freeze({ stage: "preflight blocked", event: "PREFLIGHT_BLOCKED", requiredFor: "blocked activation" }),
|
|
28
29
|
Object.freeze({ stage: "preflight ready", event: "PREFLIGHT_READY", requiredFor: "resumable readiness" }),
|
|
29
30
|
]);
|
|
30
|
-
const REPEATABLE_MILESTONES = new Set([
|
|
31
|
+
const REPEATABLE_MILESTONES = new Set([
|
|
32
|
+
"VERIFICATION_STARTED",
|
|
33
|
+
"VERIFICATION_RECORDED",
|
|
34
|
+
"REVIEW_STARTED",
|
|
35
|
+
"TERMINAL_RESULT_RECORDED",
|
|
36
|
+
]);
|
|
31
37
|
|
|
32
38
|
function eventHash(event) {
|
|
33
39
|
const { hash, ...body } = event;
|
|
@@ -126,7 +132,7 @@ export async function validateEventLedger(target, packageRoot) {
|
|
|
126
132
|
code: "E_PHASE_CHRONOLOGY_INVALID",
|
|
127
133
|
message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}`,
|
|
128
134
|
});
|
|
129
|
-
} else if (milestoneIndex < lastMilestone) {
|
|
135
|
+
} else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED") {
|
|
130
136
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
|
|
131
137
|
} else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event)) {
|
|
132
138
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
@@ -161,6 +167,181 @@ export async function validateEventLedger(target, packageRoot) {
|
|
|
161
167
|
if (event.event === "COMPLETION_VALIDATED" && !seen.has("VERIFICATION_RECORDED")) {
|
|
162
168
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion validated before verification evidence" });
|
|
163
169
|
}
|
|
170
|
+
if (event.event === "COMPLETION_REJECTED" && !seen.has("VERIFICATION_STARTED")) {
|
|
171
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion rejected before verification started" });
|
|
172
|
+
}
|
|
164
173
|
}
|
|
165
174
|
return { valid: errors.length === 0, events, errors };
|
|
166
175
|
}
|
|
176
|
+
|
|
177
|
+
export function validateStateLedgerCoherence(state, events) {
|
|
178
|
+
const errors = [];
|
|
179
|
+
if (!Number.isInteger(state.verificationCycle)) return errors;
|
|
180
|
+
const taskEvents = events.filter((event) => event.taskId === state.taskId);
|
|
181
|
+
const observed = new Set(taskEvents.map((event) => event.event));
|
|
182
|
+
const supportsReviewEvents = taskEvents.some((event) => event.event === "REVIEW_STARTED"
|
|
183
|
+
|| (event.event === "VERIFICATION_STARTED" && Number.isInteger(event.details?.verificationCycle)));
|
|
184
|
+
const phaseRequirements = {
|
|
185
|
+
EXECUTING: ["EXECUTION_STARTED"],
|
|
186
|
+
VERIFYING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
|
|
187
|
+
DIAGNOSING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
|
|
188
|
+
CORRECTING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
|
|
189
|
+
REVIEWING: ["EXECUTION_STARTED", "VERIFICATION_STARTED", ...(supportsReviewEvents ? ["REVIEW_STARTED"] : [])],
|
|
190
|
+
COMPLETE: ["EXECUTION_STARTED", "VERIFICATION_STARTED", ...(supportsReviewEvents ? ["REVIEW_STARTED"] : []), "COMPLETION_VALIDATED"],
|
|
191
|
+
};
|
|
192
|
+
for (const required of phaseRequirements[state.phase] ?? []) {
|
|
193
|
+
if (!observed.has(required)) {
|
|
194
|
+
errors.push({
|
|
195
|
+
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
196
|
+
message: `Work-state phase ${state.phase} requires ledger event ${required}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Number.isInteger(state.verificationCycle)) {
|
|
201
|
+
const verificationEvents = taskEvents
|
|
202
|
+
.map((event, index) => ({ event, index }))
|
|
203
|
+
.filter(({ event }) => event.event === "VERIFICATION_STARTED");
|
|
204
|
+
const cycles = verificationEvents.map(({ event }) => event.details?.verificationCycle ?? 1);
|
|
205
|
+
if (cycles.at(-1) !== state.verificationCycle) {
|
|
206
|
+
errors.push({
|
|
207
|
+
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
208
|
+
message: "Work-state verification cycle does not match the lifecycle ledger",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const latestVerification = verificationEvents.at(-1);
|
|
212
|
+
const latestReview = taskEvents
|
|
213
|
+
.map((event, index) => ({ event, index }))
|
|
214
|
+
.filter(({ event }) => event.event === "REVIEW_STARTED")
|
|
215
|
+
.at(-1);
|
|
216
|
+
const latestReviewCycle = latestReview?.event.details?.verificationCycle;
|
|
217
|
+
if (state.phase === "VERIFYING"
|
|
218
|
+
&& latestReviewCycle === state.verificationCycle
|
|
219
|
+
&& latestReview.index > (latestVerification?.index ?? -1)) {
|
|
220
|
+
errors.push({
|
|
221
|
+
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
222
|
+
message: "Work-state returned to VERIFYING without a new verification cycle in the lifecycle ledger",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (["REVIEWING", "COMPLETE"].includes(state.phase)
|
|
226
|
+
&& supportsReviewEvents
|
|
227
|
+
&& (latestReviewCycle !== state.verificationCycle
|
|
228
|
+
|| latestReview.index < (latestVerification?.index ?? -1))) {
|
|
229
|
+
errors.push({
|
|
230
|
+
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
231
|
+
message: "Work-state review phase does not match the current lifecycle ledger cycle",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return errors;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function validateCompletionRecoveryAuthorization({ state, receipt, events } = {}) {
|
|
239
|
+
const errors = [];
|
|
240
|
+
if (!state) {
|
|
241
|
+
return {
|
|
242
|
+
authorized: false,
|
|
243
|
+
errors: [{ code: "E_STATE_MISSING", message: "Work-state is required for recovery validation" }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const attempt = state.lastCompletionAttempt;
|
|
247
|
+
if (!attempt || attempt.status !== "REJECTED") {
|
|
248
|
+
return {
|
|
249
|
+
authorized: false,
|
|
250
|
+
errors: [{ code: "E_COMPLETION_RECOVERY_UNAUTHORIZED", message: "Recovery requires a persisted REJECTED completion attempt" }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const hasRecoverableReason = Array.isArray(attempt.reasonCodes)
|
|
254
|
+
&& attempt.reasonCodes.length > 0
|
|
255
|
+
&& attempt.reasonCodes.every((code) => isRecoverableCompletionEvidenceCode(code));
|
|
256
|
+
if (!hasRecoverableReason) {
|
|
257
|
+
return {
|
|
258
|
+
authorized: false,
|
|
259
|
+
errors: [{ code: "E_COMPLETION_RECOVERY_UNAUTHORIZED", message: "Completion attempt contains no recoverable evidence reason codes" }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const taskEvents = (events ?? []).filter((event) => event.taskId === state.taskId);
|
|
264
|
+
const targetCycle = state.verificationCycle ?? 1;
|
|
265
|
+
|
|
266
|
+
if (attempt.verificationCycle !== targetCycle) {
|
|
267
|
+
errors.push({
|
|
268
|
+
code: "E_COMPLETION_REJECTION_LEDGER_MISMATCH",
|
|
269
|
+
message: `Work-state rejection verificationCycle ${attempt.verificationCycle} does not match state verificationCycle ${targetCycle}`,
|
|
270
|
+
});
|
|
271
|
+
return { authorized: false, errors };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const reviewEvents = taskEvents
|
|
275
|
+
.map((event, index) => ({ event, index }))
|
|
276
|
+
.filter(({ event }) => event.event === "REVIEW_STARTED" && (event.details?.verificationCycle ?? 1) === targetCycle);
|
|
277
|
+
const latestReviewIndex = reviewEvents.at(-1)?.index ?? -1;
|
|
278
|
+
|
|
279
|
+
const rejectionEvents = taskEvents
|
|
280
|
+
.map((event, index) => ({ event, index }))
|
|
281
|
+
.filter(({ event }) => event.event === "COMPLETION_REJECTED");
|
|
282
|
+
|
|
283
|
+
if (rejectionEvents.length === 0) {
|
|
284
|
+
errors.push({
|
|
285
|
+
code: "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
286
|
+
message: "No COMPLETION_REJECTED event found in protocol ledger",
|
|
287
|
+
});
|
|
288
|
+
return { authorized: false, errors };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const matchingEvent = rejectionEvents.findLast(({ event, index }) => {
|
|
292
|
+
const eventCycle = event.details?.verificationCycle ?? 1;
|
|
293
|
+
if (eventCycle !== targetCycle) return false;
|
|
294
|
+
if (latestReviewIndex >= 0 && index < latestReviewIndex) return false;
|
|
295
|
+
return true;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!matchingEvent) {
|
|
299
|
+
errors.push({
|
|
300
|
+
code: "E_COMPLETION_REJECTION_LEDGER_MISMATCH",
|
|
301
|
+
message: `No matching COMPLETION_REJECTED event found for verification cycle ${targetCycle}`,
|
|
302
|
+
});
|
|
303
|
+
return { authorized: false, errors };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const eventDetails = matchingEvent.event.details ?? {};
|
|
307
|
+
const stateReasons = [...new Set(attempt.reasonCodes ?? [])].sort();
|
|
308
|
+
const eventReasons = [...new Set(eventDetails.reasonCodes ?? [])].sort();
|
|
309
|
+
if (stateReasons.join(",") !== eventReasons.join(",")) {
|
|
310
|
+
errors.push({
|
|
311
|
+
code: "E_COMPLETION_REJECTION_LEDGER_MISMATCH",
|
|
312
|
+
message: "Work-state rejection reasonCodes do not match ledger COMPLETION_REJECTED event",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const stateMissing = [...new Set(attempt.missingRequirementIds ?? [])].sort();
|
|
317
|
+
const eventMissing = [...new Set(eventDetails.missingRequirementIds ?? [])].sort();
|
|
318
|
+
if (stateMissing.join(",") !== eventMissing.join(",")) {
|
|
319
|
+
errors.push({
|
|
320
|
+
code: "E_COMPLETION_REJECTION_LEDGER_MISMATCH",
|
|
321
|
+
message: "Work-state missingRequirementIds do not match ledger COMPLETION_REJECTED event",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const currentStateFingerprint = canonicalFingerprint(state);
|
|
326
|
+
if (eventDetails.stateFingerprint && eventDetails.stateFingerprint !== currentStateFingerprint) {
|
|
327
|
+
errors.push({
|
|
328
|
+
code: "E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH",
|
|
329
|
+
message: "Current work-state no longer matches the rejected completion snapshot",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (eventDetails.receiptFingerprint !== undefined) {
|
|
334
|
+
const currentReceiptFingerprint = receipt ? canonicalFingerprint(receipt) : undefined;
|
|
335
|
+
if (currentReceiptFingerprint !== eventDetails.receiptFingerprint) {
|
|
336
|
+
errors.push({
|
|
337
|
+
code: "E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH",
|
|
338
|
+
message: "Current receipt no longer matches the rejected completion snapshot",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
authorized: errors.length === 0,
|
|
345
|
+
errors,
|
|
346
|
+
};
|
|
347
|
+
}
|