@cassiomc1/forgeloop 1.5.0 → 1.6.1

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 (133) hide show
  1. package/CONTRACT_COVERAGE.md +1 -0
  2. package/DOCS_INDEX.md +20 -10
  3. package/EXECUTION_STATE.md +20 -0
  4. package/LOOP_ENGINEERING.md +103 -1
  5. package/LOOP_SYSTEM_DESIGN.md +32 -0
  6. package/PROTOCOL_INTEGRATION.md +90 -0
  7. package/QUALITY_SCORECARD.md +2 -0
  8. package/README.md +47 -9
  9. package/THIRD_PARTY_NOTICES.md +15 -0
  10. package/THREAT_MODEL.md +41 -1
  11. package/docs/ARTIFACT_REFERENCE.md +159 -0
  12. package/docs/CLI_REFERENCE.md +294 -3
  13. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  14. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  15. package/docs/EXECUTION_TRACE.md +76 -0
  16. package/docs/MCP.md +34 -1
  17. package/docs/RECIPES.md +67 -0
  18. package/docs/RELEASE_CHECKLIST_1_6_1.md +121 -0
  19. package/docs/TROUBLESHOOTING.md +133 -2
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  21. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  22. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  23. package/docs/diagrams/README.md +55 -0
  24. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  25. package/docs/diagrams/manifest.json +42 -0
  26. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  27. package/package.json +13 -8
  28. package/schemas/action.schema.json +100 -0
  29. package/schemas/approval.schema.json +51 -0
  30. package/schemas/capability-policy.schema.json +41 -0
  31. package/schemas/diagnostic-case.schema.json +85 -0
  32. package/schemas/execution-receipt.schema.json +16 -0
  33. package/schemas/execution.schema.json +15 -0
  34. package/schemas/hypothesis-disposition.schema.json +16 -0
  35. package/schemas/intervention.schema.json +27 -0
  36. package/schemas/policy-lock.schema.json +1 -0
  37. package/schemas/policy-snapshot.schema.json +2 -0
  38. package/schemas/trajectory-evaluation.schema.json +64 -0
  39. package/schemas/trajectory-scenario.schema.json +42 -0
  40. package/src/cli.js +94 -0
  41. package/src/commands/action-authorize.js +41 -0
  42. package/src/commands/action-propose.js +10 -0
  43. package/src/commands/action-reconcile.js +10 -0
  44. package/src/commands/action-record.js +47 -0
  45. package/src/commands/action-show.js +10 -0
  46. package/src/commands/action-verify.js +10 -0
  47. package/src/commands/advance.js +7 -2
  48. package/src/commands/approval-request.js +64 -0
  49. package/src/commands/approval-resolve.js +10 -0
  50. package/src/commands/baseline.js +3 -3
  51. package/src/commands/eval.js +6 -0
  52. package/src/commands/history.js +18 -0
  53. package/src/commands/init.js +2 -2
  54. package/src/commands/inspect.js +49 -0
  55. package/src/commands/metrics.js +7 -0
  56. package/src/commands/next.js +8 -2
  57. package/src/commands/policy-discover.js +2 -2
  58. package/src/commands/record-diagnosis.js +37 -1
  59. package/src/commands/record-hypothesis-disposition.js +45 -0
  60. package/src/commands/record-intervention.js +35 -0
  61. package/src/commands/reflect.js +38 -0
  62. package/src/commands/report.js +9 -1
  63. package/src/commands/run-action.js +18 -0
  64. package/src/commands/run-check.js +1 -0
  65. package/src/commands/trace.js +34 -0
  66. package/src/commands/validate-protocol.js +21 -13
  67. package/src/core/action-authorization.js +106 -0
  68. package/src/core/action-constants.js +86 -0
  69. package/src/core/action-execution.js +106 -0
  70. package/src/core/action-ledger-projection.js +302 -0
  71. package/src/core/action-model.js +581 -0
  72. package/src/core/action-readiness.js +141 -0
  73. package/src/core/action-reconciliation-policy.js +49 -0
  74. package/src/core/action-reconciliation.js +66 -0
  75. package/src/core/action-verification.js +111 -0
  76. package/src/core/actions.js +462 -0
  77. package/src/core/approvals.js +405 -0
  78. package/src/core/artifact-registry.js +48 -0
  79. package/src/core/audit.js +25 -0
  80. package/src/core/bundles.js +15 -0
  81. package/src/core/capability-policy.js +226 -0
  82. package/src/core/cli-command-definitions.js +210 -1
  83. package/src/core/command-executors.js +171 -15
  84. package/src/core/command-runtime.js +12 -1
  85. package/src/core/completion-artifacts.js +71 -14
  86. package/src/core/completion-recovery-rebind.js +194 -0
  87. package/src/core/completion.js +70 -0
  88. package/src/core/continuity-reconciliation.js +24 -5
  89. package/src/core/diagnostic-model.js +396 -0
  90. package/src/core/diagnostic-projection.js +51 -0
  91. package/src/core/diagnostic-record.js +360 -0
  92. package/src/core/error-codes.js +363 -0
  93. package/src/core/events.js +41 -1
  94. package/src/core/execution-prerequisites.js +4 -1
  95. package/src/core/execution.js +29 -188
  96. package/src/core/failure-signature.js +70 -0
  97. package/src/core/failure-surface.js +57 -0
  98. package/src/core/history.js +110 -0
  99. package/src/core/hypothesis-projection.js +85 -0
  100. package/src/core/information-gain-projection.js +283 -0
  101. package/src/core/information-gain.js +138 -0
  102. package/src/core/inspect.js +105 -7
  103. package/src/core/integration-invocation-policy.js +55 -0
  104. package/src/core/integration-resources.js +51 -0
  105. package/src/core/next-action-model.js +35 -1
  106. package/src/core/next-action.js +459 -3
  107. package/src/core/phase.js +40 -21
  108. package/src/core/policy-engine.js +113 -6
  109. package/src/core/preflight-consistency.js +31 -5
  110. package/src/core/preflight.js +19 -2
  111. package/src/core/prepared-execution.js +256 -0
  112. package/src/core/progress.js +41 -4
  113. package/src/core/protocol-info.js +56 -0
  114. package/src/core/protocol.js +14 -0
  115. package/src/core/receipt.js +1 -0
  116. package/src/core/reconcile-closure.js +15 -12
  117. package/src/core/reflection.js +305 -0
  118. package/src/core/resumability.js +57 -3
  119. package/src/core/runtime-context.js +19 -1
  120. package/src/core/schema-validation.js +8 -0
  121. package/src/core/strategy-analysis.js +97 -0
  122. package/src/core/task-paths.js +28 -0
  123. package/src/core/task-snapshot.js +53 -0
  124. package/src/core/templates.js +8 -0
  125. package/src/core/trace.js +548 -0
  126. package/src/core/trajectory-evaluation.js +71 -0
  127. package/src/core/trajectory-metrics.js +80 -0
  128. package/src/core/transaction.js +8 -0
  129. package/src/core/verification-execution.js +257 -0
  130. package/src/core/work-state.js +10 -5
  131. package/src/integration.js +8 -0
  132. package/docs/assets/forgeloop-flow.svg +0 -1
  133. 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
@@ -138,10 +152,42 @@ export async function validateCheckExecutionProvenance(check, {
138
152
  requirement: check.requirement,
139
153
  verificationCycle: check.details?.verificationCycle ?? 1,
140
154
  });
141
- if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
155
+ const protocolProjectRoot = execution.protocolProjectRoot ?? execution.cwd;
156
+ if (
157
+ execution.executionKind !== undefined
158
+ && execution.executionKind !== "VERIFICATION"
159
+ ) {
160
+ throw artifactError(
161
+ "E_EXECUTION_REF_INVALID",
162
+ "A command check must reference a verification execution artifact",
163
+ [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
164
+ );
165
+ }
166
+ if (
167
+ execution.executionIsolation !== undefined
168
+ && execution.isolation?.mode !== execution.executionIsolation
169
+ ) {
170
+ throw artifactError(
171
+ "E_EXECUTION_REF_INVALID",
172
+ "Execution artifact isolation mode does not match its isolation metadata",
173
+ [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
174
+ );
175
+ }
176
+ if (
177
+ execution.isolation?.mode !== undefined
178
+ && execution.isolation.mode !== "NATIVE_PROJECT"
179
+ && path.resolve(execution.cwd) === path.resolve(protocolProjectRoot)
180
+ ) {
181
+ throw artifactError(
182
+ "E_EXECUTION_REF_INVALID",
183
+ "Isolated execution artifact cwd must be separate from the protocol project root",
184
+ [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
185
+ );
186
+ }
187
+ if (!allowForeignCwd && path.resolve(protocolProjectRoot) !== path.resolve(target)) {
142
188
  throw artifactError(
143
189
  "E_EXECUTION_REF_INVALID",
144
- "Execution artifact cwd does not match the current target",
190
+ "Execution artifact protocol project root does not match the current target",
145
191
  [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
146
192
  );
147
193
  }
@@ -249,6 +295,16 @@ export async function prepareCompletion({
249
295
  if (existing && existingValue.stateFingerprint === undefined) {
250
296
  throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [receiptRel]);
251
297
  }
298
+ // A checkpoint recreated after clear-state/loss starts with empty checks; a
299
+ // receipt still bound to the previous checkpoint fingerprint belongs to a
300
+ // superseded epoch and must not be adopted into the fresh one. The prior
301
+ // epoch remains auditable through the executions/ artifacts and the
302
+ // append-only ledger.
303
+ const receiptFromSupersededEpoch = Boolean(existing)
304
+ && existingValue.stateFingerprint !== undefined
305
+ && existingValue.stateFingerprint !== canonicalFingerprint(state)
306
+ && (state.checks ?? []).length === 0;
307
+ const adoptedValue = receiptFromSupersededEpoch ? {} : existingValue;
252
308
  assertStateIdentity({ contract, route, state });
253
309
 
254
310
  let writeClaims = [];
@@ -286,35 +342,36 @@ export async function prepareCompletion({
286
342
 
287
343
  const changedPaths = observedPaths !== null
288
344
  ? [...observedPaths]
289
- : existing
345
+ : !receiptFromSupersededEpoch && existing
290
346
  ? [...(existingValue.changedPaths ?? [])]
291
347
  : [];
292
- const checks = existing ? [...existingValue.checks] : [...state.checks];
293
- const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
348
+ const checks = !receiptFromSupersededEpoch && existing ? [...existingValue.checks] : [...state.checks];
349
+ const evidence = !receiptFromSupersededEpoch && existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
294
350
  const receipt = await createReceipt({
295
- ...existingValue,
351
+ ...adoptedValue,
296
352
  taskId: contract.value.taskId,
297
353
  contractFingerprint: contract.fingerprint,
298
354
  routeFingerprint: route.fingerprint,
299
355
  stateFingerprint: canonicalFingerprint(state),
300
356
  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",
357
+ status: adoptedValue.status ?? "in-progress",
358
+ taskStatus: adoptedValue.taskStatus ?? "in-progress",
359
+ verificationStatus: adoptedValue.verificationStatus ?? "not-verified",
360
+ publicationStatus: adoptedValue.publicationStatus ?? "local-only",
361
+ productionReadiness: adoptedValue.productionReadiness ?? "not-verified",
306
362
  selectedGuides: [...route.value.guides],
307
363
  changedPaths,
308
364
  checks,
365
+ actions: await actionReceiptSummary(target, packageRoot, contract.value.taskId),
309
366
  evidence,
310
367
  evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
311
368
  target,
312
369
  taskId: contract.value.taskId,
313
370
  options: { authorityContext, runtimeContext },
314
371
  }),
315
- review: existingValue.review ?? { status: "not-run", independent: false },
316
- limitations: [...(existingValue.limitations ?? [])],
317
- publication: existingValue.publication ?? {
372
+ review: adoptedValue.review ?? { status: "not-run", independent: false },
373
+ limitations: [...(adoptedValue.limitations ?? [])],
374
+ publication: adoptedValue.publication ?? {
318
375
  committed: false,
319
376
  pushed: false,
320
377
  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
+ }