@cassiomc1/forgeloop 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +90 -0
  5. package/DOCS_INDEX.md +13 -11
  6. package/ENG/c-development-eng.md +112 -0
  7. package/ENG/cpp-development-eng.md +109 -0
  8. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  9. package/ENG/flutter-development-eng.md +2106 -0
  10. package/ENG/go-development-eng.md +103 -0
  11. package/ENG/java-development-eng.md +125 -0
  12. package/ENG/nodejs-backend-development-eng.md +605 -0
  13. package/ENG/php-development-eng.md +104 -0
  14. package/ENG/rust-development-eng.md +422 -0
  15. package/ENG/sql-development-eng.md +108 -0
  16. package/ENG/swift-development-eng.md +111 -0
  17. package/ENG/typescript-development-eng.md +108 -0
  18. package/GUIDE_ROUTER.md +455 -4
  19. package/LOOP_SYSTEM_DESIGN.md +10 -6
  20. package/QUALITY_SCORECARD.md +2 -0
  21. package/README.md +50 -38
  22. package/THIRD_PARTY_NOTICES.md +19 -7
  23. package/completions/_forgeloop +3 -3
  24. package/completions/forgeloop.bash +3 -3
  25. package/completions/forgeloop.fish +7 -0
  26. package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
  27. package/docs/CLI_REFERENCE.md +28 -6
  28. package/docs/DOCUMENTATION_GUIDE.md +2 -1
  29. package/docs/GETTING_STARTED.md +59 -0
  30. package/docs/MCP.md +1 -1
  31. package/docs/PACKAGE_CONTENTS.md +29 -8
  32. package/docs/RECIPES.md +23 -0
  33. package/docs/RELEASE_CHECKLIST.md +51 -9
  34. package/docs/TROUBLESHOOTING.md +100 -2
  35. package/docs/assets/diagrams/forgeloop-engineering-flow.html +19 -6
  36. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  37. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5 -5
  38. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +12 -2
  39. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
  40. package/docs/documentation-manifest.json +652 -0
  41. package/docs/protocol-requirements.json +77 -0
  42. package/package.json +19 -4
  43. package/schemas/routing-input.schema.json +14 -1
  44. package/scripts/CI_VALIDATORS.md +84 -11
  45. package/scripts/generate-agent-protocol-summary.mjs +36 -0
  46. package/src/commands/next.js +19 -7
  47. package/src/commands/route.js +5 -0
  48. package/src/commands/task-create.js +84 -25
  49. package/src/commands/task-list.js +22 -2
  50. package/src/config/guides.json +48 -0
  51. package/src/core/build-script.js +151 -0
  52. package/src/core/c-cpp-project.js +143 -0
  53. package/src/core/cli-command-definitions.js +8 -1
  54. package/src/core/command-executors.js +5 -3
  55. package/src/core/command-input.js +140 -102
  56. package/src/core/contract-presets.js +82 -0
  57. package/src/core/error-codes.js +3 -3
  58. package/src/core/filesystem.js +1 -10
  59. package/src/core/go-project.js +206 -0
  60. package/src/core/java-project.js +403 -0
  61. package/src/core/multi-language-project.js +117 -0
  62. package/src/core/next-explanation.js +63 -0
  63. package/src/core/php-project.js +85 -0
  64. package/src/core/project-detection.js +2227 -0
  65. package/src/core/reconcile-closure.js +4 -1
  66. package/src/core/router.js +233 -7
  67. package/src/core/rust-project.js +400 -0
  68. package/src/core/sql-project.js +141 -0
  69. package/src/core/swift-project.js +200 -0
  70. package/src/core/typescript-project.js +349 -0
  71. package/src/core/xml-structure.js +123 -0
@@ -14,6 +14,133 @@ function validateSearchCommandInput(command, options, help) {
14
14
  }
15
15
  }
16
16
 
17
+ function validatePolicyInput(command, options) {
18
+ if (command === "policy" && !options.policy) throw inputError("policy requires a name");
19
+ if (command !== "policy" && options.policy) throw inputError(`Policy name is not valid for ${command}`);
20
+ }
21
+
22
+ function validateTaskCreationInput(command, options) {
23
+ if (command === "bundle" && !options.taskId) throw inputError("bundle requires --task");
24
+ if (command === "task-create" && !options.taskId) throw inputError("task-create requires --task");
25
+ if (command !== "task-create" && (options.preset || options.preview)) {
26
+ throw inputError(`preset/preview options are not valid for ${command}`);
27
+ }
28
+ if (command === "task-create" && options.preview && !options.preset && !options.contractFile) {
29
+ throw inputError("task-create --preview requires --preset or --contract-file");
30
+ }
31
+ if (command === "task-create" && options.preset && options.contractFile) {
32
+ throw inputError("task-create accepts either --preset or --contract-file, not both");
33
+ }
34
+ const hasTaskListFilter = Boolean(
35
+ options.phase
36
+ || options.active
37
+ || (options.limit !== undefined && options.limit !== null)
38
+ || (options.offset !== undefined && options.offset !== null && options.offset !== 0),
39
+ );
40
+ if (command !== "task-list" && hasTaskListFilter) {
41
+ throw inputError(`task-list filters are not valid for ${command}`);
42
+ }
43
+ }
44
+
45
+ function validateExecutionProfileInput(command, options) {
46
+ if (options.executionProfile !== null && options.executionProfile !== undefined) {
47
+ if (command !== "route") throw inputError(`executionProfile is not valid for ${command}`);
48
+ if (!EXECUTION_PROFILE_REQUESTS.includes(options.executionProfile)) {
49
+ throw inputError(`route --execution-profile must be one of ${EXECUTION_PROFILE_REQUESTS.join(", ")}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ function validateUsageInput(command, options, help) {
55
+ if (command === "usage-record" && !help) {
56
+ if (!options.taskId) throw inputError("usage-record requires --task");
57
+ if ((options.usageSource ?? "ACTOR_REPORTED") !== "ACTOR_REPORTED") {
58
+ throw inputError("usage-record accepts only --source ACTOR_REPORTED");
59
+ }
60
+ }
61
+ if (command === "efficiency" && !help && !options.taskId) {
62
+ throw inputError("efficiency requires --task");
63
+ }
64
+ }
65
+
66
+ function validateQualityInput(command, options, help) {
67
+ if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
68
+ throw inputError(`${command} requires --task`);
69
+ }
70
+ if (["quality-baseline", "quality-verify"].includes(command) && !help
71
+ && options.timeoutMs !== null && options.timeoutMs !== undefined
72
+ && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
73
+ throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
74
+ }
75
+ }
76
+
77
+ function validateProfileAndUsageInput(command, options, help) {
78
+ validateExecutionProfileInput(command, options);
79
+ validateUsageInput(command, options, help);
80
+ validateQualityInput(command, options, help);
81
+ if (command !== "quality-baseline" && options.replace === true) throw inputError("--replace is only valid for quality-baseline");
82
+ if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
83
+ throw inputError(`usageSource is not valid for ${command}`);
84
+ }
85
+ }
86
+
87
+ function validateOutputInput(command, options) {
88
+ if (options.compact === true && !["next", "task-show"].includes(command)) {
89
+ throw inputError(`compact output is not valid for ${command}`);
90
+ }
91
+ if (command !== "next" && options.explain === true) throw inputError(`explain output is not valid for ${command}`);
92
+ }
93
+
94
+ function validateTaskSelectorsInput(command, options, help) {
95
+ const taskCommands = ["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"];
96
+ if (taskCommands.includes(command) && !options.taskId) throw inputError(`${command} requires --task`);
97
+ if (command === "handoff-show" && !help && !options.handoffId) throw inputError("handoff-show requires --id");
98
+ if (command === "handoff-accept" && !help) {
99
+ if (!options.handoffId) throw inputError("handoff-accept requires --handoff");
100
+ if (!options.consumerId) throw inputError("handoff-accept requires --consumer-id");
101
+ }
102
+ if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
103
+ throw inputError("responsibility-set requires --label");
104
+ }
105
+ if (command === "verify-scope" && !help) {
106
+ const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
107
+ if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
108
+ }
109
+ if (command === "attestation-verify-range" && !help) {
110
+ if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
111
+ if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
112
+ }
113
+ }
114
+
115
+ function validateCheckInput(command, options, help) {
116
+ if (command === "record-check" && !help) {
117
+ if (!options.checkId) throw inputError("record-check requires --id");
118
+ if (!options.checkRequirement) throw inputError("record-check requires --requirement");
119
+ if (!options.checkStatus) throw inputError("record-check requires --status");
120
+ if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
121
+ if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
122
+ }
123
+ if (command === "run-check" && !help) {
124
+ if (!options.checkId) throw inputError("run-check requires --id");
125
+ if (!options.checkRequirement) throw inputError("run-check requires --requirement");
126
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
127
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
128
+ throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
129
+ }
130
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) throw inputError("run-check requires -- followed by an exact command argv");
131
+ }
132
+ if (command === "reconcile-closure" && !help) {
133
+ if (!options.taskId) throw inputError("reconcile-closure requires --task");
134
+ if (!options.checkId) throw inputError("reconcile-closure requires --id");
135
+ if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
136
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
137
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
138
+ throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
139
+ }
140
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) throw inputError("reconcile-closure requires -- followed by an exact command argv");
141
+ }
142
+ }
143
+
17
144
  /**
18
145
  * Transport-neutral option defaults shared by the CLI parser and the
19
146
  * programmatic command runtime so every executor observes the same
@@ -25,6 +152,7 @@ export function defaultCommandInputValues() {
25
152
  dryRun: false,
26
153
  json: false,
27
154
  compact: false,
155
+ explain: false,
28
156
  strict: false,
29
157
  fix: false,
30
158
  adopt: [],
@@ -64,6 +192,12 @@ export function defaultCommandInputValues() {
64
192
  to: null,
65
193
  file: null,
66
194
  contractFile: null,
195
+ preset: null,
196
+ preview: false,
197
+ phase: null,
198
+ active: false,
199
+ limit: null,
200
+ offset: 0,
67
201
  routeFile: null,
68
202
  stateFile: null,
69
203
  receiptFile: null,
@@ -128,110 +262,14 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
128
262
  const options = input ?? {};
129
263
  if (!command) return;
130
264
 
131
- if (command === "policy" && !options.policy) {
132
- throw inputError("policy requires a name");
133
- }
134
- if (command !== "policy" && options.policy) {
135
- throw inputError(`Policy name is not valid for ${command}`);
136
- }
137
- if (command === "bundle" && !options.taskId) {
138
- throw inputError("bundle requires --task");
139
- }
140
- if (command === "task-create" && !options.taskId) {
141
- throw inputError("task-create requires --task");
142
- }
265
+ validatePolicyInput(command, options);
266
+ validateTaskCreationInput(command, options);
143
267
  validateSearchCommandInput(command, options, help);
144
- if (options.executionProfile !== null && options.executionProfile !== undefined) {
145
- if (command !== "route") throw inputError(`executionProfile is not valid for ${command}`);
146
- if (!EXECUTION_PROFILE_REQUESTS.includes(options.executionProfile)) {
147
- throw inputError(`route --execution-profile must be one of ${EXECUTION_PROFILE_REQUESTS.join(", ")}`);
148
- }
149
- }
150
- if (command === "usage-record" && !help) {
151
- if (!options.taskId) throw inputError("usage-record requires --task");
152
- if ((options.usageSource ?? "ACTOR_REPORTED") !== "ACTOR_REPORTED") {
153
- throw inputError("usage-record accepts only --source ACTOR_REPORTED");
154
- }
155
- }
156
- if (command === "efficiency" && !help && !options.taskId) {
157
- throw inputError("efficiency requires --task");
158
- }
159
- if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
160
- throw inputError(`${command} requires --task`);
161
- }
162
- if (["quality-baseline", "quality-verify"].includes(command) && !help
163
- && options.timeoutMs !== null && options.timeoutMs !== undefined
164
- && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
165
- throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
166
- }
167
- if (command !== "quality-baseline" && options.replace === true) {
168
- throw inputError(`--replace is only valid for quality-baseline`);
169
- }
170
- if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
171
- throw inputError(`usageSource is not valid for ${command}`);
172
- }
173
- if (options.compact === true && !["next", "task-show"].includes(command)) {
174
- throw inputError(`compact output is not valid for ${command}`);
175
- }
176
- if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
177
- && !options.taskId) {
178
- throw inputError(`${command} requires --task`);
179
- }
180
- if (command === "handoff-show" && !help && !options.handoffId) {
181
- throw inputError("handoff-show requires --id");
182
- }
183
- if (command === "handoff-accept" && !help) {
184
- if (!options.handoffId) {
185
- throw inputError("handoff-accept requires --handoff");
186
- }
187
- if (!options.consumerId) {
188
- throw inputError("handoff-accept requires --consumer-id");
189
- }
190
- }
191
- if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
192
- throw inputError("responsibility-set requires --label");
193
- }
194
- if (command === "verify-scope" && !help) {
195
- const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
196
- if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) {
197
- throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
198
- }
199
- }
200
- if (command === "attestation-verify-range" && !help) {
201
- if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
202
- if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
203
- }
268
+ validateProfileAndUsageInput(command, options, help);
269
+ validateOutputInput(command, options);
270
+ validateTaskSelectorsInput(command, options, help);
204
271
 
205
272
  validateContinuityOptions(command, options);
206
273
 
207
- if (command === "record-check" && !help) {
208
- if (!options.checkId) throw inputError("record-check requires --id");
209
- if (!options.checkRequirement) throw inputError("record-check requires --requirement");
210
- if (!options.checkStatus) throw inputError("record-check requires --status");
211
- if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
212
- if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
213
- }
214
- if (command === "run-check" && !help) {
215
- if (!options.checkId) throw inputError("run-check requires --id");
216
- if (!options.checkRequirement) throw inputError("run-check requires --requirement");
217
- if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
218
- || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
219
- throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
220
- }
221
- if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
222
- throw inputError("run-check requires -- followed by an exact command argv");
223
- }
224
- }
225
- if (command === "reconcile-closure" && !help) {
226
- if (!options.taskId) throw inputError("reconcile-closure requires --task");
227
- if (!options.checkId) throw inputError("reconcile-closure requires --id");
228
- if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
229
- if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
230
- || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
231
- throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
232
- }
233
- if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
234
- throw inputError("reconcile-closure requires -- followed by an exact command argv");
235
- }
236
- }
274
+ validateCheckInput(command, options, help);
237
275
  }
@@ -0,0 +1,82 @@
1
+ import { createContract } from "./contract.js";
2
+
3
+ const PRESET_DEFINITIONS = Object.freeze({
4
+ documentation: Object.freeze({
5
+ objective: "Complete the requested documentation change with verified project facts.",
6
+ constraints: ["Preserve repository-local instructions and existing public documentation contracts."],
7
+ risks: [],
8
+ verification: [{ id: "verify-documentation", text: "Relevant documentation checks pass.", type: "VERIFICATION" }],
9
+ successCriteria: [{ id: "complete-documentation", text: "The requested documentation change is complete and accurate.", type: "PRODUCT" }],
10
+ stopConditions: ["A required project fact or source cannot be confirmed."],
11
+ }),
12
+ bug: Object.freeze({
13
+ objective: "Diagnose and correct the requested bug without changing unrelated behavior.",
14
+ constraints: ["Preserve the existing public contract unless a breaking change is explicitly requested."],
15
+ risks: [],
16
+ verification: [{ id: "verify-bug-fix", text: "A focused regression check and proportional regression checks pass.", type: "VERIFICATION" }],
17
+ successCriteria: [{ id: "complete-bug-fix", text: "The reported behavior is corrected and the regression is covered.", type: "PRODUCT" }],
18
+ stopConditions: ["The failure cannot be reproduced or narrowed with available evidence."],
19
+ }),
20
+ feature: Object.freeze({
21
+ objective: "Implement the requested feature within the confirmed project scope.",
22
+ constraints: ["Preserve existing architecture and public contracts unless a change is explicitly requested."],
23
+ risks: [],
24
+ verification: [{ id: "verify-feature", text: "Focused behavior checks and proportional regression checks pass.", type: "VERIFICATION" }],
25
+ successCriteria: [{ id: "complete-feature", text: "The requested feature behavior is implemented and documented where applicable.", type: "PRODUCT" }],
26
+ stopConditions: ["Required product behavior or acceptance facts remain unresolved."],
27
+ }),
28
+ release: Object.freeze({
29
+ objective: "Prepare the requested release with independently verified artifact and publication evidence.",
30
+ constraints: [
31
+ "Do not publish, deploy, or mutate external release state without explicit authority.",
32
+ "Separate local checks, repository integration, publication, and deployment evidence.",
33
+ ],
34
+ risks: ["publication"],
35
+ verification: [{ id: "verify-release", text: "Release identity, package/artifact integrity, and required checks are independently verified.", type: "VERIFICATION" }],
36
+ successCriteria: [{
37
+ id: "publish-release",
38
+ text: "The requested release publication status is independently verified.",
39
+ type: "PUBLICATION",
40
+ requiredPublicationStatus: "published",
41
+ }],
42
+ stopConditions: ["Publication authority, target, or identity evidence is not available."],
43
+ }),
44
+ });
45
+
46
+ export const CONTRACT_PRESET_IDS = Object.freeze(Object.keys(PRESET_DEFINITIONS));
47
+
48
+ function normalizedClaims(claims) {
49
+ return [...new Set((Array.isArray(claims) ? claims : [])
50
+ .filter((claim) => typeof claim === "string" && claim.trim() !== "")
51
+ .map((claim) => claim.trim()))];
52
+ }
53
+
54
+ export function createPresetContract({ taskId, preset, claims = [] } = {}) {
55
+ if (!Object.prototype.hasOwnProperty.call(PRESET_DEFINITIONS, preset)) {
56
+ const error = new Error(`Unknown contract preset: ${preset}. Expected one of ${CONTRACT_PRESET_IDS.join(", ")}`);
57
+ error.code = "E_CONTRACT_PRESET_UNKNOWN";
58
+ throw error;
59
+ }
60
+ const definition = PRESET_DEFINITIONS[preset];
61
+ const confirmedClaims = normalizedClaims(claims);
62
+ const unresolvedDecisions = confirmedClaims.length > 0
63
+ ? []
64
+ : ["Confirm the concrete deliverables and affected behavior before implementation."];
65
+ return createContract({
66
+ taskId,
67
+ objective: definition.objective,
68
+ deliverables: confirmedClaims.length > 0 ? confirmedClaims : ["Deliverables confirmed by task scope."],
69
+ constraints: definition.constraints,
70
+ risks: definition.risks,
71
+ verification: definition.verification,
72
+ successCriteria: definition.successCriteria,
73
+ stopConditions: definition.stopConditions,
74
+ unresolvedDecisions,
75
+ sourceRefs: [`contract-preset:${preset}`],
76
+ });
77
+ }
78
+
79
+ export function contractPresetDefinition(preset) {
80
+ if (!Object.prototype.hasOwnProperty.call(PRESET_DEFINITIONS, preset)) return null;
81
+ return structuredClone(PRESET_DEFINITIONS[preset]);
82
+ }
@@ -788,8 +788,8 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
788
788
  code: "E_RECONCILE_PHASE_INVALID",
789
789
  category: "lifecycle",
790
790
  classification: "PUBLIC_STABLE",
791
- meaning: "reconcile-closure was invoked for a task that is not EXECUTING or VERIFYING.",
792
- safeResolution: "reconcile-closure supports EXECUTING or VERIFYING tasks whose objective is already satisfied.",
791
+ meaning: "reconcile-closure was invoked for a task that is not EXECUTING, VERIFYING, or REVIEWING.",
792
+ safeResolution: "reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied.",
793
793
  }),
794
794
  E_RECONCILE_UNSUPPORTED_DRIFT: Object.freeze({
795
795
  code: "E_RECONCILE_UNSUPPORTED_DRIFT",
@@ -831,7 +831,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
831
831
  category: "freshness",
832
832
  classification: "PUBLIC_STABLE",
833
833
  meaning: "The work-state checkpoint must be revalidated before the lifecycle can continue.",
834
- safeResolution: "Run forgeloop reconcile-closure for externally satisfied EXECUTING tasks, or inspect the freshness reasons for other drift.",
834
+ safeResolution: "Run forgeloop reconcile-closure for externally satisfied EXECUTING, VERIFYING, or REVIEWING tasks, or inspect the freshness reasons for other drift.",
835
835
  }),
836
836
  E_DIAGNOSIS_REQUIRED: Object.freeze({
837
837
  code: "E_DIAGNOSIS_REQUIRED",
@@ -129,7 +129,6 @@ export async function assertSafePath(root, relativePath) {
129
129
  }
130
130
 
131
131
  let existing = destination;
132
- let relativeExisting = path.normalize(relativePath);
133
132
  while (true) {
134
133
  try {
135
134
  const info = await lstatWithTransientWindowsRetry(existing);
@@ -137,14 +136,7 @@ export async function assertSafePath(root, relativePath) {
137
136
  throw new Error(`Path uses a symlink inside target directory: ${relativePath}`);
138
137
  }
139
138
  const resolvedRoot = await realpathWithTransientWindowsRetry(absoluteRoot);
140
- // Resolve the comparison path from the canonical root. Windows can
141
- // return different equivalent spellings (for example, short names or
142
- // extended-length prefixes) for the root and a child path when those
143
- // paths are canonicalized independently. Reconstructing the child from
144
- // the canonical root keeps the comparison in one namespace without
145
- // weakening the realpath containment check for junctions or symlinks.
146
- const canonicalExisting = path.resolve(resolvedRoot, relativeExisting);
147
- const resolvedExisting = await realpathWithTransientWindowsRetry(canonicalExisting);
139
+ const resolvedExisting = await realpathWithTransientWindowsRetry(existing);
148
140
  if (!isPathWithin(resolvedRoot, resolvedExisting)) {
149
141
  throw new Error(`Path escapes target directory: ${relativePath}`);
150
142
  }
@@ -154,7 +146,6 @@ export async function assertSafePath(root, relativePath) {
154
146
  const parent = path.dirname(existing);
155
147
  if (parent === existing) throw new Error(`Path does not resolve inside target directory: ${relativePath}`);
156
148
  existing = parent;
157
- relativeExisting = path.dirname(relativeExisting);
158
149
  }
159
150
  }
160
151
  }
@@ -0,0 +1,206 @@
1
+ import path from "node:path";
2
+
3
+ import { createLanguageProject, portablePath } from "./multi-language-project.js";
4
+
5
+ const GO_DIRECTIVE = /^(module|go|toolchain|godebug|require|replace|exclude|retract|tool|ignore)\b/u;
6
+ const GO_WORK_DIRECTIVES = new Set(["go", "toolchain", "godebug", "use", "replace", "tool"]);
7
+
8
+ function invalidGoResult() {
9
+ return {
10
+ valid: false,
11
+ module: null,
12
+ goVersion: null,
13
+ toolchain: null,
14
+ uses: [],
15
+ localReplacements: [],
16
+ ignorePaths: [],
17
+ };
18
+ }
19
+
20
+ function stripGoComment(line) {
21
+ const index = line.indexOf("//");
22
+ return (index < 0 ? line : line.slice(0, index)).trim();
23
+ }
24
+
25
+ function parseGoDirective(line) {
26
+ const block = line.match(/^(module|go|toolchain|godebug|require|replace|exclude|retract|use|tool|ignore)\b\s*\(\s*$/u);
27
+ const match = line.match(/^(module|go|toolchain|godebug|require|replace|exclude|retract|use|tool|ignore)\b\s*(?:\(([^)]*)\)|(.+))$/u);
28
+ if (block) return { name: block[1], value: "" };
29
+ if (!match) return null;
30
+ return {
31
+ name: match[1],
32
+ value: (match[2] ?? match[3] ?? "").trim(),
33
+ };
34
+ }
35
+
36
+ function collectGoBlock(lines, start) {
37
+ const values = [];
38
+ for (let index = start + 1; index < lines.length; index += 1) {
39
+ const line = stripGoComment(lines[index]);
40
+ if (line === ")") return { values, end: index, valid: true };
41
+ if (line !== "") values.push(line);
42
+ }
43
+ return { values, end: lines.length, valid: false };
44
+ }
45
+
46
+ function validGoDirectiveValue(name, value) {
47
+ if (!value || /\s{2,}/u.test(value)) return false;
48
+ if (name === "ignore") {
49
+ const portable = value.replaceAll("\\", "/");
50
+ const segments = portable.split("/");
51
+ return !value.includes("\\")
52
+ && !portable.startsWith("/")
53
+ && !/^[A-Za-z]:\//u.test(portable)
54
+ && !segments.includes("..")
55
+ && !value.includes("=>");
56
+ }
57
+ if (["require", "exclude"].includes(name)) return /^\S+\s+v\S+$/u.test(value);
58
+ if (name === "replace") return /^\S+(?:\s+v\S+)?\s+=>\s+\S+(?:\s+v\S+)?$/u.test(value);
59
+ if (name === "retract") return /^\S+(?:\s*,\s*\S+)?$/u.test(value);
60
+ if (name === "godebug") return /^\S+=\S+$/u.test(value);
61
+ if (name === "tool") return /^\S+(?:\s+v\S+)?$/u.test(value);
62
+ return true;
63
+ }
64
+
65
+ function parseGoModMetadataDirective(state, directive) {
66
+ if (directive.name === "module") {
67
+ if (state.module !== null || directive.value === "" || /\s/u.test(directive.value)) return false;
68
+ state.module = directive.value;
69
+ return true;
70
+ }
71
+ if (directive.name === "go") {
72
+ if (state.goVersion !== null || !/^\d+(?:\.\d+){1,2}$/u.test(directive.value)) return false;
73
+ state.goVersion = directive.value;
74
+ return true;
75
+ }
76
+ if (directive.name === "toolchain") {
77
+ if (state.toolchain !== null || !/^\S+$/u.test(directive.value)) return false;
78
+ state.toolchain = directive.value;
79
+ return true;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function parseGoModBlockDirective(lines, index, directive) {
85
+ const block = collectGoBlock(lines, index);
86
+ const valid = block.valid && block.values.every((value) => validGoDirectiveValue(directive.name, value));
87
+ return {
88
+ valid,
89
+ end: block.end,
90
+ ignorePaths: valid && directive.name === "ignore" ? block.values : [],
91
+ };
92
+ }
93
+
94
+ function parseGoModLine(lines, index, state) {
95
+ const line = stripGoComment(lines[index]);
96
+ if (line === "" || line.startsWith("//")) return { valid: true, end: index, ignorePaths: [] };
97
+ const directive = parseGoDirective(line);
98
+ if (!directive || !GO_DIRECTIVE.test(line)) return { valid: false, end: index, ignorePaths: [] };
99
+ const metadataValid = parseGoModMetadataDirective(state, directive);
100
+ if (metadataValid !== null) return { valid: metadataValid, end: index, ignorePaths: [] };
101
+ if (line.endsWith("(")) return parseGoModBlockDirective(lines, index, directive);
102
+ const valid = validGoDirectiveValue(directive.name, directive.value);
103
+ return {
104
+ valid,
105
+ end: index,
106
+ ignorePaths: valid && directive.name === "ignore" ? [directive.value] : [],
107
+ };
108
+ }
109
+
110
+ export function parseGoMod(text) {
111
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidGoResult();
112
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
113
+ const state = { module: null, goVersion: null, toolchain: null, valid: true };
114
+ const ignorePaths = [];
115
+ for (let index = 0; index < lines.length; index += 1) {
116
+ const parsed = parseGoModLine(lines, index, state);
117
+ state.valid &&= parsed.valid;
118
+ ignorePaths.push(...parsed.ignorePaths);
119
+ index = parsed.end;
120
+ }
121
+ return {
122
+ valid: state.valid && state.module !== null,
123
+ module: state.module,
124
+ goVersion: state.goVersion,
125
+ toolchain: state.toolchain,
126
+ uses: [],
127
+ localReplacements: [],
128
+ ignorePaths: [...new Set(ignorePaths)],
129
+ };
130
+ }
131
+
132
+ export function parseGoWork(text) {
133
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) {
134
+ return { valid: false, goVersion: null, uses: [] };
135
+ }
136
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
137
+ let goVersion = null;
138
+ const uses = [];
139
+ let valid = true;
140
+ for (let index = 0; index < lines.length; index += 1) {
141
+ const line = stripGoComment(lines[index]);
142
+ if (line === "") continue;
143
+ const directive = parseGoDirective(line);
144
+ if (!directive) {
145
+ valid = false;
146
+ continue;
147
+ }
148
+ if (directive.name === "go") {
149
+ if (goVersion !== null || !/^\d+(?:\.\d+){1,2}$/u.test(directive.value)) valid = false;
150
+ else goVersion = directive.value;
151
+ continue;
152
+ }
153
+ if (directive.name === "toolchain") {
154
+ if (!/^\S+$/u.test(directive.value)) valid = false;
155
+ continue;
156
+ }
157
+ if (!GO_WORK_DIRECTIVES.has(directive.name)) {
158
+ valid = false;
159
+ continue;
160
+ }
161
+ if (directive.name !== "use") {
162
+ if (line.endsWith("(")) {
163
+ const block = collectGoBlock(lines, index);
164
+ if (!block.valid || !block.values.every((value) => validGoDirectiveValue(directive.name, value))) valid = false;
165
+ index = block.end;
166
+ }
167
+ continue;
168
+ }
169
+ const values = line.endsWith("(") ? collectGoBlock(lines, index) : { values: [directive.value], end: index, valid: true };
170
+ if (!values.valid || values.values.some((value) => !validGoDirectiveValue("use", value)
171
+ || value.startsWith("/") || /^[A-Za-z]:/u.test(value))) {
172
+ valid = false;
173
+ } else {
174
+ uses.push(...values.values);
175
+ }
176
+ index = values.end;
177
+ }
178
+ return { valid: valid && goVersion !== null && uses.length > 0, goVersion, uses: [...new Set(uses)] };
179
+ }
180
+
181
+ export function inspectGoProject({ projectRoot, manifestRelative, manifestText, manifestName, targetRoot, projectFiles }) {
182
+ const parsed = manifestName === "go.work" ? parseGoWork(manifestText) : parseGoMod(manifestText);
183
+ const localUses = manifestName === "go.work"
184
+ ? parsed.uses.filter((use) => projectFiles.some((file) => file.kind === "go"
185
+ && parseGoMod(file.text).valid
186
+ && portablePath(targetRoot, path.dirname(file.path)) === path.posix.normalize(path.posix.join(projectRoot, use))))
187
+ : [];
188
+ const primary = parsed.valid && (manifestName === "go.mod" || localUses.length > 0);
189
+ const primarySignals = primary
190
+ ? [manifestName === "go.mod" ? `${manifestRelative}:module` : `${manifestRelative}:workspace`]
191
+ : [];
192
+ const supportingSignals = [];
193
+ if (parsed.goVersion) supportingSignals.push(`${manifestRelative}:go=${parsed.goVersion}`);
194
+ if (parsed.toolchain) supportingSignals.push(`${manifestRelative}:toolchain=${parsed.toolchain}`);
195
+ for (const use of localUses) supportingSignals.push(`${manifestRelative}:use=${use}`);
196
+ return createLanguageProject({
197
+ kind: "go",
198
+ root: projectRoot,
199
+ manifest: manifestRelative,
200
+ frameworks: primary ? ["go"] : [],
201
+ primary,
202
+ primarySignals,
203
+ supportingSignals,
204
+ internal: { ...parsed, valid: parsed.valid, manifestName, localUses },
205
+ });
206
+ }