@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.
Files changed (69) hide show
  1. package/.cursor/rules/project-loop.mdc +16 -18
  2. package/.github/copilot-instructions.md +16 -19
  3. package/AGENTS.md +18 -24
  4. package/AGENT_COMPATIBILITY.md +4 -211
  5. package/CLAUDE.md +16 -18
  6. package/LOOP_ENGINEERING.md +73 -0
  7. package/LOOP_SYSTEM_DESIGN.md +9 -5
  8. package/ORCHESTRATOR_INTEGRATION.md +4 -0
  9. package/PROTOCOL_INTEGRATION.md +171 -0
  10. package/QUALITY_SCORECARD.md +1 -1
  11. package/README.md +123 -103
  12. package/TERMINOLOGY.md +2 -0
  13. package/THREAT_MODEL.md +4 -0
  14. package/package.json +5 -4
  15. package/schemas/current-contract.schema.json +48 -2
  16. package/schemas/evidence.schema.json +1 -0
  17. package/schemas/execution-receipt.schema.json +2 -0
  18. package/schemas/work-state.schema.json +44 -1
  19. package/src/cli.js +48 -4
  20. package/src/commands/record-terminal-result.js +18 -0
  21. package/src/commands/validate-protocol.js +14 -2
  22. package/src/core/checks.js +20 -0
  23. package/src/core/completion-artifacts.js +266 -11
  24. package/src/core/completion-recovery.js +15 -0
  25. package/src/core/completion-relationships.js +23 -9
  26. package/src/core/completion.js +130 -2
  27. package/src/core/contract.js +82 -4
  28. package/src/core/coverage.js +17 -12
  29. package/src/core/discovery-surfaces.js +22 -0
  30. package/src/core/events.js +183 -2
  31. package/src/core/evidence-readiness.js +333 -0
  32. package/src/core/evidence.js +22 -4
  33. package/src/core/execution-prerequisites.js +6 -1
  34. package/src/core/inspect.js +23 -12
  35. package/src/core/native-adapters.js +30 -2
  36. package/src/core/next-action.js +103 -27
  37. package/src/core/phase.js +68 -5
  38. package/src/core/protocol.js +28 -1
  39. package/src/core/receipt.js +5 -0
  40. package/src/core/schema-validation.js +28 -6
  41. package/src/core/templates.js +1 -0
  42. package/src/core/work-state.js +27 -0
  43. package/conformance/README.md +0 -148
  44. package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
  45. package/conformance/backend-auth/REQUEST.md +0 -4
  46. package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
  47. package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
  48. package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
  49. package/conformance/blind-premium-website/REQUEST.md +0 -6
  50. package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
  51. package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
  52. package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
  53. package/conformance/complete-website/REQUEST.md +0 -6
  54. package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
  55. package/conformance/complete-website/REQUIRED_GATES.json +0 -3
  56. package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
  57. package/conformance/docs-only/REQUEST.md +0 -4
  58. package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
  59. package/conformance/docs-only/REQUIRED_GATES.json +0 -3
  60. package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
  61. package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
  62. package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
  63. package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
  64. package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
  65. package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
  66. package/conformance/simple-bug/REQUEST.md +0 -4
  67. package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
  68. package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
  69. package/src/core/agent-support.js +0 -89
@@ -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);
@@ -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 CONTRACT_ARRAY_FIELDS = Object.freeze([
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
- for (const field of CONTRACT_ARRAY_FIELDS) {
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;
@@ -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
- if (value.status !== expected && !(value.status === "BLOCKED" && expected === "BLOCKED")) {
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 = stringArray(requirements ?? [], "requirements");
77
+ const normalizedRequirements = normalizeRequirements(requirements ?? []);
76
78
  const normalizedChecks = Array.isArray(checks) ? checks : [];
77
- const observed = new Set(
78
- normalizedChecks
79
- .filter((check) => check?.status === "passed" && check.evidenceKind === "OBSERVED")
80
- .flatMap((check) => [check.id, check.kind, check.requirement]),
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: observed.has(requirement) ? [requirement] : [],
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
+ ]);
@@ -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(["VERIFICATION_RECORDED"]);
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
+ }