@cassiomc1/forgeloop 0.1.2 → 0.1.3

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.
@@ -14,6 +14,11 @@ alwaysApply: true
14
14
  7. Diagnose failures before fixing them and do not repeat an attempt without new evidence.
15
15
  8. Request authority for unrelated external or destructive actions, installations, and publications. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
16
16
  9. After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
17
+
18
+ After implementation work for the current task is complete, run `forgeloop next`
19
+ before returning a final result. Follow the returned lifecycle action until
20
+ ForgeLoop reaches a terminal state or an explicit blocker.
21
+
17
22
  10. Report only demonstrated results and checks.
18
23
 
19
24
  Do not stop for non-blocking missing product details. When a safe, reversible
@@ -12,6 +12,11 @@ directory or file rules.
12
12
  - Diagnose the cause before fixing a failure; do not make random attempts.
13
13
  - Do not install unrelated software, publish, delete, migrate, or alter external state without authority. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
14
14
  - After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
15
+
16
+ After implementation work for the current task is complete, run `forgeloop next`
17
+ before returning a final result. Follow the returned lifecycle action until
18
+ ForgeLoop reaches a terminal state or an explicit blocker.
19
+
15
20
  - Report the result, checks actually run, limitations, and publication state.
16
21
 
17
22
  Do not stop for non-blocking missing product details. When a safe, reversible
package/AGENTS.md CHANGED
@@ -14,7 +14,11 @@ specific rule overrides them.
14
14
  9. Before implementation, create/validate `.forgeloop/current-contract.json`, persist deterministic routing, satisfy mandatory gates, and require `forgeloop preflight` to return `READY`.
15
15
  10. Before claiming `COMPLETE`, require `forgeloop complete` to return `VALID`; otherwise report completion as not protocol-verified.
16
16
  11. After implementation begins, do not return a final result in `EXECUTING`: advance through `VERIFYING`, record structured evidence, reach `REVIEWING`, prepare/update the execution receipt, and require `forgeloop complete` to return `VALID`. If closure cannot be reached, report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
17
- 12. Finish with the result, checks actually run, limitations, and publication state.
17
+
18
+ 12. After implementation work for the current task is complete, run `forgeloop next`
19
+ before returning a final result. Follow the returned lifecycle action until
20
+ ForgeLoop reaches a terminal state or an explicit blocker.
21
+ 13. Finish with the result, checks actually run, limitations, and publication state.
18
22
 
19
23
  Do not stop for non-blocking missing product details. When a safe, reversible
20
24
  local default exists, record it as an agent assumption and follow the Blocking vs Non-Blocking Decisions policy in `LOOP_ENGINEERING.md`. Ask only for
package/CLAUDE.md CHANGED
@@ -10,7 +10,11 @@ When working in this repository:
10
10
  6. Investigate the root cause of a failure before fixing it.
11
11
  7. Request authority for unrelated installation, publication, deletion, migration, or another external or destructive action. For a task-scoped missing Qwen-MM-Plugins capability, follow `LOOP_ENGINEERING.md`; API credentials and system dependencies remain separately gated.
12
12
  8. After implementation begins, do not return a final result in `EXECUTING`: follow `VERIFYING` → structured evidence → `REVIEWING` → execution receipt → validator-backed `COMPLETE`; otherwise report `BLOCKED` or `PARTIALLY VERIFIED` with exact findings.
13
- 9. Report current evidence, limitations, and publication state without claiming checks that were not run.
13
+
14
+ 9. After implementation work for the current task is complete, run `forgeloop next`
15
+ before returning a final result. Follow the returned lifecycle action until
16
+ ForgeLoop reaches a terminal state or an explicit blocker.
17
+ 10. Report current evidence, limitations, and publication state without claiming checks that were not run.
14
18
 
15
19
  Do not stop for non-blocking missing product details. When a safe, reversible
16
20
  local default exists, record it as an agent assumption and follow the Blocking vs Non-Blocking Decisions policy in `LOOP_ENGINEERING.md`. Ask only for
@@ -72,20 +72,44 @@ Implementation finished is not task finished, and tests executed are not the
72
72
  same as verification recorded. After implementation begins, do not stop or
73
73
  return the final result while the ForgeLoop task remains in `EXECUTING`.
74
74
 
75
+ ACT → QUERY NEXT → ACT → QUERY NEXT → … → TERMINAL
76
+
77
+ At each lifecycle boundary, query persisted state with `forgeloop next` before
78
+ choosing the next action. Always query after implementation, verification,
79
+ correction, and review. The query is advisory and read-only; the host agent
80
+ runs checks and applies the returned legal transition or repair action.
81
+
75
82
  ```text
76
83
  POST-IMPLEMENTATION CLOSURE
77
84
 
78
85
  EXECUTING
79
- advance
80
- VERIFYING
81
- run checks and record structured evidence
82
- REVIEWING
83
- prepare/update the execution receipt
84
- forgeloop complete
86
+ forgeloop next
87
+ advance --to VERIFYING
88
+ forgeloop next
89
+ prepare-completion
90
+
91
+ run applicable project checks
92
+
93
+ record observed results with record-check
94
+ ↓ forgeloop next
95
+ advance --to REVIEWING
96
+ ↓ forgeloop next
97
+ complete
85
98
  ↓ VALID
86
99
  COMPLETE
87
100
  ```
88
101
 
102
+ `prepare-completion` creates or refreshes the in-progress execution receipt.
103
+ It does not claim completion. The receipt is the structured container used by
104
+ subsequent `record-check` operations; completion remains invalid until required
105
+ observed evidence, review state, chronology, and validator requirements are
106
+ satisfied.
107
+
108
+ The host agent runs applicable checks after the receipt exists, records their
109
+ observed results with `record-check`, and queries `forgeloop next` before each
110
+ subsequent lifecycle action. `record-check` records results already observed by
111
+ the agent; it never executes the supplied command text.
112
+
89
113
  Continue until the terminal outcome is either validator-backed `COMPLETE` or
90
114
  an explicitly reported `BLOCKED` / `PARTIALLY VERIFIED` result with exact
91
115
  unresolved findings. Record verification evidence before review. If a required
@@ -15,6 +15,8 @@ policy are all present.
15
15
  | Portability | Node 20/22/24 Linux depth, OS smoke coverage, path/line-ending fixtures, and adapter compatibility evidence. |
16
16
  | Observability | `inspect`/`status`/`validate-protocol` shared derived state classification, real schema health, shared evidence, rich doctor findings, receipts, and no telemetry. |
17
17
  | Completion enforcement | Canonical contract, persisted route, guide-declared gates, preflight, phase ledger, structured checks, evidence coverage, `audit`, `report`, and `complete` validators. |
18
+ | Agent lifecycle navigation | Read-only `forgeloop next` decisions, stable action/reason output, persisted-state safety, and adapter guidance at lifecycle boundaries. |
19
+ | Execution → Verification handoff | Legal `EXECUTING` → `VERIFYING` transition, implementation-step reconciliation, and preservation of verification evidence. |
18
20
  | Pre-contract autonomy | Blocking vs Non-Blocking Decisions policy, explicit ASSUMPTION / source=agent-default recording, safe-boundary invariant, positive and negative tests, and a blind run showing non-blocking ambiguity continues while blocking ambiguity remains user-gated. |
19
21
  | Resume/checkpoint | Atomic local state, contract/HEAD/artifact freshness, age warning, schema/secret validation, status, safe validation, and bounded clearing without persisting derived freshness fields. |
20
22
  | Multi-agent coordination | Self-contained briefs, write/write and write/read ownership checks, dependency-set validation, reviewer independence, normalized results, and inline fallback. |
@@ -35,6 +37,16 @@ policy are all present.
35
37
  - Literal graph runtime and runtime multi-agent orchestration are `N/A by
36
38
  design`; compatible harnesses own those capabilities.
37
39
 
40
+ ## Third-run conformance position
41
+
42
+ | Dimension | Classification |
43
+ | --- | --- |
44
+ | Execution → Verification | REPRODUCED FAILURE before this fix |
45
+ | Verification serialization | NOT_REACHED |
46
+ | Review transition | NOT_REACHED |
47
+ | Receipt generation | NOT_REACHED |
48
+ | Full conformance | PARTIAL |
49
+
38
50
  ## Evidence matrix
39
51
 
40
52
  The score is evidence-backed only when the contract and its executable proof
package/README.md CHANGED
@@ -137,6 +137,8 @@ Protocol-support commands are local and do not invoke an agent or model:
137
137
  npx @cassiomc1/forgeloop route --work complete-website --surface ui --risk untrusted-input
138
138
  npx @cassiomc1/forgeloop activate
139
139
  npx @cassiomc1/forgeloop preflight --json
140
+ npx @cassiomc1/forgeloop next
141
+ npx @cassiomc1/forgeloop next --json
140
142
  npx @cassiomc1/forgeloop advance --to EXECUTING
141
143
  npx @cassiomc1/forgeloop advance --to VERIFYING
142
144
  npx @cassiomc1/forgeloop prepare-completion --json
@@ -155,6 +157,25 @@ npx @cassiomc1/forgeloop validate-receipt --file ./execution-receipt.json --json
155
157
  npx @cassiomc1/forgeloop validate-protocol --route-file ./routing-result.json --state-file .forgeloop/work-state.json --receipt-file ./execution-receipt.json --contract-file .forgeloop/current-contract.json --json
156
158
  ```
157
159
 
160
+ The query-driven post-implementation path is:
161
+
162
+ ```text
163
+ implementation
164
+ → forgeloop next
165
+ → advance --to VERIFYING
166
+ → forgeloop next
167
+ → prepare-completion
168
+ → forgeloop next
169
+ → checks + record-check
170
+ → forgeloop next
171
+ → advance --to REVIEWING
172
+ → forgeloop next
173
+ → complete
174
+ ```
175
+
176
+ `forgeloop next` and `forgeloop next --json` read persisted state only. They do
177
+ not run project checks or mutate protocol artifacts.
178
+
158
179
  `route` expands declared signals into deterministic guide IDs and reason codes.
159
180
  `activate` records a session marker without storing prompts or hidden reasoning.
160
181
  Before implementation, write the canonical contract, persist the route, create
@@ -8,6 +8,7 @@ Run a scenario in a disposable target using the Standard profile first:
8
8
 
9
9
  ```bash
10
10
  npx @cassiomc1/forgeloop preflight --json
11
+ npx @cassiomc1/forgeloop next --json
11
12
  npx @cassiomc1/forgeloop audit --json
12
13
  npx @cassiomc1/forgeloop complete --json
13
14
  ```
@@ -15,12 +16,16 @@ npx @cassiomc1/forgeloop complete --json
15
16
  The expected post-implementation path is:
16
17
 
17
18
  ```text
18
- EXECUTING
19
+ implementation
20
+ → forgeloop next
19
21
  → advance --to VERIFYING
22
+ → forgeloop next
20
23
  → prepare-completion
21
- record-check for each observed check
24
+ forgeloop next
25
+ → checks + record-check
26
+ → forgeloop next
22
27
  → advance --to REVIEWING
23
- audit
28
+ forgeloop next
24
29
  → complete
25
30
  ```
26
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cassiomc1/forgeloop",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Portable, verifiable instruction kit for AI agents and developers",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.js CHANGED
@@ -24,12 +24,13 @@ import { formatPolicyResult, runPolicy } from "./commands/policy.js";
24
24
  import { formatBundleResult, runBundle } from "./commands/bundle.js";
25
25
  import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
26
26
  import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
27
+ import { formatNextActionResult, runNext } from "./commands/next.js";
27
28
  import { resolveTarget } from "./core/filesystem.js";
28
29
  import { getPackageRoot } from "./core/templates.js";
29
30
  import { ARTIFACT_PATHS } from "./core/artifacts.js";
30
31
 
31
32
  function usage(command = null) {
32
- const commands = "init|doctor|update|activate|route|preflight|advance|prepare-completion|record-check|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
33
+ const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
33
34
  const options = [" --path <directory> target project directory (default: current directory)"];
34
35
  if (!command || command === "init" || command === "update") {
35
36
  options.push(" --dry-run show planned writes without changing files");
@@ -51,7 +52,7 @@ function usage(command = null) {
51
52
  if (!command || command === "advance") {
52
53
  options.push(" --to <phase> destination workflow phase");
53
54
  }
54
- if (!command || ["activate", "advance", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
55
+ if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
55
56
  options.push(" --json emit structured output as JSON");
56
57
  }
57
58
  if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
@@ -132,7 +133,7 @@ export function parseArgs(argv) {
132
133
 
133
134
  for (let index = 0; index < argv.length; index += 1) {
134
135
  const argument = argv[index];
135
- if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "prepare-completion", "record-check", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
136
+ if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
136
137
  if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
137
138
  command = argument;
138
139
  } else if (argument === "--help" || argument === "-h") {
@@ -214,6 +215,10 @@ export function parseArgs(argv) {
214
215
  if (!id || id.startsWith("-")) throw new Error("--id requires a check ID");
215
216
  options.checkId = id;
216
217
  index += 1;
218
+ } else if (argument.startsWith("--id=")) {
219
+ const id = argument.slice("--id=".length);
220
+ if (!id || id.startsWith("-")) throw new Error("--id requires a check ID");
221
+ options.checkId = id;
217
222
  } else if (argument === "--kind") {
218
223
  const kind = argv[index + 1];
219
224
  if (!kind || kind.startsWith("-")) throw new Error("--kind requires a check kind");
@@ -224,6 +229,10 @@ export function parseArgs(argv) {
224
229
  if (!requirement || requirement.startsWith("-")) throw new Error("--requirement requires an evidence target");
225
230
  options.checkRequirement = requirement;
226
231
  index += 1;
232
+ } else if (argument.startsWith("--requirement=")) {
233
+ const requirement = argument.slice("--requirement=".length);
234
+ if (!requirement) throw new Error("--requirement requires an evidence target");
235
+ options.checkRequirement = requirement;
227
236
  } else if (argument === "--status") {
228
237
  const status = argv[index + 1];
229
238
  if (!status || status.startsWith("-")) throw new Error("--status requires a check status");
@@ -244,6 +253,10 @@ export function parseArgs(argv) {
244
253
  if (!resultText || resultText.startsWith("-")) throw new Error("--result requires recorded text");
245
254
  options.checkResult = resultText;
246
255
  index += 1;
256
+ } else if (argument.startsWith("--result=")) {
257
+ const resultText = argument.slice("--result=".length);
258
+ if (!resultText) throw new Error("--result requires recorded text");
259
+ options.checkResult = resultText;
247
260
  } else if (argument === "--exit-code") {
248
261
  const exitCode = argv[index + 1];
249
262
  if (!exitCode || exitCode.startsWith("-")) throw new Error("--exit-code requires a non-negative integer");
@@ -282,7 +295,7 @@ export function parseArgs(argv) {
282
295
 
283
296
  if (!command) return { command: null, options };
284
297
 
285
- const jsonCommands = ["doctor", "route", "activate", "advance", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
298
+ const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
286
299
  if (!jsonCommands.includes(command) && options.json) {
287
300
  throw new Error(`Option --json is not valid for ${command}`);
288
301
  }
@@ -436,6 +449,12 @@ export async function main(argv = process.argv.slice(2)) {
436
449
  return 0;
437
450
  }
438
451
 
452
+ if (command === "next") {
453
+ const result = await runNext({ target, packageRoot });
454
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatNextActionResult(result));
455
+ return 0;
456
+ }
457
+
439
458
  if (command === "prepare-completion") {
440
459
  const result = await runPrepareCompletion({ target, packageRoot });
441
460
  console.log(options.json ? JSON.stringify(result, null, 2) : formatPrepareCompletionResult(result));
@@ -451,8 +470,8 @@ export async function main(argv = process.argv.slice(2)) {
451
470
  requirement: options.checkRequirement,
452
471
  status: options.checkStatus,
453
472
  evidenceKind: options.checkEvidenceKind,
454
- command: options.checkCommand,
455
- result: options.checkResult,
473
+ command: options.checkCommand ?? undefined,
474
+ result: options.checkResult ?? undefined,
456
475
  exitCode: options.checkExitCode,
457
476
  details: options.checkDetails ?? undefined,
458
477
  });
@@ -0,0 +1,29 @@
1
+ import { getNextAction } from "../core/next-action.js";
2
+
3
+ export async function runNext({ target, packageRoot }) {
4
+ return getNextAction({ target, packageRoot });
5
+ }
6
+
7
+ export function formatNextActionResult(result) {
8
+ const lines = [
9
+ `FORGELOOP NEXT: ${result.nextAction}`,
10
+ `PHASE: ${result.currentPhase}`,
11
+ ];
12
+ if (result.reasons.length > 0) {
13
+ lines.push("REASONS:");
14
+ lines.push(...result.reasons.map((reason) => `- ${reason.code}: ${reason.message}`));
15
+ }
16
+ if (result.commands.length > 0) {
17
+ lines.push("COMMANDS (SAFE SYNOPSIS ONLY):");
18
+ lines.push(...result.commands.map((command) => `- ${command}`));
19
+ }
20
+ if (result.commandSpecs.length > 0) {
21
+ lines.push("STRUCTURED COMMAND SPECS: Available in --json output; direct-process argv data, not shell syntax.");
22
+ }
23
+ if (result.missingArtifacts.length > 0) {
24
+ lines.push("MISSING ARTIFACTS:");
25
+ lines.push(...result.missingArtifacts.map((artifact) => `- ${artifact}`));
26
+ }
27
+ if (result.terminal) lines.push("STATE: TERMINAL");
28
+ return `${lines.join("\n")}\n`;
29
+ }
@@ -5,16 +5,18 @@ import {
5
5
  writeJsonArtifact,
6
6
  } from "./artifacts.js";
7
7
  import { readContract } from "./contract.js";
8
- import { appendProtocolEvent } from "./events.js";
8
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
9
9
  import { completionEvidenceForGuides } from "./guide-metadata.js";
10
10
  import { createCheck } from "./checks.js";
11
11
  import { createEvidence } from "./evidence.js";
12
12
  import { coverageForRequirements } from "./coverage.js";
13
+ import { assertCompletionRelationships } from "./completion-relationships.js";
13
14
  import { evaluatePreflight } from "./preflight.js";
14
15
  import { currentChangedPaths } from "./repository.js";
15
16
  import { readPersistedRoute } from "./route-artifact.js";
16
17
  import { createReceipt, validateReceipt } from "./receipt.js";
17
18
  import { readWorkState, writeWorkState } from "./work-state.js";
19
+ import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
18
20
 
19
21
  function artifactError(code, message, artifacts = []) {
20
22
  const error = new Error(message);
@@ -72,6 +74,9 @@ export async function prepareCompletion({ target, packageRoot }) {
72
74
  if (!state) {
73
75
  throw artifactError("E_STATE_MISSING", "Work state is required before preparing completion", [ARTIFACT_PATHS.state]);
74
76
  }
77
+ if (hasExecutionStarted(state.phase)) {
78
+ await assertExecutionPrerequisites({ target, state, packageRoot });
79
+ }
75
80
 
76
81
  let existing = null;
77
82
  try {
@@ -90,18 +95,25 @@ export async function prepareCompletion({ target, packageRoot }) {
90
95
  additionalEvidence: preflight.policy?.requiredEvidence ?? [],
91
96
  });
92
97
  const existingValue = existing?.value ?? {};
98
+ assertCompletionRelationships({
99
+ contract,
100
+ route,
101
+ state,
102
+ receipt: existingValue.taskId ? existingValue : null,
103
+ requiredEvidence,
104
+ requireRequiredChecks: false,
105
+ });
93
106
  const changedPaths = existing
94
107
  ? [...(existingValue.changedPaths ?? [])]
95
108
  : (await currentChangedPaths(target) ?? []);
96
- const checks = [...(existingValue.checks ?? [])];
109
+ const checks = existing ? [...existingValue.checks] : [...state.checks];
110
+ const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
97
111
  const receipt = await createReceipt({
98
112
  ...existingValue,
99
113
  taskId: contract.value.taskId,
100
114
  contractFingerprint: contract.fingerprint,
101
115
  routeFingerprint: route.fingerprint,
102
- ...(Object.hasOwn(existingValue, "stateFingerprint")
103
- ? { stateFingerprint: canonicalFingerprint(state) }
104
- : {}),
116
+ stateFingerprint: canonicalFingerprint(state),
105
117
  status: existingValue.status ?? "in-progress",
106
118
  taskStatus: existingValue.taskStatus ?? "in-progress",
107
119
  verificationStatus: existingValue.verificationStatus ?? "not-verified",
@@ -110,7 +122,7 @@ export async function prepareCompletion({ target, packageRoot }) {
110
122
  selectedGuides: [...route.value.guides],
111
123
  changedPaths,
112
124
  checks,
113
- evidence: [...(existingValue.evidence ?? [])],
125
+ evidence,
114
126
  evidenceCoverage: coverageForRequirements(requiredEvidence, checks),
115
127
  review: existingValue.review ?? { status: "not-run", independent: false },
116
128
  limitations: [...(existingValue.limitations ?? [])],
@@ -196,6 +208,7 @@ export async function recordCheck({
196
208
  [ARTIFACT_PATHS.state],
197
209
  );
198
210
  }
211
+ await assertExecutionPrerequisites({ target, state, packageRoot });
199
212
 
200
213
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
201
214
  await validateReceipt(existingReceipt.value, packageRoot);
@@ -234,6 +247,26 @@ export async function recordCheck({
234
247
  packageRoot,
235
248
  additionalEvidence: preflight.policy?.requiredEvidence ?? [],
236
249
  });
250
+ assertCompletionRelationships({
251
+ contract,
252
+ route,
253
+ state,
254
+ receipt: existingReceipt.value,
255
+ requiredEvidence,
256
+ requireRequiredChecks: false,
257
+ });
258
+ const ledger = await validateEventLedger(target, packageRoot);
259
+ if (!ledger.valid) {
260
+ const first = ledger.errors[0];
261
+ throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
262
+ }
263
+ if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
264
+ throw artifactError(
265
+ "E_PHASE_CHRONOLOGY_INVALID",
266
+ "record-check requires VERIFICATION_STARTED in the current task ledger",
267
+ [ARTIFACT_PATHS.events],
268
+ );
269
+ }
237
270
  const coverage = coverageForRequirements(requiredEvidence, checks);
238
271
  const nextState = {
239
272
  ...state,
@@ -247,11 +280,18 @@ export async function recordCheck({
247
280
  checks,
248
281
  evidence: evidenceList,
249
282
  evidenceCoverage: coverage,
250
- ...(Object.hasOwn(existingReceipt.value, "stateFingerprint")
251
- ? { stateFingerprint: canonicalFingerprint(nextState) }
252
- : {}),
283
+ stateFingerprint: canonicalFingerprint(nextState),
253
284
  }, packageRoot);
254
285
 
286
+ assertCompletionRelationships({
287
+ contract,
288
+ route,
289
+ state: nextState,
290
+ receipt: nextReceipt,
291
+ requiredEvidence,
292
+ requireRequiredChecks: false,
293
+ });
294
+
255
295
  await writeWorkState(target, nextState, { packageRoot });
256
296
  const written = await writeJsonArtifact(
257
297
  target,
@@ -0,0 +1,141 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
2
+ import { assertCheckList, requiredChecksSatisfied } from "./checks.js";
3
+ import { assertCoverageList, coverageForRequirements } from "./coverage.js";
4
+ import { assertEvidenceList } from "./evidence.js";
5
+
6
+ function issue(code, message, artifacts = []) {
7
+ return { code, message, artifacts };
8
+ }
9
+
10
+ function sameValue(left, right) {
11
+ return canonicalFingerprint(left) === canonicalFingerprint(right);
12
+ }
13
+
14
+ function addAssertion(errors, assertion, code, artifacts) {
15
+ try {
16
+ assertion();
17
+ } catch (error) {
18
+ errors.push(issue(code, error.message, artifacts));
19
+ }
20
+ }
21
+
22
+ export function stateIdentityErrors({
23
+ contract,
24
+ route,
25
+ state,
26
+ } = {}) {
27
+ const errors = [];
28
+ const contractValue = contract?.value ?? contract;
29
+ const contractFingerprint = contract?.fingerprint;
30
+ if (contractValue && state && contractValue.taskId !== state.taskId) {
31
+ errors.push(issue("E_STATE_TASK_MISMATCH", "Work state does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
32
+ }
33
+ if (contractFingerprint && state && state.contractFingerprint !== contractFingerprint) {
34
+ errors.push(issue("E_CONTRACT_STALE", "Work state does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
35
+ }
36
+ if (contractFingerprint && route && route.value.contractFingerprint !== contractFingerprint) {
37
+ errors.push(issue("E_ROUTE_STALE", "Routing result does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route]));
38
+ }
39
+ if (route && state && route.value.contractFingerprint !== undefined
40
+ && route.value.contractFingerprint !== state.contractFingerprint) {
41
+ errors.push(issue("E_ROUTE_STALE", "Routing result does not match the work state contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
42
+ }
43
+ if (route && state && (state.routeFingerprint !== route.fingerprint || !sameValue(state.selectedGuides, route.value.guides))) {
44
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Work state does not match the persisted route identity", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
45
+ }
46
+ return errors;
47
+ }
48
+
49
+ export function assertStateIdentity(input) {
50
+ const errors = stateIdentityErrors(input);
51
+ if (errors.length === 0) return;
52
+ const first = errors[0];
53
+ const error = new Error(first.message);
54
+ error.code = first.code;
55
+ error.artifacts = first.artifacts;
56
+ throw error;
57
+ }
58
+
59
+ export function completionRelationshipErrors({
60
+ contract,
61
+ route,
62
+ state,
63
+ receipt,
64
+ requiredEvidence = [],
65
+ requireReceiptStateFingerprint = true,
66
+ requireRequiredChecks = true,
67
+ } = {}) {
68
+ const errors = stateIdentityErrors({ contract, route, state });
69
+ const contractValue = contract?.value ?? contract;
70
+ const contractFingerprint = contract?.fingerprint;
71
+ if (contractValue && receipt && contractValue.taskId !== receipt.taskId) {
72
+ errors.push(issue("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
73
+ }
74
+ if (route && receipt && (receipt.routeFingerprint !== route.fingerprint || !sameValue(receipt.selectedGuides, route.value.guides))) {
75
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Execution receipt does not match the persisted route identity", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
76
+ }
77
+ if (contractFingerprint && receipt && receipt.contractFingerprint !== contractFingerprint) {
78
+ errors.push(issue("E_RECEIPT_CONTRACT_MISMATCH", "Execution receipt does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
79
+ }
80
+
81
+ if (state) {
82
+ addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
83
+ addAssertion(errors, () => assertEvidenceList(state.verificationEvidence, "work-state.verificationEvidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.state]);
84
+ }
85
+ if (receipt) {
86
+ addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
87
+ addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
88
+ if (requireRequiredChecks) {
89
+ for (const error of requiredChecksSatisfied(receipt.checks, requiredEvidence)) {
90
+ errors.push(issue(error.code, error.message, [ARTIFACT_PATHS.receipt]));
91
+ }
92
+ }
93
+ }
94
+
95
+ const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks) : [];
96
+ if (state?.evidenceCoverage !== undefined) {
97
+ addAssertion(errors, () => assertCoverageList(state.evidenceCoverage, "work-state.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.state]);
98
+ if (!sameValue(state.evidenceCoverage, expectedCoverage)) {
99
+ errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Work state evidence coverage does not match its checks", [ARTIFACT_PATHS.state]));
100
+ }
101
+ }
102
+ if (receipt) {
103
+ if (receipt.evidenceCoverage === undefined) {
104
+ errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Execution receipt requires evidence coverage", [ARTIFACT_PATHS.receipt]));
105
+ } else {
106
+ addAssertion(errors, () => assertCoverageList(receipt.evidenceCoverage, "receipt.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.receipt]);
107
+ if (!sameValue(receipt.evidenceCoverage, expectedCoverage)) {
108
+ errors.push(issue("E_EVIDENCE_COVERAGE_INVALID", "Execution receipt evidence coverage does not match current checks", [ARTIFACT_PATHS.receipt]));
109
+ }
110
+ }
111
+ }
112
+
113
+ if (state && receipt) {
114
+ if (requireReceiptStateFingerprint && receipt.stateFingerprint === undefined) {
115
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
116
+ } else if (receipt.stateFingerprint !== undefined && receipt.stateFingerprint !== canonicalFingerprint(state)) {
117
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
118
+ }
119
+ if (!sameValue(state.checks, receipt.checks)) {
120
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt checks diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
121
+ }
122
+ if (!sameValue(state.verificationEvidence, receipt.evidence ?? [])) {
123
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt evidence diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
124
+ }
125
+ if (state.evidenceCoverage !== undefined && receipt.evidenceCoverage !== undefined
126
+ && !sameValue(state.evidenceCoverage, receipt.evidenceCoverage)) {
127
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt coverage diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
128
+ }
129
+ }
130
+ return errors;
131
+ }
132
+
133
+ export function assertCompletionRelationships(input) {
134
+ const errors = completionRelationshipErrors(input);
135
+ if (errors.length === 0) return;
136
+ const first = errors[0];
137
+ const error = new Error(first.message);
138
+ error.code = first.code;
139
+ error.artifacts = first.artifacts;
140
+ throw error;
141
+ }