@cassiomc1/forgeloop 1.1.1 → 1.2.2

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 (79) hide show
  1. package/.cursor/rules/project-loop.mdc +1 -1
  2. package/.github/copilot-instructions.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/CLAUDE.md +1 -1
  5. package/DOCS_INDEX.md +3 -0
  6. package/ENG/design-code-eng.md +124 -0
  7. package/ENG/premium-sites-studio-eng.md +28 -0
  8. package/ENG/taste-frontend-eng.md +3 -2
  9. package/ENG/test-code-eng.md +45 -0
  10. package/LOOP_ENGINEERING.md +74 -0
  11. package/LOOP_SYSTEM_DESIGN.md +9 -5
  12. package/ORCHESTRATOR_INTEGRATION.md +41 -6
  13. package/PROTOCOL_INTEGRATION.md +13 -0
  14. package/README.md +40 -6
  15. package/TERMINOLOGY.md +10 -0
  16. package/THIRD_PARTY_NOTICES.md +58 -1
  17. package/THREAT_MODEL.md +12 -1
  18. package/docs/ARTIFACT_REFERENCE.md +152 -2
  19. package/docs/CLI_REFERENCE.md +346 -30
  20. package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
  21. package/docs/DOCUMENTATION_GUIDE.md +41 -4
  22. package/docs/GETTING_STARTED.md +39 -8
  23. package/docs/RECIPES.md +66 -7
  24. package/docs/TROUBLESHOOTING.md +279 -6
  25. package/package.json +1 -1
  26. package/schemas/policy-baseline.schema.json +26 -0
  27. package/schemas/policy-discovery.schema.json +45 -0
  28. package/schemas/policy-lock.schema.json +16 -0
  29. package/schemas/policy-rules.schema.json +48 -0
  30. package/schemas/policy-snapshot.schema.json +16 -0
  31. package/src/cli.js +102 -1
  32. package/src/commands/baseline.js +120 -0
  33. package/src/commands/init.js +304 -6
  34. package/src/commands/next.js +15 -1
  35. package/src/commands/policy-diff.js +51 -0
  36. package/src/commands/policy-discover.js +42 -0
  37. package/src/commands/policy-status.js +33 -0
  38. package/src/commands/profile-interview.js +50 -0
  39. package/src/commands/progress.js +51 -0
  40. package/src/commands/reconcile-closure.js +49 -0
  41. package/src/commands/record-decision-criterion.js +34 -0
  42. package/src/commands/record-diagnosis.js +49 -0
  43. package/src/commands/rule-verify.js +36 -0
  44. package/src/commands/validate-receipt.js +38 -3
  45. package/src/core/artifact-registry.js +60 -0
  46. package/src/core/audit.js +24 -0
  47. package/src/core/cli-command-definitions.js +163 -7
  48. package/src/core/cli-metadata.js +1 -1
  49. package/src/core/completion-artifacts.js +29 -3
  50. package/src/core/completion.js +101 -10
  51. package/src/core/diagnosis-model.js +214 -0
  52. package/src/core/diagnosis.js +171 -0
  53. package/src/core/error-codes.js +292 -0
  54. package/src/core/events.js +47 -1
  55. package/src/core/execution-prerequisites.js +38 -20
  56. package/src/core/execution.js +20 -3
  57. package/src/core/native-adapters.js +14 -4
  58. package/src/core/next-action-model.js +40 -5
  59. package/src/core/next-action.js +234 -91
  60. package/src/core/phase.js +29 -0
  61. package/src/core/policy-adapters.js +276 -0
  62. package/src/core/policy-baseline.js +144 -0
  63. package/src/core/policy-diff.js +133 -0
  64. package/src/core/policy-discovery.js +225 -0
  65. package/src/core/policy-engine.js +533 -0
  66. package/src/core/policy-mutation.js +139 -0
  67. package/src/core/preflight-consistency.js +23 -15
  68. package/src/core/preflight-model.js +10 -2
  69. package/src/core/preflight.js +65 -1
  70. package/src/core/progress.js +143 -0
  71. package/src/core/protocol.js +8 -0
  72. package/src/core/reconcile-closure.js +173 -0
  73. package/src/core/schema-validation.js +6 -0
  74. package/src/core/settlement-model.js +85 -0
  75. package/src/core/settlement.js +78 -0
  76. package/src/core/task-context.js +11 -0
  77. package/src/core/task-discovery.js +67 -1
  78. package/src/core/task-paths.js +9 -0
  79. package/src/core/templates.js +5 -0
@@ -1,4 +1,5 @@
1
1
  import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
2
+ import { taskArtifactPath } from "./task-paths.js";
2
3
  import { completionIdentityErrors, evaluateCompletion } from "./completion.js";
3
4
  import { readContract } from "./contract.js";
4
5
  import { evaluatePreflight, validatePersistedPreflight, validateReadyProtocolConsistency } from "./preflight.js";
@@ -17,6 +18,7 @@ import {
17
18
  commandFor,
18
19
  decision,
19
20
  recordCheckCommandSpec,
21
+ recordDiagnosisCommandSpec,
20
22
  recordTerminalResultCommandSpec,
21
23
  result,
22
24
  uniqueSorted,
@@ -31,75 +33,113 @@ import {
31
33
  } from "./next-action-artifacts.js";
32
34
  import { PHASES_REQUIRING_EXECUTION_CHRONOLOGY } from "./next-action-phases.js";
33
35
  import { evaluateContinuityNextAction } from "./next-action-continuity.js";
36
+ import { currentCycleDiagnosis } from "./diagnosis-model.js";
37
+ import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
38
+ import { criterionForDecision } from "./settlement-model.js";
39
+ import { readEvents } from "./events.js";
34
40
 
35
41
  export { NEXT_ACTIONS } from "./next-action-model.js";
36
42
 
37
- export async function getNextAction(targetOrOptions = {}, packageRootOption) {
43
+ export function policyRecoveryAction(errors = []) {
44
+ if (errors.some((e) => e.code === "E_POLICY_WEAKENING" || e.code === "E_POLICY_LOCK_MISMATCH")) {
45
+ return NEXT_ACTIONS.RESTORE_POLICY;
46
+ }
47
+ if (errors.some((e) => e.code === "E_CHECK_MUTATION_EXECUTION_ERROR" || e.code === "E_CHECK_MUTATION_NOT_DETECTED")) {
48
+ return NEXT_ACTIONS.REPAIR_CHECKER;
49
+ }
50
+ if (errors.some((e) => e.code === "E_POLICY_INVALID" || e.code === "E_POLICY_EVALUATION_FAILED")) {
51
+ return NEXT_ACTIONS.REPAIR_POLICY;
52
+ }
53
+ if (errors.some((e) => e.code === "E_POLICY_DRIFT_UNKNOWN")) {
54
+ return NEXT_ACTIONS.REVERIFY_AFTER_POLICY_CHANGE;
55
+ }
56
+ if (errors.some((e) => e.code === "E_BASELINE_EXPANSION")) {
57
+ return NEXT_ACTIONS.RESTORE_BASELINE;
58
+ }
59
+ if (errors.some((e) => e.code === "E_BASELINE_RECORD_DURING_ACTIVE_TASK")) {
60
+ return NEXT_ACTIONS.CONTINUE_WITH_EXISTING_BASELINE;
61
+ }
62
+ if (errors.some((e) => e.code === "E_CHECK_INERT")) {
63
+ return NEXT_ACTIONS.RESOLVE_INERT_CHECK;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
38
69
  const normalized = typeof targetOrOptions === "string"
39
70
  ? { target: targetOrOptions, packageRoot: packageRootOption }
40
71
  : targetOrOptions;
41
- const { target, packageRoot, authorityContext, runtimeContext } = normalized ?? {};
72
+ const { target, packageRoot, taskId: explicitTaskId, authorityContext, runtimeContext } = normalized ?? {};
73
+
74
+ const stateRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "state") : ARTIFACT_PATHS.state;
75
+ const preflightRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "preflight") : ARTIFACT_PATHS.preflight;
76
+ const contractRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "contract") : ARTIFACT_PATHS.contract;
77
+ const routeRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "route") : ARTIFACT_PATHS.route;
78
+ const receiptRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "receipt") : ARTIFACT_PATHS.receipt;
79
+ const eventsRel = explicitTaskId ? taskArtifactPath(explicitTaskId, "events") : ARTIFACT_PATHS.events;
80
+
42
81
  const workState = await loadArtifact(
43
- () => readWorkState(target, packageRoot),
44
- ARTIFACT_PATHS.state,
82
+ () => readWorkState(target, { packageRoot, taskId: explicitTaskId }),
83
+ stateRel,
45
84
  );
46
85
  if (workState.error) {
47
86
  return decision(
48
87
  {},
49
88
  NEXT_ACTIONS.RESOLVE_BLOCKER,
50
- artifactError("WORK_STATE_INVALID", workState.error.message, [ARTIFACT_PATHS.state]),
51
- [ARTIFACT_PATHS.state],
89
+ artifactError("WORK_STATE_INVALID", workState.error.message, [stateRel]),
90
+ [stateRel],
52
91
  workState.missingArtifacts,
53
92
  );
54
93
  }
55
94
  if (!workState.value) {
56
95
  const persistedPreflight = await loadArtifact(
57
- () => readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot),
58
- ARTIFACT_PATHS.preflight,
96
+ () => readJsonArtifact(target, preflightRel, "preflight", packageRoot),
97
+ preflightRel,
59
98
  );
60
99
  if (!persistedPreflight.error && persistedPreflight.value?.value?.status === "READY") {
61
100
  try {
62
101
  const consistencyErrors = await validateReadyProtocolConsistency({
63
102
  target,
64
103
  packageRoot,
104
+ taskId: explicitTaskId,
65
105
  persisted: persistedPreflight.value.value,
66
106
  });
67
107
  if (consistencyErrors.length > 0) {
68
108
  return result({
69
- taskId: persistedPreflight.value.value.taskId ?? "unknown",
109
+ taskId: persistedPreflight.value.value.taskId ?? explicitTaskId ?? "unknown",
70
110
  currentPhase: "ROUTED",
71
111
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
72
112
  reasons: consistencyErrors,
73
113
  requiredArtifacts: [
74
- ARTIFACT_PATHS.state,
75
- ARTIFACT_PATHS.contract,
76
- ARTIFACT_PATHS.route,
77
- ARTIFACT_PATHS.preflight,
78
- ARTIFACT_PATHS.events,
114
+ stateRel,
115
+ contractRel,
116
+ routeRel,
117
+ preflightRel,
118
+ eventsRel,
79
119
  ],
80
- missingArtifacts: [ARTIFACT_PATHS.state],
120
+ missingArtifacts: [stateRel],
81
121
  });
82
122
  }
83
123
  } catch (error) {
84
124
  return result({
85
- taskId: persistedPreflight.value.value.taskId ?? "unknown",
125
+ taskId: persistedPreflight.value.value.taskId ?? explicitTaskId ?? "unknown",
86
126
  currentPhase: "ROUTED",
87
127
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
88
128
  reasons: [artifactError(
89
129
  error.code ?? "E_PREFLIGHT_READY_INCONSISTENT",
90
130
  error.message,
91
- error.artifacts ?? [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
131
+ error.artifacts ?? [preflightRel, eventsRel],
92
132
  )],
93
- requiredArtifacts: [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
133
+ requiredArtifacts: [preflightRel, eventsRel],
94
134
  });
95
135
  }
96
136
  }
97
137
  return decision(
98
138
  {},
99
139
  NEXT_ACTIONS.DISCOVER,
100
- artifactError("WORK_STATE_ABSENT", "No work-state checkpoint is present", [ARTIFACT_PATHS.state]),
101
- [ARTIFACT_PATHS.state],
102
- [ARTIFACT_PATHS.state],
140
+ artifactError("WORK_STATE_ABSENT", "No work-state checkpoint is present", [stateRel]),
141
+ [stateRel],
142
+ [stateRel],
103
143
  );
104
144
  }
105
145
 
@@ -110,7 +150,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
110
150
  context,
111
151
  NEXT_ACTIONS.DISCOVER,
112
152
  artifactError("PHASE_RECEIVED", "Discovery has not started"),
113
- [ARTIFACT_PATHS.state],
153
+ [stateRel],
114
154
  );
115
155
  }
116
156
  if (state.phase === "DISCOVERING") {
@@ -118,14 +158,14 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
118
158
  context,
119
159
  NEXT_ACTIONS.CREATE_CONTRACT,
120
160
  artifactError("PHASE_DISCOVERING", "Create and validate the task contract"),
121
- [ARTIFACT_PATHS.state],
161
+ [stateRel],
122
162
  );
123
163
  }
124
164
  const contractResult = await loadArtifact(
125
- () => readContract(target, packageRoot),
126
- ARTIFACT_PATHS.contract,
165
+ () => readContract(target, packageRoot, { taskId: explicitTaskId }),
166
+ contractRel,
127
167
  );
128
- const contractArtifacts = [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract];
168
+ const contractArtifacts = [stateRel, contractRel];
129
169
 
130
170
  if (contractResult.error) {
131
171
  return result({
@@ -154,7 +194,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
154
194
  const freshness = await classifyLoadedWorkState({
155
195
  target,
156
196
  state,
157
- contractFile: ARTIFACT_PATHS.contract,
197
+ contractFile: contractRel,
158
198
  });
159
199
  if (freshness.status === "REVALIDATION_REQUIRED") {
160
200
  return result({
@@ -162,8 +202,8 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
162
202
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
163
203
  reasons: freshnessReasons(state, freshness),
164
204
  requiredArtifacts: uniqueSorted([
165
- ARTIFACT_PATHS.state,
166
- ARTIFACT_PATHS.contract,
205
+ stateRel,
206
+ contractRel,
167
207
  ...(state.requiredArtifacts?.map((artifact) => artifact.path) ?? []),
168
208
  ]),
169
209
  });
@@ -174,14 +214,14 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
174
214
  NEXT_ACTIONS.ROUTE,
175
215
  artifactError("PHASE_CONTRACT_READY", "Persist deterministic routing for the validated contract"),
176
216
  contractArtifacts,
177
- [ARTIFACT_PATHS.route],
217
+ [routeRel],
178
218
  );
179
219
  }
180
220
  const routeResult = await loadArtifact(
181
- () => readPersistedRoute(target, packageRoot),
182
- ARTIFACT_PATHS.route,
221
+ () => readPersistedRoute(target, packageRoot, { taskId: explicitTaskId }),
222
+ routeRel,
183
223
  );
184
- const requiredArtifacts = [...contractArtifacts, ARTIFACT_PATHS.route];
224
+ const requiredArtifacts = [...contractArtifacts, routeRel];
185
225
  const missingArtifacts = [...routeResult.missingArtifacts];
186
226
 
187
227
  if (routeResult.error) {
@@ -213,7 +253,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
213
253
  let executionPrerequisites = null;
214
254
  if (phaseNeedsChronology) {
215
255
  try {
216
- executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
256
+ executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId });
217
257
  } catch (error) {
218
258
  return result({
219
259
  ...context,
@@ -221,14 +261,14 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
221
261
  reasons: [artifactError(
222
262
  error?.code ?? "E_PHASE_CHRONOLOGY_INVALID",
223
263
  `Unable to evaluate post-execution prerequisites: ${error?.message ?? String(error)}`,
224
- Array.isArray(error?.artifacts) ? error.artifacts : [ARTIFACT_PATHS.events],
264
+ Array.isArray(error?.artifacts) ? error.artifacts : [eventsRel],
225
265
  )],
226
266
  requiredArtifacts: [
227
- ARTIFACT_PATHS.state,
228
- ARTIFACT_PATHS.contract,
229
- ARTIFACT_PATHS.route,
230
- ARTIFACT_PATHS.preflight,
231
- ARTIFACT_PATHS.events,
267
+ stateRel,
268
+ contractRel,
269
+ routeRel,
270
+ preflightRel,
271
+ eventsRel,
232
272
  ],
233
273
  });
234
274
  }
@@ -242,15 +282,15 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
242
282
  }
243
283
  }
244
284
 
245
- const preflight = executionPrerequisites?.preflight ?? await evaluatePreflight({ target, packageRoot });
285
+ const preflight = executionPrerequisites?.preflight ?? await evaluatePreflight({ target, packageRoot, taskId: explicitTaskId });
246
286
  const preflightArtifact = phaseNeedsChronology
247
287
  ? null
248
288
  : await loadArtifact(
249
- () => readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot),
250
- ARTIFACT_PATHS.preflight,
289
+ () => readJsonArtifact(target, preflightRel, "preflight", packageRoot),
290
+ preflightRel,
251
291
  );
252
292
  const missingGates = preflight.requiredGates.filter((gate) => !preflight.satisfiedGates.includes(gate));
253
- const preflightArtifacts = [...requiredArtifacts, ARTIFACT_PATHS.preflight];
293
+ const preflightArtifacts = [...requiredArtifacts, preflightRel];
254
294
  const persistedPreflightErrors = phaseNeedsChronology
255
295
  ? []
256
296
  : validatePersistedPreflight(preflightArtifact.value?.value, preflight);
@@ -285,7 +325,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
285
325
  return decision(context, NEXT_ACTIONS.PLAN, artifactError("PHASE_DESIGNING", "Required gates are ready for planning"));
286
326
  }
287
327
  if (state.phase === "PLANNED") {
288
- const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
328
+ const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId });
289
329
  if (prerequisites.errors.length > 0) {
290
330
  const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
291
331
  || (error.code === "E_PHASE_CHRONOLOGY_INVALID"
@@ -345,7 +385,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
345
385
  return decision(
346
386
  context,
347
387
  NEXT_ACTIONS.DIAGNOSE,
348
- artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [ARTIFACT_PATHS.state]),
388
+ artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [stateRel]),
349
389
  );
350
390
  }
351
391
  const blocked = authoritative.find(({ check }) => check?.status === "blocked");
@@ -353,28 +393,28 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
353
393
  return decision(
354
394
  context,
355
395
  NEXT_ACTIONS.RESOLVE_BLOCKER,
356
- artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [ARTIFACT_PATHS.state]),
396
+ artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [stateRel]),
357
397
  );
358
398
  }
359
399
  const receipt = await loadArtifact(
360
- () => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
361
- ARTIFACT_PATHS.receipt,
400
+ () => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
401
+ receiptRel,
362
402
  );
363
403
  if (receipt.error) {
364
404
  if (receipt.error.code === "ARTIFACT_MISSING") {
365
405
  return decision(
366
406
  context,
367
407
  NEXT_ACTIONS.PREPARE_COMPLETION,
368
- artifactError("E_RECEIPT_MISSING", "Prepare the execution receipt before recording verification checks", [ARTIFACT_PATHS.receipt]),
369
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
408
+ artifactError("E_RECEIPT_MISSING", "Prepare the execution receipt before recording verification checks", [receiptRel]),
409
+ [...requiredArtifacts, receiptRel],
370
410
  receipt.missingArtifacts,
371
411
  );
372
412
  }
373
413
  return decision(
374
414
  context,
375
415
  NEXT_ACTIONS.RESOLVE_BLOCKER,
376
- artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [ARTIFACT_PATHS.receipt]),
377
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
416
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
417
+ [...requiredArtifacts, receiptRel],
378
418
  );
379
419
  }
380
420
  try {
@@ -388,8 +428,8 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
388
428
  return decision(
389
429
  context,
390
430
  NEXT_ACTIONS.RESOLVE_BLOCKER,
391
- artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [ARTIFACT_PATHS.receipt]),
392
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
431
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
432
+ [...requiredArtifacts, receiptRel],
393
433
  );
394
434
  }
395
435
  const readiness = evaluateRequiredEvidence({
@@ -422,15 +462,15 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
422
462
  return decision(
423
463
  context,
424
464
  NEXT_ACTIONS.PREPARE_COMPLETION,
425
- artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [ARTIFACT_PATHS.receipt]),
426
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
465
+ artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
466
+ [...requiredArtifacts, receiptRel],
427
467
  );
428
468
  }
429
469
  return result({
430
470
  ...context,
431
471
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
432
472
  reasons: receiptRelationships,
433
- requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
473
+ requiredArtifacts: [...requiredArtifacts, receiptRel],
434
474
  });
435
475
  }
436
476
  if (readiness.ready) {
@@ -450,25 +490,56 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
450
490
  ? "E_EVIDENCE_PARTIAL"
451
491
  : "E_EVIDENCE_REQUIRED",
452
492
  "Run the required check and record observed evidence through the structured command specification.",
453
- [ARTIFACT_PATHS.state],
493
+ [stateRel],
454
494
  )),
455
495
  requiredArtifacts: requiredArtifacts,
456
496
  });
457
497
  }
458
498
  if (state.phase === "DIAGNOSING") {
459
- if (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis.trim()) {
460
- return decision(
461
- context,
462
- NEXT_ACTIONS.RESOLVE_BLOCKER,
463
- artifactError(
464
- "E_DIAGNOSIS_HYPOTHESIS_MISSING",
465
- "Record diagnosedHypothesis in .forgeloop/work-state.json before advancing to CORRECTING",
466
- [ARTIFACT_PATHS.state],
467
- ),
499
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
500
+ const cycle = state.verificationCycle ?? 1;
501
+ const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
502
+ if (!diagEvent) {
503
+ return result({
504
+ ...context,
505
+ nextAction: NEXT_ACTIONS.RECORD_DIAGNOSIS,
506
+ reasons: [
507
+ artifactError(
508
+ "E_DIAGNOSIS_REQUIRED",
509
+ "Current correction cycle has no append-only diagnosis record.",
510
+ [eventsRel],
511
+ ),
512
+ ],
513
+ commandSpecs: [recordDiagnosisCommandSpec()],
514
+ requiredArtifacts: [...requiredArtifacts, eventsRel],
515
+ });
516
+ }
517
+ const progress = evaluateProgress({ state, events: ledger.events });
518
+ if (progress.status === PROGRESS_STATUS.STALLED) {
519
+ return result({
520
+ ...context,
521
+ nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
522
+ reasons: [
523
+ artifactError(
524
+ "E_PROGRESS_STALLED",
525
+ "The current correction strategy has no new diagnostic information.",
526
+ [stateRel, eventsRel],
527
+ ),
528
+ ],
529
+ progress,
468
530
  requiredArtifacts,
469
- );
531
+ });
470
532
  }
471
- return decision(context, NEXT_ACTIONS.CORRECT, artifactError("PHASE_DIAGNOSING", "The persisted diagnosis hypothesis permits correction"));
533
+ return result({
534
+ ...context,
535
+ nextAction: NEXT_ACTIONS.CORRECT,
536
+ commands: [commandFor(NEXT_ACTIONS.CORRECT)],
537
+ reasons: [
538
+ artifactError("PHASE_DIAGNOSING", "The persisted diagnosis hypothesis permits correction"),
539
+ ],
540
+ ...(progress.status === PROGRESS_STATUS.WATCH ? { progress } : {}),
541
+ requiredArtifacts,
542
+ });
472
543
  }
473
544
  if (state.phase === "CORRECTING") {
474
545
  return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
@@ -504,10 +575,10 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
504
575
  let recoveryAuthorized = false;
505
576
  if (state.lastCompletionAttempt?.status === "REJECTED") {
506
577
  try {
507
- const ledger = await validateEventLedger(target, packageRoot);
578
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: explicitTaskId, eventsPath: eventsRel });
508
579
  let currentReceipt = null;
509
580
  try {
510
- const receiptArtifact = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
581
+ const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
511
582
  currentReceipt = receiptArtifact?.value;
512
583
  } catch {}
513
584
  const recoveryAuth = validateCompletionRecoveryAuthorization({
@@ -534,22 +605,22 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
534
605
  ? "E_EVIDENCE_REQUIRED"
535
606
  : item.reasonCode ?? "E_EVIDENCE_PARTIAL",
536
607
  `Evidence is not ready: ${item.text}`,
537
- [ARTIFACT_PATHS.state],
608
+ [stateRel],
538
609
  )),
539
610
  requiredArtifacts,
540
611
  });
541
612
  }
542
613
  const receipt = await loadArtifact(
543
- () => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
544
- ARTIFACT_PATHS.receipt,
614
+ () => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
615
+ receiptRel,
545
616
  );
546
617
  if (receipt.error) {
547
618
  if (receipt.error.code !== "ARTIFACT_MISSING") {
548
619
  return decision(
549
620
  context,
550
621
  NEXT_ACTIONS.RESOLVE_BLOCKER,
551
- artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [ARTIFACT_PATHS.receipt]),
552
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
622
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
623
+ [...requiredArtifacts, receiptRel],
553
624
  );
554
625
  }
555
626
  return decision(
@@ -558,9 +629,9 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
558
629
  artifactError(
559
630
  receipt.error.code === "ARTIFACT_MISSING" ? "E_RECEIPT_MISSING" : "E_RECEIPT_INVALID",
560
631
  receipt.error.message,
561
- [ARTIFACT_PATHS.receipt],
632
+ [receiptRel],
562
633
  ),
563
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
634
+ [...requiredArtifacts, receiptRel],
564
635
  receipt.missingArtifacts,
565
636
  );
566
637
  }
@@ -575,8 +646,8 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
575
646
  return decision(
576
647
  context,
577
648
  NEXT_ACTIONS.RESOLVE_BLOCKER,
578
- artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [ARTIFACT_PATHS.receipt]),
579
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
649
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
650
+ [...requiredArtifacts, receiptRel],
580
651
  );
581
652
  }
582
653
  const receiptRelationships = completionRelationshipErrors({
@@ -601,18 +672,19 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
601
672
  return decision(
602
673
  context,
603
674
  NEXT_ACTIONS.PREPARE_COMPLETION,
604
- artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [ARTIFACT_PATHS.receipt]),
605
- [...requiredArtifacts, ARTIFACT_PATHS.receipt],
675
+ artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
676
+ [...requiredArtifacts, receiptRel],
606
677
  );
607
678
  }
608
679
  return result({
609
680
  ...context,
610
681
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
611
682
  reasons: receiptRelationships,
612
- requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
683
+ requiredArtifacts: [...requiredArtifacts, receiptRel],
613
684
  });
614
685
  }
615
- const completion = await evaluateCompletion({ target, packageRoot, authorityContext, runtimeContext });
686
+
687
+ const completion = await evaluateCompletion({ target, packageRoot, taskId: explicitTaskId, authorityContext, runtimeContext });
616
688
  if (completion.status !== "VALID") {
617
689
  const terminalPendingErrors = completion.errors.filter((err) => (
618
690
  err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
@@ -631,28 +703,38 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
631
703
  commands: ["forgeloop record-terminal-result"],
632
704
  commandSpecs: terminalPendingReqs.map(recordTerminalResultCommandSpec),
633
705
  reasons: completion.errors,
634
- requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
706
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
707
+ });
708
+ }
709
+ const policyAction = policyRecoveryAction(completion.errors);
710
+ if (policyAction) {
711
+ return result({
712
+ ...context,
713
+ nextAction: policyAction,
714
+ reasons: completion.errors,
715
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
635
716
  });
636
717
  }
637
718
  return result({
638
719
  ...context,
639
720
  nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
640
721
  reasons: completion.errors,
641
- requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
722
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
642
723
  });
643
724
  }
644
725
  return decision(context, NEXT_ACTIONS.RUN_COMPLETE, artifactError("COMPLETION_READY", "Completion artifacts and cross-artifact validation are valid"));
645
726
  }
646
727
  if (state.phase === "COMPLETE") {
647
- const completion = await evaluateCompletion({ target, packageRoot, authorityContext, runtimeContext });
728
+ const completion = await evaluateCompletion({ target, packageRoot, taskId: explicitTaskId, authorityContext, runtimeContext });
648
729
  if (completion.status === "VALID") {
649
730
  return decision(context, NEXT_ACTIONS.NONE, artifactError("PHASE_COMPLETE", "Completion is validator-backed and terminal"));
650
731
  }
732
+ const policyAction = policyRecoveryAction(completion.errors);
651
733
  return result({
652
734
  ...context,
653
- nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
735
+ nextAction: policyAction ?? NEXT_ACTIONS.RESOLVE_BLOCKER,
654
736
  reasons: completion.errors,
655
- requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
737
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
656
738
  });
657
739
  }
658
740
  if (state.phase === "BLOCKED") {
@@ -662,7 +744,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
662
744
  reasons: state.blockers.map((blocker) => artifactError(
663
745
  "WORK_STATE_BLOCKED",
664
746
  blocker.reason ?? "Work state has a recorded blocker",
665
- [ARTIFACT_PATHS.state],
747
+ [stateRel],
666
748
  )),
667
749
  requiredArtifacts,
668
750
  });
@@ -675,3 +757,64 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
675
757
  requiredArtifacts,
676
758
  );
677
759
  }
760
+
761
+ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
762
+ const res = await computeNextAction(targetOrOptions, packageRootOption);
763
+ if (res && res.reasons && res.reasons.some((r) => r.code === "E_CONTRACT_UNRESOLVED_DECISION" || r.code === "E_UNRESOLVED_DECISION")) {
764
+ const normalized = typeof targetOrOptions === "string" ? { target: targetOrOptions, packageRoot: packageRootOption } : targetOrOptions;
765
+ const { target, packageRoot, taskId } = normalized ?? {};
766
+ try {
767
+ const explicitTaskId = taskId ?? null;
768
+ const contract = await readContract(target, packageRoot, { taskId: explicitTaskId });
769
+ const events = await readEvents(target, packageRoot, { taskId: explicitTaskId });
770
+ if (contract?.value?.unresolvedDecisions?.length > 0) {
771
+ const foundCriteria = [];
772
+ for (const dec of contract.value.unresolvedDecisions) {
773
+ const criterion = criterionForDecision(events, res.taskId, dec, contract.fingerprint);
774
+ if (criterion) {
775
+ foundCriteria.push({
776
+ decisionId: criterion.decisionId,
777
+ decision: dec,
778
+ settledBy: criterion.settledBy,
779
+ });
780
+ }
781
+ }
782
+
783
+ if (foundCriteria.length > 0) {
784
+ let resolution;
785
+ if (foundCriteria.length === 1) {
786
+ resolution = {
787
+ kind: "SETTLEMENT_CRITERION",
788
+ itemId: foundCriteria[0].decisionId,
789
+ decision: foundCriteria[0].decision,
790
+ settledBy: foundCriteria[0].settledBy,
791
+ };
792
+ } else {
793
+ resolution = {
794
+ kind: "SETTLEMENT_CRITERIA",
795
+ items: foundCriteria,
796
+ };
797
+ }
798
+
799
+ const enrichedReasons = res.reasons.map((r) => {
800
+ if (r.code === "E_CONTRACT_UNRESOLVED_DECISION" || r.code === "E_UNRESOLVED_DECISION") {
801
+ return {
802
+ ...r,
803
+ resolution,
804
+ };
805
+ }
806
+ return r;
807
+ });
808
+
809
+ return result({
810
+ ...res,
811
+ reasons: enrichedReasons,
812
+ });
813
+ }
814
+ }
815
+ } catch {
816
+ // Ignore read errors
817
+ }
818
+ }
819
+ return res;
820
+ }
package/src/core/phase.js CHANGED
@@ -21,6 +21,7 @@ import { discoverTasks } from "./task-discovery.js";
21
21
  import { assertNoScopeConflicts, assertScopeClean } from "./task-scope.js";
22
22
  import { readTaskDescriptor } from "./task-descriptor.js";
23
23
  import { E_TASK_SCOPE_REQUIRED } from "./error-codes.js";
24
+ import { currentCycleDiagnosis } from "./diagnosis-model.js";
24
25
 
25
26
  function phaseError(code, message, artifacts = []) {
26
27
  const error = new Error(message);
@@ -242,7 +243,35 @@ export async function advanceWorkState(target, toPhase, options = {}) {
242
243
  );
243
244
  }
244
245
  }
246
+ if (toPhase === "CORRECTING" && state.phase === "DIAGNOSING") {
247
+ const cycle = state.verificationCycle ?? 1;
248
+ const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
249
+ if (!diagEvent) {
250
+ throw phaseError(
251
+ "E_DIAGNOSIS_REQUIRED",
252
+ "DIAGNOSING -> CORRECTING requires an append-only diagnosis record for the active verification cycle",
253
+ [eventsRel],
254
+ );
255
+ }
256
+ const details = diagEvent.details;
257
+ if (!details || details.informationGain === "NONE") {
258
+ throw phaseError(
259
+ "E_DIAGNOSIS_NO_NEW_INFORMATION",
260
+ "The proposed retry repeats the previous hypothesis with the same evidence without new information",
261
+ [eventsRel],
262
+ );
263
+ }
264
+ }
245
265
  if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
266
+ const cycle = state.verificationCycle ?? 1;
267
+ const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
268
+ if (!diagEvent) {
269
+ throw phaseError(
270
+ "E_DIAGNOSIS_REQUIRED",
271
+ "CORRECTING -> VERIFYING requires an append-only diagnosis record for the current cycle",
272
+ [eventsRel],
273
+ );
274
+ }
246
275
  if (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis.trim()) {
247
276
  throw phaseError(
248
277
  "E_PHASE_PREREQUISITE_MISSING",