@cassiomc1/forgeloop 1.3.0 → 1.6.0

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 (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -16,10 +16,13 @@ import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./
16
16
  import {
17
17
  NEXT_ACTIONS,
18
18
  commandFor,
19
+ directCommandSpec,
19
20
  decision,
20
21
  recordCheckCommandSpec,
21
22
  recordDiagnosisCommandSpec,
23
+ recordInterventionCommandSpec,
22
24
  recordTerminalResultCommandSpec,
25
+ recoveryGuidanceForClassification,
23
26
  result,
24
27
  uniqueSorted,
25
28
  } from "./next-action-model.js";
@@ -33,15 +36,196 @@ import {
33
36
  } from "./next-action-artifacts.js";
34
37
  import { PHASES_REQUIRING_EXECUTION_CHRONOLOGY } from "./next-action-phases.js";
35
38
  import { evaluateContinuityNextAction } from "./next-action-continuity.js";
36
- import { currentCycleDiagnosis } from "./diagnosis-model.js";
39
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
40
+ import { buildTaskReflection } from "./reflection.js";
37
41
  import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
38
42
  import { criterionForDecision } from "./settlement-model.js";
39
43
  import { readEvents } from "./events.js";
44
+ import { inspectTaskConflictState } from "./task-conflict-inspection.js";
45
+ import { listActions } from "./actions.js";
46
+ import { listApprovals, validateApprovalForAction } from "./approvals.js";
47
+ import { evaluateActionCapability } from "./capability-policy.js";
48
+ import { loadPolicyIdentity } from "./policy-engine.js";
40
49
 
41
50
  export { NEXT_ACTIONS } from "./next-action-model.js";
42
51
 
52
+ function capabilityDecisionMetadata(action, capability, approvalId = null) {
53
+ return {
54
+ capability: action.capability,
55
+ decision: capability.decision,
56
+ reasonCode: capability.reasonCode ?? null,
57
+ policyFingerprint: capability.policyFingerprint ?? null,
58
+ ...(capability.authority?.authorityRef ? { authorityRef: capability.authority.authorityRef } : {}),
59
+ ...(capability.approval?.approvalId
60
+ ? { approvalId: capability.approval.approvalId }
61
+ : approvalId ? { approvalId } : {}),
62
+ };
63
+ }
64
+
65
+ function authorizeActionGuidance({ context, state, action, capability, eventsRel }) {
66
+ const approvalId = capability.approval?.approvalId ?? null;
67
+ const approvalSuffix = approvalId ? ` --approval ${approvalId}` : "";
68
+ return result({
69
+ ...context,
70
+ nextAction: NEXT_ACTIONS.AUTHORIZE_ACTION,
71
+ commands: [`forgeloop action-authorize --task ${state.taskId} --action ${action.actionId}${approvalSuffix}`],
72
+ reasons: [artifactError(
73
+ "E_ACTION_AUTHORIZATION_REQUIRED",
74
+ `Required action ${action.actionId} is PROPOSED and is authorizable under the canonical capability policy.`,
75
+ )],
76
+ capabilityDecision: capabilityDecisionMetadata(action, capability),
77
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
78
+ });
79
+ }
80
+
81
+ function actionApprovalGuidance({ context, state, action, capability, eventsRel }) {
82
+ return result({
83
+ ...context,
84
+ nextAction: NEXT_ACTIONS.REQUEST_ACTION_APPROVAL,
85
+ commands: [
86
+ `forgeloop approval-request --task ${state.taskId} --action ${action.actionId} --approval <approval-id> --reason <reason>`,
87
+ ],
88
+ commandSpecs: [{
89
+ ...directCommandSpec("approval-request", state.taskId, [
90
+ { name: "approvalId", option: "--approval=<approval-id>" },
91
+ { name: "actionId", option: "--action=<action-id>" },
92
+ { name: "reason", option: "--reason=<text>" },
93
+ ]),
94
+ }],
95
+ reasons: [artifactError(
96
+ "E_ACTION_APPROVAL_REQUIRED",
97
+ `Capability policy requires a current approval before required action ${action.actionId} can be authorized.`,
98
+ )],
99
+ approvalRequired: {
100
+ actionId: action.actionId,
101
+ capability: action.capability,
102
+ reason: "Capability policy requires approval before authorization.",
103
+ },
104
+ capabilityDecision: capabilityDecisionMetadata(action, capability),
105
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
106
+ });
107
+ }
108
+
109
+ function actionAuthorityGuidance({ context, state, action, capability, eventsRel }) {
110
+ return result({
111
+ ...context,
112
+ nextAction: NEXT_ACTIONS.AUTHORIZE_ACTION,
113
+ commands: [],
114
+ reasons: [artifactError(
115
+ capability.reasonCode ?? "E_ACTION_AUTHORITY_REQUIRED",
116
+ `Required action ${action.actionId} needs trusted host authority before it can be authorized.`,
117
+ )],
118
+ authorityRequired: {
119
+ kind: "HOST_ATTESTED",
120
+ actionId: action.actionId,
121
+ capability: action.capability,
122
+ reason: capability.allowed
123
+ ? "Authorization must be performed by a trusted host boundary that preserves authority context."
124
+ : "Capability policy requires trusted host authority; no standalone CLI command can create it.",
125
+ },
126
+ hostActionRequired: {
127
+ action: "action-authorize",
128
+ actionId: action.actionId,
129
+ executionBoundary: "HOST",
130
+ requiresAuthorityContext: true,
131
+ },
132
+ capabilityDecision: capabilityDecisionMetadata(action, capability),
133
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
134
+ });
135
+ }
136
+
137
+ function actionApprovalResolutionGuidance({ context, state, action, approval, eventsRel }) {
138
+ return result({
139
+ ...context,
140
+ nextAction: NEXT_ACTIONS.RESOLVE_ACTION_APPROVAL,
141
+ commands: [],
142
+ reasons: [artifactError(
143
+ "E_ACTION_APPROVAL_REQUIRED",
144
+ `Required action ${action.actionId} is waiting for approval.`,
145
+ )],
146
+ requiredArtifacts: [taskArtifactPath(state.taskId, "approvals"), eventsRel],
147
+ authorityRequired: {
148
+ kind: "HOST_ATTESTED",
149
+ approvalId: approval.approvalId,
150
+ actionId: action.actionId,
151
+ reason: "This approval must be resolved by a trusted host/operator boundary.",
152
+ },
153
+ });
154
+ }
155
+
156
+ function actionDeniedGuidance({ context, state, action, capability, eventsRel }) {
157
+ return result({
158
+ ...context,
159
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
160
+ commands: [],
161
+ reasons: [artifactError(
162
+ capability.reasonCode ?? "E_ACTION_CAPABILITY_DENIED",
163
+ `Capability policy does not authorize required action ${action.actionId}: ${capability.decision}.`,
164
+ )],
165
+ capabilityDecision: capabilityDecisionMetadata(action, capability),
166
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
167
+ });
168
+ }
169
+
170
+ function actionPolicyEpochGuidance({ context, state, action, policyIdentity, eventsRel }) {
171
+ const code = policyIdentity.code ?? "E_ACTION_POLICY_LOCK_REQUIRED";
172
+ const recoveryAction = policyRecoveryAction([{ code }]);
173
+ return result({
174
+ ...context,
175
+ nextAction: recoveryAction ?? NEXT_ACTIONS.RESOLVE_BLOCKER,
176
+ commands: [],
177
+ reasons: [artifactError(
178
+ code,
179
+ policyIdentity.message
180
+ ?? `Required action ${action.actionId} cannot be evaluated because the capability policy is not bound to the active task policy epoch.`,
181
+ [taskArtifactPath(state.taskId, "actions"), eventsRel],
182
+ )],
183
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
184
+ });
185
+ }
186
+
187
+ async function evaluateProposedActionCapability({ target, packageRoot, action, approvals, authorityContext }) {
188
+ const current = await evaluateActionCapability({ target, packageRoot, action, authorityContext });
189
+ if (current.decision !== "REQUIRE_APPROVAL" || !approvals) return current;
190
+ const approved = approvals.filter((approval) => approval.actionId === action.actionId && approval.status === "APPROVED");
191
+ for (const approval of approved) {
192
+ const evaluated = await evaluateActionCapability({
193
+ target,
194
+ packageRoot,
195
+ action,
196
+ authorityContext,
197
+ approval: { approvalId: approval.approvalId },
198
+ });
199
+ if (evaluated.allowed) return evaluated;
200
+ }
201
+ return current;
202
+ }
203
+
204
+ async function findApplicablePendingApproval({ target, packageRoot, taskId, action, approvals }) {
205
+ for (const approval of approvals) {
206
+ if (approval.status !== "PENDING" || approval.actionId !== action.actionId) continue;
207
+ try {
208
+ return await validateApprovalForAction(target, {
209
+ packageRoot,
210
+ taskId,
211
+ action,
212
+ approvalId: approval.approvalId,
213
+ requireApproved: false,
214
+ });
215
+ } catch (error) {
216
+ if (["E_APPROVAL_STALE", "E_APPROVAL_INVALID"].includes(error.code)) continue;
217
+ throw error;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+
43
223
  export function policyRecoveryAction(errors = []) {
44
- if (errors.some((e) => e.code === "E_POLICY_WEAKENING" || e.code === "E_POLICY_LOCK_MISMATCH")) {
224
+ if (errors.some((e) => [
225
+ "E_POLICY_WEAKENING",
226
+ "E_POLICY_LOCK_MISMATCH",
227
+ "E_ACTION_POLICY_DRIFT",
228
+ ].includes(e.code))) {
45
229
  return NEXT_ACTIONS.RESTORE_POLICY;
46
230
  }
47
231
  if (errors.some((e) => e.code === "E_CHECK_MUTATION_EXECUTION_ERROR" || e.code === "E_CHECK_MUTATION_NOT_DETECTED")) {
@@ -145,6 +329,269 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
145
329
 
146
330
  const state = workState.value;
147
331
  const context = { taskId: state.taskId, currentPhase: state.phase };
332
+ if (explicitTaskId) {
333
+ let inspection;
334
+ try {
335
+ inspection = await inspectTaskConflictState(target, {
336
+ taskId: explicitTaskId,
337
+ packageRoot,
338
+ });
339
+ } catch (error) {
340
+ inspection = {
341
+ classification: "INCONSISTENT",
342
+ reasonCodes: [error.code ?? "E_TASK_RECOVERY_INCONSISTENT"],
343
+ };
344
+ }
345
+ if (!["ACTIVE", "COMPLETE"].includes(inspection.classification)) {
346
+ const guidance = recoveryGuidanceForClassification(inspection.classification, explicitTaskId);
347
+ return result({
348
+ ...context,
349
+ nextAction: guidance.nextAction,
350
+ commands: guidance.commands,
351
+ commandSpecs: guidance.commandSpecs,
352
+ reasons: inspection.reasonCodes.map((code) => artifactError(
353
+ code,
354
+ `Task conflict state is ${inspection.classification}; follow the structured recovery guidance.`,
355
+ [stateRel, eventsRel, taskArtifactPath(explicitTaskId, "recovery")],
356
+ )),
357
+ requiredArtifacts: [stateRel, eventsRel],
358
+ });
359
+ }
360
+ }
361
+ let actionsForApproval;
362
+ try {
363
+ actionsForApproval = await listActions(target, { packageRoot, taskId: state.taskId });
364
+ } catch (error) {
365
+ if (error.code === "E_ACTION_INVALID") {
366
+ return result({
367
+ ...context,
368
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
369
+ commands: [],
370
+ reasons: [artifactError(
371
+ error.code,
372
+ `A durable action artifact is invalid and cannot be offered for authorization: ${error.message}`,
373
+ [taskArtifactPath(state.taskId, "actions"), eventsRel],
374
+ )],
375
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
376
+ });
377
+ }
378
+ throw error;
379
+ }
380
+ const ambiguousAction = actionsForApproval.find((action) => action.state === "COMMIT_UNKNOWN");
381
+ if (ambiguousAction) {
382
+ return result({ ...context, nextAction: NEXT_ACTIONS.RECONCILE_ACTION,
383
+ commands: [`forgeloop action-reconcile --task ${state.taskId} --action ${ambiguousAction.actionId} --outcome UNKNOWN`],
384
+ reasons: [artifactError("E_ACTION_RECONCILIATION_REQUIRED",
385
+ `Action ${ambiguousAction.actionId} has an unknown external commit outcome; retry is forbidden until reconciliation.`)],
386
+ reconciliationAuthorityRequired: {
387
+ outcomesRequiringTrust: ["COMMITTED", "NOT_COMMITTED"],
388
+ reason: "Recording an UNKNOWN observation is safe from any surface; settling COMMITTED or NOT_COMMITTED requires a trusted host boundary and evidence.",
389
+ },
390
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
391
+ }
392
+ // A PROPOSED required action ready for authorization should use the
393
+ // canonical authorization surface; host/approval blockers remain structured.
394
+ const authorizableAction = actionsForApproval.find((action) => action.requiredForCompletion
395
+ && action.state === "PROPOSED");
396
+ if (authorizableAction) {
397
+ let policyIdentity;
398
+ try {
399
+ policyIdentity = await loadPolicyIdentity(target, packageRoot, state.taskId);
400
+ } catch (error) {
401
+ return actionPolicyEpochGuidance({
402
+ context,
403
+ state,
404
+ action: authorizableAction,
405
+ policyIdentity: {
406
+ code: error.code ?? "E_POLICY_INVALID",
407
+ message: `Capability policy epoch validation failed for required action ${authorizableAction.actionId}: ${error.message}`,
408
+ },
409
+ eventsRel,
410
+ });
411
+ }
412
+ if (policyIdentity.status !== "VALID") {
413
+ return actionPolicyEpochGuidance({
414
+ context,
415
+ state,
416
+ action: authorizableAction,
417
+ policyIdentity,
418
+ eventsRel,
419
+ });
420
+ }
421
+
422
+ let capability;
423
+ try {
424
+ capability = await evaluateProposedActionCapability({
425
+ target,
426
+ packageRoot,
427
+ action: authorizableAction,
428
+ authorityContext,
429
+ });
430
+ } catch (error) {
431
+ return result({
432
+ ...context,
433
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
434
+ commands: [],
435
+ reasons: [artifactError(
436
+ error.code ?? "E_ACTION_CAPABILITY_INVALID",
437
+ `Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
438
+ [taskArtifactPath(state.taskId, "actions"), eventsRel],
439
+ )],
440
+ capabilityDecision: {
441
+ capability: authorizableAction.capability,
442
+ decision: "DENY",
443
+ reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
444
+ policyFingerprint: null,
445
+ },
446
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
447
+ });
448
+ }
449
+ if (capability.decision === "REQUIRE_AUTHORITY") {
450
+ return actionAuthorityGuidance({
451
+ context,
452
+ state,
453
+ action: authorizableAction,
454
+ capability,
455
+ eventsRel,
456
+ });
457
+ }
458
+ if (capability.allowed) {
459
+ return authorizeActionGuidance({
460
+ context,
461
+ state,
462
+ action: authorizableAction,
463
+ capability,
464
+ eventsRel,
465
+ });
466
+ }
467
+ if (capability.decision === "REQUIRE_APPROVAL") {
468
+ let approvals;
469
+ try {
470
+ approvals = await listApprovals(target, { packageRoot, taskId: state.taskId });
471
+ } catch (error) {
472
+ return result({
473
+ ...context,
474
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
475
+ commands: [],
476
+ reasons: [artifactError(
477
+ error.code ?? "E_APPROVAL_INVALID",
478
+ `Approval artifact inspection failed for required action ${authorizableAction.actionId}: ${error.message}`,
479
+ [taskArtifactPath(state.taskId, "approvals"), eventsRel],
480
+ )],
481
+ capabilityDecision: capabilityDecisionMetadata(authorizableAction, capability),
482
+ requiredArtifacts: [taskArtifactPath(state.taskId, "approvals"), eventsRel],
483
+ });
484
+ }
485
+ try {
486
+ capability = await evaluateProposedActionCapability({
487
+ target,
488
+ packageRoot,
489
+ action: authorizableAction,
490
+ approvals,
491
+ authorityContext,
492
+ });
493
+ } catch (error) {
494
+ return result({
495
+ ...context,
496
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
497
+ commands: [],
498
+ reasons: [artifactError(
499
+ error.code ?? "E_ACTION_CAPABILITY_INVALID",
500
+ `Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
501
+ [taskArtifactPath(state.taskId, "actions"), eventsRel],
502
+ )],
503
+ capabilityDecision: {
504
+ capability: authorizableAction.capability,
505
+ decision: "DENY",
506
+ reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
507
+ policyFingerprint: null,
508
+ },
509
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
510
+ });
511
+ }
512
+ if (capability.allowed) {
513
+ return authorizeActionGuidance({
514
+ context,
515
+ state,
516
+ action: authorizableAction,
517
+ capability,
518
+ eventsRel,
519
+ });
520
+ }
521
+ if (capability.decision !== "REQUIRE_APPROVAL") {
522
+ if (capability.decision === "REQUIRE_AUTHORITY") {
523
+ return actionAuthorityGuidance({
524
+ context,
525
+ state,
526
+ action: authorizableAction,
527
+ capability,
528
+ eventsRel,
529
+ });
530
+ }
531
+ if (capability.allowed) {
532
+ return authorizeActionGuidance({
533
+ context,
534
+ state,
535
+ action: authorizableAction,
536
+ capability,
537
+ eventsRel,
538
+ });
539
+ }
540
+ return actionDeniedGuidance({
541
+ context,
542
+ state,
543
+ action: authorizableAction,
544
+ capability,
545
+ eventsRel,
546
+ });
547
+ }
548
+ const pendingApproval = await findApplicablePendingApproval({
549
+ target,
550
+ packageRoot,
551
+ taskId: state.taskId,
552
+ action: authorizableAction,
553
+ approvals,
554
+ });
555
+ if (pendingApproval) {
556
+ return actionApprovalResolutionGuidance({
557
+ context,
558
+ state,
559
+ action: authorizableAction,
560
+ approval: pendingApproval,
561
+ eventsRel,
562
+ });
563
+ }
564
+ return actionApprovalGuidance({
565
+ context,
566
+ state,
567
+ action: authorizableAction,
568
+ capability,
569
+ eventsRel,
570
+ });
571
+ }
572
+ return actionDeniedGuidance({
573
+ context,
574
+ state,
575
+ action: authorizableAction,
576
+ capability,
577
+ eventsRel,
578
+ });
579
+ }
580
+ // Committed-but-unverified required action needs canonical postcondition
581
+ // evidence before it can satisfy completion.
582
+ const committedActions = actionsForApproval;
583
+ const unverifiedRequired = committedActions.find((action) => action.requiredForCompletion
584
+ && action.state === "COMMITTED");
585
+ if (unverifiedRequired) {
586
+ return result({ ...context, nextAction: NEXT_ACTIONS.VERIFY_EXTERNAL_ACTION,
587
+ commands: [
588
+ `forgeloop run-check --task ${state.taskId} --id check-postcondition --requirement <requirement> -- <independent verification argv>`,
589
+ `forgeloop action-verify --task ${state.taskId} --action ${unverifiedRequired.actionId} --evidence <execution-ref>`,
590
+ ],
591
+ reasons: [artifactError("E_ACTION_VERIFICATION_REQUIRED",
592
+ `Committed action ${unverifiedRequired.actionId} requires independent canonical postcondition evidence; exit code 0 alone is not verification.`)],
593
+ requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
594
+ }
148
595
  if (state.phase === "RECEIVED") {
149
596
  return decision(
150
597
  context,
@@ -498,7 +945,7 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
498
945
  if (state.phase === "DIAGNOSING") {
499
946
  const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
500
947
  const cycle = state.verificationCycle ?? 1;
501
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
948
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
502
949
  if (!diagEvent) {
503
950
  return result({
504
951
  ...context,
@@ -516,6 +963,13 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
516
963
  }
517
964
  const progress = evaluateProgress({ state, events: ledger.events });
518
965
  if (progress.status === PROGRESS_STATUS.STALLED) {
966
+ let oscillating = false;
967
+ try {
968
+ const reflection = await buildTaskReflection({ target, packageRoot, taskId: normalized.taskId ?? null });
969
+ oscillating = reflection.oscillation.detected;
970
+ } catch {
971
+ // Reflection is advisory guidance; stall handling must not depend on it.
972
+ }
519
973
  return result({
520
974
  ...context,
521
975
  nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
@@ -527,6 +981,11 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
527
981
  ),
528
982
  ],
529
983
  progress,
984
+ diagnosticGuidance: {
985
+ action: oscillating ? NEXT_ACTIONS.INTRODUCE_NEW_OBSERVATION : NEXT_ACTIONS.REQUIRE_NEW_DIAGNOSTIC_INFORMATION,
986
+ signals: oscillating ? ["OSCILLATING_STRATEGY"] : ["NO_INFORMATION_GAIN", ...progress.signals.map((signal) => signal.code)],
987
+ errorCodes: oscillating ? ["E_STRATEGY_OSCILLATION", "E_PROGRESS_STALLED"] : ["E_PROGRESS_STALLED"],
988
+ },
530
989
  requiredArtifacts,
531
990
  });
532
991
  }
@@ -542,6 +1001,34 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
542
1001
  });
543
1002
  }
544
1003
  if (state.phase === "CORRECTING") {
1004
+ const ledgerForCorrection = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
1005
+ const correctionCycle = state.verificationCycle ?? 1;
1006
+ const hasStructuredCase = ledgerForCorrection.events.some(
1007
+ (event) => event.event === "DIAGNOSTIC_CASE_RECORDED"
1008
+ && event.taskId === state.taskId
1009
+ && event.details?.verificationCycle === correctionCycle,
1010
+ );
1011
+ const hasIntervention = ledgerForCorrection.events.some(
1012
+ (event) => event.event === "INTERVENTION_RECORDED"
1013
+ && event.taskId === state.taskId
1014
+ && event.details?.verificationCycle === correctionCycle,
1015
+ );
1016
+ if (hasStructuredCase && !hasIntervention) {
1017
+ return result({
1018
+ ...context,
1019
+ nextAction: NEXT_ACTIONS.ENTER_VERIFYING,
1020
+ reasons: [
1021
+ artifactError("PHASE_CORRECTING", "A structured diagnostic case is awaiting a recorded intervention"),
1022
+ ],
1023
+ diagnosticGuidance: {
1024
+ action: NEXT_ACTIONS.RECORD_INTERVENTION,
1025
+ signals: ["INTERVENTION_NOT_BOUND_TO_HYPOTHESIS"],
1026
+ errorCodes: [],
1027
+ commandSpecs: [recordInterventionCommandSpec(normalized.taskId)],
1028
+ },
1029
+ requiredArtifacts,
1030
+ });
1031
+ }
545
1032
  return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
546
1033
  }
547
1034
  if (state.phase === "REVIEWING") {
package/src/core/phase.js CHANGED
@@ -3,13 +3,13 @@ import { readContract } from "./contract.js";
3
3
  import {
4
4
  appendProtocolEvent,
5
5
  LIFECYCLE_MILESTONES,
6
- validateCompletionRecoveryAuthorization,
7
6
  validateEventLedger,
8
7
  validateStateLedgerCoherence,
9
8
  } from "./events.js";
10
9
  import { readPersistedRoute } from "./route-artifact.js";
11
10
  import { assertWorkPhase, isValidTransition } from "./protocol.js";
12
11
  import { readWorkState, mutateWorkState } from "./work-state.js";
12
+ import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
13
13
  import { evaluateCompletion } from "./completion.js";
14
14
  import { evaluatePreflight } from "./preflight.js";
15
15
  import { requiredEvidenceForTarget } from "./completion-artifacts.js";
@@ -21,7 +21,11 @@ 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
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
25
+ import {
26
+ buildInformationGainProjection,
27
+ evaluateStructuredDiagnosticStall,
28
+ } from "./information-gain-projection.js";
25
29
 
26
30
  function phaseError(code, message, artifacts = []) {
27
31
  const error = new Error(message);
@@ -164,10 +168,9 @@ export async function advanceWorkState(target, toPhase, options = {}) {
164
168
  } = normalizedOptions;
165
169
  assertWorkPhase(toPhase);
166
170
 
167
- const state = await readWorkState(target, { packageRoot, taskId, statePath });
171
+ let state = await readWorkState(target, { packageRoot, taskId, statePath });
168
172
  const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
169
173
  if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [stateRel]);
170
-
171
174
  const eventsRel = eventsPath ?? (taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
172
175
  const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
173
176
 
@@ -187,7 +190,8 @@ export async function advanceWorkState(target, toPhase, options = {}) {
187
190
 
188
191
  const nonCompleteTasks = discovered.filter((t) => t.phase !== "COMPLETE");
189
192
  if (nonCompleteTasks.length > 1) {
190
- const claims = descriptor?.writeClaims ?? [];
193
+ const currentTask = discovered.find((task) => task.taskId === taskId && task.healthy !== false);
194
+ const claims = currentTask?.writeClaims ?? [];
191
195
  if (claims.length === 0) {
192
196
  throw phaseError(
193
197
  E_TASK_SCOPE_REQUIRED,
@@ -222,30 +226,33 @@ export async function advanceWorkState(target, toPhase, options = {}) {
222
226
  const eventType = PHASE_EVENTS[toPhase];
223
227
  const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
224
228
  if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
225
- let currentReceipt = null;
226
- try {
227
- const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
228
- currentReceipt = receiptArtifact?.value;
229
- } catch {
230
- // If receipt is not present, pass null
231
- }
232
- const recoveryAuth = validateCompletionRecoveryAuthorization({
233
- state,
234
- receipt: currentReceipt,
235
- events: ledger.events,
229
+ const recovery = await authorizeCompletionRecoveryOrRebind({
230
+ target,
231
+ packageRoot,
232
+ taskId,
233
+ statePath,
234
+ contractPath,
235
+ routePath,
236
+ receiptPath,
237
+ eventsPath,
238
+ authorityContext,
239
+ runtimeContext,
236
240
  });
237
- if (!recoveryAuth.authorized) {
238
- const firstError = recoveryAuth.errors[0] ?? {};
241
+ if (!recovery.recoveryAuth.authorized) {
242
+ const firstError = recovery.recoveryAuth.errors[0] ?? {};
239
243
  throw phaseError(
240
244
  firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
241
245
  firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
242
246
  [stateRel, eventsRel],
243
247
  );
244
248
  }
249
+ if (recovery.rebound) {
250
+ state = recovery.state;
251
+ }
245
252
  }
246
253
  if (toPhase === "CORRECTING" && state.phase === "DIAGNOSING") {
247
254
  const cycle = state.verificationCycle ?? 1;
248
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
255
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
249
256
  if (!diagEvent) {
250
257
  throw phaseError(
251
258
  "E_DIAGNOSIS_REQUIRED",
@@ -253,8 +260,21 @@ export async function advanceWorkState(target, toPhase, options = {}) {
253
260
  [eventsRel],
254
261
  );
255
262
  }
256
- const details = diagEvent.details;
257
- if (!details || details.informationGain === "NONE") {
263
+ let stalledNoGain = false;
264
+ if (diagEvent.sourceModel === "STRUCTURED_DIAGNOSTIC_CASE_V1") {
265
+ // One canonical structured-stall truth, shared with progress/reflect.
266
+ const stall = evaluateStructuredDiagnosticStall(
267
+ buildInformationGainProjection(ledger.events, state.taskId),
268
+ { verificationCycle: cycle },
269
+ );
270
+ stalledNoGain = stall.stalled;
271
+ }
272
+ const gainClassification = diagEvent.sourceModel === "STRUCTURED_DIAGNOSTIC_CASE_V1"
273
+ ? null
274
+ : diagEvent.details?.informationGain;
275
+ if (!diagEvent.details
276
+ || stalledNoGain
277
+ || (diagEvent.sourceModel !== "STRUCTURED_DIAGNOSTIC_CASE_V1" && gainClassification === "NONE")) {
258
278
  throw phaseError(
259
279
  "E_DIAGNOSIS_NO_NEW_INFORMATION",
260
280
  "The proposed retry repeats the previous hypothesis with the same evidence without new information",
@@ -264,7 +284,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
264
284
  }
265
285
  if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
266
286
  const cycle = state.verificationCycle ?? 1;
267
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
287
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
268
288
  if (!diagEvent) {
269
289
  throw phaseError(
270
290
  "E_DIAGNOSIS_REQUIRED",