@cassiomc1/forgeloop 1.6.2 → 1.6.4

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 (127) hide show
  1. package/DOCS_INDEX.md +26 -4
  2. package/EXECUTION_STATE.md +7 -0
  3. package/LOOP_ENGINEERING.md +58 -0
  4. package/LOOP_SYSTEM_DESIGN.md +54 -0
  5. package/ORCHESTRATOR_INTEGRATION.md +23 -0
  6. package/PROTOCOL_INTEGRATION.md +36 -0
  7. package/QUALITY_SCORECARD.md +21 -0
  8. package/README.md +57 -2
  9. package/TERMINOLOGY.md +11 -0
  10. package/THREAT_MODEL.md +7 -0
  11. package/completions/_forgeloop +85 -0
  12. package/completions/forgeloop.bash +173 -0
  13. package/completions/forgeloop.fish +544 -0
  14. package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
  15. package/docs/ARTIFACT_REFERENCE.md +226 -0
  16. package/docs/CLI_REFERENCE.md +308 -2
  17. package/docs/CODE_ATTESTATION.md +141 -0
  18. package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
  19. package/docs/DOCUMENTATION_GUIDE.md +13 -11
  20. package/docs/GETTING_STARTED.md +98 -1
  21. package/docs/MCP.md +8 -0
  22. package/docs/PLATFORM_ADAPTERS.md +69 -0
  23. package/docs/RECIPES.md +138 -0
  24. package/docs/RELEASE_CHECKLIST.md +54 -0
  25. package/docs/REVISION_PROVIDERS.md +136 -0
  26. package/docs/SIGNING_PROVIDERS.md +83 -0
  27. package/docs/TROUBLESHOOTING.md +184 -0
  28. package/docs/UNIVERSAL_INTEGRATION.md +11 -0
  29. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
  30. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
  31. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
  32. package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
  33. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  34. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
  35. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
  36. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
  37. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
  38. package/docs/diagrams/README.md +30 -22
  39. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
  40. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
  41. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
  42. package/docs/diagrams/manifest.json +42 -0
  43. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
  44. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
  45. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
  46. package/integrations/generic-ci/verify.sh +14 -0
  47. package/package.json +31 -9
  48. package/schemas/attestation-verification-result.schema.json +34 -0
  49. package/schemas/code-attestation.schema.json +61 -0
  50. package/schemas/code-manifest.schema.json +57 -0
  51. package/schemas/config.schema.json +57 -1
  52. package/schemas/handoff-envelope.schema.json +57 -0
  53. package/schemas/in-toto-statement.schema.json +32 -0
  54. package/schemas/responsibility.schema.json +37 -0
  55. package/schemas/verification-scope.schema.json +26 -0
  56. package/schemas/workspace-binding.schema.json +20 -0
  57. package/scripts/CI_VALIDATORS.md +3 -0
  58. package/scripts/benchmark-cli-startup.mjs +60 -0
  59. package/scripts/check-changelog-freshness.mjs +88 -0
  60. package/scripts/check-critical-coverage.mjs +58 -0
  61. package/scripts/generate-agent-protocol-summary.mjs +141 -0
  62. package/scripts/generate-shell-completions.mjs +132 -0
  63. package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
  64. package/src/cli.js +114 -6
  65. package/src/commands/attestation-create.js +30 -0
  66. package/src/commands/attestation-status.js +12 -0
  67. package/src/commands/attestation-verify-range.js +32 -0
  68. package/src/commands/attestation-verify.js +23 -0
  69. package/src/commands/handoff-create.js +9 -0
  70. package/src/commands/handoff-list.js +12 -0
  71. package/src/commands/handoff-show.js +10 -0
  72. package/src/commands/responsibility-set.js +20 -0
  73. package/src/commands/responsibility-status.js +12 -0
  74. package/src/commands/run-check.js +43 -3
  75. package/src/commands/task-repair-legacy-recovery.js +1 -2
  76. package/src/commands/verify-scope.js +9 -0
  77. package/src/commands/workspace-bind.js +17 -0
  78. package/src/commands/workspace-status.js +19 -0
  79. package/src/core/artifact-registry.js +84 -0
  80. package/src/core/attestation-coverage.js +134 -0
  81. package/src/core/attestation-verifier.js +227 -0
  82. package/src/core/attestation.js +263 -0
  83. package/src/core/audit.js +67 -1
  84. package/src/core/bundles.js +210 -9
  85. package/src/core/cli-command-definitions.js +196 -0
  86. package/src/core/code-manifest.js +293 -0
  87. package/src/core/command-executors.js +79 -0
  88. package/src/core/command-input.js +69 -18
  89. package/src/core/command-runtime.js +2 -1
  90. package/src/core/completion.js +170 -8
  91. package/src/core/config.js +60 -2
  92. package/src/core/continuity-cli-options.js +9 -3
  93. package/src/core/continuity-reconciliation.js +20 -0
  94. package/src/core/diagnostic-projection.js +3 -1
  95. package/src/core/error-codes.js +146 -0
  96. package/src/core/events.js +109 -13
  97. package/src/core/exit-codes.js +22 -0
  98. package/src/core/handoff.js +208 -0
  99. package/src/core/information-gain-projection.js +3 -3
  100. package/src/core/integration-invocation-policy.js +40 -0
  101. package/src/core/integration-resources.js +54 -0
  102. package/src/core/next-action-phases.js +968 -0
  103. package/src/core/next-action.js +32 -951
  104. package/src/core/phase.js +29 -1
  105. package/src/core/protocol-info.js +41 -1
  106. package/src/core/repository.js +256 -11
  107. package/src/core/responsibility.js +265 -0
  108. package/src/core/revision/git.js +207 -0
  109. package/src/core/revision/provider.js +90 -0
  110. package/src/core/revision/registry.js +5 -0
  111. package/src/core/route-artifact.js +1 -1
  112. package/src/core/schema-validation.js +8 -0
  113. package/src/core/signing/none.js +23 -0
  114. package/src/core/signing/provider.js +21 -0
  115. package/src/core/signing/registry.js +7 -0
  116. package/src/core/signing/sigstore.js +158 -0
  117. package/src/core/task-command.js +9 -1
  118. package/src/core/task-paths.js +72 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/verification-scope-capability.js +179 -0
  121. package/src/core/verification-scope.js +271 -0
  122. package/src/core/workspace-binding.js +211 -0
  123. package/src/integration.d.ts +91 -0
  124. package/AGENT_COMPATIBILITY.md +0 -11
  125. package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
  126. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
  127. package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
@@ -1,4 +1,11 @@
1
1
  import { continuityOptionDefaults, validateContinuityOptions } from "./continuity-cli-options.js";
2
+ import { E_CLI_INVOCATION_INVALID } from "./error-codes.js";
3
+
4
+ function inputError(message) {
5
+ const error = new Error(message);
6
+ error.code = E_CLI_INVOCATION_INVALID;
7
+ return error;
8
+ }
2
9
 
3
10
  /**
4
11
  * Transport-neutral option defaults shared by the CLI parser and the
@@ -39,11 +46,35 @@ export function defaultCommandInputValues() {
39
46
  checkDetails: null,
40
47
  checkExecutionRef: null,
41
48
  checkProvenance: null,
49
+ timeoutMs: null,
50
+ scopeRef: null,
42
51
  commandArgv: [],
43
52
  checkType: null,
44
53
  checkSource: null,
45
54
  policy: null,
46
55
  taskId: null,
56
+ recipientHint: null,
57
+ handoffNote: null,
58
+ handoffId: null,
59
+ responsibilityLabel: null,
60
+ responsibilityAllowedPaths: [],
61
+ responsibilityReadOnlyPaths: [],
62
+ responsibilityRequiredChecks: [],
63
+ responsibilityFreezeContract: false,
64
+ responsibilityFreezeRoute: false,
65
+ responsibilityFreezeClaims: false,
66
+ verificationScopeMode: null,
67
+ revisionProvider: null,
68
+ signingProvider: null,
69
+ trustedRoot: null,
70
+ baseRevision: null,
71
+ headRevision: null,
72
+ requireCompleteCoverage: false,
73
+ requireSignature: false,
74
+ attestationRef: null,
75
+ attestationBundle: null,
76
+ attestationIdentity: null,
77
+ attestationIssuer: null,
47
78
  help: false,
48
79
  version: false,
49
80
  ...continuityOptionDefaults(),
@@ -60,48 +91,68 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
60
91
  if (!command) return;
61
92
 
62
93
  if (command === "policy" && !options.policy) {
63
- throw new Error("policy requires a name");
94
+ throw inputError("policy requires a name");
64
95
  }
65
96
  if (command !== "policy" && options.policy) {
66
- throw new Error(`Policy name is not valid for ${command}`);
97
+ throw inputError(`Policy name is not valid for ${command}`);
67
98
  }
68
99
  if (command === "bundle" && !options.taskId) {
69
- throw new Error("bundle requires --task");
100
+ throw inputError("bundle requires --task");
70
101
  }
71
102
  if (command === "task-create" && !options.taskId) {
72
- throw new Error("task-create requires --task");
103
+ throw inputError("task-create requires --task");
104
+ }
105
+ if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
106
+ && !options.taskId) {
107
+ throw inputError(`${command} requires --task`);
108
+ }
109
+ if (command === "handoff-show" && !help && !options.handoffId) {
110
+ throw inputError("handoff-show requires --id");
111
+ }
112
+ if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
113
+ throw inputError("responsibility-set requires --label");
114
+ }
115
+ if (command === "verify-scope" && !help) {
116
+ const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
117
+ if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) {
118
+ throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
119
+ }
120
+ }
121
+ if (command === "attestation-verify-range" && !help) {
122
+ if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
123
+ if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
73
124
  }
74
125
 
75
126
  validateContinuityOptions(command, options);
76
127
 
77
128
  if (command === "record-check" && !help) {
78
- if (!options.checkId) throw new Error("record-check requires --id");
79
- if (!options.checkRequirement) throw new Error("record-check requires --requirement");
80
- if (!options.checkStatus) throw new Error("record-check requires --status");
81
- if (!options.checkEvidenceKind) throw new Error("record-check requires --evidence-kind");
82
- if (!options.checkCommand && !options.checkResult) throw new Error("record-check requires --command or --result");
129
+ if (!options.checkId) throw inputError("record-check requires --id");
130
+ if (!options.checkRequirement) throw inputError("record-check requires --requirement");
131
+ if (!options.checkStatus) throw inputError("record-check requires --status");
132
+ if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
133
+ if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
83
134
  }
84
135
  if (command === "run-check" && !help) {
85
- if (!options.checkId) throw new Error("run-check requires --id");
86
- if (!options.checkRequirement) throw new Error("run-check requires --requirement");
136
+ if (!options.checkId) throw inputError("run-check requires --id");
137
+ if (!options.checkRequirement) throw inputError("run-check requires --requirement");
87
138
  if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
88
139
  || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
89
- throw new Error("run-check accepts only --id, --requirement, --details, --timeout-ms, and -- <argv>");
140
+ throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
90
141
  }
91
142
  if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
92
- throw new Error("run-check requires -- followed by an exact command argv");
143
+ throw inputError("run-check requires -- followed by an exact command argv");
93
144
  }
94
145
  }
95
146
  if (command === "reconcile-closure" && !help) {
96
- if (!options.taskId) throw new Error("reconcile-closure requires --task");
97
- if (!options.checkId) throw new Error("reconcile-closure requires --id");
98
- if (!options.checkRequirement) throw new Error("reconcile-closure requires --requirement");
147
+ if (!options.taskId) throw inputError("reconcile-closure requires --task");
148
+ if (!options.checkId) throw inputError("reconcile-closure requires --id");
149
+ if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
99
150
  if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
100
151
  || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
101
- throw new Error("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
152
+ throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
102
153
  }
103
154
  if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
104
- throw new Error("reconcile-closure requires -- followed by an exact command argv");
155
+ throw inputError("reconcile-closure requires -- followed by an exact command argv");
105
156
  }
106
157
  }
107
158
  }
@@ -4,6 +4,7 @@ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
4
4
  import { COMMAND_EXECUTORS } from "./command-executors.js";
5
5
  import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./command-input.js";
6
6
  import { PROTOCOL_VERSION } from "./protocol.js";
7
+ import { exitCodeForError } from "./exit-codes.js";
7
8
 
8
9
  export const FORGELOOP_INTEGRATION_RUNTIME_VERSION = 1;
9
10
 
@@ -105,7 +106,7 @@ export async function executeForgeLoopCommand({
105
106
  return {
106
107
  ok: false,
107
108
  command,
108
- exitCode: 1,
109
+ exitCode: exitCodeForError(error),
109
110
  result: null,
110
111
  error: {
111
112
  code: error.code ?? "E_COMMAND_EXECUTION_FAILED",
@@ -1,6 +1,6 @@
1
1
  import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
2
  import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
3
- import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
3
+ import { appendProtocolEvent, previewProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
4
4
  import { evaluatePreflight } from "./preflight.js";
5
5
  import { readContract } from "./contract.js";
6
6
  import { readPersistedRoute } from "./route-artifact.js";
@@ -11,9 +11,29 @@ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
11
11
  import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
12
12
  import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
13
13
  import { evaluateTerminalRequirements } from "./evidence-readiness.js";
14
- import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
14
+ import { PROJECT_ARTIFACT_PATHS, taskArtifactPath, taskCodeManifestPath } from "./task-paths.js";
15
15
  import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
16
16
  import { listActions } from "./actions.js";
17
+ import { readConfig } from "./config.js";
18
+ import { resolveResponsibilityStatus } from "./responsibility.js";
19
+ import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
20
+ import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
21
+
22
+ async function attestationConfiguration(target, packageRoot, errors) {
23
+ try {
24
+ const config = await readConfig(target, packageRoot);
25
+ return {
26
+ mode: config.attestation?.mode ?? "off",
27
+ revisionProvider: config.attestation?.revisionProvider ?? "git",
28
+ requireCompleteCoverage: config.attestation?.requireCompleteCoverage === true,
29
+ signing: config.attestation?.signing ?? { provider: "none", required: false, policy: {} },
30
+ };
31
+ } catch (error) {
32
+ if (error.code === "ARTIFACT_MISSING") return { mode: "off", revisionProvider: "git", requireCompleteCoverage: false, signing: { provider: "none", required: false, policy: {} } };
33
+ errors.push(issue(error.code ?? "E_ATTESTATION_INVALID", `Attestation configuration is invalid: ${error.message}`, [PROJECT_ARTIFACT_PATHS.config]));
34
+ return { mode: "off", revisionProvider: "git", requireCompleteCoverage: false, signing: { provider: "none", required: false, policy: {} } };
35
+ }
36
+ }
17
37
 
18
38
  async function actionReceiptSummary(target, packageRoot, taskId) {
19
39
  const actions = await listActions(target, { packageRoot, taskId });
@@ -290,6 +310,59 @@ export async function evaluateCompletion({
290
310
  [receiptRel],
291
311
  errors,
292
312
  );
313
+ const attestationConfig = await attestationConfiguration(target, packageRoot, errors);
314
+ let attestation = {
315
+ mode: attestationConfig.mode,
316
+ status: attestationConfig.mode === "off" ? "DISABLED" : "MISSING",
317
+ level: "PROCESSED",
318
+ manifest: null,
319
+ revisionProvider: attestationConfig.revisionProvider,
320
+ requireCompleteCoverage: attestationConfig.requireCompleteCoverage,
321
+ };
322
+
323
+ if (state?.phase === "COMPLETE" && attestationConfig.mode !== "off") {
324
+ try {
325
+ const manifest = await readCodeManifest({ target, packageRoot, taskId: contract?.value?.taskId ?? taskId });
326
+ await validateCodeManifestBindings({
327
+ target,
328
+ packageRoot,
329
+ taskId: contract?.value?.taskId ?? taskId,
330
+ manifest: manifest.value,
331
+ revisionProvider: attestationConfig.revisionProvider,
332
+ });
333
+ attestation = {
334
+ ...attestation,
335
+ status: "CAPTURED",
336
+ level: "VERIFIED",
337
+ manifest: { path: manifest.path, fingerprint: manifest.fingerprint, contentDigest: manifest.value.contentDigest },
338
+ };
339
+ } catch (error) {
340
+ if (attestationConfig.mode === "required") {
341
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_ATTESTATION_MANIFEST_MISSING" : error.code ?? "E_ATTESTATION_MANIFEST_STALE", error.message, error.artifacts ?? [taskArtifactPath(contract?.value?.taskId ?? taskId, "attestations")]));
342
+ }
343
+ attestation = {
344
+ ...attestation,
345
+ status: error.code === "ARTIFACT_MISSING" ? "MISSING" : "UNAVAILABLE",
346
+ error: { code: error.code ?? "E_ATTESTATION_MANIFEST_STALE", message: error.message },
347
+ };
348
+ }
349
+ }
350
+
351
+ let responsibility = { status: "NOT_APPLICABLE", taskId: contract?.value?.taskId ?? taskId, errors: [] };
352
+ if (taskId || contract?.value?.taskId) {
353
+ try {
354
+ responsibility = await resolveResponsibilityStatus(target, {
355
+ packageRoot,
356
+ taskId: contract?.value?.taskId ?? taskId,
357
+ });
358
+ for (const error of responsibility.errors ?? []) {
359
+ errors.push(issue(error.code, error.message, [responsibility.path ?? taskArtifactPath(taskId, "responsibility")]));
360
+ }
361
+ } catch (error) {
362
+ errors.push(issue(error.code ?? "E_RESPONSIBILITY_INVALID", error.message, [taskArtifactPath(taskId, "responsibility")]));
363
+ responsibility = { status: "INVALID", taskId: contract?.value?.taskId ?? taskId, errors: [{ code: error.code ?? "E_RESPONSIBILITY_INVALID", message: error.message }] };
364
+ }
365
+ }
293
366
 
294
367
  if (state && hasExecutionStarted(state.phase)) {
295
368
  const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId, statePath, contractPath, routePath });
@@ -508,10 +581,15 @@ export async function evaluateCompletion({
508
581
  pending: durableActions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
509
582
  actionRefs: durableActions.map((action) => action.actionId),
510
583
  },
584
+ responsibility: {
585
+ status: responsibility.status,
586
+ ...(responsibility.fingerprint ? { fingerprint: responsibility.fingerprint } : {}),
587
+ },
588
+ attestation,
511
589
  };
512
590
  }
513
591
 
514
- export async function runComplete({
592
+ async function runCompleteInternal({
515
593
  target,
516
594
  packageRoot,
517
595
  strict = false,
@@ -631,6 +709,7 @@ export async function runComplete({
631
709
  const state = await readWorkState(target, { packageRoot, taskId, statePath });
632
710
  if (state && state.phase !== "COMPLETE") {
633
711
  const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
712
+ const contract = await readContract(target, packageRoot, { taskId, contractPath });
634
713
  const next = {
635
714
  ...state,
636
715
  previousPhase: state.phase,
@@ -647,6 +726,47 @@ export async function runComplete({
647
726
  stateFingerprint: canonicalFingerprint(next),
648
727
  verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
649
728
  }, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
729
+ const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
730
+ const existingCompletion = ledger.events.find((event) => event.taskId === contract.value.taskId && event.event === "COMPLETION_VALIDATED");
731
+ const completionEvent = existingCompletion ?? await previewProtocolEvent(target, {
732
+ taskId: contract.value.taskId,
733
+ event: "COMPLETION_VALIDATED",
734
+ }, packageRoot, { taskId, eventsPath });
735
+ let manifestCapture = null;
736
+ const attestationConfig = result.attestation ?? { mode: "off", revisionProvider: "git" };
737
+ if (attestationConfig.mode !== "off") {
738
+ try {
739
+ manifestCapture = await createCodeManifest({
740
+ target,
741
+ packageRoot,
742
+ taskId: contract.value.taskId,
743
+ revisionProvider: attestationConfig.revisionProvider,
744
+ baseRevision: state.repositoryFingerprint?.head ?? null,
745
+ headRevision: "WORKTREE",
746
+ captureMode: "WORKTREE",
747
+ candidateState: next,
748
+ candidateReceipt: nextReceipt,
749
+ candidateCompletionCheckpoint: { seq: completionEvent.seq, hash: completionEvent.hash },
750
+ });
751
+ result.attestation = {
752
+ ...result.attestation,
753
+ status: "CAPTURED",
754
+ level: "VERIFIED",
755
+ manifest: {
756
+ path: taskCodeManifestPath(contract.value.taskId),
757
+ fingerprint: manifestCapture.fingerprint,
758
+ contentDigest: manifestCapture.manifest.contentDigest,
759
+ },
760
+ };
761
+ } catch (error) {
762
+ if (attestationConfig.mode === "required") throw error;
763
+ result.attestation = {
764
+ ...result.attestation,
765
+ status: "UNAVAILABLE",
766
+ error: { code: error.code ?? "E_ATTESTATION_MANIFEST_INVALID", message: error.message },
767
+ };
768
+ }
769
+ }
650
770
  await mutateWorkState(target, {
651
771
  expectedRevision: state.revision ?? 0,
652
772
  packageRoot,
@@ -654,12 +774,54 @@ export async function runComplete({
654
774
  statePath,
655
775
  }, () => next);
656
776
  await writeJsonArtifact(target, receiptRel, nextReceipt, "execution-receipt", packageRoot);
657
- }
658
- const contract = await readContract(target, packageRoot, { taskId, contractPath });
659
- const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
660
- if (!ledger.events.some((event) => event.taskId === contract.value.taskId && event.event === "COMPLETION_VALIDATED")) {
661
- await appendProtocolEvent(target, { taskId: contract.value.taskId, event: "COMPLETION_VALIDATED" }, packageRoot, { taskId, eventsPath });
777
+ if (!existingCompletion) {
778
+ const persistedCompletion = await appendProtocolEvent(target, {
779
+ taskId: contract.value.taskId,
780
+ event: "COMPLETION_VALIDATED",
781
+ at: completionEvent.at,
782
+ }, packageRoot, { taskId, eventsPath });
783
+ if (persistedCompletion.hash !== completionEvent.hash) {
784
+ throw new Error("Precomputed completion event changed before persistence");
785
+ }
786
+ }
787
+ if (manifestCapture) {
788
+ const manifestArtifact = await writeCodeManifest({
789
+ target,
790
+ packageRoot,
791
+ taskId: contract.value.taskId,
792
+ manifest: manifestCapture.manifest,
793
+ });
794
+ await appendProtocolEvent(target, {
795
+ taskId: contract.value.taskId,
796
+ event: "CODE_MANIFEST_CAPTURED",
797
+ fingerprint: manifestArtifact.fingerprint,
798
+ details: {
799
+ manifestFingerprint: manifestArtifact.fingerprint,
800
+ contentDigest: manifestCapture.manifest.contentDigest,
801
+ coveredPaths: new Set(manifestCapture.manifest.entries.flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean))).size,
802
+ verificationCycle: manifestCapture.manifest.verificationCycle,
803
+ },
804
+ }, packageRoot, { taskId, eventsPath });
805
+ }
806
+ } else if (state?.phase === "COMPLETE" && result.attestation?.status === "MISSING" && result.attestation.mode === "required") {
807
+ const error = new Error("Required code manifest is missing from a COMPLETE task");
808
+ error.code = "E_ATTESTATION_MANIFEST_MISSING";
809
+ throw error;
662
810
  }
663
811
  }
664
812
  return result;
665
813
  }
814
+
815
+ export async function runComplete(options = {}) {
816
+ const taskId = options.taskId ?? null;
817
+ if (options.persist !== false && taskId && !getActiveTaskTransaction()) {
818
+ return withTaskTransaction({
819
+ target: options.target,
820
+ taskId,
821
+ packageRoot: options.packageRoot,
822
+ operation: "complete",
823
+ recordCommitEvent: true,
824
+ }, () => runCompleteInternal(options));
825
+ }
826
+ return runCompleteInternal(options);
827
+ }
@@ -1,20 +1,76 @@
1
1
  import { PROTOCOL_VERSION } from "./protocol.js";
2
2
  import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
+ import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
4
+ import { normalizeVerificationConfiguration } from "./verification-scope-capability.js";
3
5
 
4
6
  export const CONFIG_SCHEMA_VERSION = 1;
5
7
  export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
8
+ export const ATTESTATION_MODES = Object.freeze(["off", "optional", "required"]);
6
9
 
7
10
  function stringArray(value, label) {
8
11
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
9
- throw new Error(`${label} must be an array of non-empty strings`);
12
+ const error = new Error(`${label} must be an array of non-empty strings`);
13
+ error.code = E_ATTESTATION_CONFIGURATION_INVALID;
14
+ throw error;
10
15
  }
11
16
  return [...new Set(value)];
12
17
  }
13
18
 
19
+ function configurationError(message) {
20
+ const error = new Error(message);
21
+ error.code = E_ATTESTATION_CONFIGURATION_INVALID;
22
+ return error;
23
+ }
24
+
14
25
  export function createConfig(input = {}) {
15
26
  const complianceMode = input.complianceMode ?? "standard";
16
27
  if (!COMPLIANCE_MODES.includes(complianceMode)) {
17
- throw new Error(`Unknown compliance mode: ${complianceMode}`);
28
+ throw configurationError(`Unknown compliance mode: ${complianceMode}`);
29
+ }
30
+ let attestation;
31
+ if (input.attestation !== undefined) {
32
+ if (!input.attestation || typeof input.attestation !== "object" || Array.isArray(input.attestation)) {
33
+ throw configurationError("attestation must be an object");
34
+ }
35
+ const mode = input.attestation.mode ?? "optional";
36
+ if (!ATTESTATION_MODES.includes(mode)) throw configurationError(`Unknown attestation mode: ${mode}`);
37
+ const revisionProvider = input.attestation.revisionProvider ?? "git";
38
+ if (typeof revisionProvider !== "string" || !revisionProvider.trim()) throw configurationError("attestation.revisionProvider must be a non-empty string");
39
+ const signing = input.attestation.signing ?? {};
40
+ if (!signing || typeof signing !== "object" || Array.isArray(signing)) {
41
+ throw configurationError("attestation.signing must be an object");
42
+ }
43
+ const provider = signing.provider ?? "none";
44
+ if (!["none", "sigstore"].includes(provider)) throw configurationError(`Unknown attestation signing provider: ${provider}`);
45
+ const policy = signing.policy ?? {};
46
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
47
+ throw configurationError("attestation.signing.policy must be an object");
48
+ }
49
+ if (policy.issuer !== undefined && typeof policy.issuer !== "string") throw configurationError("attestation.signing.policy.issuer must be a string");
50
+ if (policy.identities !== undefined) stringArray(policy.identities, "attestation.signing.policy.identities");
51
+ attestation = {
52
+ mode,
53
+ revisionProvider,
54
+ requireCompleteCoverage: input.attestation.requireCompleteCoverage === true,
55
+ coverage: { exclude: stringArray(input.attestation.coverage?.exclude ?? [], "attestation.coverage.exclude") },
56
+ signing: {
57
+ provider,
58
+ required: signing.required === true,
59
+ policy: {
60
+ ...(policy.issuer !== undefined ? { issuer: policy.issuer } : {}),
61
+ identities: [...(policy.identities ?? [])],
62
+ requireTransparencyLog: policy.requireTransparencyLog !== false,
63
+ },
64
+ },
65
+ };
66
+ }
67
+ let verification;
68
+ if (input.verification !== undefined) {
69
+ try {
70
+ verification = normalizeVerificationConfiguration(input.verification);
71
+ } catch (error) {
72
+ throw configurationError(error.message);
73
+ }
18
74
  }
19
75
  return {
20
76
  schemaVersion: CONFIG_SCHEMA_VERSION,
@@ -23,6 +79,8 @@ export function createConfig(input = {}) {
23
79
  ...(input.policy !== undefined ? { policy: input.policy } : {}),
24
80
  ...(input.requiredGates !== undefined ? { requiredGates: stringArray(input.requiredGates, "requiredGates") } : {}),
25
81
  ...(input.requiredEvidence !== undefined ? { requiredEvidence: stringArray(input.requiredEvidence, "requiredEvidence") } : {}),
82
+ ...(verification ? { verification } : {}),
83
+ ...(attestation ? { attestation } : {}),
26
84
  };
27
85
  }
28
86
 
@@ -8,6 +8,12 @@ const OPTION_FIELDS = Object.freeze({
8
8
  "--resume-note": ["continuityResumeNote", false],
9
9
  });
10
10
 
11
+ function continuityInputError(message) {
12
+ const error = new Error(message);
13
+ error.code = "E_CLI_INVOCATION_INVALID";
14
+ return error;
15
+ }
16
+
11
17
  export function continuityOptionDefaults() {
12
18
  return {
13
19
  continuityFocusId: null,
@@ -24,7 +30,7 @@ export function consumeContinuityOption({ argument, argv, index, options }) {
24
30
  const definition = OPTION_FIELDS[argument];
25
31
  if (!definition) return { handled: false, index };
26
32
  const value = argv[index + 1];
27
- if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value`);
33
+ if (!value || value.startsWith("-")) throw continuityInputError(`${argument} requires a value`);
28
34
  const [field, repeatable] = definition;
29
35
  if (repeatable) options[field].push(value);
30
36
  else options[field] = value;
@@ -45,13 +51,13 @@ export function hasContinuityOptions(options = {}) {
45
51
 
46
52
  export function validateContinuityOptions(command, options = {}) {
47
53
  if (command !== "record-continuity" && hasContinuityOptions(options)) {
48
- throw new Error(`Continuity recording options are not valid for ${command}`);
54
+ throw continuityInputError(`Continuity recording options are not valid for ${command}`);
49
55
  }
50
56
  if (command === "record-continuity") {
51
57
  const hasFocusId = Boolean(options.continuityFocusId);
52
58
  const hasFocusSummary = Boolean(options.continuityFocusSummary);
53
59
  if (hasFocusId !== hasFocusSummary) {
54
- throw new Error("record-continuity requires --focus-id and --focus-summary together");
60
+ throw continuityInputError("record-continuity requires --focus-id and --focus-summary together");
55
61
  }
56
62
  }
57
63
  return options;
@@ -177,6 +177,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
177
177
  ]);
178
178
 
179
179
  const state = await readWorkState(target, { packageRoot, taskId });
180
+ const latestHandoff = await resolveLatestHandoffSafe(target, { packageRoot, taskId: state?.taskId ?? taskId });
180
181
  let continuityArtifact;
181
182
  try {
182
183
  continuityArtifact = await readContinuity(target, { packageRoot, taskId });
@@ -186,6 +187,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
186
187
  ...classifyContinuity({ continuity: null, state }),
187
188
  path: ".forgeloop/continuity.json",
188
189
  present: false,
190
+ latestHandoff,
189
191
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
190
192
  };
191
193
  }
@@ -197,6 +199,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
197
199
  path: ".forgeloop/continuity.json",
198
200
  present: true,
199
201
  error: error.message,
202
+ latestHandoff,
200
203
  };
201
204
  }
202
205
 
@@ -225,10 +228,27 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
225
228
  present: true,
226
229
  fingerprint: continuityArtifact.fingerprint,
227
230
  continuity: continuityArtifact.value,
231
+ latestHandoff,
228
232
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
229
233
  };
230
234
  }
231
235
 
236
+ async function resolveLatestHandoffSafe(target, { packageRoot, taskId } = {}) {
237
+ if (!taskId) return null;
238
+ try {
239
+ const { resolveLatestHandoff } = await import("./handoff.js");
240
+ return await resolveLatestHandoff(target, { packageRoot, taskId });
241
+ } catch (error) {
242
+ return {
243
+ status: "INVALID",
244
+ error: {
245
+ code: error.code ?? "E_HANDOFF_INVALID",
246
+ message: error.message,
247
+ },
248
+ };
249
+ }
250
+ }
251
+
232
252
  async function deriveDiagnosticContextSafe({ target, packageRoot, state }) {
233
253
  try {
234
254
  const [{ readEvents }, { deriveDiagnosticContext }] = await Promise.all([
@@ -30,7 +30,9 @@ export function resolveCurrentCycleDiagnostic(events, taskId, verificationCycle)
30
30
  const candidates = diagnosticEventsForTask(events, taskId)
31
31
  .filter((event) => {
32
32
  const cycle = event.details?.verificationCycle;
33
- return verificationCycle == null || cycle == null || cycle === verificationCycle;
33
+ return (verificationCycle === null || verificationCycle === undefined)
34
+ || (cycle === null || cycle === undefined)
35
+ || cycle === verificationCycle;
34
36
  })
35
37
  .sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
36
38