@cassiomc1/forgeloop 1.5.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 (126) hide show
  1. package/DOCS_INDEX.md +13 -8
  2. package/EXECUTION_STATE.md +20 -0
  3. package/LOOP_ENGINEERING.md +81 -0
  4. package/LOOP_SYSTEM_DESIGN.md +32 -0
  5. package/PROTOCOL_INTEGRATION.md +46 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +31 -9
  8. package/THIRD_PARTY_NOTICES.md +15 -0
  9. package/THREAT_MODEL.md +39 -0
  10. package/docs/ARTIFACT_REFERENCE.md +140 -0
  11. package/docs/CLI_REFERENCE.md +294 -3
  12. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  13. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  14. package/docs/EXECUTION_TRACE.md +76 -0
  15. package/docs/MCP.md +33 -0
  16. package/docs/RECIPES.md +67 -0
  17. package/docs/TROUBLESHOOTING.md +106 -2
  18. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  19. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  21. package/docs/diagrams/README.md +55 -0
  22. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  23. package/docs/diagrams/manifest.json +42 -0
  24. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  25. package/package.json +8 -6
  26. package/schemas/action.schema.json +100 -0
  27. package/schemas/approval.schema.json +51 -0
  28. package/schemas/capability-policy.schema.json +41 -0
  29. package/schemas/diagnostic-case.schema.json +85 -0
  30. package/schemas/execution-receipt.schema.json +16 -0
  31. package/schemas/hypothesis-disposition.schema.json +16 -0
  32. package/schemas/intervention.schema.json +27 -0
  33. package/schemas/policy-lock.schema.json +1 -0
  34. package/schemas/policy-snapshot.schema.json +2 -0
  35. package/schemas/trajectory-evaluation.schema.json +64 -0
  36. package/schemas/trajectory-scenario.schema.json +42 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/action-authorize.js +41 -0
  39. package/src/commands/action-propose.js +10 -0
  40. package/src/commands/action-reconcile.js +10 -0
  41. package/src/commands/action-record.js +47 -0
  42. package/src/commands/action-show.js +10 -0
  43. package/src/commands/action-verify.js +10 -0
  44. package/src/commands/advance.js +7 -2
  45. package/src/commands/approval-request.js +64 -0
  46. package/src/commands/approval-resolve.js +10 -0
  47. package/src/commands/baseline.js +3 -3
  48. package/src/commands/eval.js +6 -0
  49. package/src/commands/history.js +18 -0
  50. package/src/commands/init.js +2 -2
  51. package/src/commands/inspect.js +49 -0
  52. package/src/commands/metrics.js +7 -0
  53. package/src/commands/next.js +8 -2
  54. package/src/commands/policy-discover.js +2 -2
  55. package/src/commands/record-diagnosis.js +37 -1
  56. package/src/commands/record-hypothesis-disposition.js +45 -0
  57. package/src/commands/record-intervention.js +35 -0
  58. package/src/commands/reflect.js +38 -0
  59. package/src/commands/report.js +9 -1
  60. package/src/commands/run-action.js +18 -0
  61. package/src/commands/trace.js +34 -0
  62. package/src/commands/validate-protocol.js +21 -13
  63. package/src/core/action-authorization.js +106 -0
  64. package/src/core/action-constants.js +86 -0
  65. package/src/core/action-execution.js +105 -0
  66. package/src/core/action-ledger-projection.js +302 -0
  67. package/src/core/action-model.js +581 -0
  68. package/src/core/action-readiness.js +141 -0
  69. package/src/core/action-reconciliation-policy.js +49 -0
  70. package/src/core/action-reconciliation.js +66 -0
  71. package/src/core/action-verification.js +111 -0
  72. package/src/core/actions.js +462 -0
  73. package/src/core/approvals.js +405 -0
  74. package/src/core/artifact-registry.js +48 -0
  75. package/src/core/audit.js +25 -0
  76. package/src/core/bundles.js +15 -0
  77. package/src/core/capability-policy.js +226 -0
  78. package/src/core/cli-command-definitions.js +210 -1
  79. package/src/core/command-executors.js +171 -15
  80. package/src/core/command-runtime.js +12 -1
  81. package/src/core/completion-artifacts.js +37 -12
  82. package/src/core/completion-recovery-rebind.js +194 -0
  83. package/src/core/completion.js +70 -0
  84. package/src/core/continuity-reconciliation.js +24 -5
  85. package/src/core/diagnostic-model.js +396 -0
  86. package/src/core/diagnostic-projection.js +51 -0
  87. package/src/core/diagnostic-record.js +360 -0
  88. package/src/core/error-codes.js +343 -0
  89. package/src/core/events.js +41 -1
  90. package/src/core/execution-prerequisites.js +4 -1
  91. package/src/core/execution.js +26 -188
  92. package/src/core/failure-signature.js +70 -0
  93. package/src/core/failure-surface.js +57 -0
  94. package/src/core/history.js +110 -0
  95. package/src/core/hypothesis-projection.js +85 -0
  96. package/src/core/information-gain-projection.js +283 -0
  97. package/src/core/information-gain.js +138 -0
  98. package/src/core/inspect.js +105 -7
  99. package/src/core/integration-invocation-policy.js +47 -0
  100. package/src/core/integration-resources.js +51 -0
  101. package/src/core/next-action-model.js +35 -1
  102. package/src/core/next-action.js +459 -3
  103. package/src/core/phase.js +40 -21
  104. package/src/core/policy-engine.js +113 -6
  105. package/src/core/preflight-consistency.js +31 -5
  106. package/src/core/preflight.js +19 -2
  107. package/src/core/prepared-execution.js +227 -0
  108. package/src/core/progress.js +41 -4
  109. package/src/core/protocol-info.js +48 -0
  110. package/src/core/protocol.js +14 -0
  111. package/src/core/receipt.js +1 -0
  112. package/src/core/reconcile-closure.js +15 -12
  113. package/src/core/reflection.js +305 -0
  114. package/src/core/resumability.js +57 -3
  115. package/src/core/schema-validation.js +8 -0
  116. package/src/core/strategy-analysis.js +97 -0
  117. package/src/core/task-paths.js +28 -0
  118. package/src/core/task-snapshot.js +53 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/trace.js +548 -0
  121. package/src/core/trajectory-evaluation.js +71 -0
  122. package/src/core/trajectory-metrics.js +80 -0
  123. package/src/core/transaction.js +8 -0
  124. package/src/core/work-state.js +10 -5
  125. package/docs/assets/forgeloop-flow.svg +0 -1
  126. package/docs/forgeloop-flow.mmd +0 -51
@@ -16,9 +16,11 @@ 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,
23
25
  recoveryGuidanceForClassification,
24
26
  result,
@@ -34,16 +36,196 @@ import {
34
36
  } from "./next-action-artifacts.js";
35
37
  import { PHASES_REQUIRING_EXECUTION_CHRONOLOGY } from "./next-action-phases.js";
36
38
  import { evaluateContinuityNextAction } from "./next-action-continuity.js";
37
- import { currentCycleDiagnosis } from "./diagnosis-model.js";
39
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
40
+ import { buildTaskReflection } from "./reflection.js";
38
41
  import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
39
42
  import { criterionForDecision } from "./settlement-model.js";
40
43
  import { readEvents } from "./events.js";
41
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";
42
49
 
43
50
  export { NEXT_ACTIONS } from "./next-action-model.js";
44
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
+
45
223
  export function policyRecoveryAction(errors = []) {
46
- 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))) {
47
229
  return NEXT_ACTIONS.RESTORE_POLICY;
48
230
  }
49
231
  if (errors.some((e) => e.code === "E_CHECK_MUTATION_EXECUTION_ERROR" || e.code === "E_CHECK_MUTATION_NOT_DETECTED")) {
@@ -176,6 +358,240 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
176
358
  });
177
359
  }
178
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
+ }
179
595
  if (state.phase === "RECEIVED") {
180
596
  return decision(
181
597
  context,
@@ -529,7 +945,7 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
529
945
  if (state.phase === "DIAGNOSING") {
530
946
  const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
531
947
  const cycle = state.verificationCycle ?? 1;
532
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
948
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
533
949
  if (!diagEvent) {
534
950
  return result({
535
951
  ...context,
@@ -547,6 +963,13 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
547
963
  }
548
964
  const progress = evaluateProgress({ state, events: ledger.events });
549
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
+ }
550
973
  return result({
551
974
  ...context,
552
975
  nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
@@ -558,6 +981,11 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
558
981
  ),
559
982
  ],
560
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
+ },
561
989
  requiredArtifacts,
562
990
  });
563
991
  }
@@ -573,6 +1001,34 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
573
1001
  });
574
1002
  }
575
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
+ }
576
1032
  return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
577
1033
  }
578
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
 
@@ -223,30 +226,33 @@ export async function advanceWorkState(target, toPhase, options = {}) {
223
226
  const eventType = PHASE_EVENTS[toPhase];
224
227
  const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
225
228
  if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
226
- let currentReceipt = null;
227
- try {
228
- const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
229
- currentReceipt = receiptArtifact?.value;
230
- } catch {
231
- // If receipt is not present, pass null
232
- }
233
- const recoveryAuth = validateCompletionRecoveryAuthorization({
234
- state,
235
- receipt: currentReceipt,
236
- 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,
237
240
  });
238
- if (!recoveryAuth.authorized) {
239
- const firstError = recoveryAuth.errors[0] ?? {};
241
+ if (!recovery.recoveryAuth.authorized) {
242
+ const firstError = recovery.recoveryAuth.errors[0] ?? {};
240
243
  throw phaseError(
241
244
  firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
242
245
  firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
243
246
  [stateRel, eventsRel],
244
247
  );
245
248
  }
249
+ if (recovery.rebound) {
250
+ state = recovery.state;
251
+ }
246
252
  }
247
253
  if (toPhase === "CORRECTING" && state.phase === "DIAGNOSING") {
248
254
  const cycle = state.verificationCycle ?? 1;
249
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
255
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
250
256
  if (!diagEvent) {
251
257
  throw phaseError(
252
258
  "E_DIAGNOSIS_REQUIRED",
@@ -254,8 +260,21 @@ export async function advanceWorkState(target, toPhase, options = {}) {
254
260
  [eventsRel],
255
261
  );
256
262
  }
257
- const details = diagEvent.details;
258
- 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")) {
259
278
  throw phaseError(
260
279
  "E_DIAGNOSIS_NO_NEW_INFORMATION",
261
280
  "The proposed retry repeats the previous hypothesis with the same evidence without new information",
@@ -265,7 +284,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
265
284
  }
266
285
  if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
267
286
  const cycle = state.verificationCycle ?? 1;
268
- const diagEvent = currentCycleDiagnosis(ledger.events, state.taskId, cycle);
287
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
269
288
  if (!diagEvent) {
270
289
  throw phaseError(
271
290
  "E_DIAGNOSIS_REQUIRED",