@cassiomc1/forgeloop 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -0,0 +1,117 @@
1
+ import { resolveTarget } from "./filesystem.js";
2
+ import { getPackageRoot } from "./templates.js";
3
+ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
4
+ import { COMMAND_EXECUTORS } from "./command-executors.js";
5
+ import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./command-input.js";
6
+ import { PROTOCOL_VERSION } from "./protocol.js";
7
+
8
+ export const FORGELOOP_INTEGRATION_RUNTIME_VERSION = 1;
9
+
10
+ async function readPackageVersion(packageRoot) {
11
+ const { readFile } = await import("node:fs/promises");
12
+ const path = await import("node:path");
13
+ const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
14
+ return packageJson.version;
15
+ }
16
+
17
+ /**
18
+ * Transport-neutral ForgeLoop command execution.
19
+ *
20
+ * Resolves the project target, validates the command and its structured
21
+ * input, invokes the canonical executor, and returns a deterministic
22
+ * envelope. Performs zero terminal output, never spawns a shell to run
23
+ * ForgeLoop itself, never bypasses task ownership/recovery guards, and never
24
+ * retries mutations.
25
+ *
26
+ * A non-zero exitCode is a protocol/domain outcome (e.g. preflight BLOCKED),
27
+ * not an invocation failure: `ok` stays true. `ok:false` means the command
28
+ * could not be executed (unknown command, invalid input, canonical error).
29
+ */
30
+ export async function executeForgeLoopCommand({
31
+ command,
32
+ projectPath = ".",
33
+ input = {},
34
+ authorityContext,
35
+ runtimeContext,
36
+ } = {}) {
37
+ const metadata = Object.freeze({
38
+ protocolVersion: PROTOCOL_VERSION,
39
+ integrationApiVersion: FORGELOOP_INTEGRATION_RUNTIME_VERSION,
40
+ });
41
+
42
+ if (typeof command !== "string" || !CLI_COMMAND_DEFINITIONS[command]) {
43
+ return {
44
+ ok: false,
45
+ command: command ?? null,
46
+ exitCode: 1,
47
+ result: null,
48
+ error: { code: "E_COMMAND_UNSUPPORTED", message: `Unsupported ForgeLoop command: ${command ?? "(missing)"}` },
49
+ metadata,
50
+ };
51
+ }
52
+ if (CLI_COMMAND_DEFINITIONS[command].bootstrapOnly) {
53
+ return {
54
+ ok: false,
55
+ command,
56
+ exitCode: 1,
57
+ result: null,
58
+ error: { code: "E_COMMAND_UNSUPPORTED", message: `Command ${command} is not available through the programmatic runtime` },
59
+ metadata,
60
+ };
61
+ }
62
+
63
+ const executor = COMMAND_EXECUTORS[command];
64
+ if (typeof executor !== "function") {
65
+ return {
66
+ ok: false,
67
+ command,
68
+ exitCode: 1,
69
+ result: null,
70
+ error: { code: "E_COMMAND_UNSUPPORTED", message: `Command ${command} has no canonical executor` },
71
+ metadata,
72
+ };
73
+ }
74
+
75
+ try {
76
+ const options = {
77
+ ...defaultCommandInputValues(),
78
+ ...(input ?? {}),
79
+ };
80
+ validateForgeLoopCommandInput({ command, input: options });
81
+
82
+ const target = await resolveTarget(process.cwd(), projectPath);
83
+ const packageRoot = getPackageRoot();
84
+ const packageVersion = await readPackageVersion(packageRoot);
85
+
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
+ });
96
+ return {
97
+ ok: true,
98
+ command,
99
+ exitCode,
100
+ result,
101
+ error: null,
102
+ metadata: Object.freeze({ ...metadata, packageVersion }),
103
+ };
104
+ } catch (error) {
105
+ return {
106
+ ok: false,
107
+ command,
108
+ exitCode: 1,
109
+ result: null,
110
+ error: {
111
+ code: error.code ?? "E_COMMAND_EXECUTION_FAILED",
112
+ message: error.message,
113
+ },
114
+ metadata,
115
+ };
116
+ }
117
+ }
@@ -22,9 +22,22 @@ import { normalizeRequirements } from "./evidence-readiness.js";
22
22
  import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
23
23
  import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
24
24
  import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
25
- import { readTaskDescriptor } from "./task-descriptor.js";
26
25
  import { assertClaimsCoverChangedPaths } from "./task-scope.js";
27
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
+ }
28
41
 
29
42
  /**
30
43
  * Canonical terminal-result types shared by runtime validation, tests, and
@@ -250,12 +263,22 @@ export async function prepareCompletion({
250
263
  if (existing && existingValue.stateFingerprint === undefined) {
251
264
  throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [receiptRel]);
252
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;
253
276
  assertStateIdentity({ contract, route, state });
254
277
 
255
278
  let writeClaims = [];
256
279
  try {
257
- const desc = taskId ? await readTaskDescriptor(target, taskId, packageRoot) : null;
258
- writeClaims = desc?.value?.writeClaims ?? [];
280
+ const tasks = taskId ? await discoverTasks(target, packageRoot) : [];
281
+ writeClaims = tasks.find((task) => task.taskId === taskId && task.healthy !== false)?.writeClaims ?? [];
259
282
  } catch {
260
283
  // descriptor may not exist
261
284
  }
@@ -287,35 +310,36 @@ export async function prepareCompletion({
287
310
 
288
311
  const changedPaths = observedPaths !== null
289
312
  ? [...observedPaths]
290
- : existing
313
+ : !receiptFromSupersededEpoch && existing
291
314
  ? [...(existingValue.changedPaths ?? [])]
292
315
  : [];
293
- const checks = existing ? [...existingValue.checks] : [...state.checks];
294
- 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];
295
318
  const receipt = await createReceipt({
296
- ...existingValue,
319
+ ...adoptedValue,
297
320
  taskId: contract.value.taskId,
298
321
  contractFingerprint: contract.fingerprint,
299
322
  routeFingerprint: route.fingerprint,
300
323
  stateFingerprint: canonicalFingerprint(state),
301
324
  verificationCycle: state.verificationCycle ?? 1,
302
- status: existingValue.status ?? "in-progress",
303
- taskStatus: existingValue.taskStatus ?? "in-progress",
304
- verificationStatus: existingValue.verificationStatus ?? "not-verified",
305
- publicationStatus: existingValue.publicationStatus ?? "local-only",
306
- 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",
307
330
  selectedGuides: [...route.value.guides],
308
331
  changedPaths,
309
332
  checks,
333
+ actions: await actionReceiptSummary(target, packageRoot, contract.value.taskId),
310
334
  evidence,
311
335
  evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
312
336
  target,
313
337
  taskId: contract.value.taskId,
314
338
  options: { authorityContext, runtimeContext },
315
339
  }),
316
- review: existingValue.review ?? { status: "not-run", independent: false },
317
- limitations: [...(existingValue.limitations ?? [])],
318
- publication: existingValue.publication ?? {
340
+ review: adoptedValue.review ?? { status: "not-run", independent: false },
341
+ limitations: [...(adoptedValue.limitations ?? [])],
342
+ publication: adoptedValue.publication ?? {
319
343
  committed: false,
320
344
  pushed: false,
321
345
  pullRequest: null,
@@ -0,0 +1,88 @@
1
+ import { LIFECYCLE_MILESTONES, validateStateLedgerCoherence } from "./events.js";
2
+
3
+ export const CANONICAL_COMPLETION_EVENT = "COMPLETION_VALIDATED";
4
+
5
+ /**
6
+ * Canonical completion ownership proof: the minimal, validator-backed evidence
7
+ * that the lifecycle itself officially reached COMPLETE. This is intentionally
8
+ * NOT a re-run of full publication/receipt/evidence semantics — it only proves
9
+ * that claim ownership may be released because canonical completion exists.
10
+ *
11
+ * Returns `{ valid: true, completionEvent }` or `{ valid: false, errors }`.
12
+ */
13
+ export function validateCompletionOwnershipProof({ taskId, state, ledger }) {
14
+ const errors = [];
15
+ if (!taskId || typeof taskId !== "string") {
16
+ return { valid: false, errors: [{ code: "E_COMPLETION_OWNERSHIP_UNPROVEN", message: "Completion ownership proof requires a taskId" }] };
17
+ }
18
+ if (!state || state.phase !== "COMPLETE") {
19
+ errors.push({ code: "E_COMPLETION_OWNERSHIP_UNPROVEN", message: "Work-state phase is not COMPLETE" });
20
+ }
21
+ if (!ledger || ledger.valid !== true) {
22
+ errors.push({
23
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
24
+ message: `Task event ledger is invalid; completion cannot be proven${ledger?.errors?.length
25
+ ? `: ${ledger.errors.map((error) => error.message).join("; ")}`
26
+ : ""}`,
27
+ });
28
+ }
29
+
30
+ let completionEvent = null;
31
+ if (ledger && Array.isArray(ledger.events)) {
32
+ if (ledger.events.some((event) => event.taskId !== taskId)) {
33
+ errors.push({
34
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
35
+ message: "Ledger contains an event belonging to a different task",
36
+ });
37
+ }
38
+ const candidates = ledger.events
39
+ .filter((event) => event.event === CANONICAL_COMPLETION_EVENT && event.taskId === taskId);
40
+ if (candidates.length === 0) {
41
+ errors.push({
42
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
43
+ message: `No canonical ${CANONICAL_COMPLETION_EVENT} event exists for this task`,
44
+ });
45
+ } else if (candidates.length > 1) {
46
+ errors.push({
47
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
48
+ message: `Multiple ${CANONICAL_COMPLETION_EVENT} events exist; completion is ambiguous`,
49
+ });
50
+ } else {
51
+ completionEvent = candidates[0];
52
+ }
53
+ }
54
+
55
+ if (state && ledger && Array.isArray(ledger.events)) {
56
+ const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
57
+ if (coherenceErrors.length > 0) {
58
+ for (const error of coherenceErrors) {
59
+ errors.push({
60
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
61
+ message: `State/ledger coherence invalid: ${error.message}`,
62
+ });
63
+ }
64
+ }
65
+ }
66
+
67
+ // No contradictory lifecycle activity may follow the canonical completion:
68
+ // any milestone at or after VERIFICATION_RECORDED occurring after the
69
+ // completion event means the lifecycle moved past terminal state.
70
+ if (completionEvent && ledger && Array.isArray(ledger.events)) {
71
+ const completionIndex = ledger.events.indexOf(completionEvent);
72
+ const contradiction = ledger.events.slice(completionIndex + 1).find((event) => {
73
+ const index = LIFECYCLE_MILESTONES.indexOf(event.event);
74
+ return index >= LIFECYCLE_MILESTONES.indexOf("VERIFICATION_RECORDED");
75
+ });
76
+ if (contradiction) {
77
+ errors.push({
78
+ code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
79
+ message: `Lifecycle event ${contradiction.event} follows canonical completion; terminal state contradicted`,
80
+ });
81
+ }
82
+ }
83
+
84
+ if (errors.length > 0) {
85
+ return { valid: false, completionEvent: null, errors };
86
+ }
87
+ return { valid: true, completionEvent };
88
+ }
@@ -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 });
@@ -169,20 +169,25 @@ export function classifyContinuity({
169
169
  });
170
170
  }
171
171
 
172
- export async function reconcileContinuity({ target, packageRoot } = {}) {
172
+ export async function reconcileContinuity({ target, packageRoot, taskId = null } = {}) {
173
173
  const [{ readWorkState }, { readContract }, repository] = await Promise.all([
174
174
  import("./work-state.js"),
175
175
  import("./contract.js"),
176
176
  import("./repository.js"),
177
177
  ]);
178
178
 
179
- const state = await readWorkState(target, packageRoot);
179
+ const state = await readWorkState(target, { packageRoot, taskId });
180
180
  let continuityArtifact;
181
181
  try {
182
- continuityArtifact = await readContinuity(target, packageRoot);
182
+ continuityArtifact = await readContinuity(target, { packageRoot, taskId });
183
183
  } catch (error) {
184
184
  if (error.code === "ARTIFACT_MISSING") {
185
- return { ...classifyContinuity({ continuity: null, state }), path: ".forgeloop/continuity.json", present: false };
185
+ return {
186
+ ...classifyContinuity({ continuity: null, state }),
187
+ path: ".forgeloop/continuity.json",
188
+ present: false,
189
+ diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
190
+ };
186
191
  }
187
192
  return {
188
193
  ...baseResult("INVALID", {
@@ -197,7 +202,7 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
197
202
 
198
203
  let contractFingerprint = null;
199
204
  try {
200
- const contract = await readContract(target, packageRoot);
205
+ const contract = await readContract(target, packageRoot, { taskId });
201
206
  contractFingerprint = contract.fingerprint;
202
207
  } catch {
203
208
  contractFingerprint = null;
@@ -220,5 +225,19 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
220
225
  present: true,
221
226
  fingerprint: continuityArtifact.fingerprint,
222
227
  continuity: continuityArtifact.value,
228
+ diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
223
229
  };
224
230
  }
231
+
232
+ async function deriveDiagnosticContextSafe({ target, packageRoot, state }) {
233
+ try {
234
+ const [{ readEvents }, { deriveDiagnosticContext }] = await Promise.all([
235
+ import("./events.js"),
236
+ import("./reflection.js"),
237
+ ]);
238
+ const events = await readEvents(target, packageRoot, { taskId: state?.taskId ?? null });
239
+ return { present: true, ...deriveDiagnosticContext(events, state) };
240
+ } catch {
241
+ return { present: false };
242
+ }
243
+ }