@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
@@ -25,9 +25,25 @@ import { runBundle } from "../commands/bundle.js";
25
25
  import { runPrepareCompletion } from "../commands/prepare-completion.js";
26
26
  import { runRecordCheck } from "../commands/record-check.js";
27
27
  import { runCheck } from "../commands/run-check.js";
28
+ import { runAction } from "../commands/run-action.js";
29
+ import { runActionPropose } from "../commands/action-propose.js";
30
+ import { runActionRecord } from "../commands/action-record.js";
31
+ import { runActionShow } from "../commands/action-show.js";
32
+ import { runActionAuthorize } from "../commands/action-authorize.js";
33
+ import { runActionVerify } from "../commands/action-verify.js";
34
+ import { runActionReconcile } from "../commands/action-reconcile.js";
35
+ import { runMetrics } from "../commands/metrics.js";
36
+ import { runEval } from "../commands/eval.js";
37
+ import { runApprovalRequest } from "../commands/approval-request.js";
38
+ import { runApprovalResolve } from "../commands/approval-resolve.js";
28
39
  import { reconcileClosure } from "../commands/reconcile-closure.js";
29
40
  import { runRecordTerminalResult } from "../commands/record-terminal-result.js";
30
41
  import { runRecordDiagnosis } from "../commands/record-diagnosis.js";
42
+ import { runRecordIntervention } from "../commands/record-intervention.js";
43
+ import { runRecordHypothesisDisposition } from "../commands/record-hypothesis-disposition.js";
44
+ import { runHistory } from "../commands/history.js";
45
+ import { runTrace } from "../commands/trace.js";
46
+ import { runReflect } from "../commands/reflect.js";
31
47
  import { runProgress } from "../commands/progress.js";
32
48
  import { runRecordDecisionCriterion } from "../commands/record-decision-criterion.js";
33
49
  import { runNext } from "../commands/next.js";
@@ -100,12 +116,19 @@ export const COMMAND_EXECUTORS = {
100
116
  const result = await runPreflight({ target, packageRoot, strict: options.strict, taskId: options.taskId });
101
117
  return { result, exitCode: result.status === "READY" ? 0 : 1 };
102
118
  },
103
- advance: async ({ target, packageRoot, options }) => ({
104
- result: await runAdvance({ target, packageRoot, to: options.to, taskId: options.taskId }),
119
+ advance: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
120
+ result: await runAdvance({
121
+ target,
122
+ packageRoot,
123
+ to: options.to,
124
+ taskId: options.taskId,
125
+ authorityContext,
126
+ runtimeContext,
127
+ }),
105
128
  exitCode: 0,
106
129
  }),
107
- next: async ({ target, packageRoot, options }) => ({
108
- result: await runNext({ target, packageRoot, taskId: options.taskId }),
130
+ next: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
131
+ result: await runNext({ target, packageRoot, taskId: options.taskId, authorityContext, runtimeContext }),
109
132
  exitCode: 0,
110
133
  }),
111
134
  continuity: async ({ target, packageRoot, options }) => ({
@@ -135,11 +158,11 @@ export const COMMAND_EXECUTORS = {
135
158
  result: await runClearContinuity({ target, taskId: options.taskId }),
136
159
  exitCode: 0,
137
160
  }),
138
- "prepare-completion": async ({ target, packageRoot, options }) => ({
139
- result: await runPrepareCompletion({ target, packageRoot, taskId: options.taskId }),
161
+ "prepare-completion": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
162
+ result: await runPrepareCompletion({ target, packageRoot, taskId: options.taskId, authorityContext, runtimeContext }),
140
163
  exitCode: 0,
141
164
  }),
142
- "run-check": async ({ target, packageRoot, options }) => {
165
+ "run-check": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
143
166
  const result = await runCheck({
144
167
  target,
145
168
  packageRoot,
@@ -149,9 +172,67 @@ export const COMMAND_EXECUTORS = {
149
172
  details: options.checkDetails ?? undefined,
150
173
  timeoutMs: options.timeoutMs ?? undefined,
151
174
  taskId: options.taskId,
175
+ authorityContext,
176
+ runtimeContext,
152
177
  });
153
178
  return { result, exitCode: result.check.status === "passed" ? 0 : 1 };
154
179
  },
180
+ "run-action": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
181
+ const result = await runAction({ target, packageRoot, taskId: options.taskId,
182
+ actionId: options.actionId, capability: options.actionCapability,
183
+ effectClass: options.actionEffectClass, actionTarget: options.actionTarget,
184
+ idempotencyKey: options.actionIdempotencyKey, requirement: options.actionRequirement,
185
+ requiredForCompletion: options.actionRequiredForCompletion, argv: options.commandArgv,
186
+ approvalId: options.approvalId, timeoutMs: options.timeoutMs,
187
+ authorityContext, runtimeContext });
188
+ return { result, exitCode: result.action.state === "COMMITTED" ? 0 : 1 };
189
+ },
190
+ "action-propose": async ({ target, packageRoot, options }) => ({ result: await runActionPropose({
191
+ target, packageRoot, taskId: options.taskId, input: { actionId: options.actionId,
192
+ capability: options.actionCapability, effectClass: options.actionEffectClass,
193
+ target: options.actionTarget, operation: options.actionOperation,
194
+ idempotencyKey: options.actionIdempotencyKey, requirement: options.actionRequirement,
195
+ requiredForCompletion: options.actionRequiredForCompletion },
196
+ }), exitCode: 0 }),
197
+ "action-record": async ({ target, packageRoot, options }) => ({ result: await runActionRecord({
198
+ target, packageRoot, taskId: options.taskId, actionId: options.actionId,
199
+ state: options.actionState, provenance: options.actionProvenance,
200
+ evidenceRef: options.actionEvidenceRef,
201
+ }), exitCode: 0 }),
202
+ "action-show": async ({ target, packageRoot, options }) => ({ result: await runActionShow({
203
+ target, packageRoot, taskId: options.taskId, actionId: options.actionId,
204
+ }), exitCode: 0 }),
205
+ "action-authorize": async ({ target, packageRoot, options, authorityContext }) => ({
206
+ result: await runActionAuthorize({
207
+ target,
208
+ packageRoot,
209
+ taskId: options.taskId,
210
+ actionId: options.actionId,
211
+ approvalId: options.approvalId,
212
+ // Trusted authority arrives out-of-band only; never from actor input.
213
+ authorityContext,
214
+ }),
215
+ exitCode: 0,
216
+ }),
217
+ "action-verify": async ({ target, packageRoot, options }) => ({ result: await runActionVerify({
218
+ target, packageRoot, taskId: options.taskId, actionId: options.actionId,
219
+ evidenceRef: options.actionEvidenceRef,
220
+ }), exitCode: 0 }),
221
+ "action-reconcile": async ({ target, packageRoot, options, authorityContext }) => ({ result: await runActionReconcile({
222
+ target, packageRoot, taskId: options.taskId, actionId: options.actionId,
223
+ outcome: options.reconciliationOutcome, evidenceRefs: options.evidenceRefs ?? [],
224
+ observedAt: options.observedAt,
225
+ // Trusted settlement authority travels only through the out-of-band
226
+ // executor parameter; actor input can never supply it.
227
+ authorityContext,
228
+ }), exitCode: 0 }),
229
+ metrics: async ({ target, packageRoot, options }) => ({ result: await runMetrics({ target, packageRoot, taskId: options.taskId }), exitCode: 0 }),
230
+ eval: async ({ target, packageRoot, options }) => {
231
+ const result = await runEval({ target, packageRoot, taskId: options.taskId, scenarioPath: options.scenarioPath });
232
+ return { result, exitCode: result.result === "PASS" ? 0 : 1 };
233
+ },
234
+ "approval-request": async ({ target, packageRoot, options }) => ({ result: await runApprovalRequest({ target, packageRoot, taskId: options.taskId, approvalId: options.approvalId, actionId: options.actionId, reason: options.reason }), exitCode: 0 }),
235
+ "approval-resolve": async ({ target, packageRoot, options, authorityContext }) => ({ result: await runApprovalResolve({ target, packageRoot, taskId: options.taskId, approvalId: options.approvalId, decision: options.approvalDecision, authorityKind: options.approvalAuthorityKind, hostGrantRef: options.hostGrantRef, reason: options.reason, authorityContext }), exitCode: 0 }),
155
236
  "record-check": async ({ target, packageRoot, options }) => ({
156
237
  result: await runRecordCheck({
157
238
  target,
@@ -189,6 +270,7 @@ export const COMMAND_EXECUTORS = {
189
270
  result: await runRecordDiagnosis({
190
271
  target,
191
272
  packageRoot,
273
+ file: options.file ?? null,
192
274
  hypothesis: options.hypothesis,
193
275
  failureClass: options.failureClass,
194
276
  evidenceRefs: options.evidenceRefs,
@@ -198,6 +280,52 @@ export const COMMAND_EXECUTORS = {
198
280
  }),
199
281
  exitCode: 0,
200
282
  }),
283
+ "record-intervention": async ({ target, packageRoot, options }) => ({
284
+ result: await runRecordIntervention({
285
+ target,
286
+ packageRoot,
287
+ file: options.file ?? null,
288
+ taskId: options.taskId,
289
+ }),
290
+ exitCode: 0,
291
+ }),
292
+ "record-hypothesis-disposition": async ({ target, packageRoot, options }) => ({
293
+ result: await runRecordHypothesisDisposition({
294
+ target,
295
+ packageRoot,
296
+ hypothesis: options.hypothesis,
297
+ status: options.dispositionStatus,
298
+ evidenceRefs: options.evidenceRefs,
299
+ reason: options.reason,
300
+ taskId: options.taskId,
301
+ }),
302
+ exitCode: 0,
303
+ }),
304
+ history: async ({ target, packageRoot, options }) => {
305
+ const result = await runHistory({
306
+ target,
307
+ packageRoot,
308
+ taskId: options.taskId,
309
+ filters: {
310
+ type: options.historyType ?? null,
311
+ phase: options.historyPhase ?? null,
312
+ failures: Boolean(options.historyFailures),
313
+ checks: Boolean(options.historyChecks),
314
+ since: options.historySince ?? null,
315
+ until: options.historyUntil ?? null,
316
+ limit: Number.isInteger(options.historyLimit) ? options.historyLimit : null,
317
+ },
318
+ });
319
+ return { result, exitCode: result.integrity.valid ? 0 : 1 };
320
+ },
321
+ trace: async ({ target, packageRoot, options }) => {
322
+ const result = await runTrace({ target, packageRoot, taskId: options.taskId });
323
+ return { result, exitCode: result.integrity.valid ? 0 : 1 };
324
+ },
325
+ reflect: async ({ target, packageRoot, options }) => {
326
+ const result = await runReflect({ target, packageRoot, taskId: options.taskId });
327
+ return { result, exitCode: result.status === "STALLED" ? 1 : 0 };
328
+ },
201
329
  progress: async ({ target, packageRoot, options }) => {
202
330
  const result = await runProgress({ target, packageRoot, taskId: options.taskId });
203
331
  return { result, exitCode: result.status === "STALLED" ? 1 : 0 };
@@ -212,16 +340,37 @@ export const COMMAND_EXECUTORS = {
212
340
  }),
213
341
  exitCode: 0,
214
342
  }),
215
- complete: async ({ target, packageRoot, options }) => {
216
- const result = await runComplete({ target, packageRoot, strict: options.strict, taskId: options.taskId });
343
+ complete: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
344
+ const result = await runComplete({
345
+ target,
346
+ packageRoot,
347
+ strict: options.strict,
348
+ taskId: options.taskId,
349
+ authorityContext,
350
+ runtimeContext,
351
+ });
217
352
  return { result, exitCode: result.status === "VALID" ? 0 : 1 };
218
353
  },
219
- audit: async ({ target, packageRoot, options }) => {
220
- const result = await runAudit({ target, packageRoot, strict: options.strict, taskId: options.taskId });
354
+ audit: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
355
+ const result = await runAudit({
356
+ target,
357
+ packageRoot,
358
+ strict: options.strict,
359
+ taskId: options.taskId,
360
+ authorityContext,
361
+ runtimeContext,
362
+ });
221
363
  return { result, exitCode: result.status === "VALID" ? 0 : 1 };
222
364
  },
223
- report: async ({ target, packageRoot, options }) => {
224
- const result = await runReport({ target, packageRoot, strict: options.strict, taskId: options.taskId });
365
+ report: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
366
+ const result = await runReport({
367
+ target,
368
+ packageRoot,
369
+ strict: options.strict,
370
+ taskId: options.taskId,
371
+ authorityContext,
372
+ runtimeContext,
373
+ });
225
374
  return { result, exitCode: result.verdict === "VALID" ? 0 : 1 };
226
375
  },
227
376
  policy: async ({ target, packageRoot, options }) => ({
@@ -262,8 +411,15 @@ export const COMMAND_EXECUTORS = {
262
411
  result: await runBundle({ target, packageRoot, taskId: options.taskId }),
263
412
  exitCode: 0,
264
413
  }),
265
- inspect: async ({ target, packageRoot, options }) => {
266
- const result = await inspectTarget({ target, packageRoot, contractFile: options.contractFile, taskId: options.taskId });
414
+ inspect: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
415
+ const result = await inspectTarget({
416
+ target,
417
+ packageRoot,
418
+ contractFile: options.contractFile,
419
+ taskId: options.taskId,
420
+ authorityContext,
421
+ runtimeContext,
422
+ });
267
423
  return { result, exitCode: result.ok ? 0 : 1 };
268
424
  },
269
425
  "validate-receipt": async ({ target, packageRoot, options }) => ({
@@ -31,6 +31,8 @@ export async function executeForgeLoopCommand({
31
31
  command,
32
32
  projectPath = ".",
33
33
  input = {},
34
+ authorityContext,
35
+ runtimeContext,
34
36
  } = {}) {
35
37
  const metadata = Object.freeze({
36
38
  protocolVersion: PROTOCOL_VERSION,
@@ -81,7 +83,16 @@ export async function executeForgeLoopCommand({
81
83
  const packageRoot = getPackageRoot();
82
84
  const packageVersion = await readPackageVersion(packageRoot);
83
85
 
84
- const { result, exitCode } = await executor({ target, packageRoot, packageVersion, options });
86
+ // Trusted host authority and runtime context travel out-of-band, never
87
+ // inside actor-controlled command input (INV-AUTH-03).
88
+ const { result, exitCode } = await executor({
89
+ target,
90
+ packageRoot,
91
+ packageVersion,
92
+ options,
93
+ authorityContext,
94
+ runtimeContext,
95
+ });
85
96
  return {
86
97
  ok: true,
87
98
  command,
@@ -24,6 +24,20 @@ import { readExecutionArtifact, validateExecutionBinding } from "./execution.js"
24
24
  import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
25
25
  import { assertClaimsCoverChangedPaths } from "./task-scope.js";
26
26
  import { discoverTasks } from "./task-discovery.js";
27
+ import { listActions } from "./actions.js";
28
+
29
+ async function actionReceiptSummary(target, packageRoot, taskId) {
30
+ const actions = await listActions(target, { packageRoot, taskId });
31
+ return {
32
+ count: actions.length,
33
+ required: actions.filter((action) => action.requiredForCompletion).length,
34
+ verified: actions.filter((action) => action.state === "VERIFIED").length,
35
+ failed: actions.filter((action) => action.state === "FAILED").length,
36
+ ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
37
+ pending: actions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
38
+ actionRefs: actions.map((action) => action.actionId),
39
+ };
40
+ }
27
41
 
28
42
  /**
29
43
  * Canonical terminal-result types shared by runtime validation, tests, and
@@ -249,6 +263,16 @@ export async function prepareCompletion({
249
263
  if (existing && existingValue.stateFingerprint === undefined) {
250
264
  throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [receiptRel]);
251
265
  }
266
+ // A checkpoint recreated after clear-state/loss starts with empty checks; a
267
+ // receipt still bound to the previous checkpoint fingerprint belongs to a
268
+ // superseded epoch and must not be adopted into the fresh one. The prior
269
+ // epoch remains auditable through the executions/ artifacts and the
270
+ // append-only ledger.
271
+ const receiptFromSupersededEpoch = Boolean(existing)
272
+ && existingValue.stateFingerprint !== undefined
273
+ && existingValue.stateFingerprint !== canonicalFingerprint(state)
274
+ && (state.checks ?? []).length === 0;
275
+ const adoptedValue = receiptFromSupersededEpoch ? {} : existingValue;
252
276
  assertStateIdentity({ contract, route, state });
253
277
 
254
278
  let writeClaims = [];
@@ -286,35 +310,36 @@ export async function prepareCompletion({
286
310
 
287
311
  const changedPaths = observedPaths !== null
288
312
  ? [...observedPaths]
289
- : existing
313
+ : !receiptFromSupersededEpoch && existing
290
314
  ? [...(existingValue.changedPaths ?? [])]
291
315
  : [];
292
- const checks = existing ? [...existingValue.checks] : [...state.checks];
293
- const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
316
+ const checks = !receiptFromSupersededEpoch && existing ? [...existingValue.checks] : [...state.checks];
317
+ const evidence = !receiptFromSupersededEpoch && existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
294
318
  const receipt = await createReceipt({
295
- ...existingValue,
319
+ ...adoptedValue,
296
320
  taskId: contract.value.taskId,
297
321
  contractFingerprint: contract.fingerprint,
298
322
  routeFingerprint: route.fingerprint,
299
323
  stateFingerprint: canonicalFingerprint(state),
300
324
  verificationCycle: state.verificationCycle ?? 1,
301
- status: existingValue.status ?? "in-progress",
302
- taskStatus: existingValue.taskStatus ?? "in-progress",
303
- verificationStatus: existingValue.verificationStatus ?? "not-verified",
304
- publicationStatus: existingValue.publicationStatus ?? "local-only",
305
- productionReadiness: existingValue.productionReadiness ?? "not-verified",
325
+ status: adoptedValue.status ?? "in-progress",
326
+ taskStatus: adoptedValue.taskStatus ?? "in-progress",
327
+ verificationStatus: adoptedValue.verificationStatus ?? "not-verified",
328
+ publicationStatus: adoptedValue.publicationStatus ?? "local-only",
329
+ productionReadiness: adoptedValue.productionReadiness ?? "not-verified",
306
330
  selectedGuides: [...route.value.guides],
307
331
  changedPaths,
308
332
  checks,
333
+ actions: await actionReceiptSummary(target, packageRoot, contract.value.taskId),
309
334
  evidence,
310
335
  evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
311
336
  target,
312
337
  taskId: contract.value.taskId,
313
338
  options: { authorityContext, runtimeContext },
314
339
  }),
315
- review: existingValue.review ?? { status: "not-run", independent: false },
316
- limitations: [...(existingValue.limitations ?? [])],
317
- publication: existingValue.publication ?? {
340
+ review: adoptedValue.review ?? { status: "not-run", independent: false },
341
+ limitations: [...(adoptedValue.limitations ?? [])],
342
+ publication: adoptedValue.publication ?? {
318
343
  committed: false,
319
344
  pushed: false,
320
345
  pullRequest: null,
@@ -0,0 +1,194 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
+ import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
3
+ import { createReceipt } from "./receipt.js";
4
+ import { taskArtifactPath } from "./task-paths.js";
5
+ import { mutateWorkState, readWorkState } from "./work-state.js";
6
+
7
+ function resolveArtifactPath(key, taskId, override) {
8
+ if (override) return override;
9
+ return taskId ? taskArtifactPath(taskId, key) : ARTIFACT_PATHS[key];
10
+ }
11
+
12
+ const FINGERPRINT_MISMATCH_CODES = new Set([
13
+ "E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH",
14
+ "E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH",
15
+ ]);
16
+
17
+ export function isFingerprintOnlyRecoveryMismatch(errors = []) {
18
+ return Array.isArray(errors)
19
+ && errors.length > 0
20
+ && errors.every((error) => FINGERPRINT_MISMATCH_CODES.has(error?.code));
21
+ }
22
+
23
+ function sortedValues(values) {
24
+ return [...new Set(values ?? [])].sort();
25
+ }
26
+
27
+ function sameSortedValues(left, right) {
28
+ return JSON.stringify(sortedValues(left)) === JSON.stringify(sortedValues(right));
29
+ }
30
+
31
+ function findMatchingRejectionEvent(events, attempt, cycle) {
32
+ let latestReviewIndex = -1;
33
+ for (let index = 0; index < events.length; index += 1) {
34
+ const event = events[index];
35
+ if (event.event === "REVIEW_STARTED" && (event.details?.verificationCycle ?? 1) === cycle) {
36
+ latestReviewIndex = index;
37
+ }
38
+ }
39
+ for (let index = events.length - 1; index >= 0; index -= 1) {
40
+ const event = events[index];
41
+ if (event.event !== "COMPLETION_REJECTED") continue;
42
+ if ((event.details?.verificationCycle ?? 1) !== cycle) continue;
43
+ if (index < latestReviewIndex) continue;
44
+ return { event, index };
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Rebind a persisted REJECTED completion attempt to the current work-state
51
+ * checkpoint when the only authorization failures are fingerprint mismatches.
52
+ *
53
+ * Repository drift or a recovery/resume cycle can mutate work-state after a
54
+ * completion rejection was persisted, so the ledger snapshot no longer matches
55
+ * the live checkpoint. Without rebinding, every sanctioned closure path refuses
56
+ * (reconcile-closure and REVIEWING -> VERIFYING require authorized completion
57
+ * recovery; complete cannot persist a fresh evidence-only rejection while the
58
+ * checkpoint is stale), which deadlocks the task.
59
+ *
60
+ * The rebind is append-only and logically conservative:
61
+ * - the rejection reasonCodes, missingRequirementIds, and verification cycle
62
+ * must be logically identical between work-state and the latest matching
63
+ * ledger rejection; any logical difference is refused,
64
+ * - the original COMPLETION_REJECTED event is never modified; a rebound
65
+ * rejection carrying the current fingerprints is appended,
66
+ * - the execution receipt is re-bound to the current checkpoint when present.
67
+ */
68
+ export async function rebindCompletionRejectionSnapshot({
69
+ target,
70
+ packageRoot,
71
+ taskId = null,
72
+ statePath = null,
73
+ receiptPath = null,
74
+ eventsPath = null,
75
+ authorityContext,
76
+ runtimeContext,
77
+ } = {}) {
78
+ const statePathResolved = resolveArtifactPath("state", taskId, statePath);
79
+ const receiptPathResolved = resolveArtifactPath("receipt", taskId, receiptPath);
80
+ const state = await readWorkState(target, { packageRoot, taskId, statePath: statePathResolved });
81
+ if (!state || state.phase !== "REVIEWING") {
82
+ return { rebound: false };
83
+ }
84
+ const attempt = state.lastCompletionAttempt;
85
+ if (!attempt || attempt.status !== "REJECTED") {
86
+ return { rebound: false };
87
+ }
88
+
89
+ const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
90
+ if (!ledger.valid) {
91
+ return { rebound: false };
92
+ }
93
+ const cycle = attempt.verificationCycle ?? state.verificationCycle ?? 1;
94
+ const matching = findMatchingRejectionEvent(ledger.events, attempt, cycle);
95
+ if (!matching) {
96
+ return { rebound: false };
97
+ }
98
+ const details = matching.event.details ?? {};
99
+ const logicalMatch = (details.verificationCycle ?? 1) === cycle
100
+ && sameSortedValues(details.reasonCodes, attempt.reasonCodes)
101
+ && sameSortedValues(details.missingRequirementIds, attempt.missingRequirementIds);
102
+ if (!logicalMatch) {
103
+ return { rebound: false };
104
+ }
105
+
106
+ const next = await mutateWorkState(target, {
107
+ expectedRevision: state.revision ?? 0,
108
+ packageRoot,
109
+ taskId,
110
+ statePath: statePathResolved,
111
+ }, () => ({
112
+ ...state,
113
+ revision: (state.revision ?? 0) + 1,
114
+ lastUpdated: new Date().toISOString(),
115
+ }));
116
+
117
+ let reboundReceiptFingerprint;
118
+ try {
119
+ const receipt = await readJsonArtifact(target, receiptPathResolved, "execution-receipt", packageRoot);
120
+ const reboundReceipt = await createReceipt({
121
+ ...receipt.value,
122
+ stateFingerprint: canonicalFingerprint(next),
123
+ verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
124
+ }, packageRoot, { target, taskId, authorityContext, runtimeContext });
125
+ await writeJsonArtifact(target, receiptPathResolved, reboundReceipt, "execution-receipt", packageRoot);
126
+ reboundReceiptFingerprint = canonicalFingerprint(reboundReceipt);
127
+ } catch (error) {
128
+ if (error.code !== "ARTIFACT_MISSING") throw error;
129
+ }
130
+
131
+ await appendProtocolEvent(target, {
132
+ taskId: state.taskId,
133
+ event: "COMPLETION_REJECTED",
134
+ details: {
135
+ verificationCycle: cycle,
136
+ reasonCodes: sortedValues(details.reasonCodes),
137
+ missingRequirementIds: sortedValues(details.missingRequirementIds),
138
+ stateFingerprint: canonicalFingerprint(next),
139
+ ...(reboundReceiptFingerprint ? { receiptFingerprint: reboundReceiptFingerprint } : {}),
140
+ ...(details.stateFingerprint ? { reboundFromStateFingerprint: details.stateFingerprint } : {}),
141
+ },
142
+ }, packageRoot, { taskId, eventsPath });
143
+
144
+ return { rebound: true, state: next };
145
+ }
146
+
147
+ export async function authorizeCompletionRecoveryOrRebind({
148
+ target,
149
+ packageRoot,
150
+ taskId = null,
151
+ statePath = null,
152
+ receiptPath = null,
153
+ eventsPath = null,
154
+ authorityContext,
155
+ runtimeContext,
156
+ } = {}) {
157
+ const statePathResolved = resolveArtifactPath("state", taskId, statePath);
158
+ const receiptPathResolved = resolveArtifactPath("receipt", taskId, receiptPath);
159
+ const resolveArtifacts = async () => {
160
+ const state = await readWorkState(target, { packageRoot, taskId, statePath: statePathResolved });
161
+ let receipt = null;
162
+ try {
163
+ receipt = (await readJsonArtifact(target, receiptPathResolved, "execution-receipt", packageRoot))?.value ?? null;
164
+ } catch {
165
+ receipt = null;
166
+ }
167
+ const events = (await validateEventLedger(target, packageRoot, { taskId, eventsPath })).events;
168
+ return { state, receipt, events };
169
+ };
170
+
171
+ const initial = await resolveArtifacts();
172
+ const recoveryAuth = validateCompletionRecoveryAuthorization(initial);
173
+ if (recoveryAuth.authorized || !isFingerprintOnlyRecoveryMismatch(recoveryAuth.errors)) {
174
+ return { ...initial, recoveryAuth, rebound: false };
175
+ }
176
+
177
+ const reboundResult = await rebindCompletionRejectionSnapshot({
178
+ target,
179
+ packageRoot,
180
+ taskId,
181
+ statePath,
182
+ receiptPath,
183
+ eventsPath,
184
+ authorityContext,
185
+ runtimeContext,
186
+ });
187
+ if (!reboundResult.rebound) {
188
+ return { ...initial, recoveryAuth, rebound: false };
189
+ }
190
+
191
+ const rebounded = await resolveArtifacts();
192
+ const reboundAuth = validateCompletionRecoveryAuthorization(rebounded);
193
+ return { ...rebounded, recoveryAuth: reboundAuth, rebound: reboundAuth.authorized };
194
+ }
@@ -13,6 +13,25 @@ import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
13
13
  import { evaluateTerminalRequirements } from "./evidence-readiness.js";
14
14
  import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
15
15
  import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
16
+ import { listActions } from "./actions.js";
17
+
18
+ async function actionReceiptSummary(target, packageRoot, taskId) {
19
+ const actions = await listActions(target, { packageRoot, taskId });
20
+ const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
21
+ const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
22
+ return {
23
+ count: actions.length,
24
+ required: readiness.total,
25
+ verified: actions.filter((action) => action.state === "VERIFIED").length,
26
+ // Raw state counts are observability only; trusted completion is below.
27
+ trustedSatisfied: readiness.satisfied,
28
+ unresolvedRequired: readiness.unresolved,
29
+ failed: actions.filter((action) => action.state === "FAILED").length,
30
+ ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
31
+ pending: actions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
32
+ actionRefs: actions.map((action) => action.actionId),
33
+ };
34
+ }
16
35
 
17
36
  /**
18
37
  * Canonical completion return statuses shared by the runtime, tests, and
@@ -424,6 +443,44 @@ export async function evaluateCompletion({
424
443
  }
425
444
  }
426
445
 
446
+ const actionTaskId = contract?.value?.taskId ?? taskId;
447
+ const durableActions = actionTaskId
448
+ ? await listActions(target, { packageRoot, taskId: actionTaskId })
449
+ : [];
450
+ const contractRequirements = new Set([
451
+ ...(contract?.value?.verification ?? []), ...(contract?.value?.successCriteria ?? []),
452
+ ]);
453
+ // Completion truth consumes the canonical action-readiness projection, not
454
+ // raw state labels: a forged VERIFIED label can never satisfy a required
455
+ // action (INV-VERIFY-02).
456
+ const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
457
+ const requiredActionReadiness = actionTaskId
458
+ ? await evaluateRequiredActionReadiness({ target, packageRoot, taskId: actionTaskId })
459
+ : { total: 0, satisfied: 0, unresolved: 0, ambiguous: 0, failed: 0, untrusted: 0, actions: [] };
460
+ for (const readiness of requiredActionReadiness.actions) {
461
+ if (readiness.status === "SATISFIED") continue;
462
+ const action = durableActions.find((candidate) => candidate.actionId === readiness.actionId);
463
+ if (
464
+ readiness.status === "FAILED"
465
+ && action?.state === "CANCELLED"
466
+ && action.requirement
467
+ && !contractRequirements.has(action.requirement)
468
+ ) {
469
+ continue;
470
+ }
471
+ const code = readiness.status === "AMBIGUOUS"
472
+ ? "E_ACTION_RECONCILIATION_REQUIRED"
473
+ : readiness.status === "UNTRUSTED"
474
+ ? "E_ACTION_VERIFICATION_REQUIRED"
475
+ : "E_ACTION_STATE_MISMATCH";
476
+ errors.push(issue(
477
+ code,
478
+ `Required action ${readiness.actionId} is not trusted-satisfied (${readiness.status}): ${readiness.reasons[0] ?? ""}`,
479
+ [taskArtifactPath(action?.taskId ?? actionTaskId, "actions")],
480
+ { actionId: readiness.actionId, actionState: action?.state, readiness: readiness.status },
481
+ ));
482
+ }
483
+
427
484
  const sortedErrors = sortIssues(errors);
428
485
  const valid = sortedErrors.length === 0;
429
486
  return {
@@ -440,6 +497,17 @@ export async function evaluateCompletion({
440
497
  status: ledger.valid ? "valid" : "invalid",
441
498
  events: ledger.events.length,
442
499
  },
500
+ actions: {
501
+ count: durableActions.length,
502
+ required: requiredActionReadiness.total,
503
+ verified: durableActions.filter((action) => action.state === "VERIFIED").length,
504
+ trustedSatisfied: requiredActionReadiness.satisfied,
505
+ unresolvedRequired: requiredActionReadiness.unresolved,
506
+ failed: durableActions.filter((action) => action.state === "FAILED").length,
507
+ ambiguous: durableActions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
508
+ pending: durableActions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
509
+ actionRefs: durableActions.map((action) => action.actionId),
510
+ },
443
511
  };
444
512
  }
445
513
 
@@ -528,6 +596,7 @@ export async function runComplete({
528
596
  if (receipt) {
529
597
  nextReceipt = await createReceipt({
530
598
  ...receipt.value,
599
+ actions: await actionReceiptSummary(target, packageRoot, state.taskId),
531
600
  stateFingerprint: canonicalFingerprint(next),
532
601
  verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
533
602
  }, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
@@ -574,6 +643,7 @@ export async function runComplete({
574
643
  next.revision = (state.revision ?? 0) + 1;
575
644
  const nextReceipt = await createReceipt({
576
645
  ...receipt.value,
646
+ actions: await actionReceiptSummary(target, packageRoot, state.taskId),
577
647
  stateFingerprint: canonicalFingerprint(next),
578
648
  verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
579
649
  }, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });