@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
@@ -11,10 +11,12 @@ import { assertCheckList } from "./checks.js";
11
11
  import { completionRelationshipErrors } from "./completion-relationships.js";
12
12
  import { classifyLoadedWorkState, readWorkState } from "./work-state.js";
13
13
  import { sha256 } from "./manifest.js";
14
+ import { evaluateRequiredEvidence, authoritativeChecksForRequirements, ordinaryLeafRequirements, classifyRequirement } from "./evidence-readiness.js";
14
15
  import {
15
16
  evaluateStartExecutionPrerequisites,
16
17
  PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE,
17
18
  } from "./execution-prerequisites.js";
19
+ import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./events.js";
18
20
 
19
21
  export const NEXT_ACTIONS = Object.freeze({
20
22
  DISCOVER: "DISCOVER",
@@ -29,6 +31,7 @@ export const NEXT_ACTIONS = Object.freeze({
29
31
  DIAGNOSE: "DIAGNOSE",
30
32
  CORRECT: "CORRECT",
31
33
  ENTER_REVIEWING: "ENTER_REVIEWING",
34
+ RECORD_TERMINAL_RESULT: "RECORD_TERMINAL_RESULT",
32
35
  PREPARE_COMPLETION: "PREPARE_COMPLETION",
33
36
  RUN_COMPLETE: "RUN_COMPLETE",
34
37
  RESOLVE_STALE_ROUTE: "RESOLVE_STALE_ROUTE",
@@ -99,6 +102,7 @@ function commandFor(action) {
99
102
  [NEXT_ACTIONS.DIAGNOSE]: "forgeloop advance --to DIAGNOSING",
100
103
  [NEXT_ACTIONS.CORRECT]: "forgeloop advance --to CORRECTING",
101
104
  [NEXT_ACTIONS.ENTER_REVIEWING]: "forgeloop advance --to REVIEWING",
105
+ [NEXT_ACTIONS.RECORD_TERMINAL_RESULT]: "forgeloop record-terminal-result",
102
106
  [NEXT_ACTIONS.PREPARE_COMPLETION]: "forgeloop prepare-completion --json",
103
107
  [NEXT_ACTIONS.RUN_COMPLETE]: "forgeloop complete --json",
104
108
  }[action];
@@ -120,6 +124,23 @@ function recordCheckCommandSpec(requirement) {
120
124
  };
121
125
  }
122
126
 
127
+ function recordTerminalResultCommandSpec(requirement) {
128
+ const reqId = requirement.id ?? requirement;
129
+ const type = requirement.type ?? "PUBLICATION";
130
+ const status = requirement.requiredPublicationStatus ?? (type === "PUBLICATION" ? "published" : "ready");
131
+ return {
132
+ commandId: "record-terminal-result",
133
+ executable: "forgeloop",
134
+ subcommand: "record-terminal-result",
135
+ argv: ["record-terminal-result", `--requirement=${reqId}`, `--type=${type}`, `--status=${status}`],
136
+ requiredInputs: [
137
+ { name: "source", option: "--source=<text>" },
138
+ { name: "result", option: "--result=<text>" },
139
+ { name: "details", option: "--details=<json>", optional: true },
140
+ ],
141
+ };
142
+ }
143
+
123
144
  function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = []) {
124
145
  const command = commandFor(action);
125
146
  return result({
@@ -138,10 +159,6 @@ function missingArtifact(error, fallback) {
138
159
  : [];
139
160
  }
140
161
 
141
- function allCoverageCovered(coverage) {
142
- return coverage.every((item) => item.status === "COVERED");
143
- }
144
-
145
162
  function staleReasons(state, contract, route) {
146
163
  const reasons = [];
147
164
  if (state.contractFingerprint !== contract.fingerprint) {
@@ -230,7 +247,11 @@ async function requirementsAndCoverage({ target, packageRoot, contract, route, c
230
247
  return { requirements, coverage: coverageForRequirements(requirements, checks) };
231
248
  }
232
249
 
233
- export async function getNextAction({ target, packageRoot } = {}) {
250
+ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
251
+ const normalized = typeof targetOrOptions === "string"
252
+ ? { target: targetOrOptions, packageRoot: packageRootOption }
253
+ : targetOrOptions;
254
+ const { target, packageRoot } = normalized ?? {};
234
255
  const workState = await loadArtifact(
235
256
  () => readWorkState(target, packageRoot),
236
257
  ARTIFACT_PATHS.state,
@@ -516,20 +537,32 @@ export async function getNextAction({ target, packageRoot } = {}) {
516
537
  requiredArtifacts,
517
538
  });
518
539
  }
519
- const failed = state.checks.find((check) => check?.status === "failed");
540
+ const evidence = await requirementsAndCoverage({
541
+ target,
542
+ packageRoot,
543
+ contract,
544
+ route,
545
+ checks: state.checks,
546
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
547
+ });
548
+ const authoritative = authoritativeChecksForRequirements({
549
+ requirements: ordinaryLeafRequirements(evidence.requirements),
550
+ checks: state.checks,
551
+ });
552
+ const failed = authoritative.find(({ check }) => check?.status === "failed");
520
553
  if (failed) {
521
554
  return decision(
522
555
  context,
523
556
  NEXT_ACTIONS.DIAGNOSE,
524
- artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.id}`, [ARTIFACT_PATHS.state]),
557
+ artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [ARTIFACT_PATHS.state]),
525
558
  );
526
559
  }
527
- const blocked = state.checks.find((check) => check?.status === "blocked");
560
+ const blocked = authoritative.find(({ check }) => check?.status === "blocked");
528
561
  if (blocked) {
529
562
  return decision(
530
563
  context,
531
564
  NEXT_ACTIONS.RESOLVE_BLOCKER,
532
- artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.id}`, [ARTIFACT_PATHS.state]),
565
+ artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [ARTIFACT_PATHS.state]),
533
566
  );
534
567
  }
535
568
  const receipt = await loadArtifact(
@@ -563,14 +596,7 @@ export async function getNextAction({ target, packageRoot } = {}) {
563
596
  [...requiredArtifacts, ARTIFACT_PATHS.receipt],
564
597
  );
565
598
  }
566
- const evidence = await requirementsAndCoverage({
567
- target,
568
- packageRoot,
569
- contract,
570
- route,
571
- checks: state.checks,
572
- additionalEvidence: preflight.policy?.requiredEvidence ?? [],
573
- });
599
+ const readiness = evaluateRequiredEvidence({ requirements: evidence.requirements, checks: state.checks });
574
600
  const receiptRelationships = completionRelationshipErrors({
575
601
  contract,
576
602
  route,
@@ -587,18 +613,22 @@ export async function getNextAction({ target, packageRoot } = {}) {
587
613
  requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
588
614
  });
589
615
  }
590
- if (allCoverageCovered(evidence.coverage)) {
616
+ if (readiness.ready) {
591
617
  return decision(context, NEXT_ACTIONS.ENTER_REVIEWING, artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"));
592
618
  }
593
- const uncovered = evidence.coverage.filter((item) => item.status !== "COVERED");
619
+ const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
594
620
  return result({
595
621
  ...context,
596
622
  nextAction: NEXT_ACTIONS.RECORD_VERIFICATION,
597
623
  commands: ["forgeloop record-check"],
598
- commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.requirement)),
624
+ commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.text)),
599
625
  reasons: uncovered
600
626
  .map((item) => artifactError(
601
- "E_EVIDENCE_REQUIRED",
627
+ readiness.invalid.some((candidate) => candidate.id === item.id)
628
+ ? "E_EVIDENCE_INVALID"
629
+ : readiness.partial.some((candidate) => candidate.id === item.id)
630
+ ? "E_EVIDENCE_PARTIAL"
631
+ : "E_EVIDENCE_REQUIRED",
602
632
  "Run the required check and record observed evidence through the structured command specification.",
603
633
  [ARTIFACT_PATHS.state],
604
634
  )),
@@ -641,15 +671,41 @@ export async function getNextAction({ target, packageRoot } = {}) {
641
671
  checks: state.checks,
642
672
  additionalEvidence: preflight.policy?.requiredEvidence ?? [],
643
673
  });
644
- if (!allCoverageCovered(evidence.coverage)) {
674
+ const readiness = evaluateRequiredEvidence({ requirements: evidence.requirements, checks: state.checks });
675
+ if (!readiness.ready) {
676
+ let recoveryAuthorized = false;
677
+ if (state.lastCompletionAttempt?.status === "REJECTED") {
678
+ try {
679
+ const ledger = await validateEventLedger(target, packageRoot);
680
+ let currentReceipt = null;
681
+ try {
682
+ const receiptArtifact = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
683
+ currentReceipt = receiptArtifact?.value;
684
+ } catch {}
685
+ const recoveryAuth = validateCompletionRecoveryAuthorization({
686
+ state,
687
+ receipt: currentReceipt,
688
+ events: ledger.events,
689
+ });
690
+ recoveryAuthorized = recoveryAuth.authorized;
691
+ } catch {
692
+ recoveryAuthorized = false;
693
+ }
694
+ }
645
695
  return result({
646
696
  ...context,
647
- nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
648
- reasons: evidence.coverage
649
- .filter((item) => item.status !== "COVERED")
697
+ nextAction: recoveryAuthorized
698
+ ? NEXT_ACTIONS.ENTER_VERIFYING
699
+ : NEXT_ACTIONS.RESOLVE_BLOCKER,
700
+ commands: recoveryAuthorized
701
+ ? [commandFor(NEXT_ACTIONS.ENTER_VERIFYING)]
702
+ : [],
703
+ reasons: [...readiness.invalid, ...readiness.partial, ...readiness.missing]
650
704
  .map((item) => artifactError(
651
- "E_EVIDENCE_COVERAGE_PARTIAL",
652
- `Evidence coverage is ${item.status}: ${item.requirement}`,
705
+ readiness.missing.some((candidate) => candidate.id === item.id)
706
+ ? "E_EVIDENCE_REQUIRED"
707
+ : item.reasonCode ?? "E_EVIDENCE_PARTIAL",
708
+ `Evidence is not ready: ${item.text}`,
653
709
  [ARTIFACT_PATHS.state],
654
710
  )),
655
711
  requiredArtifacts,
@@ -707,6 +763,26 @@ export async function getNextAction({ target, packageRoot } = {}) {
707
763
  }
708
764
  const completion = await evaluateCompletion({ target, packageRoot });
709
765
  if (completion.status !== "VALID") {
766
+ const terminalPendingErrors = completion.errors.filter((err) => (
767
+ err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
768
+ ));
769
+ if (terminalPendingErrors.length > 0 && terminalPendingErrors.length === completion.errors.length) {
770
+ const terminalPendingReqs = terminalPendingErrors.map((err) => {
771
+ const reqId = err.requirementId;
772
+ const matchingReq = evidence.requirements.find((r) => r.id === reqId)
773
+ ?? evidence.requirements.find((r) => r.text === reqId)
774
+ ?? classifyRequirement(reqId ?? err.message);
775
+ return matchingReq;
776
+ });
777
+ return result({
778
+ ...context,
779
+ nextAction: NEXT_ACTIONS.RECORD_TERMINAL_RESULT,
780
+ commands: ["forgeloop record-terminal-result"],
781
+ commandSpecs: terminalPendingReqs.map(recordTerminalResultCommandSpec),
782
+ reasons: completion.errors,
783
+ requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
784
+ });
785
+ }
710
786
  return result({
711
787
  ...context,
712
788
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
package/src/core/phase.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
2
  import { readContract } from "./contract.js";
3
- import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
3
+ import {
4
+ appendProtocolEvent,
5
+ LIFECYCLE_MILESTONES,
6
+ validateCompletionRecoveryAuthorization,
7
+ validateEventLedger,
8
+ validateStateLedgerCoherence,
9
+ } from "./events.js";
4
10
  import { readPersistedRoute } from "./route-artifact.js";
5
11
  import { assertWorkPhase, isValidTransition } from "./protocol.js";
6
12
  import { readWorkState, writeWorkState } from "./work-state.js";
@@ -25,6 +31,7 @@ const PHASE_EVENTS = Object.freeze({
25
31
  PLANNED: "PLAN_RECORDED",
26
32
  EXECUTING: "EXECUTION_STARTED",
27
33
  VERIFYING: "VERIFICATION_STARTED",
34
+ REVIEWING: "REVIEW_STARTED",
28
35
  COMPLETE: "COMPLETION_VALIDATED",
29
36
  });
30
37
 
@@ -127,7 +134,9 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
127
134
  }
128
135
  }
129
136
 
130
- export async function advanceWorkState(target, toPhase, { packageRoot, now = new Date().toISOString() } = {}) {
137
+ export async function advanceWorkState(target, toPhase, options = {}) {
138
+ const normalizedOptions = typeof options === "string" ? { packageRoot: options } : options;
139
+ const { packageRoot, now = new Date().toISOString() } = normalizedOptions;
131
140
  assertWorkPhase(toPhase);
132
141
  const state = await readWorkState(target, packageRoot);
133
142
  if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
@@ -148,11 +157,50 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
148
157
  [ARTIFACT_PATHS.events, ARTIFACT_PATHS.state],
149
158
  );
150
159
  }
160
+ const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
161
+ if (coherenceErrors.length > 0) {
162
+ throw phaseError(coherenceErrors[0].code, coherenceErrors[0].message, [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events]);
163
+ }
151
164
  const eventType = PHASE_EVENTS[toPhase];
152
- if (eventType && ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
165
+ const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
166
+ if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
167
+ let currentReceipt = null;
168
+ try {
169
+ const receiptArtifact = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
170
+ currentReceipt = receiptArtifact?.value;
171
+ } catch {
172
+ // If receipt is not present, pass null
173
+ }
174
+ const recoveryAuth = validateCompletionRecoveryAuthorization({
175
+ state,
176
+ receipt: currentReceipt,
177
+ events: ledger.events,
178
+ });
179
+ if (!recoveryAuth.authorized) {
180
+ const firstError = recoveryAuth.errors[0] ?? {};
181
+ throw phaseError(
182
+ firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
183
+ firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
184
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events],
185
+ );
186
+ }
187
+ }
188
+ if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
189
+ if (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis.trim()) {
190
+ throw phaseError(
191
+ "E_PHASE_PREREQUISITE_MISSING",
192
+ "CORRECTING -> VERIFYING requires a diagnosed hypothesis",
193
+ [ARTIFACT_PATHS.state],
194
+ );
195
+ }
196
+ }
197
+ const repeatedReview = state.phase === "VERIFYING" && toPhase === "REVIEWING"
198
+ && ledger.events.some((event) => event.taskId === state.taskId && event.event === "REVIEW_STARTED");
199
+ if (eventType && !reenteringVerification && !repeatedReview
200
+ && ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
153
201
  throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [ARTIFACT_PATHS.events]);
154
202
  }
155
- const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(eventType);
203
+ const milestoneIndex = reenteringVerification || repeatedReview ? -1 : LIFECYCLE_MILESTONES.indexOf(eventType);
156
204
  if (milestoneIndex >= 0) {
157
205
  const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
158
206
  if (lastMilestone !== milestoneIndex - 1) {
@@ -166,6 +214,10 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
166
214
  phase: toPhase,
167
215
  lastUpdated: now,
168
216
  };
217
+ if (toPhase === "VERIFYING") {
218
+ next.verificationCycle = reenteringVerification ? (state.verificationCycle ?? 1) + 1 : (state.verificationCycle ?? 1);
219
+ }
220
+ if (reenteringVerification) delete next.lastCompletionAttempt;
169
221
  let nextReceipt = null;
170
222
  try {
171
223
  const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
@@ -187,10 +239,12 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
187
239
  receipt: receipt.value,
188
240
  requiredEvidence,
189
241
  requireRequiredChecks: false,
242
+ requireReceiptStateFingerprint: false,
190
243
  });
191
244
  nextReceipt = await createReceipt({
192
245
  ...receipt.value,
193
246
  stateFingerprint: canonicalFingerprint(next),
247
+ verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
194
248
  }, packageRoot);
195
249
  assertCompletionRelationships({
196
250
  contract,
@@ -207,6 +261,15 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
207
261
  if (nextReceipt) {
208
262
  await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
209
263
  }
210
- if (eventType) await appendProtocolEvent(target, { taskId: state.taskId, event: eventType, at: now }, packageRoot);
264
+ if (eventType) await appendProtocolEvent(target, {
265
+ taskId: state.taskId,
266
+ event: eventType,
267
+ at: now,
268
+ details: eventType === "VERIFICATION_STARTED"
269
+ ? { verificationCycle: next.verificationCycle }
270
+ : eventType === "REVIEW_STARTED"
271
+ ? { verificationCycle: next.verificationCycle ?? 1 }
272
+ : undefined,
273
+ }, packageRoot);
211
274
  return next;
212
275
  }
@@ -36,16 +36,35 @@ export const FAILURE_CODES = Object.freeze([
36
36
  "E_EVIDENCE_REQUIRED",
37
37
  "E_EVIDENCE_KIND_INVALID",
38
38
  "E_EVIDENCE_STALE",
39
+ "E_EVIDENCE_PARTIAL",
40
+ "E_EVIDENCE_INVALID",
41
+ "E_FUTURE_LIFECYCLE_EVIDENCE",
42
+ "E_CIRCULAR_COMPLETION_REQUIREMENT",
43
+ "E_MIXED_TERMINAL_REQUIREMENT",
39
44
  "E_EVIDENCE_COVERAGE_PARTIAL",
45
+ "E_EVIDENCE_COVERAGE_INVALID",
46
+ "E_CHECK_INVALID",
40
47
  "E_CHECK_STATUS_CONTRADICTION",
41
48
  "E_PROFILE_SOURCE_MISSING",
42
49
  "E_PROFILE_SOURCE_UNKNOWN",
43
50
  "E_PROFILE_SOURCE_MISCLASSIFIED",
51
+ "E_PROFILE_UNVERIFIED",
52
+ "E_RECEIPT_MISSING",
53
+ "E_RECEIPT_INVALID",
54
+ "E_RECEIPT_TASK_MISMATCH",
44
55
  "E_RECEIPT_ROUTE_MISMATCH",
45
56
  "E_RECEIPT_STATE_MISMATCH",
46
57
  "E_RECEIPT_CONTRACT_MISMATCH",
47
58
  "E_RECEIPT_PATH_MISMATCH",
59
+ "E_RECEIPT_CYCLE_MISMATCH",
48
60
  "E_COMPLETION_REJECTED",
61
+ "E_COMPLETION_RECOVERY_UNAUTHORIZED",
62
+ "E_COMPLETION_REJECTION_LEDGER_MISMATCH",
63
+ "E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH",
64
+ "E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH",
65
+ "E_STATE_MISSING",
66
+ "E_STATE_TASK_MISMATCH",
67
+ "E_STATE_LEDGER_DIVERGENCE",
49
68
  "E_PUBLICATION_CLAIM_UNVERIFIED",
50
69
  "E_PRODUCTION_READINESS_UNVERIFIED",
51
70
  "E_STATE_MISSING_AFTER_PREFLIGHT_READY",
@@ -54,6 +73,14 @@ export const FAILURE_CODES = Object.freeze([
54
73
  "E_PREFLIGHT_EVENT_MISSING",
55
74
  "E_PREFLIGHT_GATE_EVENT_MISSING",
56
75
  "E_PREFLIGHT_GATES_STALE",
76
+ "E_TERMINAL_REQUIREMENT_PENDING",
77
+ "E_PUBLICATION_REQUIREMENT_PENDING",
78
+ "E_PRODUCTION_REQUIREMENT_PENDING",
79
+ "E_FUTURE_TERMINAL_EVIDENCE",
80
+ "E_TERMINAL_REQUIREMENT_UNKNOWN",
81
+ "E_TERMINAL_REQUIREMENT_NOT_TERMINAL",
82
+ "E_TERMINAL_REQUIREMENT_TYPE_MISMATCH",
83
+ "E_TERMINAL_STATUS_REGRESSION",
57
84
  ]);
58
85
 
59
86
  export const WORK_PHASES = Object.freeze([
@@ -97,7 +124,7 @@ export const WORK_TRANSITIONS = Object.freeze({
97
124
  VERIFYING: ["DIAGNOSING", "REVIEWING"],
98
125
  DIAGNOSING: ["CORRECTING"],
99
126
  CORRECTING: ["VERIFYING"],
100
- REVIEWING: ["COMPLETE", "CORRECTING"],
127
+ REVIEWING: ["COMPLETE", "CORRECTING", "VERIFYING"],
101
128
  COMPLETE: [],
102
129
  BLOCKED: [],
103
130
  });
@@ -58,6 +58,10 @@ function assertKnownGuides(guides) {
58
58
 
59
59
  export function assertReceiptSemantics(receipt) {
60
60
  const evidence = receipt.evidence ?? [];
61
+ if (receipt.verificationCycle !== undefined
62
+ && (!Number.isInteger(receipt.verificationCycle) || receipt.verificationCycle < 1)) {
63
+ throw new Error("receipt.verificationCycle must be a positive integer");
64
+ }
61
65
  assertEvidenceList(evidence, "receipt.evidence");
62
66
 
63
67
  for (const [index, check] of receipt.checks.entries()) {
@@ -145,6 +149,7 @@ export async function createReceipt(input, packageRoot) {
145
149
  contractFingerprint: input.contractFingerprint,
146
150
  ...(input.routeFingerprint !== undefined ? { routeFingerprint: input.routeFingerprint } : {}),
147
151
  ...(input.stateFingerprint !== undefined ? { stateFingerprint: input.stateFingerprint } : {}),
152
+ ...(input.verificationCycle !== undefined ? { verificationCycle: input.verificationCycle } : {}),
148
153
  status: input.status ?? "in-progress",
149
154
  ...(input.taskStatus !== undefined ? { taskStatus: input.taskStatus } : {}),
150
155
  ...(input.verificationStatus !== undefined ? { verificationStatus: input.verificationStatus } : {}),
@@ -48,7 +48,29 @@ function display(value) {
48
48
  return typeof value === "string" ? JSON.stringify(value) : String(value);
49
49
  }
50
50
 
51
- function validate(value, schema, location, errors) {
51
+ function resolveRef(ref, rootSchema) {
52
+ if (typeof ref !== "string" || !rootSchema) return null;
53
+ if (ref.startsWith("#/")) {
54
+ const parts = ref.slice(2).split("/");
55
+ let target = rootSchema;
56
+ for (const part of parts) {
57
+ target = target?.[part];
58
+ }
59
+ return target ?? null;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function validate(value, schema, location, errors, rootSchema = schema) {
65
+ if (!schema || typeof schema !== "object") return;
66
+ if (schema.$ref) {
67
+ const target = resolveRef(schema.$ref, rootSchema);
68
+ if (target) {
69
+ validate(value, target, location, errors, rootSchema);
70
+ return;
71
+ }
72
+ }
73
+
52
74
  if (schema.const !== undefined && value !== schema.const) {
53
75
  errors.push(`${location}: expected ${display(schema.const)}`);
54
76
  return;
@@ -62,7 +84,7 @@ function validate(value, schema, location, errors) {
62
84
  if (schema.oneOf) {
63
85
  const matches = schema.oneOf.filter((candidate) => {
64
86
  const candidateErrors = [];
65
- validate(value, candidate, location, candidateErrors);
87
+ validate(value, candidate, location, candidateErrors, rootSchema);
66
88
  return candidateErrors.length === 0;
67
89
  });
68
90
  if (matches.length !== 1) errors.push(`${location}: expected exactly one matching schema`);
@@ -91,7 +113,7 @@ function validate(value, schema, location, errors) {
91
113
  errors.push(`${location}: must contain at most ${schema.maxItems} items`);
92
114
  }
93
115
  if (schema.items) {
94
- value.forEach((item, index) => validate(item, schema.items, `${location}[${index}]`, errors));
116
+ value.forEach((item, index) => validate(item, schema.items, `${location}[${index}]`, errors, rootSchema));
95
117
  }
96
118
  }
97
119
 
@@ -105,7 +127,7 @@ function validate(value, schema, location, errors) {
105
127
  const properties = schema.properties ?? {};
106
128
  for (const [key, child] of Object.entries(properties)) {
107
129
  if (Object.prototype.hasOwnProperty.call(value, key)) {
108
- validate(value[key], child, `${location}.${key}`, errors);
130
+ validate(value[key], child, `${location}.${key}`, errors, rootSchema);
109
131
  }
110
132
  }
111
133
 
@@ -118,7 +140,7 @@ function validate(value, schema, location, errors) {
118
140
  } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
119
141
  for (const key of Object.keys(value)) {
120
142
  if (!Object.prototype.hasOwnProperty.call(properties, key)) {
121
- validate(value[key], schema.additionalProperties, `${location}.${key}`, errors);
143
+ validate(value[key], schema.additionalProperties, `${location}.${key}`, errors, rootSchema);
122
144
  }
123
145
  }
124
146
  }
@@ -128,7 +150,7 @@ function validate(value, schema, location, errors) {
128
150
  export function validateSchema(value, schema, { label = "$" } = {}) {
129
151
  assertJsonLimits(value, label);
130
152
  const errors = [];
131
- validate(value, schema, label, errors);
153
+ validate(value, schema, label, errors, schema);
132
154
  return errors;
133
155
  }
134
156
 
@@ -32,6 +32,7 @@ export const TEMPLATE_PATHS = [
32
32
  "ORCHESTRATOR_INTEGRATION.md",
33
33
  "THREAT_MODEL.md",
34
34
  "CONTRACT_COVERAGE.md",
35
+ "PROTOCOL_INTEGRATION.md",
35
36
  "AGENT_COMPATIBILITY.md",
36
37
  "THIRD_PARTY_NOTICES.md",
37
38
  "LICENSE",
@@ -159,6 +159,31 @@ export function assertWorkStateSemantics(state) {
159
159
  throw new WorkStateError(`Invalid work-state transition: ${state.previousPhase} -> ${state.phase}`);
160
160
  }
161
161
  }
162
+ if (state.verificationCycle !== undefined && (!Number.isInteger(state.verificationCycle) || state.verificationCycle < 1)) {
163
+ throw new WorkStateError("verificationCycle must be a positive integer");
164
+ }
165
+ if (state.lastCompletionAttempt !== undefined) {
166
+ if (!state.lastCompletionAttempt || typeof state.lastCompletionAttempt !== "object" || Array.isArray(state.lastCompletionAttempt)) {
167
+ throw new WorkStateError("lastCompletionAttempt must be an object");
168
+ }
169
+ if (state.lastCompletionAttempt.status !== "REJECTED") {
170
+ throw new WorkStateError("lastCompletionAttempt.status must be REJECTED");
171
+ }
172
+ if (!Array.isArray(state.lastCompletionAttempt.reasonCodes)
173
+ || state.lastCompletionAttempt.reasonCodes.some((code) => typeof code !== "string" || !code.trim())) {
174
+ throw new WorkStateError("lastCompletionAttempt.reasonCodes must be an array of non-empty strings");
175
+ }
176
+ if (!Array.isArray(state.lastCompletionAttempt.missingRequirementIds)
177
+ || state.lastCompletionAttempt.missingRequirementIds.some((id) => typeof id !== "string" || !id.trim())) {
178
+ throw new WorkStateError("lastCompletionAttempt.missingRequirementIds must be an array of non-empty strings");
179
+ }
180
+ if (!Number.isInteger(state.lastCompletionAttempt.verificationCycle) || state.lastCompletionAttempt.verificationCycle < 1) {
181
+ throw new WorkStateError("lastCompletionAttempt.verificationCycle must be a positive integer");
182
+ }
183
+ if (typeof state.lastCompletionAttempt.timestamp !== "string" || !state.lastCompletionAttempt.timestamp.trim()) {
184
+ throw new WorkStateError("lastCompletionAttempt.timestamp must be a non-empty string");
185
+ }
186
+ }
162
187
  for (const failure of state.failures) {
163
188
  if (failure?.failureClass !== undefined) assertFailureClass(failure.failureClass);
164
189
  }
@@ -191,6 +216,8 @@ export function createWorkState(input) {
191
216
  verificationEvidence: [...(input.verificationEvidence ?? [])],
192
217
  lastUpdated: input.lastUpdated ?? new Date().toISOString(),
193
218
  };
219
+ if (input.verificationCycle !== undefined) state.verificationCycle = input.verificationCycle;
220
+ if (input.lastCompletionAttempt !== undefined) state.lastCompletionAttempt = structuredClone(input.lastCompletionAttempt);
194
221
  if (input.previousPhase !== undefined) state.previousPhase = input.previousPhase;
195
222
  if (input.diagnosedHypothesis !== undefined) state.diagnosedHypothesis = input.diagnosedHypothesis;
196
223
  return assertWorkStateSemantics(state);