@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.
- package/DOCS_INDEX.md +26 -4
- package/EXECUTION_STATE.md +7 -0
- package/LOOP_ENGINEERING.md +58 -0
- package/LOOP_SYSTEM_DESIGN.md +54 -0
- package/ORCHESTRATOR_INTEGRATION.md +23 -0
- package/PROTOCOL_INTEGRATION.md +36 -0
- package/QUALITY_SCORECARD.md +21 -0
- package/README.md +57 -2
- package/TERMINOLOGY.md +11 -0
- package/THREAT_MODEL.md +7 -0
- package/completions/_forgeloop +85 -0
- package/completions/forgeloop.bash +173 -0
- package/completions/forgeloop.fish +544 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
- package/docs/ARTIFACT_REFERENCE.md +226 -0
- package/docs/CLI_REFERENCE.md +308 -2
- package/docs/CODE_ATTESTATION.md +141 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
- package/docs/DOCUMENTATION_GUIDE.md +13 -11
- package/docs/GETTING_STARTED.md +98 -1
- package/docs/MCP.md +8 -0
- package/docs/PLATFORM_ADAPTERS.md +69 -0
- package/docs/RECIPES.md +138 -0
- package/docs/RELEASE_CHECKLIST.md +54 -0
- package/docs/REVISION_PROVIDERS.md +136 -0
- package/docs/SIGNING_PROVIDERS.md +83 -0
- package/docs/TROUBLESHOOTING.md +184 -0
- package/docs/UNIVERSAL_INTEGRATION.md +11 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
- package/docs/diagrams/README.md +30 -22
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
- package/integrations/generic-ci/verify.sh +14 -0
- package/package.json +31 -9
- package/schemas/attestation-verification-result.schema.json +34 -0
- package/schemas/code-attestation.schema.json +61 -0
- package/schemas/code-manifest.schema.json +57 -0
- package/schemas/config.schema.json +57 -1
- package/schemas/handoff-envelope.schema.json +57 -0
- package/schemas/in-toto-statement.schema.json +32 -0
- package/schemas/responsibility.schema.json +37 -0
- package/schemas/verification-scope.schema.json +26 -0
- package/schemas/workspace-binding.schema.json +20 -0
- package/scripts/CI_VALIDATORS.md +3 -0
- package/scripts/benchmark-cli-startup.mjs +60 -0
- package/scripts/check-changelog-freshness.mjs +88 -0
- package/scripts/check-critical-coverage.mjs +58 -0
- package/scripts/generate-agent-protocol-summary.mjs +141 -0
- package/scripts/generate-shell-completions.mjs +132 -0
- package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
- package/src/cli.js +114 -6
- package/src/commands/attestation-create.js +30 -0
- package/src/commands/attestation-status.js +12 -0
- package/src/commands/attestation-verify-range.js +32 -0
- package/src/commands/attestation-verify.js +23 -0
- package/src/commands/handoff-create.js +9 -0
- package/src/commands/handoff-list.js +12 -0
- package/src/commands/handoff-show.js +10 -0
- package/src/commands/responsibility-set.js +20 -0
- package/src/commands/responsibility-status.js +12 -0
- package/src/commands/run-check.js +43 -3
- package/src/commands/task-repair-legacy-recovery.js +1 -2
- package/src/commands/verify-scope.js +9 -0
- package/src/commands/workspace-bind.js +17 -0
- package/src/commands/workspace-status.js +19 -0
- package/src/core/artifact-registry.js +84 -0
- package/src/core/attestation-coverage.js +134 -0
- package/src/core/attestation-verifier.js +227 -0
- package/src/core/attestation.js +263 -0
- package/src/core/audit.js +67 -1
- package/src/core/bundles.js +210 -9
- package/src/core/cli-command-definitions.js +196 -0
- package/src/core/code-manifest.js +293 -0
- package/src/core/command-executors.js +79 -0
- package/src/core/command-input.js +69 -18
- package/src/core/command-runtime.js +2 -1
- package/src/core/completion.js +170 -8
- package/src/core/config.js +60 -2
- package/src/core/continuity-cli-options.js +9 -3
- package/src/core/continuity-reconciliation.js +20 -0
- package/src/core/diagnostic-projection.js +3 -1
- package/src/core/error-codes.js +146 -0
- package/src/core/events.js +109 -13
- package/src/core/exit-codes.js +22 -0
- package/src/core/handoff.js +208 -0
- package/src/core/information-gain-projection.js +3 -3
- package/src/core/integration-invocation-policy.js +40 -0
- package/src/core/integration-resources.js +54 -0
- package/src/core/next-action-phases.js +968 -0
- package/src/core/next-action.js +32 -951
- package/src/core/phase.js +29 -1
- package/src/core/protocol-info.js +41 -1
- package/src/core/repository.js +256 -11
- package/src/core/responsibility.js +265 -0
- package/src/core/revision/git.js +207 -0
- package/src/core/revision/provider.js +90 -0
- package/src/core/revision/registry.js +5 -0
- package/src/core/route-artifact.js +1 -1
- package/src/core/schema-validation.js +8 -0
- package/src/core/signing/none.js +23 -0
- package/src/core/signing/provider.js +21 -0
- package/src/core/signing/registry.js +7 -0
- package/src/core/signing/sigstore.js +158 -0
- package/src/core/task-command.js +9 -1
- package/src/core/task-paths.js +72 -0
- package/src/core/templates.js +8 -0
- package/src/core/verification-scope-capability.js +179 -0
- package/src/core/verification-scope.js +271 -0
- package/src/core/workspace-binding.js +211 -0
- package/src/integration.d.ts +91 -0
- package/AGENT_COMPATIBILITY.md +0 -11
- package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
- package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
package/src/core/error-codes.js
CHANGED
|
@@ -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,
|
package/src/core/events.js
CHANGED
|
@@ -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
|
|
159
|
+
const previousSurface = previousCycle !== null && previousCycle !== undefined
|
|
160
160
|
? [...(failure.surfaces.get(previousCycle) ?? new Set())].sort()
|
|
161
161
|
: null;
|
|
162
|
-
const previousSignatures = previousCycle
|
|
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
|
|
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" },
|