@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.
@@ -0,0 +1,709 @@
1
+ import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
2
+ import { completionIdentityErrors, evaluateCompletion } from "./completion.js";
3
+ import { requiredEvidenceForTarget } from "./completion-artifacts.js";
4
+ import { coverageForRequirements } from "./coverage.js";
5
+ import { readContract } from "./contract.js";
6
+ import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
7
+ import { PROTOCOL_VERSION } from "./protocol.js";
8
+ import { validateReceipt } from "./receipt.js";
9
+ import { readPersistedRoute } from "./route-artifact.js";
10
+ import { assertCheckList } from "./checks.js";
11
+ import { completionRelationshipErrors } from "./completion-relationships.js";
12
+ import { classifyLoadedWorkState, readWorkState } from "./work-state.js";
13
+ import { sha256 } from "./manifest.js";
14
+ import {
15
+ evaluateStartExecutionPrerequisites,
16
+ PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE,
17
+ } from "./execution-prerequisites.js";
18
+
19
+ export const NEXT_ACTIONS = Object.freeze({
20
+ DISCOVER: "DISCOVER",
21
+ CREATE_CONTRACT: "CREATE_CONTRACT",
22
+ ROUTE: "ROUTE",
23
+ SATISFY_GATES: "SATISFY_GATES",
24
+ RUN_PREFLIGHT: "RUN_PREFLIGHT",
25
+ PLAN: "PLAN",
26
+ START_EXECUTION: "START_EXECUTION",
27
+ ENTER_VERIFYING: "ENTER_VERIFYING",
28
+ RECORD_VERIFICATION: "RECORD_VERIFICATION",
29
+ DIAGNOSE: "DIAGNOSE",
30
+ CORRECT: "CORRECT",
31
+ ENTER_REVIEWING: "ENTER_REVIEWING",
32
+ PREPARE_COMPLETION: "PREPARE_COMPLETION",
33
+ RUN_COMPLETE: "RUN_COMPLETE",
34
+ RESOLVE_STALE_ROUTE: "RESOLVE_STALE_ROUTE",
35
+ RESOLVE_BLOCKER: "RESOLVE_BLOCKER",
36
+ NONE: "NONE",
37
+ });
38
+
39
+ const PHASES_REQUIRING_EXECUTION_CHRONOLOGY = new Set([
40
+ "EXECUTING",
41
+ "VERIFYING",
42
+ "DIAGNOSING",
43
+ "CORRECTING",
44
+ "REVIEWING",
45
+ "COMPLETE",
46
+ ]);
47
+
48
+ function artifactError(code, message, artifacts = []) {
49
+ return { code, message, artifacts };
50
+ }
51
+
52
+ function uniqueSorted(values) {
53
+ return [...new Set(values.filter(Boolean))].sort();
54
+ }
55
+
56
+ function result({
57
+ taskId = "unknown",
58
+ currentPhase = "RECEIVED",
59
+ nextAction,
60
+ reasons = [],
61
+ commands = [],
62
+ commandSpecs = [],
63
+ requiredArtifacts = [],
64
+ missingArtifacts = [],
65
+ }) {
66
+ const normalizedReasons = reasons
67
+ .map((reason) => ({
68
+ code: reason.code ?? "E_NEXT_ACTION_BLOCKED",
69
+ message: reason.message ?? String(reason),
70
+ artifacts: uniqueSorted(reason.artifacts ?? []),
71
+ }))
72
+ .sort((left, right) => left.code.localeCompare(right.code)
73
+ || left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
74
+ || left.message.localeCompare(right.message));
75
+
76
+ return {
77
+ schemaVersion: 1,
78
+ protocolVersion: PROTOCOL_VERSION,
79
+ taskId,
80
+ currentPhase,
81
+ nextAction,
82
+ terminal: nextAction === NEXT_ACTIONS.NONE,
83
+ reasonCodes: uniqueSorted(normalizedReasons.map((reason) => reason.code)),
84
+ reasons: normalizedReasons,
85
+ commands: uniqueSorted(commands),
86
+ commandSpecs: [...new Map(commandSpecs.map((spec) => [JSON.stringify(spec), spec])).values()]
87
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))),
88
+ requiredArtifacts: uniqueSorted(requiredArtifacts),
89
+ missingArtifacts: uniqueSorted(missingArtifacts),
90
+ };
91
+ }
92
+
93
+ function commandFor(action) {
94
+ return {
95
+ [NEXT_ACTIONS.PLAN]: "forgeloop advance --to PLANNED",
96
+ [NEXT_ACTIONS.RUN_PREFLIGHT]: "forgeloop preflight --json",
97
+ [NEXT_ACTIONS.START_EXECUTION]: "forgeloop advance --to EXECUTING",
98
+ [NEXT_ACTIONS.ENTER_VERIFYING]: "forgeloop advance --to VERIFYING",
99
+ [NEXT_ACTIONS.DIAGNOSE]: "forgeloop advance --to DIAGNOSING",
100
+ [NEXT_ACTIONS.CORRECT]: "forgeloop advance --to CORRECTING",
101
+ [NEXT_ACTIONS.ENTER_REVIEWING]: "forgeloop advance --to REVIEWING",
102
+ [NEXT_ACTIONS.PREPARE_COMPLETION]: "forgeloop prepare-completion --json",
103
+ [NEXT_ACTIONS.RUN_COMPLETE]: "forgeloop complete --json",
104
+ }[action];
105
+ }
106
+
107
+ function recordCheckCommandSpec(requirement) {
108
+ const checkId = `requirement-${sha256(Buffer.from(requirement)).slice(0, 16)}`;
109
+ return {
110
+ commandId: "record-check",
111
+ executable: "forgeloop",
112
+ subcommand: "record-check",
113
+ argv: ["record-check", `--id=${checkId}`, `--requirement=${requirement}`],
114
+ requiredInputs: [
115
+ { name: "status", option: "--status=<passed|failed|blocked|not-run>" },
116
+ { name: "evidenceKind", option: "--evidence-kind=<OBSERVED|INFERRED|NOT_VERIFIED|BLOCKED>" },
117
+ { name: "result", option: "--result=<text>" },
118
+ { name: "exitCode", option: "--exit-code=<number>", optional: true },
119
+ ],
120
+ };
121
+ }
122
+
123
+ function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = []) {
124
+ const command = commandFor(action);
125
+ return result({
126
+ ...input,
127
+ nextAction: action,
128
+ reasons: [reason],
129
+ ...(command ? { commands: [command] } : {}),
130
+ requiredArtifacts,
131
+ missingArtifacts,
132
+ });
133
+ }
134
+
135
+ function missingArtifact(error, fallback) {
136
+ return error?.code === "ARTIFACT_MISSING"
137
+ ? (error.artifacts?.length ? error.artifacts : [fallback])
138
+ : [];
139
+ }
140
+
141
+ function allCoverageCovered(coverage) {
142
+ return coverage.every((item) => item.status === "COVERED");
143
+ }
144
+
145
+ function staleReasons(state, contract, route) {
146
+ const reasons = [];
147
+ if (state.contractFingerprint !== contract.fingerprint) {
148
+ reasons.push(artifactError(
149
+ "E_CONTRACT_STALE",
150
+ "Work state references a different current contract",
151
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
152
+ ));
153
+ }
154
+ if (route.value.contractFingerprint !== undefined && route.value.contractFingerprint !== contract.fingerprint) {
155
+ reasons.push(artifactError(
156
+ "E_ROUTE_STALE",
157
+ "Routing result references a different current contract",
158
+ [ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract],
159
+ ));
160
+ }
161
+ if (state.routeFingerprint !== route.fingerprint) {
162
+ reasons.push(artifactError(
163
+ "E_ROUTE_STALE",
164
+ "Work state references a different routing result",
165
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
166
+ ));
167
+ }
168
+ if (JSON.stringify(state.selectedGuides) !== JSON.stringify(route.value.guides)) {
169
+ reasons.push(artifactError(
170
+ "E_ROUTE_STALE",
171
+ "Work state guides do not match the persisted routing result",
172
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
173
+ ));
174
+ }
175
+ return reasons;
176
+ }
177
+
178
+ function freshnessReasons(state, classification) {
179
+ const requiredArtifactPaths = state.requiredArtifacts?.map((artifact) => artifact.path) ?? [];
180
+ const reasons = [artifactError(
181
+ "E_STATE_REVALIDATION_REQUIRED",
182
+ `Work-state checkpoint requires revalidation: ${classification.reasons.join(", ")}`,
183
+ [ARTIFACT_PATHS.state],
184
+ )];
185
+ for (const reason of classification.reasons) {
186
+ const contractRelated = ["CONTRACT_CHANGED", "CONTRACT_INVALID", "CONTRACT_NOT_VERIFIED"].includes(reason);
187
+ const artifactRelated = reason.startsWith("REQUIRED_ARTIFACT");
188
+ reasons.push(artifactError(
189
+ contractRelated ? "E_CONTRACT_STALE" : artifactRelated ? "E_REQUIRED_ARTIFACT_STALE" : "E_REPOSITORY_CHANGED",
190
+ `Work-state freshness check failed: ${reason}`,
191
+ uniqueSorted([
192
+ ARTIFACT_PATHS.state,
193
+ ...(contractRelated ? [ARTIFACT_PATHS.contract] : []),
194
+ ...(artifactRelated ? requiredArtifactPaths : []),
195
+ ]),
196
+ ));
197
+ }
198
+ return reasons;
199
+ }
200
+
201
+ function checkListReasons(state) {
202
+ try {
203
+ assertCheckList(state.checks, "work-state.checks");
204
+ return [];
205
+ } catch (error) {
206
+ return [artifactError(
207
+ error.code ?? "E_CHECK_INVALID",
208
+ error.message,
209
+ [ARTIFACT_PATHS.state],
210
+ )];
211
+ }
212
+ }
213
+
214
+ async function loadArtifact(loader, fallback) {
215
+ try {
216
+ return { value: await loader(), error: null, missingArtifacts: [] };
217
+ } catch (error) {
218
+ return { value: null, error, missingArtifacts: missingArtifact(error, fallback) };
219
+ }
220
+ }
221
+
222
+ async function requirementsAndCoverage({ target, packageRoot, contract, route, checks, additionalEvidence = [] }) {
223
+ const requirements = await requiredEvidenceForTarget({
224
+ target,
225
+ contract,
226
+ route,
227
+ packageRoot,
228
+ additionalEvidence,
229
+ });
230
+ return { requirements, coverage: coverageForRequirements(requirements, checks) };
231
+ }
232
+
233
+ export async function getNextAction({ target, packageRoot } = {}) {
234
+ const workState = await loadArtifact(
235
+ () => readWorkState(target, packageRoot),
236
+ ARTIFACT_PATHS.state,
237
+ );
238
+ if (workState.error) {
239
+ return decision(
240
+ {},
241
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
242
+ artifactError("WORK_STATE_INVALID", workState.error.message, [ARTIFACT_PATHS.state]),
243
+ [ARTIFACT_PATHS.state],
244
+ workState.missingArtifacts,
245
+ );
246
+ }
247
+ if (!workState.value) {
248
+ return decision(
249
+ {},
250
+ NEXT_ACTIONS.DISCOVER,
251
+ artifactError("WORK_STATE_ABSENT", "No work-state checkpoint is present", [ARTIFACT_PATHS.state]),
252
+ [ARTIFACT_PATHS.state],
253
+ [ARTIFACT_PATHS.state],
254
+ );
255
+ }
256
+
257
+ const state = workState.value;
258
+ const context = { taskId: state.taskId, currentPhase: state.phase };
259
+ if (state.phase === "RECEIVED") {
260
+ return decision(
261
+ context,
262
+ NEXT_ACTIONS.DISCOVER,
263
+ artifactError("PHASE_RECEIVED", "Discovery has not started"),
264
+ [ARTIFACT_PATHS.state],
265
+ );
266
+ }
267
+ if (state.phase === "DISCOVERING") {
268
+ return decision(
269
+ context,
270
+ NEXT_ACTIONS.CREATE_CONTRACT,
271
+ artifactError("PHASE_DISCOVERING", "Create and validate the task contract"),
272
+ [ARTIFACT_PATHS.state],
273
+ );
274
+ }
275
+ const contractResult = await loadArtifact(
276
+ () => readContract(target, packageRoot),
277
+ ARTIFACT_PATHS.contract,
278
+ );
279
+ const contractArtifacts = [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract];
280
+
281
+ if (contractResult.error) {
282
+ return result({
283
+ ...context,
284
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
285
+ reasons: [artifactError(
286
+ contractResult.error.code === "ARTIFACT_MISSING" ? "E_CONTRACT_MISSING" : "E_CONTRACT_INVALID",
287
+ contractResult.error.message,
288
+ contractResult.error.artifacts ?? [],
289
+ )],
290
+ requiredArtifacts: contractArtifacts,
291
+ missingArtifacts: contractResult.missingArtifacts,
292
+ });
293
+ }
294
+
295
+ const contract = contractResult.value;
296
+ const identityErrors = completionIdentityErrors({ contract: contract.value, state });
297
+ if (identityErrors.length > 0) {
298
+ return result({
299
+ ...context,
300
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
301
+ reasons: identityErrors,
302
+ requiredArtifacts: contractArtifacts,
303
+ });
304
+ }
305
+ const freshness = await classifyLoadedWorkState({
306
+ target,
307
+ state,
308
+ contractFile: ARTIFACT_PATHS.contract,
309
+ });
310
+ if (freshness.status === "REVALIDATION_REQUIRED") {
311
+ return result({
312
+ ...context,
313
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
314
+ reasons: freshnessReasons(state, freshness),
315
+ requiredArtifacts: uniqueSorted([
316
+ ARTIFACT_PATHS.state,
317
+ ARTIFACT_PATHS.contract,
318
+ ...(state.requiredArtifacts?.map((artifact) => artifact.path) ?? []),
319
+ ]),
320
+ });
321
+ }
322
+ if (state.phase === "CONTRACT_READY") {
323
+ return decision(
324
+ context,
325
+ NEXT_ACTIONS.ROUTE,
326
+ artifactError("PHASE_CONTRACT_READY", "Persist deterministic routing for the validated contract"),
327
+ contractArtifacts,
328
+ [ARTIFACT_PATHS.route],
329
+ );
330
+ }
331
+ const routeResult = await loadArtifact(
332
+ () => readPersistedRoute(target, packageRoot),
333
+ ARTIFACT_PATHS.route,
334
+ );
335
+ const requiredArtifacts = [...contractArtifacts, ARTIFACT_PATHS.route];
336
+ const missingArtifacts = [...routeResult.missingArtifacts];
337
+
338
+ if (routeResult.error) {
339
+ return result({
340
+ ...context,
341
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
342
+ reasons: [artifactError(
343
+ routeResult.error.code === "ARTIFACT_MISSING" ? "E_ROUTE_MISSING" : "E_ROUTE_INVALID",
344
+ routeResult.error.message,
345
+ routeResult.error.artifacts ?? [],
346
+ )],
347
+ requiredArtifacts,
348
+ missingArtifacts,
349
+ });
350
+ }
351
+
352
+ const route = routeResult.value;
353
+ const stale = staleReasons(state, contract, route);
354
+ if (stale.length > 0) {
355
+ return result({
356
+ ...context,
357
+ nextAction: NEXT_ACTIONS.RESOLVE_STALE_ROUTE,
358
+ reasons: stale,
359
+ requiredArtifacts,
360
+ });
361
+ }
362
+
363
+ const phaseNeedsChronology = PHASES_REQUIRING_EXECUTION_CHRONOLOGY.has(state.phase);
364
+ let executionPrerequisites = null;
365
+ if (phaseNeedsChronology) {
366
+ try {
367
+ executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
368
+ } catch (error) {
369
+ return result({
370
+ ...context,
371
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
372
+ reasons: [artifactError(
373
+ error?.code ?? "E_PHASE_CHRONOLOGY_INVALID",
374
+ `Unable to evaluate post-execution prerequisites: ${error?.message ?? String(error)}`,
375
+ Array.isArray(error?.artifacts) ? error.artifacts : [ARTIFACT_PATHS.events],
376
+ )],
377
+ requiredArtifacts: [
378
+ ARTIFACT_PATHS.state,
379
+ ARTIFACT_PATHS.contract,
380
+ ARTIFACT_PATHS.route,
381
+ ARTIFACT_PATHS.preflight,
382
+ ARTIFACT_PATHS.events,
383
+ ],
384
+ });
385
+ }
386
+ if (executionPrerequisites.errors.length > 0) {
387
+ return result({
388
+ ...context,
389
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
390
+ reasons: executionPrerequisites.errors,
391
+ requiredArtifacts: executionPrerequisites.requiredArtifacts,
392
+ });
393
+ }
394
+ }
395
+
396
+ const preflight = executionPrerequisites?.preflight ?? await evaluatePreflight({ target, packageRoot });
397
+ const preflightArtifact = phaseNeedsChronology
398
+ ? null
399
+ : await loadArtifact(
400
+ () => readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot),
401
+ ARTIFACT_PATHS.preflight,
402
+ );
403
+ const missingGates = preflight.requiredGates.filter((gate) => !preflight.satisfiedGates.includes(gate));
404
+ const preflightArtifacts = [...requiredArtifacts, ARTIFACT_PATHS.preflight];
405
+ const persistedPreflightErrors = phaseNeedsChronology
406
+ ? []
407
+ : validatePersistedPreflight(preflightArtifact.value?.value, preflight);
408
+
409
+ if (["ROUTED", "DESIGNING", "PLANNED"].includes(state.phase) && missingGates.length > 0) {
410
+ return result({
411
+ ...context,
412
+ nextAction: NEXT_ACTIONS.SATISFY_GATES,
413
+ reasons: missingGates.map((gate) => artifactError(
414
+ "E_GATE_UNVERIFIED",
415
+ `Required gate is missing or unverified: ${gate}`,
416
+ [`${ARTIFACT_PATHS.gates}/${gate}.json`],
417
+ )),
418
+ requiredArtifacts: [...preflightArtifacts, ...missingGates.map((gate) => `${ARTIFACT_PATHS.gates}/${gate}.json`)],
419
+ missingArtifacts: missingGates.map((gate) => `${ARTIFACT_PATHS.gates}/${gate}.json`),
420
+ });
421
+ }
422
+ if (state.phase === "ROUTED") {
423
+ if (persistedPreflightErrors.length > 0) {
424
+ return result({
425
+ ...context,
426
+ nextAction: NEXT_ACTIONS.RUN_PREFLIGHT,
427
+ reasons: persistedPreflightErrors,
428
+ commands: [commandFor(NEXT_ACTIONS.RUN_PREFLIGHT)],
429
+ requiredArtifacts: preflightArtifacts,
430
+ missingArtifacts: preflightArtifact.missingArtifacts,
431
+ });
432
+ }
433
+ return decision(context, NEXT_ACTIONS.PLAN, artifactError("PHASE_ROUTED", "Routing and required gates are ready for planning"));
434
+ }
435
+ if (state.phase === "DESIGNING") {
436
+ return decision(context, NEXT_ACTIONS.PLAN, artifactError("PHASE_DESIGNING", "Required gates are ready for planning"));
437
+ }
438
+ if (state.phase === "PLANNED") {
439
+ const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
440
+ if (prerequisites.errors.length > 0) {
441
+ const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
442
+ || (error.code === "E_PHASE_CHRONOLOGY_INVALID"
443
+ && error.message === PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE));
444
+ if (preflightOnly) {
445
+ return result({
446
+ ...context,
447
+ nextAction: NEXT_ACTIONS.RUN_PREFLIGHT,
448
+ reasons: prerequisites.errors,
449
+ commands: [commandFor(NEXT_ACTIONS.RUN_PREFLIGHT)],
450
+ requiredArtifacts: prerequisites.requiredArtifacts,
451
+ missingArtifacts: preflightArtifact.missingArtifacts,
452
+ });
453
+ }
454
+ const routeOnly = prerequisites.errors.every((error) => error.code === "E_ROUTE_STALE" || error.code === "E_ROUTE_GUIDE_MISMATCH");
455
+ return result({
456
+ ...context,
457
+ nextAction: routeOnly ? NEXT_ACTIONS.RESOLVE_STALE_ROUTE : NEXT_ACTIONS.RESOLVE_BLOCKER,
458
+ reasons: prerequisites.errors,
459
+ requiredArtifacts: prerequisites.requiredArtifacts,
460
+ missingArtifacts: preflightArtifact.missingArtifacts,
461
+ });
462
+ }
463
+ return decision(context, NEXT_ACTIONS.START_EXECUTION, artifactError("PHASE_PLANNED", "The persisted preflight is READY"));
464
+ }
465
+ if (state.phase === "EXECUTING") {
466
+ return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_EXECUTING", "Execution is complete enough to enter verification"));
467
+ }
468
+ if (state.phase === "VERIFYING") {
469
+ const invalidChecks = checkListReasons(state);
470
+ if (invalidChecks.length > 0) {
471
+ return result({
472
+ ...context,
473
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
474
+ reasons: invalidChecks,
475
+ requiredArtifacts,
476
+ });
477
+ }
478
+ const failed = state.checks.find((check) => check?.status === "failed");
479
+ if (failed) {
480
+ return decision(
481
+ context,
482
+ NEXT_ACTIONS.DIAGNOSE,
483
+ artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.id}`, [ARTIFACT_PATHS.state]),
484
+ );
485
+ }
486
+ const blocked = state.checks.find((check) => check?.status === "blocked");
487
+ if (blocked) {
488
+ return decision(
489
+ context,
490
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
491
+ artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.id}`, [ARTIFACT_PATHS.state]),
492
+ );
493
+ }
494
+ const receipt = await loadArtifact(
495
+ () => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
496
+ ARTIFACT_PATHS.receipt,
497
+ );
498
+ if (receipt.error) {
499
+ if (receipt.error.code === "ARTIFACT_MISSING") {
500
+ return decision(
501
+ context,
502
+ NEXT_ACTIONS.PREPARE_COMPLETION,
503
+ artifactError("E_RECEIPT_MISSING", "Prepare the execution receipt before recording verification checks", [ARTIFACT_PATHS.receipt]),
504
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
505
+ receipt.missingArtifacts,
506
+ );
507
+ }
508
+ return decision(
509
+ context,
510
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
511
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [ARTIFACT_PATHS.receipt]),
512
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
513
+ );
514
+ }
515
+ try {
516
+ await validateReceipt(receipt.value.value, packageRoot);
517
+ } catch (error) {
518
+ return decision(
519
+ context,
520
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
521
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [ARTIFACT_PATHS.receipt]),
522
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
523
+ );
524
+ }
525
+ const evidence = await requirementsAndCoverage({
526
+ target,
527
+ packageRoot,
528
+ contract,
529
+ route,
530
+ checks: state.checks,
531
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
532
+ });
533
+ const receiptRelationships = completionRelationshipErrors({
534
+ contract,
535
+ route,
536
+ state,
537
+ receipt: receipt.value.value,
538
+ requiredEvidence: evidence.requirements,
539
+ requireRequiredChecks: false,
540
+ });
541
+ if (receiptRelationships.length > 0) {
542
+ return result({
543
+ ...context,
544
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
545
+ reasons: receiptRelationships,
546
+ requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
547
+ });
548
+ }
549
+ if (allCoverageCovered(evidence.coverage)) {
550
+ return decision(context, NEXT_ACTIONS.ENTER_REVIEWING, artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"));
551
+ }
552
+ const uncovered = evidence.coverage.filter((item) => item.status !== "COVERED");
553
+ return result({
554
+ ...context,
555
+ nextAction: NEXT_ACTIONS.RECORD_VERIFICATION,
556
+ commands: ["forgeloop record-check"],
557
+ commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.requirement)),
558
+ reasons: uncovered
559
+ .map((item) => artifactError(
560
+ "E_EVIDENCE_REQUIRED",
561
+ "Run the required check and record observed evidence through the structured command specification.",
562
+ [ARTIFACT_PATHS.state],
563
+ )),
564
+ requiredArtifacts: requiredArtifacts,
565
+ });
566
+ }
567
+ if (state.phase === "DIAGNOSING") {
568
+ if (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis.trim()) {
569
+ return decision(
570
+ context,
571
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
572
+ artifactError(
573
+ "E_DIAGNOSIS_HYPOTHESIS_MISSING",
574
+ "Record diagnosedHypothesis in .forgeloop/work-state.json before advancing to CORRECTING",
575
+ [ARTIFACT_PATHS.state],
576
+ ),
577
+ requiredArtifacts,
578
+ );
579
+ }
580
+ return decision(context, NEXT_ACTIONS.CORRECT, artifactError("PHASE_DIAGNOSING", "The persisted diagnosis hypothesis permits correction"));
581
+ }
582
+ if (state.phase === "CORRECTING") {
583
+ return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
584
+ }
585
+ if (state.phase === "REVIEWING") {
586
+ const invalidChecks = checkListReasons(state);
587
+ if (invalidChecks.length > 0) {
588
+ return result({
589
+ ...context,
590
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
591
+ reasons: invalidChecks,
592
+ requiredArtifacts,
593
+ });
594
+ }
595
+ const evidence = await requirementsAndCoverage({
596
+ target,
597
+ packageRoot,
598
+ contract,
599
+ route,
600
+ checks: state.checks,
601
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
602
+ });
603
+ if (!allCoverageCovered(evidence.coverage)) {
604
+ return result({
605
+ ...context,
606
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
607
+ reasons: evidence.coverage
608
+ .filter((item) => item.status !== "COVERED")
609
+ .map((item) => artifactError(
610
+ "E_EVIDENCE_COVERAGE_PARTIAL",
611
+ `Evidence coverage is ${item.status}: ${item.requirement}`,
612
+ [ARTIFACT_PATHS.state],
613
+ )),
614
+ requiredArtifacts,
615
+ });
616
+ }
617
+ const receipt = await loadArtifact(
618
+ () => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
619
+ ARTIFACT_PATHS.receipt,
620
+ );
621
+ if (receipt.error) {
622
+ if (receipt.error.code !== "ARTIFACT_MISSING") {
623
+ return decision(
624
+ context,
625
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
626
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [ARTIFACT_PATHS.receipt]),
627
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
628
+ );
629
+ }
630
+ return decision(
631
+ context,
632
+ NEXT_ACTIONS.PREPARE_COMPLETION,
633
+ artifactError(
634
+ receipt.error.code === "ARTIFACT_MISSING" ? "E_RECEIPT_MISSING" : "E_RECEIPT_INVALID",
635
+ receipt.error.message,
636
+ [ARTIFACT_PATHS.receipt],
637
+ ),
638
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
639
+ receipt.missingArtifacts,
640
+ );
641
+ }
642
+ try {
643
+ await validateReceipt(receipt.value.value, packageRoot);
644
+ } catch (error) {
645
+ return decision(
646
+ context,
647
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
648
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [ARTIFACT_PATHS.receipt]),
649
+ [...requiredArtifacts, ARTIFACT_PATHS.receipt],
650
+ );
651
+ }
652
+ const receiptRelationships = completionRelationshipErrors({
653
+ contract,
654
+ route,
655
+ state,
656
+ receipt: receipt.value.value,
657
+ requiredEvidence: evidence.requirements,
658
+ });
659
+ if (receiptRelationships.length > 0) {
660
+ return result({
661
+ ...context,
662
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
663
+ reasons: receiptRelationships,
664
+ requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
665
+ });
666
+ }
667
+ const completion = await evaluateCompletion({ target, packageRoot });
668
+ if (completion.status !== "VALID") {
669
+ return result({
670
+ ...context,
671
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
672
+ reasons: completion.errors,
673
+ requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
674
+ });
675
+ }
676
+ return decision(context, NEXT_ACTIONS.RUN_COMPLETE, artifactError("COMPLETION_READY", "Completion artifacts and cross-artifact validation are valid"));
677
+ }
678
+ if (state.phase === "COMPLETE") {
679
+ const completion = await evaluateCompletion({ target, packageRoot });
680
+ if (completion.status === "VALID") {
681
+ return decision(context, NEXT_ACTIONS.NONE, artifactError("PHASE_COMPLETE", "Completion is validator-backed and terminal"));
682
+ }
683
+ return result({
684
+ ...context,
685
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
686
+ reasons: completion.errors,
687
+ requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
688
+ });
689
+ }
690
+ if (state.phase === "BLOCKED") {
691
+ return result({
692
+ ...context,
693
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
694
+ reasons: state.blockers.map((blocker) => artifactError(
695
+ "WORK_STATE_BLOCKED",
696
+ blocker.reason ?? "Work state has a recorded blocker",
697
+ [ARTIFACT_PATHS.state],
698
+ )),
699
+ requiredArtifacts,
700
+ });
701
+ }
702
+
703
+ return decision(
704
+ context,
705
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
706
+ artifactError("E_PHASE_UNSUPPORTED", `Unsupported persisted phase: ${state.phase}`, [ARTIFACT_PATHS.state]),
707
+ requiredArtifacts,
708
+ );
709
+ }