@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
@@ -0,0 +1,179 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact } from "./artifacts.js";
2
+
3
+ export const SCOPED_CHECKER_SCOPE_MODE = "PATH_ARGUMENTS";
4
+ export const SCOPED_CHECKER_PATH_INSERTION = "APPEND";
5
+ export const E_VERIFICATION_SCOPE_UNRESOLVED = "E_VERIFICATION_SCOPE_UNRESOLVED";
6
+
7
+ function capabilityError(message, cause = null) {
8
+ const error = new Error(message);
9
+ error.name = "VerificationScopeCapabilityError";
10
+ error.code = E_VERIFICATION_SCOPE_UNRESOLVED;
11
+ if (cause) error.cause = cause;
12
+ return error;
13
+ }
14
+
15
+ function nonEmptyString(value, label) {
16
+ if (typeof value !== "string" || value.trim() === "") {
17
+ throw capabilityError(`${label} must be a non-empty string`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function argvPrefix(value, label) {
23
+ if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.trim() === "")) {
24
+ throw capabilityError(`${label} must be a non-empty exact argv prefix`);
25
+ }
26
+ return [...value];
27
+ }
28
+
29
+ function assertDescriptorKeys(value, label) {
30
+ const allowed = new Set(["checkId", "scopeMode", "argvPrefix", "pathInsertion"]);
31
+ const unexpected = Object.keys(value).find((key) => !allowed.has(key));
32
+ if (unexpected) throw capabilityError(`${label} contains unknown property: ${unexpected}`);
33
+ }
34
+
35
+ export function normalizeScopedCheckerDescriptor(value, label = "verification.checkers[]") {
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37
+ throw capabilityError(`${label} must be an object`);
38
+ }
39
+ assertDescriptorKeys(value, label);
40
+ const checkId = nonEmptyString(value.checkId, `${label}.checkId`).trim();
41
+ if (value.scopeMode !== SCOPED_CHECKER_SCOPE_MODE) {
42
+ throw capabilityError(`${label}.scopeMode must be ${SCOPED_CHECKER_SCOPE_MODE}`);
43
+ }
44
+ if (value.pathInsertion !== SCOPED_CHECKER_PATH_INSERTION) {
45
+ throw capabilityError(`${label}.pathInsertion must be ${SCOPED_CHECKER_PATH_INSERTION}`);
46
+ }
47
+ return {
48
+ checkId,
49
+ scopeMode: SCOPED_CHECKER_SCOPE_MODE,
50
+ argvPrefix: argvPrefix(value.argvPrefix, `${label}.argvPrefix`),
51
+ pathInsertion: SCOPED_CHECKER_PATH_INSERTION,
52
+ };
53
+ }
54
+
55
+ export function normalizeScopedCheckerDescriptors(value, label = "verification.checkers") {
56
+ if (!Array.isArray(value)) throw capabilityError(`${label} must be an array`);
57
+ const descriptors = value.map((item, index) => normalizeScopedCheckerDescriptor(item, `${label}[${index}]`));
58
+ const seen = new Set();
59
+ for (const descriptor of descriptors) {
60
+ if (seen.has(descriptor.checkId)) throw capabilityError(`${label} contains duplicate checkId: ${descriptor.checkId}`);
61
+ seen.add(descriptor.checkId);
62
+ }
63
+ return descriptors.sort((left, right) => left.checkId.localeCompare(right.checkId));
64
+ }
65
+
66
+ export function normalizeVerificationConfiguration(value, label = "verification") {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
68
+ throw capabilityError(`${label} must be an object`);
69
+ }
70
+ const unexpected = Object.keys(value).find((key) => key !== "checkers");
71
+ if (unexpected) throw capabilityError(`${label} contains unknown property: ${unexpected}`);
72
+ return {
73
+ checkers: normalizeScopedCheckerDescriptors(value.checkers ?? [], `${label}.checkers`),
74
+ };
75
+ }
76
+
77
+ export function scopedCheckerCapabilitiesFingerprint(checkers = []) {
78
+ return canonicalFingerprint({
79
+ schemaVersion: 1,
80
+ scopeMode: SCOPED_CHECKER_SCOPE_MODE,
81
+ checkers: normalizeScopedCheckerDescriptors(checkers),
82
+ });
83
+ }
84
+
85
+ export async function readScopedCheckerCapabilities(target, packageRoot) {
86
+ try {
87
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot);
88
+ const verification = artifact.value.verification;
89
+ const checkers = verification
90
+ ? normalizeVerificationConfiguration(verification).checkers
91
+ : [];
92
+ return {
93
+ valid: true,
94
+ source: "project-config",
95
+ checkers,
96
+ fingerprint: scopedCheckerCapabilitiesFingerprint(checkers),
97
+ error: null,
98
+ };
99
+ } catch (error) {
100
+ if (error.code === "ARTIFACT_MISSING") {
101
+ return {
102
+ valid: true,
103
+ source: "project-config",
104
+ checkers: [],
105
+ fingerprint: scopedCheckerCapabilitiesFingerprint([]),
106
+ error: null,
107
+ };
108
+ }
109
+ return {
110
+ valid: false,
111
+ source: "project-config",
112
+ checkers: [],
113
+ fingerprint: null,
114
+ error,
115
+ };
116
+ }
117
+ }
118
+
119
+ function equalArgv(left, right) {
120
+ return Array.isArray(left)
121
+ && Array.isArray(right)
122
+ && left.length === right.length
123
+ && left.every((item, index) => item === right[index]);
124
+ }
125
+
126
+ export function scopedCheckerArgv(checker, selectedPaths) {
127
+ const descriptor = normalizeScopedCheckerDescriptor(checker, "scoped checker");
128
+ if (!Array.isArray(selectedPaths) || selectedPaths.some((item) => typeof item !== "string" || item.length === 0)) {
129
+ throw capabilityError("Verification scope selectedPaths must be an array of non-empty strings");
130
+ }
131
+ return [...descriptor.argvPrefix, ...selectedPaths];
132
+ }
133
+
134
+ export async function bindVerificationScopeCommand({
135
+ target,
136
+ packageRoot,
137
+ checkId,
138
+ argv,
139
+ scope,
140
+ capabilities = null,
141
+ } = {}) {
142
+ if (!scope || scope.resolvedMode === "FULL") {
143
+ return {
144
+ argv: [...argv],
145
+ checker: null,
146
+ capabilityFingerprint: null,
147
+ };
148
+ }
149
+ if (!["CHANGED", "CLAIMED"].includes(scope.resolvedMode)) {
150
+ throw capabilityError("Verification scope has no resolved narrow mode");
151
+ }
152
+ const resolvedCapabilities = capabilities ?? await readScopedCheckerCapabilities(target, packageRoot);
153
+ if (!resolvedCapabilities.valid) {
154
+ throw capabilityError("Trusted scoped-checker capability is invalid", resolvedCapabilities.error);
155
+ }
156
+ const checker = resolvedCapabilities.checkers.find((item) => item.checkId === checkId);
157
+ if (!checker) {
158
+ throw capabilityError(`No trusted scoped checker is registered for check ${checkId}`);
159
+ }
160
+ const expectedArgv = scopedCheckerArgv(checker, scope.selectedPaths);
161
+ if (!equalArgv(argv, expectedArgv)) {
162
+ const error = capabilityError("Scoped checker argv does not match the canonical scope binding");
163
+ error.reason = "ARGV_MISMATCH";
164
+ throw error;
165
+ }
166
+ return {
167
+ argv: expectedArgv,
168
+ checker,
169
+ capabilityFingerprint: resolvedCapabilities.fingerprint,
170
+ };
171
+ }
172
+
173
+ export async function requireTrustedScopedChecker({ target, packageRoot } = {}) {
174
+ const capabilities = await readScopedCheckerCapabilities(target, packageRoot);
175
+ if (!capabilities.valid || capabilities.checkers.length === 0) {
176
+ throw capabilityError("A trusted scoped-checker capability is required for a narrow verification scope", capabilities.error);
177
+ }
178
+ return capabilities;
179
+ }
@@ -0,0 +1,271 @@
1
+ import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
+ import { assertSchema, readSchema } from "./schema-validation.js";
3
+ import { getPackageRoot } from "./templates.js";
4
+ import { currentRepositoryFingerprint } from "./repository.js";
5
+ import { resolveRevisionProvider } from "./revision/provider.js";
6
+ import { REVISION_PROVIDERS } from "./revision/registry.js";
7
+ import { readScopedCheckerCapabilities } from "./verification-scope-capability.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 { taskVerificationScopePath } from "./task-paths.js";
13
+ import { ensureWithin, fileExists } from "./filesystem.js";
14
+ import { appendProtocolEvent } from "./events.js";
15
+ import { withTaskTransaction } from "./transaction.js";
16
+ import { assertTaskMutationAllowed } from "./task-claim-state.js";
17
+ import { assertWorkspaceBinding } from "./workspace-binding.js";
18
+ import path from "node:path";
19
+
20
+ const MODES = Object.freeze(["AUTO", "CHANGED", "CLAIMED", "FULL"]);
21
+
22
+ function scopeError(code, message, artifacts = []) {
23
+ const error = new Error(message);
24
+ error.name = "VerificationScopeError";
25
+ error.code = code;
26
+ error.artifacts = artifacts;
27
+ return error;
28
+ }
29
+
30
+ function normalizeScopePath(value, label = "path") {
31
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
32
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be a non-empty relative path`);
33
+ }
34
+ const replaced = value.replaceAll("\\", "/");
35
+ if (replaced.startsWith("/") || /^[A-Za-z]:\//u.test(replaced)) {
36
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be relative: ${value}`);
37
+ }
38
+ const normalized = path.posix.normalize(replaced);
39
+ const canonical = normalized.replace(/^\.\//u, "");
40
+ if (canonical === ".." || canonical.startsWith("../") || canonical === ".forgeloop" || canonical.startsWith(".forgeloop/")) {
41
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} is outside the source boundary: ${value}`);
42
+ }
43
+ if (replaced !== canonical && replaced !== `./${canonical}`) {
44
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must use a canonical relative path: ${value}`);
45
+ }
46
+ return canonical;
47
+ }
48
+
49
+ function sortedPaths(paths, label = "paths") {
50
+ if (!Array.isArray(paths)) throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be an array`);
51
+ return [...new Set(paths.map((value, index) => normalizeScopePath(value, `${label}[${index}]`)))].sort();
52
+ }
53
+
54
+ function pathCovered(pathValue, claims) {
55
+ return claims.includes(".") || claims.some((claim) => pathValue === claim || pathValue.startsWith(`${claim}/`));
56
+ }
57
+
58
+ function requirementText(requirement) {
59
+ return typeof requirement === "string" ? requirement : requirement?.text ?? "";
60
+ }
61
+
62
+ function requiresFullVerification(contract) {
63
+ return [...(contract?.value?.verification ?? []), ...(contract?.value?.successCriteria ?? [])]
64
+ .some((requirement) => /(?:full|entire|whole|global)\s+(?:project|repository|repo|suite|verification)|all\s+(?:tests|checks)/iu.test(requirementText(requirement)));
65
+ }
66
+
67
+ async function currentChangedPathsFromRevisionProvider(target) {
68
+ try {
69
+ const provider = await resolveRevisionProvider({ target, registry: REVISION_PROVIDERS });
70
+ const entries = await provider.getChangedEntries({ target, headRevision: "WORKTREE" });
71
+ return [...new Set(entries
72
+ .flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean)))].sort((left, right) => left.localeCompare(right));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function currentInputs(target, { taskId, packageRoot }) {
79
+ const [contract, route, state, claims, repositoryFingerprint, changedPaths, checkerCapabilities] = await Promise.all([
80
+ readContract(target, packageRoot, { taskId }),
81
+ readPersistedRoute(target, packageRoot, { taskId }),
82
+ readWorkState(target, { packageRoot, taskId }),
83
+ resolveTaskClaimState(target, { packageRoot, taskId }),
84
+ currentRepositoryFingerprint(target),
85
+ currentChangedPathsFromRevisionProvider(target),
86
+ readScopedCheckerCapabilities(target, packageRoot),
87
+ ]);
88
+ return {
89
+ contract,
90
+ route,
91
+ state,
92
+ claims,
93
+ claimsFingerprint: canonicalFingerprint(claims.effectiveWriteClaims ?? []),
94
+ repositoryFingerprint,
95
+ changedPaths: changedPaths === null ? null : sortedPaths(changedPaths),
96
+ checkerCapabilities,
97
+ };
98
+ }
99
+
100
+ function normalizeMode(value) {
101
+ const mode = String(value ?? "AUTO").toUpperCase();
102
+ if (!MODES.includes(mode)) throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Unsupported verification scope mode: ${value}`);
103
+ return mode;
104
+ }
105
+
106
+ export async function validateVerificationScope(scope, packageRoot = getPackageRoot()) {
107
+ try {
108
+ assertSchema(scope, await readSchema("verification-scope", packageRoot), "verification scope");
109
+ } catch (error) {
110
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message);
111
+ }
112
+ if (scope.taskId.length === 0 || scope.requestedMode === "IMPACTED" || scope.resolvedMode === "IMPACTED") {
113
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", "IMPACTED is reserved for an explicit future mapping capability");
114
+ }
115
+ const canonical = {
116
+ ...scope,
117
+ changedPaths: sortedPaths(scope.changedPaths),
118
+ claimedPaths: sortedPaths(scope.claimedPaths),
119
+ selectedPaths: sortedPaths(scope.selectedPaths),
120
+ reasons: [...scope.reasons],
121
+ };
122
+ return canonical;
123
+ }
124
+
125
+ export async function readVerificationScope(target, { taskId, packageRoot = getPackageRoot(), scopePath = null } = {}) {
126
+ const relativePath = scopePath ?? taskVerificationScopePath(taskId);
127
+ if (!(await fileExists(ensureWithin(target, relativePath)))) {
128
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Verification scope is missing: ${relativePath}`, [relativePath]);
129
+ }
130
+ try {
131
+ const artifact = await readJsonArtifact(target, relativePath, "verification-scope", packageRoot);
132
+ return { ...artifact, value: await validateVerificationScope(artifact.value, packageRoot) };
133
+ } catch (error) {
134
+ if (error.code === "E_VERIFICATION_SCOPE_INVALID") throw error;
135
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message, [relativePath]);
136
+ }
137
+ }
138
+
139
+ export async function validateVerificationScopeFreshness(target, {
140
+ taskId,
141
+ scope,
142
+ packageRoot = getPackageRoot(),
143
+ } = {}) {
144
+ const current = await currentInputs(target, { taskId, packageRoot });
145
+ const mismatches = [];
146
+ if (scope.taskId !== taskId) mismatches.push("taskId");
147
+ if (scope.verificationCycle !== (current.state?.verificationCycle ?? 1)) mismatches.push("verificationCycle");
148
+ if (scope.contractFingerprint !== current.contract.fingerprint) mismatches.push("contractFingerprint");
149
+ if (JSON.stringify(scope.repositoryFingerprint) !== JSON.stringify(current.repositoryFingerprint)) mismatches.push("repositoryFingerprint");
150
+ if (JSON.stringify(sortedPaths(scope.changedPaths)) !== JSON.stringify(current.changedPaths)) mismatches.push("changedPaths");
151
+ if (scope.claimsFingerprint && scope.claimsFingerprint !== current.claimsFingerprint) mismatches.push("claimsFingerprint");
152
+ if (JSON.stringify(sortedPaths(scope.claimedPaths)) !== JSON.stringify(sortedPaths(current.claims.effectiveWriteClaims ?? []))) mismatches.push("claimedPaths");
153
+ if (scope.checkerCapabilityFingerprint !== undefined
154
+ && scope.checkerCapabilityFingerprint !== current.checkerCapabilities.fingerprint) mismatches.push("checkerCapabilityFingerprint");
155
+ if (mismatches.length > 0) {
156
+ throw scopeError("E_VERIFICATION_SCOPE_STALE", `Verification scope is stale; changed bindings: ${mismatches.join(", ")}`, [taskVerificationScopePath(taskId)]);
157
+ }
158
+ return { fresh: true, current };
159
+ }
160
+
161
+ export async function resolveVerificationScope(target, {
162
+ taskId,
163
+ mode = "AUTO",
164
+ packageRoot = getPackageRoot(),
165
+ createdAt = new Date().toISOString(),
166
+ } = {}) {
167
+ const requestedMode = normalizeMode(mode);
168
+ const inputs = await currentInputs(target, { taskId, packageRoot });
169
+ if (!inputs.state) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "Work state is required to resolve verification scope");
170
+ const changedPaths = inputs.changedPaths;
171
+ const claimedPaths = sortedPaths(inputs.claims.effectiveWriteClaims ?? []);
172
+ const hasTrustedScopedChecker = inputs.checkerCapabilities.valid && inputs.checkerCapabilities.checkers.length > 0;
173
+ const reasons = [];
174
+ let resolvedMode = requestedMode;
175
+ let selectedPaths = [];
176
+ let fallback = null;
177
+
178
+ const changedIsSafe = changedPaths !== null
179
+ && inputs.claims.valid
180
+ && changedPaths.every((item) => pathCovered(item, claimedPaths));
181
+ if (requiresFullVerification(inputs.contract)) {
182
+ if (requestedMode !== "FULL") reasons.push("The current contract explicitly requires full-project verification.");
183
+ resolvedMode = "FULL";
184
+ selectedPaths = [];
185
+ } else if (requestedMode === "FULL") {
186
+ resolvedMode = "FULL";
187
+ reasons.push("Full verification was explicitly requested.");
188
+ } else if (requestedMode === "CHANGED") {
189
+ if (!hasTrustedScopedChecker) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope requires a trusted scoped-checker capability");
190
+ if (changedPaths === null) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope cannot be proved because repository changes are unavailable");
191
+ if (!inputs.claims.valid || !changedIsSafe) {
192
+ throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope cannot be used because every changed path is not inside valid effective task claims");
193
+ }
194
+ resolvedMode = "CHANGED";
195
+ selectedPaths = changedPaths;
196
+ reasons.push("Selected exact canonical changed paths inside effective task claims.");
197
+ } else if (requestedMode === "CLAIMED") {
198
+ if (!hasTrustedScopedChecker) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CLAIMED scope requires a trusted scoped-checker capability");
199
+ if (!inputs.claims.valid || claimedPaths.length === 0) {
200
+ throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CLAIMED scope cannot be proved because effective task claims are unavailable");
201
+ }
202
+ resolvedMode = "CLAIMED";
203
+ selectedPaths = claimedPaths;
204
+ reasons.push("Selected the canonical effective task claims.");
205
+ } else if (hasTrustedScopedChecker && changedIsSafe) {
206
+ resolvedMode = "CHANGED";
207
+ selectedPaths = changedPaths;
208
+ reasons.push("AUTO selected exact changed paths because a trusted scoped checker exists and all paths are inside valid effective task claims.");
209
+ } else if (hasTrustedScopedChecker && inputs.claims.valid && claimedPaths.length > 0 && changedPaths !== null) {
210
+ resolvedMode = "CLAIMED";
211
+ selectedPaths = claimedPaths;
212
+ fallback = { from: "CHANGED", to: "CLAIMED", reason: "Changed paths could not be safely narrowed to the effective claims." };
213
+ reasons.push("AUTO escalated to effective claims because a narrower changed-path boundary was not provable.");
214
+ } else {
215
+ resolvedMode = "FULL";
216
+ selectedPaths = [];
217
+ fallback = {
218
+ from: "CHANGED",
219
+ to: "FULL",
220
+ reason: hasTrustedScopedChecker
221
+ ? "Repository changes or claim ownership could not be proved."
222
+ : "No trusted scoped-checker capability was established.",
223
+ };
224
+ reasons.push(hasTrustedScopedChecker
225
+ ? "AUTO escalated to full verification because a narrow boundary was not provable."
226
+ : "AUTO selected full verification because no trusted scoped-checker capability was established.");
227
+ }
228
+
229
+ const scope = await validateVerificationScope({
230
+ schemaVersion: 1,
231
+ protocolVersion: 1,
232
+ taskId,
233
+ requestedMode,
234
+ resolvedMode,
235
+ verificationCycle: inputs.state.verificationCycle ?? 1,
236
+ changedPaths: changedPaths ?? [],
237
+ claimedPaths,
238
+ selectedPaths,
239
+ reasons,
240
+ fallback,
241
+ contractFingerprint: inputs.contract.fingerprint,
242
+ repositoryFingerprint: inputs.repositoryFingerprint,
243
+ claimsFingerprint: inputs.claimsFingerprint,
244
+ ...(inputs.checkerCapabilities.fingerprint
245
+ ? { checkerCapabilityFingerprint: inputs.checkerCapabilities.fingerprint }
246
+ : {}),
247
+ createdAt,
248
+ }, packageRoot);
249
+ return { scope, inputs };
250
+ }
251
+
252
+ export async function persistVerificationScope(target, scope, { taskId, packageRoot = getPackageRoot() } = {}) {
253
+ const relativePath = taskVerificationScopePath(taskId);
254
+ const artifact = await writeJsonArtifact(target, relativePath, scope, "verification-scope", packageRoot, { taskId, operation: "verify-scope" });
255
+ await appendProtocolEvent(target, {
256
+ taskId,
257
+ event: "VERIFICATION_SCOPE_CAPTURED",
258
+ fingerprint: artifact.fingerprint,
259
+ details: { scopeFingerprint: artifact.fingerprint, resolvedMode: scope.resolvedMode, verificationCycle: scope.verificationCycle },
260
+ }, packageRoot, { taskId });
261
+ return { path: relativePath, fingerprint: artifact.fingerprint, scope };
262
+ }
263
+
264
+ export async function captureVerificationScope(target, options = {}) {
265
+ return withTaskTransaction({ target, taskId: options.taskId, packageRoot: options.packageRoot, operation: "verify-scope", recordCommitEvent: true }, async () => {
266
+ await assertTaskMutationAllowed(target, { taskId: options.taskId, packageRoot: options.packageRoot });
267
+ await assertWorkspaceBinding(target, { taskId: options.taskId, packageRoot: options.packageRoot, operation: "verify-scope" });
268
+ const result = await resolveVerificationScope(target, options);
269
+ return persistVerificationScope(target, result.scope, options);
270
+ });
271
+ }
@@ -0,0 +1,211 @@
1
+ import path from "node:path";
2
+ import { realpath } from "node:fs/promises";
3
+
4
+ import { appendProtocolEvent } from "./events.js";
5
+ import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
6
+ import { assertSchema, readSchema } from "./schema-validation.js";
7
+ import { assertSecretFree } from "./receipt.js";
8
+ import { getPackageRoot } from "./templates.js";
9
+ import {
10
+ taskWorkspaceBindingPath,
11
+ } from "./task-paths.js";
12
+ import {
13
+ currentRepositoryFingerprint,
14
+ repositoryWorktreeMetadata,
15
+ } from "./repository.js";
16
+ import { withTaskTransaction } from "./transaction.js";
17
+ import { assertTaskMutationAllowed } from "./task-claim-state.js";
18
+
19
+ const SHA256 = /^[a-f0-9]{64}$/u;
20
+
21
+ function bindingError(code, message, artifacts = []) {
22
+ const error = new Error(message);
23
+ error.name = "WorkspaceBindingError";
24
+ error.code = code;
25
+ if (artifacts.length > 0) error.artifacts = artifacts;
26
+ return error;
27
+ }
28
+
29
+ function normalizeIdentityPath(value) {
30
+ const normalized = path.normalize(value).replaceAll("\\", "/");
31
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
32
+ }
33
+
34
+ async function absoluteGitPath(target, value) {
35
+ const candidate = path.isAbsolute(value) ? value : path.resolve(target, value);
36
+ try {
37
+ return normalizeIdentityPath(await realpath(candidate));
38
+ } catch {
39
+ return normalizeIdentityPath(candidate);
40
+ }
41
+ }
42
+
43
+ export async function captureWorkspaceIdentity(target) {
44
+ let metadata;
45
+ try {
46
+ metadata = await repositoryWorktreeMetadata(target);
47
+ } catch (error) {
48
+ throw bindingError(
49
+ "E_WORKSPACE_IDENTITY_UNAVAILABLE",
50
+ "The current directory is not a Git worktree whose identity ForgeLoop can prove",
51
+ error?.artifacts ?? [],
52
+ );
53
+ }
54
+
55
+ const commonDirectory = await absoluteGitPath(target, metadata.commonDirectory);
56
+ const gitDirectory = await absoluteGitPath(target, metadata.gitDirectory);
57
+ const topLevel = await absoluteGitPath(target, metadata.topLevel);
58
+ const repositoryIdentity = canonicalFingerprint({
59
+ commonDirectory,
60
+ objectFormat: metadata.objectFormat,
61
+ });
62
+ const workspaceIdentity = canonicalFingerprint({
63
+ commonDirectory,
64
+ gitDirectory,
65
+ topLevel,
66
+ objectFormat: metadata.objectFormat,
67
+ });
68
+ const repositoryFingerprint = await currentRepositoryFingerprint(target);
69
+ return {
70
+ repositoryIdentity,
71
+ workspaceIdentity,
72
+ branchAtBind: repositoryFingerprint.branch,
73
+ headAtBind: repositoryFingerprint.head,
74
+ metadata: { objectFormat: metadata.objectFormat },
75
+ };
76
+ }
77
+
78
+ export async function validateWorkspaceBinding(binding, packageRoot = getPackageRoot()) {
79
+ try {
80
+ const schema = await readSchema("workspace-binding", packageRoot);
81
+ assertSchema(binding, schema, "workspace binding");
82
+ } catch (error) {
83
+ throw bindingError("E_WORKSPACE_BINDING_INVALID", error.message);
84
+ }
85
+ if (!SHA256.test(binding.repositoryIdentity) || !SHA256.test(binding.workspaceIdentity)) {
86
+ throw bindingError("E_WORKSPACE_BINDING_INVALID", "Workspace binding identities must be lowercase SHA-256 values");
87
+ }
88
+ assertSecretFree(binding);
89
+ return binding;
90
+ }
91
+
92
+ export async function readWorkspaceBinding(target, { taskId, packageRoot = getPackageRoot() } = {}) {
93
+ if (!taskId) throw bindingError("E_WORKSPACE_BINDING_INVALID", "taskId is required to read a workspace binding");
94
+ const relativePath = taskWorkspaceBindingPath(taskId);
95
+ try {
96
+ const artifact = await readJsonArtifact(target, relativePath, "workspace-binding", packageRoot);
97
+ await validateWorkspaceBinding(artifact.value, packageRoot);
98
+ if (artifact.value.taskId !== taskId) {
99
+ throw bindingError("E_WORKSPACE_BINDING_INVALID", "Workspace binding taskId does not match its task namespace", [relativePath]);
100
+ }
101
+ return { ...artifact, value: artifact.value };
102
+ } catch (error) {
103
+ if (error.code === "ARTIFACT_MISSING") return null;
104
+ if (error.code === "E_WORKSPACE_BINDING_INVALID") throw error;
105
+ throw bindingError("E_WORKSPACE_BINDING_INVALID", `Workspace binding is invalid: ${error.message}`, [relativePath]);
106
+ }
107
+ }
108
+
109
+ export async function resolveWorkspaceBindingStatus(target, { taskId, packageRoot = getPackageRoot() } = {}) {
110
+ const relativePath = taskWorkspaceBindingPath(taskId);
111
+ let artifact;
112
+ try {
113
+ artifact = await readWorkspaceBinding(target, { taskId, packageRoot });
114
+ } catch (error) {
115
+ return {
116
+ status: "INVALID",
117
+ taskId,
118
+ path: relativePath,
119
+ binding: null,
120
+ current: null,
121
+ error: { code: error.code ?? "E_WORKSPACE_BINDING_INVALID", message: error.message },
122
+ };
123
+ }
124
+ if (!artifact) return { status: "UNBOUND", taskId, path: relativePath, binding: null, current: null };
125
+
126
+ let current;
127
+ try {
128
+ current = await captureWorkspaceIdentity(target);
129
+ } catch (error) {
130
+ return {
131
+ status: "UNAVAILABLE",
132
+ taskId,
133
+ path: relativePath,
134
+ binding: artifact.value,
135
+ bindingFingerprint: artifact.fingerprint,
136
+ current: null,
137
+ error: { code: error.code ?? "E_WORKSPACE_IDENTITY_UNAVAILABLE", message: error.message },
138
+ };
139
+ }
140
+ const matches = artifact.value.repositoryIdentity === current.repositoryIdentity
141
+ && artifact.value.workspaceIdentity === current.workspaceIdentity;
142
+ return {
143
+ status: matches ? "MATCH" : "MISMATCH",
144
+ taskId,
145
+ path: relativePath,
146
+ binding: artifact.value,
147
+ bindingFingerprint: artifact.fingerprint,
148
+ current,
149
+ };
150
+ }
151
+
152
+ export async function assertWorkspaceBinding(target, { taskId, packageRoot = getPackageRoot(), operation = "mutation" } = {}) {
153
+ const result = await resolveWorkspaceBindingStatus(target, { taskId, packageRoot });
154
+ if (result.status === "UNBOUND" || result.status === "MATCH") return result;
155
+ const code = result.status === "MISMATCH"
156
+ ? "E_WORKSPACE_BINDING_MISMATCH"
157
+ : result.status === "UNAVAILABLE"
158
+ ? "E_WORKSPACE_IDENTITY_UNAVAILABLE"
159
+ : "E_WORKSPACE_BINDING_INVALID";
160
+ const message = result.status === "MISMATCH"
161
+ ? `Workspace binding does not match the current Git worktree before ${operation}`
162
+ : result.status === "UNAVAILABLE"
163
+ ? `Current Git worktree identity is unavailable before ${operation}`
164
+ : `Workspace binding is invalid before ${operation}`;
165
+ throw bindingError(code, message, [result.path]);
166
+ }
167
+
168
+ export async function bindTaskWorkspace(target, { taskId, packageRoot = getPackageRoot(), now = new Date().toISOString() } = {}) {
169
+ if (!taskId) throw bindingError("E_WORKSPACE_BINDING_INVALID", "workspace-bind requires an explicit taskId");
170
+ const relativePath = taskWorkspaceBindingPath(taskId);
171
+ return withTaskTransaction({ target, taskId, packageRoot, operation: "workspace-bind", recordCommitEvent: true }, async () => {
172
+ await assertTaskMutationAllowed(target, { taskId, packageRoot });
173
+ const existing = await readWorkspaceBinding(target, { taskId, packageRoot });
174
+ const current = await captureWorkspaceIdentity(target);
175
+ if (existing) {
176
+ if (existing.value.repositoryIdentity === current.repositoryIdentity
177
+ && existing.value.workspaceIdentity === current.workspaceIdentity) {
178
+ return {
179
+ taskId,
180
+ status: "MATCH",
181
+ bound: false,
182
+ alreadyBound: true,
183
+ path: relativePath,
184
+ binding: existing.value,
185
+ fingerprint: existing.fingerprint,
186
+ };
187
+ }
188
+ throw bindingError("E_WORKSPACE_BINDING_MISMATCH", "Task is already bound to a different Git worktree; unrestricted rebinding is not supported", [relativePath]);
189
+ }
190
+ const binding = await validateWorkspaceBinding({
191
+ schemaVersion: 1,
192
+ protocolVersion: 1,
193
+ taskId,
194
+ mode: "GIT_WORKTREE",
195
+ repositoryIdentity: current.repositoryIdentity,
196
+ workspaceIdentity: current.workspaceIdentity,
197
+ branchAtBind: current.branchAtBind,
198
+ headAtBind: current.headAtBind,
199
+ boundAt: now,
200
+ metadata: current.metadata,
201
+ }, packageRoot);
202
+ const artifact = await writeJsonArtifact(target, relativePath, binding, "workspace-binding", packageRoot, { taskId, operation: "workspace-bind" });
203
+ await appendProtocolEvent(target, {
204
+ taskId,
205
+ event: "WORKSPACE_BOUND",
206
+ fingerprint: artifact.fingerprint,
207
+ details: { workspaceFingerprint: artifact.fingerprint },
208
+ }, packageRoot, { taskId });
209
+ return { taskId, status: "MATCH", bound: true, alreadyBound: false, path: relativePath, binding, fingerprint: artifact.fingerprint };
210
+ });
211
+ }