@cassiomc1/forgeloop 1.13.0 → 1.14.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 (211) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/DOCS_INDEX.md +35 -3
  3. package/ENG/nodejs-backend-development-eng.md +2 -2
  4. package/ENG/sec-code-eng.md +7 -7
  5. package/EXECUTION_STATE.md +12 -0
  6. package/LOOP_ENGINEERING.md +28 -2
  7. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  8. package/PROTOCOL_INTEGRATION.md +55 -2
  9. package/README.md +40 -25
  10. package/TERMINOLOGY.md +2 -0
  11. package/THREAT_MODEL.md +140 -1
  12. package/completions/_forgeloop +19 -1
  13. package/completions/forgeloop.bash +37 -1
  14. package/completions/forgeloop.fish +123 -1
  15. package/docs/ADVISORY_CONTEXT.md +25 -0
  16. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  17. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  18. package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
  19. package/docs/AGENT_SKILL.md +66 -0
  20. package/docs/ARTIFACT_REFERENCE.md +123 -0
  21. package/docs/AUDIT_UX.md +46 -0
  22. package/docs/BROWSER_VERIFICATION.md +136 -0
  23. package/docs/CLI_REFERENCE.md +366 -6
  24. package/docs/CODE_ATTESTATION.md +2 -2
  25. package/docs/DOCUMENTATION_GUIDE.md +32 -11
  26. package/docs/JEV_BENCHMARKS.md +31 -0
  27. package/docs/MODEL_ROUTING.md +37 -0
  28. package/docs/OPENSRC_ADAPTER.md +241 -0
  29. package/docs/PACKAGE_CONTENTS.md +35 -8
  30. package/docs/PROVIDERS.md +126 -0
  31. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  32. package/docs/RECIPES.md +9 -0
  33. package/docs/RELEASE_CHECKLIST.md +38 -5
  34. package/docs/SECURITY_REVIEW.md +71 -0
  35. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  36. package/docs/TEST_INTELLIGENCE.md +29 -0
  37. package/docs/TEST_PRUNING.md +14 -0
  38. package/docs/TROUBLESHOOTING.md +198 -1
  39. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  40. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  41. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  42. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  43. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  44. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  45. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  46. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  47. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  48. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  49. package/docs/diagrams/README.md +13 -9
  50. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  51. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  52. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  53. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  54. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  55. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  56. package/docs/documentation-manifest.json +750 -5
  57. package/docs/protocol-requirements.json +24 -0
  58. package/package.json +30 -3
  59. package/schemas/config.schema.json +14 -0
  60. package/schemas/context-plan.schema.json +18 -0
  61. package/schemas/semantic-decision.schema.json +46 -0
  62. package/schemas/test-utility.schema.json +44 -0
  63. package/scripts/CI_VALIDATORS.md +6 -6
  64. package/scripts/benchmark-jev.mjs +5 -0
  65. package/scripts/benchmark-test-intelligence.mjs +4 -0
  66. package/scripts/generate-agent-protocol-summary.mjs +4 -1
  67. package/scripts/generate-forgeloop-skill.mjs +133 -0
  68. package/scripts/jev-smoke.mjs +19 -0
  69. package/skills/forgeloop/README.md +9 -0
  70. package/skills/forgeloop/SKILL.md +77 -0
  71. package/skills/forgeloop/references/lifecycle.md +9 -0
  72. package/skills/forgeloop/references/recovery.md +7 -0
  73. package/skills/forgeloop/references/verification.md +7 -0
  74. package/src/adapters/agent-browser/assertions.js +47 -0
  75. package/src/adapters/agent-browser/commands.js +54 -0
  76. package/src/adapters/agent-browser/index.js +3 -0
  77. package/src/adapters/agent-browser/locator.js +40 -0
  78. package/src/adapters/agent-browser/process.js +215 -0
  79. package/src/adapters/agent-browser/provider.js +313 -0
  80. package/src/adapters/emulated-services/constants.js +24 -0
  81. package/src/adapters/emulated-services/index.js +7 -0
  82. package/src/adapters/emulated-services/process.js +162 -0
  83. package/src/adapters/emulated-services/provider.js +282 -0
  84. package/src/adapters/opensrc/normalize.js +90 -0
  85. package/src/adapters/opensrc/process.js +248 -0
  86. package/src/adapters/opensrc/provider.js +338 -0
  87. package/src/adapters/opensrc/search.js +264 -0
  88. package/src/adapters/typesafe/client.js +28 -0
  89. package/src/adapters/typesafe/engine.js +63 -0
  90. package/src/adapters/typesafe/normalize.js +41 -0
  91. package/src/cli.js +108 -0
  92. package/src/commands/checkpoint-revalidate.js +176 -0
  93. package/src/commands/context-plan.js +38 -0
  94. package/src/commands/contract-create.js +264 -0
  95. package/src/commands/contract-revise.js +236 -0
  96. package/src/commands/decision-show.js +14 -0
  97. package/src/commands/decision-status.js +22 -0
  98. package/src/commands/discover.js +41 -0
  99. package/src/commands/doctor.js +15 -0
  100. package/src/commands/gate-record.js +205 -0
  101. package/src/commands/gate-revalidate.js +137 -0
  102. package/src/commands/model-route.js +32 -0
  103. package/src/commands/route.js +146 -18
  104. package/src/commands/semantic-plan.js +17 -0
  105. package/src/commands/task-abandon.js +224 -0
  106. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  107. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  108. package/src/commands/test-inventory.js +5 -0
  109. package/src/commands/test-prune-plan.js +5 -0
  110. package/src/commands/test-prune-probe.js +5 -0
  111. package/src/commands/test-utility.js +5 -0
  112. package/src/commands/validate-protocol.js +10 -1
  113. package/src/core/artifact-registry.js +24 -0
  114. package/src/core/audit-ux.js +514 -0
  115. package/src/core/browser-verification/constants.js +149 -0
  116. package/src/core/browser-verification/normalize.js +254 -0
  117. package/src/core/browser-verification/provider.js +519 -0
  118. package/src/core/browser-verification/service.js +115 -0
  119. package/src/core/checkpoint-revalidation.js +319 -0
  120. package/src/core/cli-command-definitions.js +241 -0
  121. package/src/core/command-executors.js +110 -0
  122. package/src/core/command-input.js +115 -43
  123. package/src/core/completion-artifacts.js +14 -5
  124. package/src/core/completion.js +4 -6
  125. package/src/core/config.js +3 -0
  126. package/src/core/context-compiler/budget.js +9 -0
  127. package/src/core/context-compiler/candidates.js +39 -0
  128. package/src/core/context-compiler/compiler.js +63 -0
  129. package/src/core/context-compiler/fingerprint.js +11 -0
  130. package/src/core/context-compiler/policy.js +13 -0
  131. package/src/core/context-compiler/result.js +23 -0
  132. package/src/core/contract-bootstrap-recovery.js +655 -0
  133. package/src/core/contract-revision.js +210 -0
  134. package/src/core/decision/artifact.js +69 -0
  135. package/src/core/decision/benchmarks.js +103 -0
  136. package/src/core/decision/cache.js +27 -0
  137. package/src/core/decision/constants.js +58 -0
  138. package/src/core/decision/cutover.js +34 -0
  139. package/src/core/decision/engine.js +22 -0
  140. package/src/core/decision/errors.js +68 -0
  141. package/src/core/decision/events.js +101 -0
  142. package/src/core/decision/freshness.js +19 -0
  143. package/src/core/decision/normalizers/index.js +115 -0
  144. package/src/core/decision/policy.js +18 -0
  145. package/src/core/decision/projection.js +16 -0
  146. package/src/core/decision/question-registry.js +201 -0
  147. package/src/core/decision/request.js +26 -0
  148. package/src/core/decision/resolver.js +130 -0
  149. package/src/core/decision/result.js +58 -0
  150. package/src/core/decision/service.js +156 -0
  151. package/src/core/decision/state-builder.js +65 -0
  152. package/src/core/decision/task-bindings.js +30 -0
  153. package/src/core/decision/test-provider.js +32 -0
  154. package/src/core/decision/thresholds.js +15 -0
  155. package/src/core/error-codes.js +278 -0
  156. package/src/core/events.js +226 -57
  157. package/src/core/evidence-readiness.js +9 -0
  158. package/src/core/execution-prerequisites.js +14 -0
  159. package/src/core/execution-profile.js +63 -38
  160. package/src/core/gate-provenance.js +124 -0
  161. package/src/core/integration-invocation-policy.js +27 -4
  162. package/src/core/integration-resources.js +86 -61
  163. package/src/core/model-router/constants.js +10 -0
  164. package/src/core/model-router/policy.js +103 -0
  165. package/src/core/model-router/router.js +37 -0
  166. package/src/core/next-action-model.js +58 -0
  167. package/src/core/next-action-phases.js +130 -42
  168. package/src/core/next-action-refresh.js +43 -9
  169. package/src/core/next-action-review-phase.js +7 -2
  170. package/src/core/next-action.js +35 -7
  171. package/src/core/phase.js +128 -10
  172. package/src/core/preflight-consistency.js +23 -9
  173. package/src/core/preflight-loaders.js +37 -5
  174. package/src/core/protocol-info.js +65 -0
  175. package/src/core/protocol.js +20 -0
  176. package/src/core/reconcile-closure.js +128 -52
  177. package/src/core/recovery-history.js +1 -0
  178. package/src/core/resumability.js +154 -44
  179. package/src/core/route-artifact.js +15 -1
  180. package/src/core/router.js +67 -1
  181. package/src/core/runtime-context.js +118 -61
  182. package/src/core/schema-validation.js +3 -0
  183. package/src/core/security-review/constants.js +64 -0
  184. package/src/core/security-review/normalize.js +245 -0
  185. package/src/core/security-review/provider.js +204 -0
  186. package/src/core/security-review/service.js +134 -0
  187. package/src/core/semantic-planning/constants.js +19 -0
  188. package/src/core/semantic-planning/projection.js +94 -0
  189. package/src/core/semantic-planning/service.js +15 -0
  190. package/src/core/sources.js +37 -0
  191. package/src/core/task-claim-state.js +201 -1
  192. package/src/core/task-conflict-inspection.js +31 -5
  193. package/src/core/task-paths.js +13 -0
  194. package/src/core/task-recovery.js +1 -0
  195. package/src/core/templates.js +3 -0
  196. package/src/core/test-intelligence/benchmarks.js +68 -0
  197. package/src/core/test-intelligence/inventory.js +73 -0
  198. package/src/core/test-intelligence/prune.js +90 -0
  199. package/src/core/test-intelligence/semantic-state.js +15 -0
  200. package/src/core/test-intelligence/service.js +40 -0
  201. package/src/core/test-intelligence/utility.js +50 -0
  202. package/src/core/trace.js +11 -7
  203. package/src/core/transaction.js +1 -0
  204. package/src/integration.d.ts +492 -0
  205. package/src/integration.js +54 -0
  206. package/src/providers/README.md +47 -0
  207. package/src/providers/capabilities.js +46 -0
  208. package/src/providers/errors.js +15 -0
  209. package/src/providers/index.js +29 -0
  210. package/src/providers/json-snapshot.js +105 -0
  211. package/src/providers/registry.js +152 -0
@@ -0,0 +1,248 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
6
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
7
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
8
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
9
+ E_ADVISORY_CONTEXT_TIMEOUT,
10
+ } from "../../core/error-codes.js";
11
+
12
+ export const OPENSRC_PROCESS_LIMITS = Object.freeze({
13
+ maxStdoutBytes: 64 * 1024,
14
+ maxStderrBytes: 64 * 1024,
15
+ terminationGraceMs: 250,
16
+ });
17
+
18
+ function processError(code, message) {
19
+ const error = new Error(message);
20
+ error.name = "OpenSrcProcessError";
21
+ error.code = code;
22
+ return error;
23
+ }
24
+
25
+ function assertSafeText(value, label) {
26
+ if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
27
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be a non-empty portable string`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function assertAbsolutePath(value, label) {
33
+ assertSafeText(value, label);
34
+ if (!path.isAbsolute(value)) {
35
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an absolute path`);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function outputBytes(chunk) {
41
+ return Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(String(chunk));
42
+ }
43
+
44
+ function terminateChild(child) {
45
+ if (!child || typeof child.kill !== "function") return;
46
+ try {
47
+ child.kill("SIGTERM");
48
+ } catch {
49
+ // The process may have exited between the limit check and kill.
50
+ }
51
+ setTimeout(() => {
52
+ try {
53
+ child.kill("SIGKILL");
54
+ } catch {
55
+ // Preserve the original timeout or output-limit error.
56
+ }
57
+ }, OPENSRC_PROCESS_LIMITS.terminationGraceMs).unref?.();
58
+ }
59
+
60
+ function waitForChildClose(child) {
61
+ return new Promise((resolve) => {
62
+ let settled = false;
63
+ const finish = () => {
64
+ if (settled) return;
65
+ settled = true;
66
+ clearTimeout(fallbackTimer);
67
+ resolve();
68
+ };
69
+ const finishAfterClose = () => {
70
+ // Windows can retain a child working directory for a short interval
71
+ // after close; allow the OS to release it before the caller tears down
72
+ // a temporary project tree.
73
+ setTimeout(finish, 500);
74
+ };
75
+ child.once?.("close", finishAfterClose);
76
+ const fallbackTimer = setTimeout(finish, OPENSRC_PROCESS_LIMITS.terminationGraceMs + 1000);
77
+ });
78
+ }
79
+
80
+ function normalizeRunOptions({
81
+ cwd,
82
+ timeoutMs,
83
+ maxStdoutBytes,
84
+ maxStderrBytes,
85
+ spawnImpl,
86
+ env,
87
+ } = {}) {
88
+ assertAbsolutePath(cwd, "OpenSrc project path");
89
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
90
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc timeout must be a positive integer");
91
+ }
92
+ if (!Number.isSafeInteger(maxStdoutBytes) || maxStdoutBytes < 1) {
93
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc stdout limit must be a positive integer");
94
+ }
95
+ if (!Number.isSafeInteger(maxStderrBytes) || maxStderrBytes < 1) {
96
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc stderr limit must be a positive integer");
97
+ }
98
+ if (typeof spawnImpl !== "function") {
99
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc spawn implementation must be a function");
100
+ }
101
+ if (env !== undefined && (!env || typeof env !== "object" || Array.isArray(env))) {
102
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc process environment must be an object");
103
+ }
104
+ return {
105
+ cwd,
106
+ timeoutMs,
107
+ maxStdoutBytes,
108
+ maxStderrBytes,
109
+ spawnImpl,
110
+ env,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Run one explicitly constructed OpenSrc command.
116
+ *
117
+ * This helper is intentionally private to the adapter boundary: callers pass
118
+ * an absolute executable and an argv array, while this function always keeps
119
+ * shell execution disabled and never returns raw stderr in an error message.
120
+ */
121
+ export function runOpenSrcCommand(executablePath, args, options = {}) {
122
+ assertAbsolutePath(executablePath, "OpenSrc executable path");
123
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
124
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc arguments must be a portable string array");
125
+ }
126
+
127
+ const runOptions = normalizeRunOptions({
128
+ cwd: options.cwd,
129
+ timeoutMs: options.timeoutMs,
130
+ maxStdoutBytes: options.maxStdoutBytes ?? OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
131
+ maxStderrBytes: options.maxStderrBytes ?? OPENSRC_PROCESS_LIMITS.maxStderrBytes,
132
+ spawnImpl: options.spawnImpl ?? nodeSpawn,
133
+ env: options.env,
134
+ });
135
+
136
+ const startedAt = Date.now();
137
+ let child;
138
+ try {
139
+ child = runOptions.spawnImpl(executablePath, [...args], {
140
+ cwd: runOptions.cwd,
141
+ shell: false,
142
+ stdio: ["ignore", "pipe", "pipe"],
143
+ ...(runOptions.env ? { env: { ...process.env, ...runOptions.env } } : {}),
144
+ });
145
+ } catch (error) {
146
+ throw processError(
147
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
148
+ error?.code === "ENOENT" ? "OpenSrc executable is unavailable" : "Unable to start OpenSrc",
149
+ error,
150
+ );
151
+ }
152
+
153
+ if (!child || !child.stdout || !child.stderr || typeof child.on !== "function") {
154
+ terminateChild(child);
155
+ throw processError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc process did not expose piped stdout and stderr");
156
+ }
157
+
158
+ return new Promise((resolve, reject) => {
159
+ let stdoutBytes = 0;
160
+ let stderrBytes = 0;
161
+ const stdoutChunks = [];
162
+ let settled = false;
163
+ let timer = null;
164
+
165
+ const cleanup = () => {
166
+ if (timer) clearTimeout(timer);
167
+ child.stdout?.removeAllListeners?.("data");
168
+ child.stderr?.removeAllListeners?.("data");
169
+ };
170
+
171
+ const fail = (error) => {
172
+ if (settled) return;
173
+ settled = true;
174
+ cleanup();
175
+ terminateChild(child);
176
+ child.stdout?.resume?.();
177
+ child.stderr?.resume?.();
178
+ waitForChildClose(child).then(() => reject(error));
179
+ };
180
+
181
+ const succeed = (code, signal) => {
182
+ if (settled) return;
183
+ settled = true;
184
+ cleanup();
185
+ if (code !== 0) {
186
+ reject(processError(
187
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
188
+ `OpenSrc exited unsuccessfully (${code ?? "null"}/${signal ?? "none"})`,
189
+ ));
190
+ return;
191
+ }
192
+ resolve({
193
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
194
+ stderrBytes,
195
+ stdoutBytes,
196
+ durationMs: Math.max(0, Date.now() - startedAt),
197
+ });
198
+ };
199
+
200
+ child.stdout.on("data", (chunk) => {
201
+ stdoutBytes += outputBytes(chunk);
202
+ if (stdoutBytes > runOptions.maxStdoutBytes) {
203
+ fail(processError(
204
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
205
+ `OpenSrc stdout exceeded ${runOptions.maxStdoutBytes} bytes`,
206
+ ));
207
+ return;
208
+ }
209
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
210
+ });
211
+ child.stderr.on("data", (chunk) => {
212
+ stderrBytes += outputBytes(chunk);
213
+ if (stderrBytes > runOptions.maxStderrBytes) {
214
+ fail(processError(
215
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
216
+ `OpenSrc stderr exceeded ${runOptions.maxStderrBytes} bytes`,
217
+ ));
218
+ }
219
+ });
220
+ child.on("error", (error) => {
221
+ fail(processError(
222
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
223
+ error?.code === "ENOENT" ? "OpenSrc executable is unavailable" : "OpenSrc process failed",
224
+ error,
225
+ ));
226
+ });
227
+ child.on("close", (code, signal) => succeed(code, signal));
228
+ timer = setTimeout(() => {
229
+ fail(processError(
230
+ E_ADVISORY_CONTEXT_TIMEOUT,
231
+ `OpenSrc exceeded the ${runOptions.timeoutMs}ms timeout`,
232
+ ));
233
+ }, runOptions.timeoutMs);
234
+ });
235
+ }
236
+
237
+ export function parseOpenSrcVersion(stdout) {
238
+ if (typeof stdout !== "string") {
239
+ throw processError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc version output was not text");
240
+ }
241
+ const match = /^\s*opensrc\s+([^\s]+)(?:\s|$)/iu.exec(stdout);
242
+ if (!match) {
243
+ throw processError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc version output did not contain a qualified version");
244
+ }
245
+ return match[1];
246
+ }
247
+
248
+ export { processError as openSrcProcessError };
@@ -0,0 +1,338 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { assertSafePath, isPathWithin } from "../../core/filesystem.js";
5
+ import {
6
+ normalizeAdvisoryRecallOptions,
7
+ } from "../../core/advisory-context/constants.js";
8
+ import {
9
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
10
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
11
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
12
+ E_ADVISORY_CONTEXT_TIMEOUT,
13
+ } from "../../core/error-codes.js";
14
+ import {
15
+ OPENSRC_PROCESS_LIMITS,
16
+ parseOpenSrcVersion,
17
+ runOpenSrcCommand,
18
+ } from "./process.js";
19
+ import { OPENSRC_SEARCH_LIMITS, searchSourceRoot } from "./search.js";
20
+ import { normalizeOpenSrcResult } from "./normalize.js";
21
+
22
+ const MAX_SOURCES = OPENSRC_SEARCH_LIMITS.maxSources;
23
+ const MAX_SOURCE_SPEC_CHARS = 256;
24
+
25
+ function providerError(code, message) {
26
+ const error = new Error(message);
27
+ error.name = "OpenSrcAdvisoryProviderError";
28
+ error.code = code;
29
+ return error;
30
+ }
31
+
32
+ function assertAbsolutePath(value, label) {
33
+ if (typeof value !== "string" || value.trim() === "" || !path.isAbsolute(value) || /\p{Cc}/u.test(value)) {
34
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an absolute portable path`);
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function assertExpectedVersion(value) {
40
+ if (
41
+ typeof value !== "string"
42
+ || value.trim() === ""
43
+ || value.length > 64
44
+ || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(value)
45
+ ) {
46
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc expectedVersion must be a qualified version token under 64 characters");
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function assertSourceSpec(value) {
52
+ if (typeof value !== "string" || value.trim() === "" || value.length > MAX_SOURCE_SPEC_CHARS) {
53
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must be non-empty strings under 256 characters");
54
+ }
55
+ if (/[\0\p{Cc}\s]/u.test(value)) {
56
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not contain whitespace or control characters");
57
+ }
58
+ if (value.startsWith("-")) {
59
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not begin with '-'");
60
+ }
61
+ if (/[;|&$`<>(){}[\]!*?~#"']/u.test(value)) {
62
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not contain shell metacharacters");
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function assertSources(value) {
68
+ if (!Array.isArray(value) || value.length === 0) {
69
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc sources must be a non-empty array");
70
+ }
71
+ if (value.length > MAX_SOURCES) {
72
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `OpenSrc sources must contain at most ${MAX_SOURCES} entries`);
73
+ }
74
+ return value.map(assertSourceSpec);
75
+ }
76
+
77
+ function remainingTimeout(deadline) {
78
+ const remaining = deadline - Date.now();
79
+ if (remaining < 1) {
80
+ throw providerError(E_ADVISORY_CONTEXT_TIMEOUT, "OpenSrc advisory recall deadline expired");
81
+ }
82
+ return remaining;
83
+ }
84
+
85
+ function reportTransport(callback, payload) {
86
+ if (typeof callback !== "function") return;
87
+ try {
88
+ callback(Object.freeze({ ...payload }));
89
+ } catch {
90
+ // Observability hooks cannot change advisory correctness or failure mapping.
91
+ }
92
+ }
93
+
94
+ async function canonicalizeExisting(candidate) {
95
+ try {
96
+ return await realpath(candidate);
97
+ } catch {
98
+ return path.resolve(candidate);
99
+ }
100
+ }
101
+
102
+ async function assertCacheOutsideProject(cacheRoot, projectRoot) {
103
+ // Compare both the lexical and the canonicalized spellings: either side may
104
+ // traverse symlinked prefixes (for example /var versus /private/var), so a
105
+ // single spelling can miss a genuine containment relationship. The roots
106
+ // must be disjoint in both directions: neither may contain the other.
107
+ const spellings = [
108
+ [path.resolve(cacheRoot), path.resolve(projectRoot)],
109
+ [await canonicalizeExisting(cacheRoot), await canonicalizeExisting(projectRoot)],
110
+ ];
111
+ for (const [candidateCache, candidateProject] of spellings) {
112
+ if (isPathWithin(candidateProject, candidateCache) || isPathWithin(candidateCache, candidateProject)) {
113
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc cacheRoot and project root must be disjoint");
114
+ }
115
+ if (isPathWithin(path.join(candidateProject, ".forgeloop"), candidateCache)) {
116
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc cacheRoot must be outside .forgeloop");
117
+ }
118
+ }
119
+ return path.resolve(cacheRoot);
120
+ }
121
+
122
+ async function runAndReport(executablePath, args, {
123
+ cwd,
124
+ timeoutMs,
125
+ spawnImpl,
126
+ env,
127
+ maxStdoutBytes,
128
+ maxStderrBytes,
129
+ onTransport,
130
+ kind,
131
+ sourceIndex,
132
+ } = {}) {
133
+ const result = await runOpenSrcCommand(executablePath, args, {
134
+ cwd,
135
+ timeoutMs,
136
+ spawnImpl,
137
+ env,
138
+ maxStdoutBytes,
139
+ maxStderrBytes,
140
+ });
141
+ reportTransport(onTransport, {
142
+ kind,
143
+ ...(sourceIndex === undefined ? {} : { sourceIndex }),
144
+ stdoutBytes: result.stdoutBytes,
145
+ stderrBytes: result.stderrBytes,
146
+ durationMs: result.durationMs,
147
+ });
148
+ return result;
149
+ }
150
+
151
+ async function resolveSourceRoot({
152
+ executablePath,
153
+ source,
154
+ sourceIndex,
155
+ projectRoot,
156
+ cacheRoot,
157
+ timeoutMs,
158
+ spawnImpl,
159
+ env,
160
+ onTransport,
161
+ }) {
162
+ let result;
163
+ try {
164
+ result = await runAndReport(executablePath, ["path", source, "--cwd", projectRoot], {
165
+ cwd: projectRoot,
166
+ timeoutMs,
167
+ spawnImpl,
168
+ env,
169
+ maxStdoutBytes: OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
170
+ maxStderrBytes: OPENSRC_PROCESS_LIMITS.maxStderrBytes,
171
+ onTransport,
172
+ kind: "path",
173
+ sourceIndex,
174
+ });
175
+ } catch (error) {
176
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc source resolution failed");
177
+ }
178
+ const lines = result.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
179
+ if (lines.length !== 1) {
180
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path must print exactly one path");
181
+ }
182
+ const [printed] = lines;
183
+ if (/\p{Cc}/u.test(printed)) {
184
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path output must be portable text");
185
+ }
186
+ if (!path.isAbsolute(printed)) {
187
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path output must be absolute");
188
+ }
189
+ let resolved;
190
+ try {
191
+ resolved = await realpath(printed);
192
+ } catch {
193
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path could not be canonicalized");
194
+ }
195
+ let canonicalCache;
196
+ try {
197
+ canonicalCache = await realpath(cacheRoot);
198
+ } catch {
199
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc cache root could not be canonicalized");
200
+ }
201
+ if (resolved !== canonicalCache && !isPathWithin(canonicalCache, resolved)) {
202
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path escaped the configured cache");
203
+ }
204
+ let rootStat;
205
+ try {
206
+ rootStat = await stat(resolved);
207
+ } catch {
208
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path is not an accessible directory");
209
+ }
210
+ if (!rootStat.isDirectory()) {
211
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path is not a directory");
212
+ }
213
+ return resolved;
214
+ }
215
+
216
+ /**
217
+ * Create an optional host-injected OpenSrc adapter.
218
+ *
219
+ * Construction is inert: it validates the explicit executable, version,
220
+ * cache, and source allowlist but does not discover binaries, spawn
221
+ * processes, access the network, or write lifecycle state. Version
222
+ * qualification repeats lazily before every recall so a changed executable
223
+ * cannot silently change the provider identity.
224
+ */
225
+ export function createOpenSrcAdvisoryContextProvider({
226
+ executablePath,
227
+ expectedVersion,
228
+ cacheRoot,
229
+ sources,
230
+ spawnImpl,
231
+ env,
232
+ onTransport,
233
+ } = {}) {
234
+ const qualifiedExecutablePath = assertAbsolutePath(executablePath, "OpenSrc executablePath");
235
+ const qualifiedExpectedVersion = assertExpectedVersion(expectedVersion);
236
+ const qualifiedCacheRoot = assertAbsolutePath(cacheRoot, "OpenSrc cacheRoot");
237
+ const qualifiedSources = assertSources(sources);
238
+ if (spawnImpl !== undefined && typeof spawnImpl !== "function") {
239
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc spawn implementation must be a function");
240
+ }
241
+ if (env !== undefined && (!env || typeof env !== "object" || Array.isArray(env))) {
242
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc process environment must be an object");
243
+ }
244
+
245
+ const provider = {
246
+ id: "opensrc",
247
+ version: qualifiedExpectedVersion,
248
+ async recall({
249
+ projectPath,
250
+ taskId,
251
+ query,
252
+ limit,
253
+ maxItemChars,
254
+ maxTotalChars,
255
+ timeoutMs,
256
+ } = {}) {
257
+ if (typeof projectPath !== "string" || !path.isAbsolute(projectPath) || /\p{Cc}/u.test(projectPath)) {
258
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc projectPath must be an absolute path");
259
+ }
260
+ if (typeof taskId !== "string" || taskId.trim() === "") {
261
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc taskId must be a non-empty string");
262
+ }
263
+ if (typeof query !== "string" || query.trim() === "" || /\p{Cc}/u.test(query)) {
264
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc query must be a non-empty portable string");
265
+ }
266
+ const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
267
+ const projectRoot = path.resolve(projectPath);
268
+ try {
269
+ await assertSafePath(projectRoot, ".");
270
+ } catch {
271
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc target directory is unavailable or unsafe");
272
+ }
273
+ await assertCacheOutsideProject(qualifiedCacheRoot, projectRoot);
274
+
275
+ const childEnv = { ...(env ?? {}), OPENSRC_HOME: qualifiedCacheRoot };
276
+ const deadline = Date.now() + options.timeoutMs;
277
+ let versionResult;
278
+ try {
279
+ versionResult = await runAndReport(qualifiedExecutablePath, ["--version"], {
280
+ cwd: projectRoot,
281
+ timeoutMs: remainingTimeout(deadline),
282
+ spawnImpl,
283
+ env: childEnv,
284
+ maxStdoutBytes: OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
285
+ maxStderrBytes: OPENSRC_PROCESS_LIMITS.maxStderrBytes,
286
+ onTransport,
287
+ kind: "version",
288
+ });
289
+ } catch (error) {
290
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc version probe failed");
291
+ }
292
+
293
+ let actualVersion;
294
+ try {
295
+ actualVersion = parseOpenSrcVersion(versionResult.stdout);
296
+ } catch {
297
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc version probe was not qualified");
298
+ }
299
+ if (actualVersion !== qualifiedExpectedVersion) {
300
+ throw providerError(
301
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
302
+ "OpenSrc executable version does not match the expected qualified version",
303
+ );
304
+ }
305
+
306
+ const matches = [];
307
+ const sharedBudget = { remainingReadBytes: OPENSRC_SEARCH_LIMITS.maxTotalReadBytes };
308
+ const boundedSources = qualifiedSources.slice(0, MAX_SOURCES);
309
+ for (let sourceIndex = 0; sourceIndex < boundedSources.length; sourceIndex += 1) {
310
+ const source = boundedSources[sourceIndex];
311
+ const sourceRoot = await resolveSourceRoot({
312
+ executablePath: qualifiedExecutablePath,
313
+ source,
314
+ sourceIndex,
315
+ projectRoot,
316
+ cacheRoot: qualifiedCacheRoot,
317
+ timeoutMs: remainingTimeout(deadline),
318
+ spawnImpl,
319
+ env: childEnv,
320
+ onTransport,
321
+ });
322
+ const sourceMatches = await searchSourceRoot({
323
+ sourceRoot,
324
+ sourceSpec: source,
325
+ sourceIndex,
326
+ query,
327
+ budget: sharedBudget,
328
+ deadline,
329
+ });
330
+ matches.push(...sourceMatches);
331
+ }
332
+
333
+ return normalizeOpenSrcResult(matches, options);
334
+ },
335
+ };
336
+
337
+ return Object.freeze(provider);
338
+ }