@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
@@ -32,6 +32,106 @@ export const E_TASK_SCOPE_FROZEN = "E_TASK_SCOPE_FROZEN";
32
32
  export const E_TASK_CHANGE_OUTSIDE_SCOPE = "E_TASK_CHANGE_OUTSIDE_SCOPE";
33
33
  export const E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE = "E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE";
34
34
 
35
+ export const E_WORKSPACE_IDENTITY_UNAVAILABLE = "E_WORKSPACE_IDENTITY_UNAVAILABLE";
36
+ export const E_WORKSPACE_BINDING_INVALID = "E_WORKSPACE_BINDING_INVALID";
37
+ export const E_WORKSPACE_BINDING_MISMATCH = "E_WORKSPACE_BINDING_MISMATCH";
38
+ export const E_HANDOFF_INVALID = "E_HANDOFF_INVALID";
39
+ export const E_HANDOFF_STATE_UNAVAILABLE = "E_HANDOFF_STATE_UNAVAILABLE";
40
+ export const E_HANDOFF_TAMPERED = "E_HANDOFF_TAMPERED";
41
+ export const E_HANDOFF_NOT_FOUND = "E_HANDOFF_NOT_FOUND";
42
+ export const E_RESPONSIBILITY_INVALID = "E_RESPONSIBILITY_INVALID";
43
+ export const E_RESPONSIBILITY_SCOPE_VIOLATION = "E_RESPONSIBILITY_SCOPE_VIOLATION";
44
+ export const E_RESPONSIBILITY_FROZEN_INPUT_DRIFT = "E_RESPONSIBILITY_FROZEN_INPUT_DRIFT";
45
+ export const E_RESPONSIBILITY_REQUIRED_CHECK_MISSING = "E_RESPONSIBILITY_REQUIRED_CHECK_MISSING";
46
+ export const E_VERIFICATION_SCOPE_INVALID = "E_VERIFICATION_SCOPE_INVALID";
47
+ export const E_VERIFICATION_SCOPE_STALE = "E_VERIFICATION_SCOPE_STALE";
48
+ export const E_VERIFICATION_SCOPE_UNRESOLVED = "E_VERIFICATION_SCOPE_UNRESOLVED";
49
+ export const E_REVISION_PROVIDER_UNAVAILABLE = "E_REVISION_PROVIDER_UNAVAILABLE";
50
+ export const E_REVISION_PROVIDER_AMBIGUOUS = "E_REVISION_PROVIDER_AMBIGUOUS";
51
+ export const E_REVISION_PROVIDER_INVALID = "E_REVISION_PROVIDER_INVALID";
52
+ export const E_REVISION_NOT_FOUND = "E_REVISION_NOT_FOUND";
53
+ export const E_REVISION_CONTENT_UNAVAILABLE = "E_REVISION_CONTENT_UNAVAILABLE";
54
+ export const E_ATTESTATION_DISABLED = "E_ATTESTATION_DISABLED";
55
+ export const E_ATTESTATION_GIT_REQUIRED = "E_ATTESTATION_GIT_REQUIRED";
56
+ export const E_ATTESTATION_MANIFEST_MISSING = "E_ATTESTATION_MANIFEST_MISSING";
57
+ export const E_ATTESTATION_MANIFEST_INVALID = "E_ATTESTATION_MANIFEST_INVALID";
58
+ export const E_ATTESTATION_MANIFEST_STALE = "E_ATTESTATION_MANIFEST_STALE";
59
+ export const E_ATTESTATION_CONTENT_MISMATCH = "E_ATTESTATION_CONTENT_MISMATCH";
60
+ export const E_ATTESTATION_SCOPE_MISMATCH = "E_ATTESTATION_SCOPE_MISMATCH";
61
+ export const E_ATTESTATION_COVERAGE_GAP = "E_ATTESTATION_COVERAGE_GAP";
62
+ export const E_ATTESTATION_COVERAGE_CONFLICT = "E_ATTESTATION_COVERAGE_CONFLICT";
63
+ export const E_ATTESTATION_STATEMENT_MISSING = "E_ATTESTATION_STATEMENT_MISSING";
64
+ export const E_ATTESTATION_STATEMENT_INVALID = "E_ATTESTATION_STATEMENT_INVALID";
65
+ export const E_ATTESTATION_SUBJECT_MISMATCH = "E_ATTESTATION_SUBJECT_MISMATCH";
66
+ export const E_ATTESTATION_RECEIPT_MISMATCH = "E_ATTESTATION_RECEIPT_MISMATCH";
67
+ export const E_ATTESTATION_STATE_MISMATCH = "E_ATTESTATION_STATE_MISMATCH";
68
+ export const E_ATTESTATION_ROUTE_MISMATCH = "E_ATTESTATION_ROUTE_MISMATCH";
69
+ export const E_ATTESTATION_CONTRACT_MISMATCH = "E_ATTESTATION_CONTRACT_MISMATCH";
70
+ export const E_ATTESTATION_LEDGER_MISMATCH = "E_ATTESTATION_LEDGER_MISMATCH";
71
+ export const E_ATTESTATION_UNSIGNED = "E_ATTESTATION_UNSIGNED";
72
+ export const E_ATTESTATION_SIGNATURE_INVALID = "E_ATTESTATION_SIGNATURE_INVALID";
73
+ export const E_ATTESTATION_SIGNER_UNAVAILABLE = "E_ATTESTATION_SIGNER_UNAVAILABLE";
74
+ export const E_ATTESTATION_IDENTITY_UNTRUSTED = "E_ATTESTATION_IDENTITY_UNTRUSTED";
75
+ export const E_ATTESTATION_ISSUER_UNTRUSTED = "E_ATTESTATION_ISSUER_UNTRUSTED";
76
+ export const E_ATTESTATION_TARGET_REF_INVALID = "E_ATTESTATION_TARGET_REF_INVALID";
77
+ export const E_ATTESTATION_INVALID = "E_ATTESTATION_INVALID";
78
+ export const E_ATTESTATION_CONFIGURATION_INVALID = "E_ATTESTATION_CONFIGURATION_INVALID";
79
+ export const E_CLI_INVOCATION_INVALID = "E_CLI_INVOCATION_INVALID";
80
+
81
+ const EXTENSION_PUBLIC_ERROR_CODES = Object.freeze(Object.fromEntries([
82
+ E_WORKSPACE_IDENTITY_UNAVAILABLE,
83
+ E_WORKSPACE_BINDING_INVALID,
84
+ E_WORKSPACE_BINDING_MISMATCH,
85
+ E_HANDOFF_INVALID,
86
+ E_HANDOFF_STATE_UNAVAILABLE,
87
+ E_HANDOFF_TAMPERED,
88
+ E_HANDOFF_NOT_FOUND,
89
+ E_RESPONSIBILITY_INVALID,
90
+ E_RESPONSIBILITY_SCOPE_VIOLATION,
91
+ E_RESPONSIBILITY_FROZEN_INPUT_DRIFT,
92
+ E_RESPONSIBILITY_REQUIRED_CHECK_MISSING,
93
+ E_VERIFICATION_SCOPE_INVALID,
94
+ E_VERIFICATION_SCOPE_STALE,
95
+ E_VERIFICATION_SCOPE_UNRESOLVED,
96
+ E_REVISION_PROVIDER_UNAVAILABLE,
97
+ E_REVISION_PROVIDER_AMBIGUOUS,
98
+ E_REVISION_PROVIDER_INVALID,
99
+ E_REVISION_NOT_FOUND,
100
+ E_REVISION_CONTENT_UNAVAILABLE,
101
+ E_ATTESTATION_DISABLED,
102
+ E_ATTESTATION_GIT_REQUIRED,
103
+ E_ATTESTATION_MANIFEST_MISSING,
104
+ E_ATTESTATION_MANIFEST_INVALID,
105
+ E_ATTESTATION_MANIFEST_STALE,
106
+ E_ATTESTATION_CONTENT_MISMATCH,
107
+ E_ATTESTATION_SCOPE_MISMATCH,
108
+ E_ATTESTATION_COVERAGE_GAP,
109
+ E_ATTESTATION_COVERAGE_CONFLICT,
110
+ E_ATTESTATION_STATEMENT_MISSING,
111
+ E_ATTESTATION_STATEMENT_INVALID,
112
+ E_ATTESTATION_SUBJECT_MISMATCH,
113
+ E_ATTESTATION_RECEIPT_MISMATCH,
114
+ E_ATTESTATION_STATE_MISMATCH,
115
+ E_ATTESTATION_ROUTE_MISMATCH,
116
+ E_ATTESTATION_CONTRACT_MISMATCH,
117
+ E_ATTESTATION_LEDGER_MISMATCH,
118
+ E_ATTESTATION_UNSIGNED,
119
+ E_ATTESTATION_SIGNATURE_INVALID,
120
+ E_ATTESTATION_SIGNER_UNAVAILABLE,
121
+ E_ATTESTATION_IDENTITY_UNTRUSTED,
122
+ E_ATTESTATION_ISSUER_UNTRUSTED,
123
+ E_ATTESTATION_TARGET_REF_INVALID,
124
+ E_ATTESTATION_INVALID,
125
+ E_ATTESTATION_CONFIGURATION_INVALID,
126
+ E_CLI_INVOCATION_INVALID,
127
+ ].map((code) => [code, Object.freeze({
128
+ code,
129
+ category: code.slice(2).split("_")[0].toLowerCase(),
130
+ classification: "PUBLIC_STABLE",
131
+ meaning: "A ForgeLoop boundary, artifact, provider, or attestation validation condition was not satisfied.",
132
+ safeResolution: "Inspect the structured command result, correct the named boundary or artifact, then retry the canonical command.",
133
+ })])));
134
+
35
135
  export const E_TASK_LAYOUT_LEGACY = "E_TASK_LAYOUT_LEGACY";
36
136
  export const E_TASK_MIGRATION_INVALID = "E_TASK_MIGRATION_INVALID";
37
137
  export const E_TASK_RECOVERY_UNSAFE = "E_TASK_RECOVERY_UNSAFE";
@@ -114,6 +214,7 @@ export const E_TRAJECTORY_REFERENCE_REQUIRED = "E_TRAJECTORY_REFERENCE_REQUIRED"
114
214
  * Public, stable ForgeLoop error and reason codes documented for users and harnesses.
115
215
  */
116
216
  export const PUBLIC_ERROR_CODES = Object.freeze({
217
+ ...EXTENSION_PUBLIC_ERROR_CODES,
117
218
  E_PREFLIGHT_NOT_READY: Object.freeze({
118
219
  code: "E_PREFLIGHT_NOT_READY",
119
220
  category: "preflight",
@@ -880,6 +981,51 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
880
981
  E_TASK_SCOPE_DIRTY,
881
982
  E_TASK_SCOPE_FROZEN,
882
983
  E_TASK_CHANGE_OUTSIDE_SCOPE,
984
+ E_WORKSPACE_IDENTITY_UNAVAILABLE,
985
+ E_WORKSPACE_BINDING_INVALID,
986
+ E_WORKSPACE_BINDING_MISMATCH,
987
+ E_HANDOFF_INVALID,
988
+ E_HANDOFF_STATE_UNAVAILABLE,
989
+ E_HANDOFF_TAMPERED,
990
+ E_HANDOFF_NOT_FOUND,
991
+ E_RESPONSIBILITY_INVALID,
992
+ E_RESPONSIBILITY_SCOPE_VIOLATION,
993
+ E_RESPONSIBILITY_FROZEN_INPUT_DRIFT,
994
+ E_RESPONSIBILITY_REQUIRED_CHECK_MISSING,
995
+ E_VERIFICATION_SCOPE_INVALID,
996
+ E_VERIFICATION_SCOPE_STALE,
997
+ E_VERIFICATION_SCOPE_UNRESOLVED,
998
+ E_REVISION_PROVIDER_UNAVAILABLE,
999
+ E_REVISION_PROVIDER_AMBIGUOUS,
1000
+ E_REVISION_PROVIDER_INVALID,
1001
+ E_REVISION_NOT_FOUND,
1002
+ E_REVISION_CONTENT_UNAVAILABLE,
1003
+ E_ATTESTATION_DISABLED,
1004
+ E_ATTESTATION_GIT_REQUIRED,
1005
+ E_ATTESTATION_MANIFEST_MISSING,
1006
+ E_ATTESTATION_MANIFEST_INVALID,
1007
+ E_ATTESTATION_MANIFEST_STALE,
1008
+ E_ATTESTATION_CONTENT_MISMATCH,
1009
+ E_ATTESTATION_SCOPE_MISMATCH,
1010
+ E_ATTESTATION_COVERAGE_GAP,
1011
+ E_ATTESTATION_COVERAGE_CONFLICT,
1012
+ E_ATTESTATION_STATEMENT_MISSING,
1013
+ E_ATTESTATION_STATEMENT_INVALID,
1014
+ E_ATTESTATION_SUBJECT_MISMATCH,
1015
+ E_ATTESTATION_RECEIPT_MISMATCH,
1016
+ E_ATTESTATION_STATE_MISMATCH,
1017
+ E_ATTESTATION_ROUTE_MISMATCH,
1018
+ E_ATTESTATION_CONTRACT_MISMATCH,
1019
+ E_ATTESTATION_LEDGER_MISMATCH,
1020
+ E_ATTESTATION_UNSIGNED,
1021
+ E_ATTESTATION_SIGNATURE_INVALID,
1022
+ E_ATTESTATION_SIGNER_UNAVAILABLE,
1023
+ E_ATTESTATION_IDENTITY_UNTRUSTED,
1024
+ E_ATTESTATION_ISSUER_UNTRUSTED,
1025
+ E_ATTESTATION_TARGET_REF_INVALID,
1026
+ E_ATTESTATION_INVALID,
1027
+ E_ATTESTATION_CONFIGURATION_INVALID,
1028
+ E_CLI_INVOCATION_INVALID,
883
1029
  E_RECONCILE_NOT_STALE,
884
1030
  E_RECONCILE_PHASE_INVALID,
885
1031
  E_RECONCILE_UNSUPPORTED_DRIFT,
@@ -148,6 +148,42 @@ export function validateKnownEventDetails(event) {
148
148
  throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
149
149
  }
150
150
  return;
151
+ case "WORKSPACE_BOUND":
152
+ assertStructuredArtifactEvent(event, ["workspaceFingerprint"], "WORKSPACE_BOUND");
153
+ return;
154
+ case "HANDOFF_CREATED":
155
+ if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
156
+ || typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
157
+ || typeof event.details.artifact !== "string" || !event.details.artifact
158
+ || typeof event.details.digest !== "string") {
159
+ throw protocolError("E_EVENT_INVALID", "HANDOFF_CREATED requires a handoff ID, artifact path, and digest");
160
+ }
161
+ assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
162
+ return;
163
+ case "RESPONSIBILITY_SET":
164
+ assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
165
+ if (typeof event.details.label !== "string" || !event.details.label) {
166
+ throw protocolError("E_EVENT_INVALID", "RESPONSIBILITY_SET details.label must be a non-empty string");
167
+ }
168
+ return;
169
+ case "VERIFICATION_SCOPE_CAPTURED":
170
+ assertStructuredArtifactEvent(event, ["scopeFingerprint"], "VERIFICATION_SCOPE_CAPTURED");
171
+ if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(event.details.resolvedMode)
172
+ || !Number.isInteger(event.details.verificationCycle) || event.details.verificationCycle < 1) {
173
+ throw protocolError("E_EVENT_INVALID", "VERIFICATION_SCOPE_CAPTURED requires a valid mode and verification cycle");
174
+ }
175
+ return;
176
+ case "CODE_MANIFEST_CAPTURED":
177
+ assertStructuredArtifactEvent(event, ["manifestFingerprint"], "CODE_MANIFEST_CAPTURED");
178
+ assertFingerprint(event.details.contentDigest, "CODE_MANIFEST_CAPTURED details.contentDigest");
179
+ if (!Number.isInteger(event.details.coveredPaths) || event.details.coveredPaths < 0) {
180
+ throw protocolError("E_EVENT_INVALID", "CODE_MANIFEST_CAPTURED details.coveredPaths must be a non-negative integer");
181
+ }
182
+ return;
183
+ case "ATTESTATION_STATEMENT_CREATED":
184
+ assertStructuredArtifactEvent(event, ["statementFingerprint"], "ATTESTATION_STATEMENT_CREATED");
185
+ assertFingerprint(event.details.manifestFingerprint, "ATTESTATION_STATEMENT_CREATED details.manifestFingerprint");
186
+ return;
151
187
  default:
152
188
  if (isActionEventName(event.event)) {
153
189
  assertActionEventDetails(event);
@@ -167,6 +203,27 @@ function assertStringList(value, label) {
167
203
  }
168
204
  }
169
205
 
206
+ function assertFingerprint(value, label) {
207
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
208
+ throw protocolError("E_EVENT_INVALID", `${label} must be a lowercase SHA-256 fingerprint`);
209
+ }
210
+ }
211
+
212
+ function assertStructuredArtifactEvent(event, requiredDetails, label) {
213
+ if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)) {
214
+ throw protocolError("E_EVENT_INVALID", `${label} requires structured details`);
215
+ }
216
+ for (const key of requiredDetails) {
217
+ if (typeof event.details[key] !== "string" || !event.details[key]) {
218
+ throw protocolError("E_EVENT_INVALID", `${label} details.${key} must be a non-empty string`);
219
+ }
220
+ assertFingerprint(event.details[key], `${label} details.${key}`);
221
+ }
222
+ if (event.fingerprint !== event.details[requiredDetails[0]]) {
223
+ throw protocolError("E_EVENT_INVALID", `${label} fingerprint must match details.${requiredDetails[0]}`);
224
+ }
225
+ }
226
+
170
227
  function assertRecoveryRecordedDetails(details) {
171
228
  if (!details || typeof details !== "object" || Array.isArray(details)) {
172
229
  throw protocolError("E_EVENT_INVALID", "recovery event requires structured details");
@@ -221,6 +278,44 @@ export function eventHash(event) {
221
278
  return canonicalFingerprint(body);
222
279
  }
223
280
 
281
+ export function buildProtocolEvent(input, { checkpoint } = {}) {
282
+ if (!checkpoint || !Number.isInteger(checkpoint.seq) || checkpoint.seq < 0
283
+ || (checkpoint.lastHash !== null && !/^[a-f0-9]{64}$/.test(checkpoint.lastHash))) {
284
+ throw protocolError("E_EVENT_INVALID", "event checkpoint is invalid");
285
+ }
286
+ const event = {
287
+ seq: checkpoint.seq + 1,
288
+ schemaVersion: EVENT_SCHEMA_VERSION,
289
+ protocolVersion: PROTOCOL_VERSION,
290
+ taskId: input.taskId,
291
+ event: input.event,
292
+ at: input.at ?? new Date().toISOString(),
293
+ ...(input.fingerprint ? { fingerprint: input.fingerprint } : {}),
294
+ previousHash: checkpoint.lastHash,
295
+ ...(input.details ? { details: structuredClone(input.details) } : {}),
296
+ };
297
+ validateKnownEventDetails(event);
298
+ assertSecretFree(event);
299
+ event.hash = eventHash(event);
300
+ return event;
301
+ }
302
+
303
+ export async function previewProtocolEvent(target, input, packageRoot, options = {}) {
304
+ const activeTransaction = getActiveTaskTransaction();
305
+ if (!activeTransaction) {
306
+ return withTaskTransaction({
307
+ target,
308
+ taskId: options.taskId ?? input.taskId,
309
+ lockTaskId: options.taskId ?? input.taskId,
310
+ operation: "preview-event",
311
+ packageRoot,
312
+ }, async () => previewProtocolEvent(target, input, packageRoot, options));
313
+ }
314
+ const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
315
+ const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
316
+ return buildProtocolEvent(input, { checkpoint });
317
+ }
318
+
224
319
  function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
225
320
  const error = new Error(message);
226
321
  error.code = code;
@@ -232,6 +327,11 @@ export async function readEvents(target, packageRoot, options = {}) {
232
327
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
233
328
  await assertSafePath(target, relPath);
234
329
  const eventsPath = ensureWithin(target, relPath);
330
+ const transaction = getActiveTaskTransaction();
331
+ if (transaction) {
332
+ const stagedText = await transaction.readText(relPath);
333
+ if (stagedText !== null) return parseEventsText(stagedText, relPath, packageRoot);
334
+ }
235
335
  if (!(await fileExists(eventsPath))) return [];
236
336
  const text = await readFile(eventsPath, "utf8");
237
337
  return parseEventsText(text, relPath, packageRoot);
@@ -245,6 +345,14 @@ export async function readEventTail(target, packageRoot, options = {}) {
245
345
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
246
346
  await assertSafePath(target, relPath);
247
347
  const eventsPath = ensureWithin(target, relPath);
348
+ const transaction = getActiveTaskTransaction();
349
+ if (transaction) {
350
+ const stagedText = await transaction.readText(relPath);
351
+ if (stagedText !== null) {
352
+ const lines = stagedText.split(/\r?\n/).filter((line) => line.trim() !== "");
353
+ return parseEventsText(lines.slice(-limit).join("\n"), relPath, packageRoot);
354
+ }
355
+ }
248
356
  if (!(await fileExists(eventsPath))) return [];
249
357
  const size = (await stat(eventsPath)).size;
250
358
  let window = Math.min(size, 64 * 1024);
@@ -306,19 +414,7 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
306
414
  }, async () => appendProtocolEvent(target, input, packageRoot, options));
307
415
  }
308
416
  const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
309
- const event = {
310
- seq: checkpoint.seq + 1,
311
- schemaVersion: EVENT_SCHEMA_VERSION,
312
- protocolVersion: PROTOCOL_VERSION,
313
- taskId: input.taskId,
314
- event: input.event,
315
- at: input.at ?? new Date().toISOString(),
316
- ...(input.fingerprint ? { fingerprint: input.fingerprint } : {}),
317
- previousHash: checkpoint.lastHash,
318
- ...(input.details ? { details: structuredClone(input.details) } : {}),
319
- };
320
- validateKnownEventDetails(event);
321
- assertSecretFree(event);
417
+ const event = buildProtocolEvent(input, { checkpoint });
322
418
  const schema = await readSchema("event", packageRoot);
323
419
  assertSchema(event, schema, relPath);
324
420
  event.hash = eventHash(event);
@@ -0,0 +1,22 @@
1
+ const ENVIRONMENT_OR_PROVIDER_CODES = new Set([
2
+ "E_CLI_INVOCATION_INVALID",
3
+ "E_REPOSITORY_GIT_COMMAND_FAILED",
4
+ "E_REVISION_PROVIDER_UNAVAILABLE",
5
+ "E_REVISION_PROVIDER_AMBIGUOUS",
6
+ "E_REVISION_PROVIDER_INVALID",
7
+ "E_REVISION_NOT_FOUND",
8
+ "E_REVISION_CONTENT_UNAVAILABLE",
9
+ "E_ATTESTATION_GIT_REQUIRED",
10
+ "E_ATTESTATION_CONFIGURATION_INVALID",
11
+ "E_ATTESTATION_SIGNER_UNAVAILABLE",
12
+ "E_ATTESTATION_TARGET_REF_INVALID",
13
+ ]);
14
+
15
+ export function exitCodeForError(error) {
16
+ return ENVIRONMENT_OR_PROVIDER_CODES.has(error?.code) ? 2 : 1;
17
+ }
18
+
19
+ export function exitCodeForAttestationResult(result) {
20
+ if (result?.status === "VALID") return 0;
21
+ return result?.errors?.some((error) => ENVIRONMENT_OR_PROVIDER_CODES.has(error?.code)) ? 2 : 1;
22
+ }
@@ -0,0 +1,208 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readdir } from "node:fs/promises";
3
+
4
+ import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
5
+ import { assertSecretFree } from "./receipt.js";
6
+ import { appendProtocolEvent } from "./events.js";
7
+ import { currentChangedPaths } from "./repository.js";
8
+ import { readContract } from "./contract.js";
9
+ import { readPersistedRoute } from "./route-artifact.js";
10
+ import { readWorkState } from "./work-state.js";
11
+ import { resolveTaskClaimState } from "./task-claim-state.js";
12
+ import { readContinuity } from "./continuity.js";
13
+ import { getPackageRoot } from "./templates.js";
14
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
15
+ import { taskArtifactPath, taskHandoffDirectory, taskHandoffPath } from "./task-paths.js";
16
+ import { PROTOCOL_VERSION, WORK_PHASES } from "./protocol.js";
17
+ import { assertTaskMutationAllowed } from "./task-claim-state.js";
18
+ import { assertWorkspaceBinding } from "./workspace-binding.js";
19
+ import { withTaskTransaction } from "./transaction.js";
20
+
21
+ function handoffError(code, message, artifacts = []) {
22
+ const error = new Error(message);
23
+ error.name = "HandoffError";
24
+ error.code = code;
25
+ error.artifacts = artifacts;
26
+ return error;
27
+ }
28
+
29
+ function optionalText(value, label) {
30
+ if (value === undefined || value === null) return null;
31
+ if (typeof value !== "string" || value.trim() === "") {
32
+ throw handoffError("E_HANDOFF_INVALID", `${label} must be a non-empty string when provided`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function pathList(value) {
38
+ return [...new Set((value ?? []).map((item) => String(item).replaceAll("\\", "/")))].sort();
39
+ }
40
+
41
+ function handoffWithoutDigest(value) {
42
+ const { artifactDigest: _artifactDigest, ...body } = value;
43
+ return body;
44
+ }
45
+
46
+ export function validateHandoffDigest(handoff) {
47
+ assertSecretFree(handoff);
48
+ if (typeof handoff?.artifactDigest !== "string"
49
+ || handoff.artifactDigest !== canonicalFingerprint(handoffWithoutDigest(handoff))) {
50
+ throw handoffError("E_HANDOFF_TAMPERED", "Handoff artifact digest does not match its canonical content");
51
+ }
52
+ return handoff;
53
+ }
54
+
55
+ async function currentContinuity(target, packageRoot, taskId) {
56
+ try {
57
+ const artifact = await readContinuity(target, { packageRoot, taskId });
58
+ return { ref: artifact.path, fingerprint: artifact.fingerprint };
59
+ } catch (error) {
60
+ if (error.code === "ARTIFACT_MISSING") {
61
+ return { ref: taskArtifactPath(taskId, "continuity"), fingerprint: null };
62
+ }
63
+ throw handoffError("E_HANDOFF_STATE_UNAVAILABLE", `Continuity state cannot be read: ${error.message}`, [taskArtifactPath(taskId, "continuity")]);
64
+ }
65
+ }
66
+
67
+ export async function buildCanonicalHandoff(target, {
68
+ taskId,
69
+ recipientHint,
70
+ note,
71
+ packageRoot = getPackageRoot(),
72
+ handoffId = `handoff-${randomUUID()}`,
73
+ createdAt = new Date().toISOString(),
74
+ } = {}) {
75
+ if (!taskId) throw handoffError("E_HANDOFF_STATE_UNAVAILABLE", "taskId is required to build a handoff");
76
+ const [state, contract, route, claims, changedPaths, continuity] = await Promise.all([
77
+ readWorkState(target, { packageRoot, taskId }),
78
+ readContract(target, packageRoot, { taskId }),
79
+ readPersistedRoute(target, packageRoot, { taskId }),
80
+ resolveTaskClaimState(target, { packageRoot, taskId }),
81
+ currentChangedPaths(target),
82
+ currentContinuity(target, packageRoot, taskId),
83
+ ]);
84
+ if (!state || !contract || !route || changedPaths === null || !claims.valid) {
85
+ throw handoffError("E_HANDOFF_STATE_UNAVAILABLE", "Canonical task state, route, claims, and changed paths are required for a handoff");
86
+ }
87
+ const checkItems = [...(state.checks ?? [])];
88
+ const executionRefs = pathList(checkItems.map((check) => check.executionRef).filter(Boolean));
89
+ const checkIds = pathList(checkItems.map((check) => check.id).filter(Boolean));
90
+ const body = {
91
+ schemaVersion: 1,
92
+ protocolVersion: PROTOCOL_VERSION,
93
+ handoffId,
94
+ taskId: state.taskId,
95
+ createdAt,
96
+ intent: {
97
+ ...(optionalText(recipientHint, "recipientHint") ? { recipientHint } : {}),
98
+ ...(optionalText(note, "note") ? { note } : {}),
99
+ },
100
+ state: {
101
+ phase: WORK_PHASES.includes(state.phase) ? state.phase : "UNKNOWN",
102
+ revision: state.revision ?? 0,
103
+ verificationCycle: state.verificationCycle ?? 1,
104
+ contractFingerprint: contract.fingerprint,
105
+ routeFingerprint: route.fingerprint ?? null,
106
+ repositoryFingerprint: state.repositoryFingerprint ?? { branch: null, head: null },
107
+ writeClaims: pathList(claims.effectiveWriteClaims ?? []),
108
+ changedPaths: pathList(changedPaths),
109
+ },
110
+ evidence: { executionRefs, checkIds },
111
+ continuity,
112
+ };
113
+ return {
114
+ ...body,
115
+ artifactDigest: canonicalFingerprint(body),
116
+ };
117
+ }
118
+
119
+ export async function validateCanonicalHandoff(target, handoff, {
120
+ taskId,
121
+ packageRoot = getPackageRoot(),
122
+ } = {}) {
123
+ const { readSchema, assertSchema } = await import("./schema-validation.js");
124
+ try {
125
+ assertSchema(handoff, await readSchema("handoff-envelope", packageRoot), "handoff envelope");
126
+ } catch (error) {
127
+ throw handoffError("E_HANDOFF_INVALID", error.message);
128
+ }
129
+ if (taskId && handoff.taskId !== taskId) {
130
+ throw handoffError("E_HANDOFF_INVALID", "Handoff taskId does not match the selected task");
131
+ }
132
+ return validateHandoffDigest(handoff);
133
+ }
134
+
135
+ export async function writeCanonicalHandoff(target, handoff, {
136
+ packageRoot = getPackageRoot(),
137
+ taskId = handoff?.taskId,
138
+ } = {}) {
139
+ await validateCanonicalHandoff(target, handoff, { taskId, packageRoot });
140
+ const relativePath = taskHandoffPath(taskId, handoff.handoffId);
141
+ await assertSafePath(target, relativePath);
142
+ if (await fileExists(ensureWithin(target, relativePath))) {
143
+ throw handoffError("E_HANDOFF_INVALID", "An existing handoff cannot be overwritten", [relativePath]);
144
+ }
145
+ const artifact = await writeJsonArtifact(target, relativePath, handoff, "handoff-envelope", packageRoot, { taskId, operation: "handoff-create" });
146
+ await appendProtocolEvent(target, {
147
+ taskId,
148
+ event: "HANDOFF_CREATED",
149
+ fingerprint: artifact.fingerprint,
150
+ details: { handoffId: handoff.handoffId, artifact: relativePath, digest: handoff.artifactDigest },
151
+ }, packageRoot, { taskId });
152
+ return { path: relativePath, fingerprint: artifact.fingerprint, handoff };
153
+ }
154
+
155
+ export async function createCanonicalHandoff(target, options = {}) {
156
+ return withTaskTransaction({ target, taskId: options.taskId, packageRoot: options.packageRoot, operation: "handoff-create", recordCommitEvent: true }, async () => {
157
+ await assertTaskMutationAllowed(target, { taskId: options.taskId, packageRoot: options.packageRoot }).catch((error) => {
158
+ if (error.code !== "E_TASK_COMPLETE") throw error;
159
+ });
160
+ await assertWorkspaceBinding(target, { taskId: options.taskId, packageRoot: options.packageRoot, operation: "handoff-create" });
161
+ const handoff = await buildCanonicalHandoff(target, options);
162
+ return writeCanonicalHandoff(target, handoff, options);
163
+ });
164
+ }
165
+
166
+ export async function readCanonicalHandoff(target, {
167
+ taskId,
168
+ handoffId,
169
+ packageRoot = getPackageRoot(),
170
+ } = {}) {
171
+ const relativePath = taskHandoffPath(taskId, handoffId);
172
+ try {
173
+ const artifact = await readJsonArtifact(target, relativePath, "handoff-envelope", packageRoot);
174
+ await validateCanonicalHandoff(target, artifact.value, { taskId, packageRoot });
175
+ if (artifact.value.handoffId !== handoffId) {
176
+ throw handoffError("E_HANDOFF_INVALID", "Handoff ID does not match its path", [relativePath]);
177
+ }
178
+ return { ...artifact, value: artifact.value };
179
+ } catch (error) {
180
+ if (error.code === "ARTIFACT_MISSING") {
181
+ throw handoffError("E_HANDOFF_NOT_FOUND", `Handoff not found: ${handoffId}`, [relativePath]);
182
+ }
183
+ if (error.code === "E_HANDOFF_TAMPERED" || error.code === "E_HANDOFF_INVALID") throw error;
184
+ throw handoffError("E_HANDOFF_INVALID", error.message, [relativePath]);
185
+ }
186
+ }
187
+
188
+ export async function listCanonicalHandoffs(target, { taskId, packageRoot = getPackageRoot() } = {}) {
189
+ const directory = taskHandoffDirectory(taskId);
190
+ if (!(await fileExists(ensureWithin(target, directory)))) return [];
191
+ const entries = await readdir(ensureWithin(target, directory), { withFileTypes: true });
192
+ const handoffs = [];
193
+ for (const entry of entries) {
194
+ if (!entry.isFile() || !/^handoff-[A-Za-z0-9_-]+\.json$/u.test(entry.name)) continue;
195
+ const handoffId = entry.name.slice(0, -5);
196
+ const artifact = await readCanonicalHandoff(target, { taskId, handoffId, packageRoot });
197
+ handoffs.push(artifact.value);
198
+ }
199
+ return handoffs.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.handoffId.localeCompare(right.handoffId));
200
+ }
201
+
202
+ export async function resolveLatestHandoff(target, { taskId, packageRoot = getPackageRoot() } = {}) {
203
+ const handoffs = await listCanonicalHandoffs(target, { taskId, packageRoot });
204
+ const latest = handoffs.at(-1);
205
+ return latest
206
+ ? { handoffId: latest.handoffId, createdAt: latest.createdAt, digest: latest.artifactDigest }
207
+ : null;
208
+ }
@@ -156,10 +156,10 @@ export function buildInformationGainProjection(events, taskId) {
156
156
  const previousCycle = previousDiagnostic?.details?.verificationCycle ?? null;
157
157
  const surface = [...(failure.surfaces.get(cycle) ?? new Set())].sort();
158
158
  const signatures = [...(failure.signatures.get(cycle) ?? new Set())].sort();
159
- const previousSurface = previousCycle != null
159
+ const previousSurface = previousCycle !== null && previousCycle !== undefined
160
160
  ? [...(failure.surfaces.get(previousCycle) ?? new Set())].sort()
161
161
  : null;
162
- const previousSignatures = previousCycle != null
162
+ const previousSignatures = previousCycle !== null && previousCycle !== undefined
163
163
  ? [...(failure.signatures.get(previousCycle) ?? new Set())].sort()
164
164
  : null;
165
165
  const hasPreviousFailureState = previousSurface !== null;
@@ -257,7 +257,7 @@ export function buildInformationGainProjection(events, taskId) {
257
257
  // retry. The first diagnosis is never stalled. Legacy diagnosis keeps its
258
258
  // own compatibility rule (informationGain === NONE).
259
259
  export function evaluateStructuredDiagnosticStall(gainProjection, { verificationCycle = null } = {}) {
260
- const candidates = verificationCycle == null
260
+ const candidates = verificationCycle === null || verificationCycle === undefined
261
261
  ? (gainProjection ?? [])
262
262
  : (gainProjection ?? []).filter((entry) => entry.verificationCycle === verificationCycle);
263
263
 
@@ -29,6 +29,8 @@ const READ_ONLY_COMMANDS = Object.freeze(new Set([
29
29
  "inspect", "validate-state", "validate-protocol", "validate-receipt",
30
30
  "policy-status", "policy-diff", "rule-verify", "policy",
31
31
  "history", "trace", "reflect",
32
+ "workspace-status", "handoff-list", "handoff-show", "responsibility-status",
33
+ "attestation-verify", "attestation-status", "attestation-verify-range",
32
34
  ]));
33
35
 
34
36
  const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
@@ -37,6 +39,7 @@ const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
37
39
  "record-check", "record-diagnosis", "record-decision-criterion",
38
40
  "record-terminal-result", "complete",
39
41
  "record-intervention", "record-hypothesis-disposition",
42
+ "workspace-bind", "handoff-create", "responsibility-set", "verify-scope", "attestation-create",
40
43
  ]));
41
44
 
42
45
  const STATIC_RISK_CLASSES = Object.freeze({
@@ -134,6 +137,38 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
134
137
  readOnlyMetrics: true,
135
138
  projectLocalReference: true,
136
139
  },
140
+ workspaceBinding: {
141
+ version: 1,
142
+ supported: true,
143
+ optional: true,
144
+ explicitRebinding: false,
145
+ },
146
+ canonicalHandoffs: {
147
+ version: 1,
148
+ supported: true,
149
+ immutable: true,
150
+ lifecycleAuthority: false,
151
+ },
152
+ responsibilityConstraints: {
153
+ version: 1,
154
+ supported: true,
155
+ immutableDuringPass: true,
156
+ completionEnforced: true,
157
+ },
158
+ differentialVerificationScope: {
159
+ version: 1,
160
+ supported: true,
161
+ modes: ["AUTO", "CHANGED", "CLAIMED", "FULL"],
162
+ impactedMode: false,
163
+ },
164
+ codeAttestation: {
165
+ version: 1,
166
+ supported: true,
167
+ modes: ["off", "optional", "required"],
168
+ revisionProviders: ["git"],
169
+ signingProviders: ["none", "sigstore"],
170
+ completionLedgerBound: true,
171
+ },
137
172
  verificationExecutionIsolation: {
138
173
  version: 1,
139
174
  supported: true,
@@ -150,6 +185,11 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
150
185
  { name: "task/ownership", scope: "TASK" },
151
186
  { name: "task/contract", scope: "TASK" },
152
187
  { name: "task/continuity", scope: "TASK" },
188
+ { name: "task/workspace-binding", scope: "TASK" },
189
+ { name: "task/handoffs", scope: "TASK" },
190
+ { name: "task/responsibility", scope: "TASK" },
191
+ { name: "task/verification-scope", scope: "TASK" },
192
+ { name: "task/attestation", scope: "TASK" },
153
193
  { name: "task/actions", scope: "TASK" },
154
194
  { name: "task/action", scope: "TASK" },
155
195
  { name: "task/approvals", scope: "TASK" },