@lnilluv/pi-ralph-loop 0.2.1 → 1.0.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 (48) hide show
  1. package/.github/workflows/ci.yml +5 -2
  2. package/.github/workflows/release.yml +15 -43
  3. package/README.md +51 -113
  4. package/package.json +13 -5
  5. package/scripts/version-helper.ts +210 -0
  6. package/src/index.ts +1360 -275
  7. package/src/ralph-draft-context.ts +618 -0
  8. package/src/ralph-draft-llm.ts +297 -0
  9. package/src/ralph-draft.ts +33 -0
  10. package/src/ralph.ts +1457 -0
  11. package/src/runner-rpc.ts +434 -0
  12. package/src/runner-state.ts +822 -0
  13. package/src/runner.ts +957 -0
  14. package/src/secret-paths.ts +66 -0
  15. package/src/shims.d.ts +0 -3
  16. package/tests/fixtures/parity/migrate/OPEN_QUESTIONS.md +3 -0
  17. package/tests/fixtures/parity/migrate/RALPH.md +27 -0
  18. package/tests/fixtures/parity/migrate/golden/MIGRATED.md +15 -0
  19. package/tests/fixtures/parity/migrate/legacy/source.md +6 -0
  20. package/tests/fixtures/parity/migrate/legacy/source.yaml +3 -0
  21. package/tests/fixtures/parity/migrate/scripts/show-legacy.sh +10 -0
  22. package/tests/fixtures/parity/migrate/scripts/verify.sh +15 -0
  23. package/tests/fixtures/parity/research/OPEN_QUESTIONS.md +3 -0
  24. package/tests/fixtures/parity/research/RALPH.md +45 -0
  25. package/tests/fixtures/parity/research/claim-evidence-checklist.md +15 -0
  26. package/tests/fixtures/parity/research/expected-outputs.md +22 -0
  27. package/tests/fixtures/parity/research/scripts/show-snapshots.sh +13 -0
  28. package/tests/fixtures/parity/research/scripts/verify.sh +55 -0
  29. package/tests/fixtures/parity/research/snapshots/app-factory-ai-cli.md +11 -0
  30. package/tests/fixtures/parity/research/snapshots/docs-factory-ai-cli-features-missions.md +11 -0
  31. package/tests/fixtures/parity/research/snapshots/factory-ai-news-missions.md +11 -0
  32. package/tests/fixtures/parity/research/source-manifest.md +20 -0
  33. package/tests/index.test.ts +3529 -0
  34. package/tests/parity/README.md +9 -0
  35. package/tests/parity/harness.py +526 -0
  36. package/tests/parity-harness.test.ts +42 -0
  37. package/tests/parity-research-fixture.test.ts +34 -0
  38. package/tests/ralph-draft-context.test.ts +672 -0
  39. package/tests/ralph-draft-llm.test.ts +434 -0
  40. package/tests/ralph-draft.test.ts +168 -0
  41. package/tests/ralph.test.ts +1840 -0
  42. package/tests/runner-event-contract.test.ts +235 -0
  43. package/tests/runner-rpc.test.ts +358 -0
  44. package/tests/runner-state.test.ts +553 -0
  45. package/tests/runner.test.ts +1347 -0
  46. package/tests/secret-paths.test.ts +55 -0
  47. package/tests/version-helper.test.ts +75 -0
  48. package/tsconfig.json +3 -2
@@ -0,0 +1,672 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { createRequire, syncBuiltinESMExports } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { assembleRepoContext } from "../src/ralph-draft-context.ts";
8
+
9
+ function createTempRepo(): string {
10
+ return mkdtempSync(join(tmpdir(), "pi-ralph-context-"));
11
+ }
12
+
13
+ function writeTextFile(root: string, relativePath: string, content: string): void {
14
+ const fullPath = join(root, relativePath);
15
+ mkdirSync(join(fullPath, ".."), { recursive: true });
16
+ writeFileSync(fullPath, content, "utf8");
17
+ }
18
+
19
+ function makeSignals(overrides: Partial<{ packageManager: "npm" | "pnpm" | "yarn" | "bun"; testCommand: string; lintCommand: string; hasGit: boolean; topLevelDirs: string[]; topLevelFiles: string[]; }> = {}) {
20
+ return {
21
+ packageManager: overrides.packageManager,
22
+ testCommand: overrides.testCommand,
23
+ lintCommand: overrides.lintCommand,
24
+ hasGit: overrides.hasGit ?? false,
25
+ topLevelDirs: overrides.topLevelDirs ?? [],
26
+ topLevelFiles: overrides.topLevelFiles ?? [],
27
+ };
28
+ }
29
+
30
+ test("analysis mode prioritizes repo overview, manifests, and likely entrypoints", (t) => {
31
+ const cwd = createTempRepo();
32
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
33
+
34
+ writeTextFile(cwd, "README.md", "# Demo app\n");
35
+ writeTextFile(cwd, "package.json", JSON.stringify({ name: "demo", scripts: { test: "vitest", lint: "eslint ." } }, null, 2));
36
+ writeTextFile(cwd, "tsconfig.json", "{\n \"compilerOptions\": {}\n}\n");
37
+ writeTextFile(cwd, "src/index.ts", "export function main() { return 'ok'; }\n");
38
+ writeTextFile(cwd, "src/router.ts", "export const router = [];\n");
39
+ writeTextFile(cwd, "tests/auth.test.ts", "import assert from 'node:assert/strict';\n");
40
+ writeTextFile(cwd, "node_modules/ignored.js", "ignored\n");
41
+ writeTextFile(cwd, "dist/bundle.js", "ignored\n");
42
+
43
+ const context = assembleRepoContext(
44
+ cwd,
45
+ "Reverse engineer this app",
46
+ "analysis",
47
+ makeSignals({
48
+ packageManager: "npm",
49
+ testCommand: "npm test",
50
+ lintCommand: "npm run lint",
51
+ hasGit: true,
52
+ topLevelDirs: ["src", "tests", "node_modules", "dist"],
53
+ topLevelFiles: ["README.md", "package.json", "tsconfig.json"],
54
+ }),
55
+ );
56
+
57
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
58
+
59
+ assert.ok(selectedPaths.includes("README.md"));
60
+ assert.ok(selectedPaths.includes("package.json"));
61
+ assert.ok(selectedPaths.includes("src/index.ts"));
62
+ assert.ok(selectedPaths.indexOf("README.md") < selectedPaths.indexOf("package.json"));
63
+ assert.ok(selectedPaths.indexOf("package.json") < selectedPaths.indexOf("src/index.ts"));
64
+ assert.match(context.selectedFiles.find((file) => file.path === "README.md")?.reason ?? "", /repo overview/i);
65
+ assert.match(context.selectedFiles.find((file) => file.path === "package.json")?.reason ?? "", /package manifest/i);
66
+ assert.match(context.selectedFiles.find((file) => file.path === "src/index.ts")?.reason ?? "", /entrypoint/i);
67
+ assert.ok(context.summaryLines.some((line) => line.includes("package manager: npm")));
68
+ assert.ok(context.summaryLines.some((line) => line.includes("scripts: test=npm test, lint=npm run lint")));
69
+ assert.ok(context.summaryLines.some((line) => line.includes("git repository: present")));
70
+ assert.ok(context.summaryLines.some((line) => line.includes("top-level dirs:")));
71
+ assert.ok(context.summaryLines.some((line) => line.includes("top-level files:")));
72
+ assert.ok(context.summaryLines.some((line) => line.includes("selected files:")));
73
+ });
74
+
75
+ test("analysis mode probes wildcard src entrypoints even after the first 200 src entries", (t) => {
76
+ const cwd = createTempRepo();
77
+ const fs = createRequire(import.meta.url)("node:fs") as any;
78
+ const originalOpendirSync = fs.opendirSync;
79
+ t.after(() => {
80
+ fs.opendirSync = originalOpendirSync;
81
+ syncBuiltinESMExports();
82
+ rmSync(cwd, { recursive: true, force: true });
83
+ });
84
+
85
+ writeTextFile(cwd, "README.md", "# Demo app\n");
86
+ writeTextFile(cwd, "package.json", JSON.stringify({ name: "demo", scripts: { test: "vitest", lint: "eslint ." } }, null, 2));
87
+ writeTextFile(cwd, "tsconfig.json", "{\n \"compilerOptions\": {}\n}\n");
88
+ for (let index = 0; index < 205; index++) {
89
+ writeTextFile(cwd, `src/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
90
+ }
91
+ writeTextFile(cwd, "src/main.py", "def main():\n return 'ok'\n");
92
+
93
+ const srcEntries = [
94
+ ...Array.from({ length: 200 }, (_, index) => ({
95
+ name: `file-${String(index).padStart(3, "0")}.ts`,
96
+ isFile: () => true,
97
+ isDirectory: () => false,
98
+ })),
99
+ { name: "main.py", isFile: () => true, isDirectory: () => false },
100
+ ];
101
+
102
+ let opendirCalls = 0;
103
+ fs.opendirSync = (...args: any[]) => {
104
+ const [dirPath] = args as [string];
105
+ if (dirPath === join(cwd, "src")) {
106
+ opendirCalls += 1;
107
+ let entryIndex = 0;
108
+ return {
109
+ readSync: () => (entryIndex < srcEntries.length ? srcEntries[entryIndex++] : null),
110
+ closeSync: () => {},
111
+ };
112
+ }
113
+ return Reflect.apply(originalOpendirSync, fs, args);
114
+ };
115
+ syncBuiltinESMExports();
116
+
117
+ const context = assembleRepoContext(
118
+ cwd,
119
+ "Reverse engineer this app",
120
+ "analysis",
121
+ makeSignals({
122
+ topLevelDirs: ["src"],
123
+ topLevelFiles: ["README.md", "package.json", "tsconfig.json"],
124
+ }),
125
+ );
126
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
127
+
128
+ assert.ok(selectedPaths.includes("README.md"));
129
+ assert.ok(selectedPaths.includes("package.json"));
130
+ assert.ok(selectedPaths.includes("tsconfig.json"));
131
+ assert.ok(selectedPaths.includes("src/main.py"));
132
+ assert.ok(opendirCalls <= 3);
133
+ assert.match(context.selectedFiles.find((file) => file.path === "src/main.py")?.reason ?? "", /entrypoint/i);
134
+ });
135
+
136
+ test("fix mode discovers src/auth/login.ts after 200 unrelated sibling files", (t) => {
137
+ const cwd = createTempRepo();
138
+ const fs = createRequire(import.meta.url)("node:fs") as any;
139
+ const originalOpendirSync = fs.opendirSync;
140
+ t.after(() => {
141
+ fs.opendirSync = originalOpendirSync;
142
+ syncBuiltinESMExports();
143
+ rmSync(cwd, { recursive: true, force: true });
144
+ });
145
+
146
+ for (let index = 0; index < 200; index++) {
147
+ writeTextFile(cwd, `src/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
148
+ }
149
+ writeTextFile(cwd, "src/auth/login.ts", "export const login = true;\n");
150
+
151
+ const srcEntries = [
152
+ ...Array.from({ length: 200 }, (_, index) => ({
153
+ name: `file-${String(index).padStart(3, "0")}.ts`,
154
+ isFile: () => true,
155
+ isDirectory: () => false,
156
+ })),
157
+ { name: "auth", isFile: () => false, isDirectory: () => true },
158
+ ];
159
+ const authEntries = [{ name: "login.ts", isFile: () => true, isDirectory: () => false }];
160
+
161
+ let opendirCalls = 0;
162
+ fs.opendirSync = (...args: any[]) => {
163
+ const [dirPath] = args as [string];
164
+ if (dirPath === join(cwd, "src")) {
165
+ opendirCalls += 1;
166
+ let entryIndex = 0;
167
+ return {
168
+ readSync: () => (entryIndex < srcEntries.length ? srcEntries[entryIndex++] : null),
169
+ closeSync: () => {},
170
+ };
171
+ }
172
+ if (dirPath === join(cwd, "src", "auth")) {
173
+ opendirCalls += 1;
174
+ let entryIndex = 0;
175
+ return {
176
+ readSync: () => (entryIndex < authEntries.length ? authEntries[entryIndex++] : null),
177
+ closeSync: () => {},
178
+ };
179
+ }
180
+ return Reflect.apply(originalOpendirSync, fs, args);
181
+ };
182
+ syncBuiltinESMExports();
183
+
184
+ const context = assembleRepoContext(cwd, "Fix auth bug", "fix", makeSignals({ topLevelDirs: ["src"] }));
185
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
186
+
187
+ assert.ok(selectedPaths.includes("src/auth/login.ts"));
188
+ assert.equal(selectedPaths[0], "src/auth/login.ts");
189
+ assert.match(context.selectedFiles.find((file) => file.path === "src/auth/login.ts")?.reason ?? "", /auth|task keyword/i);
190
+ assert.ok(opendirCalls <= 4);
191
+ });
192
+
193
+ test("fix mode keeps src/auth.ts ahead of a second-page auth subtree", (t) => {
194
+ const cwd = createTempRepo();
195
+ const fs = createRequire(import.meta.url)("node:fs") as any;
196
+ const originalOpendirSync = fs.opendirSync;
197
+ t.after(() => {
198
+ fs.opendirSync = originalOpendirSync;
199
+ syncBuiltinESMExports();
200
+ rmSync(cwd, { recursive: true, force: true });
201
+ });
202
+
203
+ writeTextFile(cwd, "src/auth.ts", "export const auth = true;\n");
204
+ for (let index = 0; index < 199; index++) {
205
+ writeTextFile(cwd, `src/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
206
+ }
207
+ for (let index = 0; index < 200; index++) {
208
+ writeTextFile(cwd, `src/auth/nested-${String(index).padStart(3, "0")}.ts`, `export const nested${index} = ${index};\n`);
209
+ }
210
+
211
+ const srcEntries = [
212
+ { name: "auth.ts", isFile: () => true, isDirectory: () => false },
213
+ ...Array.from({ length: 199 }, (_, index) => ({
214
+ name: `file-${String(index).padStart(3, "0")}.ts`,
215
+ isFile: () => true,
216
+ isDirectory: () => false,
217
+ })),
218
+ { name: "auth", isFile: () => false, isDirectory: () => true },
219
+ ];
220
+ const authEntries = Array.from({ length: 200 }, (_, index) => ({
221
+ name: `nested-${String(index).padStart(3, "0")}.ts`,
222
+ isFile: () => true,
223
+ isDirectory: () => false,
224
+ }));
225
+
226
+ fs.opendirSync = (...args: any[]) => {
227
+ const [dirPath] = args as [string];
228
+ if (dirPath === join(cwd, "src")) {
229
+ let entryIndex = 0;
230
+ return {
231
+ readSync: () => (entryIndex < srcEntries.length ? srcEntries[entryIndex++] : null),
232
+ closeSync: () => {},
233
+ };
234
+ }
235
+ if (dirPath === join(cwd, "src", "auth")) {
236
+ let entryIndex = 0;
237
+ return {
238
+ readSync: () => (entryIndex < authEntries.length ? authEntries[entryIndex++] : null),
239
+ closeSync: () => {},
240
+ };
241
+ }
242
+ return Reflect.apply(originalOpendirSync, fs, args);
243
+ };
244
+ syncBuiltinESMExports();
245
+
246
+ const context = assembleRepoContext(cwd, "Fix auth bug", "fix", makeSignals({ topLevelDirs: ["src"] }));
247
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
248
+
249
+ assert.ok(selectedPaths.includes("src/auth.ts"));
250
+ assert.equal(selectedPaths[0], "src/auth.ts");
251
+ assert.match(context.selectedFiles.find((file) => file.path === "src/auth.ts")?.reason ?? "", /auth|task keyword/i);
252
+ });
253
+
254
+ test("analysis mode probes wildcard root configs and entrypoints beyond the first 200 root entries", (t) => {
255
+ const cwd = createTempRepo();
256
+ const fs = createRequire(import.meta.url)("node:fs") as any;
257
+ const originalOpendirSync = fs.opendirSync;
258
+ t.after(() => {
259
+ fs.opendirSync = originalOpendirSync;
260
+ syncBuiltinESMExports();
261
+ rmSync(cwd, { recursive: true, force: true });
262
+ });
263
+
264
+ for (let index = 0; index < 205; index++) {
265
+ writeTextFile(cwd, `root-${String(index).padStart(3, "0")}.txt`, `unrelated ${index}\n`);
266
+ }
267
+ writeTextFile(cwd, "app.py", "def main():\n return 'ok'\n");
268
+ writeTextFile(cwd, "custom-tool.config.py", "value = True\n");
269
+
270
+ const rootEntries = [
271
+ ...Array.from({ length: 200 }, (_, index) => ({
272
+ name: `root-${String(index).padStart(3, "0")}.txt`,
273
+ isFile: () => true,
274
+ isDirectory: () => false,
275
+ })),
276
+ { name: "app.py", isFile: () => true, isDirectory: () => false },
277
+ { name: "custom-tool.config.py", isFile: () => true, isDirectory: () => false },
278
+ ];
279
+
280
+ let opendirCalls = 0;
281
+ fs.opendirSync = (...args: any[]) => {
282
+ const [dirPath] = args as [string];
283
+ if (dirPath === cwd) {
284
+ opendirCalls += 1;
285
+ let entryIndex = 0;
286
+ return {
287
+ readSync: () => (entryIndex < rootEntries.length ? rootEntries[entryIndex++] : null),
288
+ closeSync: () => {},
289
+ };
290
+ }
291
+ return Reflect.apply(originalOpendirSync, fs, args);
292
+ };
293
+ syncBuiltinESMExports();
294
+
295
+ const context = assembleRepoContext(cwd, "Reverse engineer this app", "analysis", makeSignals());
296
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
297
+
298
+ assert.equal(opendirCalls, 1);
299
+ assert.equal(selectedPaths.length, 2);
300
+ assert.equal(selectedPaths[0], "custom-tool.config.py");
301
+ assert.equal(selectedPaths[1], "app.py");
302
+ assert.match(context.selectedFiles.find((file) => file.path === "custom-tool.config.py")?.reason ?? "", /config/i);
303
+ assert.match(context.selectedFiles.find((file) => file.path === "app.py")?.reason ?? "", /entrypoint/i);
304
+ });
305
+
306
+ test("analysis mode keeps top-level configs and explicit entrypoints ahead of the candidate cap", (t) => {
307
+ const cwd = createTempRepo();
308
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
309
+
310
+ writeTextFile(cwd, "tsconfig.json", "{\n \"compilerOptions\": {}\n}\n");
311
+ writeTextFile(cwd, "src/index.ts", "export function main() { return 'ok'; }\n");
312
+ for (let index = 0; index < 205; index++) {
313
+ writeTextFile(cwd, `src/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
314
+ }
315
+
316
+ const context = assembleRepoContext(
317
+ cwd,
318
+ "Reverse engineer this app",
319
+ "analysis",
320
+ makeSignals({
321
+ topLevelDirs: ["src"],
322
+ topLevelFiles: ["tsconfig.json"],
323
+ }),
324
+ );
325
+
326
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
327
+
328
+ assert.ok(selectedPaths.includes("tsconfig.json"));
329
+ assert.ok(selectedPaths.includes("src/index.ts"));
330
+ assert.equal(selectedPaths[0], "tsconfig.json");
331
+ assert.equal(selectedPaths[1], "src/index.ts");
332
+ assert.match(context.selectedFiles.find((file) => file.path === "src/index.ts")?.reason ?? "", /entrypoint/i);
333
+ });
334
+
335
+ test("analysis mode still selects src/index.ts when an earlier sibling directory exceeds the candidate cap", (t) => {
336
+ const cwd = createTempRepo();
337
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
338
+
339
+ writeTextFile(cwd, "src/index.ts", "export function main() { return 'ok'; }\n");
340
+ for (let index = 0; index < 205; index++) {
341
+ writeTextFile(cwd, `aaa/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
342
+ }
343
+
344
+ const context = assembleRepoContext(
345
+ cwd,
346
+ "Reverse engineer this app",
347
+ "analysis",
348
+ makeSignals({
349
+ topLevelDirs: ["aaa", "src"],
350
+ }),
351
+ );
352
+
353
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
354
+
355
+ assert.ok(selectedPaths.includes("src/index.ts"));
356
+ assert.match(context.selectedFiles.find((file) => file.path === "src/index.ts")?.reason ?? "", /entrypoint/i);
357
+ });
358
+
359
+
360
+ test("analysis mode keeps src/index.ts ahead of 205 unrelated root files", (t) => {
361
+ const cwd = createTempRepo();
362
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
363
+
364
+ writeTextFile(cwd, "src/index.ts", "export function main() { return 'ok'; }\n");
365
+ for (let index = 0; index < 205; index++) {
366
+ writeTextFile(cwd, `root-${String(index).padStart(3, "0")}.txt`, `unrelated ${index}\n`);
367
+ }
368
+
369
+ const context = assembleRepoContext(
370
+ cwd,
371
+ "Reverse engineer this app",
372
+ "analysis",
373
+ makeSignals({
374
+ topLevelDirs: ["src"],
375
+ }),
376
+ );
377
+
378
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
379
+
380
+ assert.ok(selectedPaths.includes("src/index.ts"));
381
+ assert.equal(selectedPaths[0], "src/index.ts");
382
+ assert.match(context.selectedFiles.find((file) => file.path === "src/index.ts")?.reason ?? "", /entrypoint/i);
383
+ });
384
+
385
+
386
+ test("fix mode ranks task-matching source and test files above unrelated files", (t) => {
387
+ const cwd = createTempRepo();
388
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
389
+
390
+ writeTextFile(cwd, "src/auth.ts", "export const auth = true;\n");
391
+ writeTextFile(cwd, "tests/auth.test.ts", "import assert from 'node:assert/strict';\n");
392
+ writeTextFile(cwd, "src/payments.ts", "export const payments = true;\n");
393
+
394
+ const context = assembleRepoContext(cwd, "Fix flaky auth tests", "fix", makeSignals({ topLevelDirs: ["src", "tests"] }));
395
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
396
+
397
+ assert.ok(selectedPaths.indexOf("src/auth.ts") >= 0);
398
+ assert.ok(selectedPaths.indexOf("tests/auth.test.ts") >= 0);
399
+ assert.ok(selectedPaths.indexOf("src/payments.ts") >= 0);
400
+ assert.ok(selectedPaths.indexOf("src/auth.ts") < selectedPaths.indexOf("src/payments.ts"));
401
+ assert.ok(selectedPaths.indexOf("tests/auth.test.ts") < selectedPaths.indexOf("src/payments.ts"));
402
+ assert.match(context.selectedFiles.find((file) => file.path === "src/auth.ts")?.reason ?? "", /auth|task keyword/i);
403
+ assert.match(context.selectedFiles.find((file) => file.path === "tests/auth.test.ts")?.reason ?? "", /test|auth|task keyword/i);
404
+ });
405
+
406
+ test("fix mode keeps src/auth.ts and tests/auth.test.ts ahead of 205 unrelated root files", (t) => {
407
+ const cwd = createTempRepo();
408
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
409
+
410
+ writeTextFile(cwd, "src/auth.ts", "export const auth = true;\n");
411
+ writeTextFile(cwd, "tests/auth.test.ts", "import assert from 'node:assert/strict';\n");
412
+ for (let index = 0; index < 205; index++) {
413
+ writeTextFile(cwd, `root-${String(index).padStart(3, "0")}.txt`, `unrelated ${index}\n`);
414
+ }
415
+
416
+ const context = assembleRepoContext(
417
+ cwd,
418
+ "Fix flaky auth tests",
419
+ "fix",
420
+ makeSignals({
421
+ topLevelDirs: ["src", "tests"],
422
+ }),
423
+ );
424
+
425
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
426
+
427
+ assert.ok(selectedPaths.includes("src/auth.ts"));
428
+ assert.ok(selectedPaths.includes("tests/auth.test.ts"));
429
+ assert.match(context.selectedFiles.find((file) => file.path === "src/auth.ts")?.reason ?? "", /auth|task keyword/i);
430
+ assert.match(context.selectedFiles.find((file) => file.path === "tests/auth.test.ts")?.reason ?? "", /test|auth|task keyword/i);
431
+ });
432
+
433
+ test("oversized selected files are truncated and total context stays within budget", (t) => {
434
+ const cwd = createTempRepo();
435
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
436
+
437
+ const huge = "x".repeat(9_500);
438
+ writeTextFile(cwd, "README.md", huge);
439
+ writeTextFile(cwd, "package.json", huge);
440
+ writeTextFile(cwd, "src/index.ts", huge);
441
+ writeTextFile(cwd, "src/auth.ts", huge);
442
+ writeTextFile(cwd, "tests/auth.test.ts", huge);
443
+ writeTextFile(cwd, "src/router.ts", huge);
444
+
445
+ const context = assembleRepoContext(
446
+ cwd,
447
+ "Fix flaky auth tests",
448
+ "fix",
449
+ makeSignals({ packageManager: "npm", testCommand: "npm test", lintCommand: "npm run lint", hasGit: true, topLevelDirs: ["src", "tests"], topLevelFiles: ["README.md", "package.json"] }),
450
+ );
451
+
452
+ const totalBytes = context.selectedFiles.reduce((sum, file) => sum + Buffer.byteLength(file.content, "utf8"), 0);
453
+ assert.ok(context.selectedFiles.length <= 6);
454
+ assert.ok(context.selectedFiles.some((file) => Buffer.byteLength(file.content, "utf8") === 8_000));
455
+ assert.ok(context.selectedFiles.every((file) => Buffer.byteLength(file.content, "utf8") <= 8_000));
456
+ assert.ok(totalBytes <= 40_000);
457
+ });
458
+
459
+ test("analysis mode keeps deep sibling trees from being recursively scored before selection", (t) => {
460
+ const cwd = createTempRepo();
461
+ const fs = createRequire(import.meta.url)("node:fs") as any;
462
+ const originalOpendirSync = fs.opendirSync;
463
+ t.after(() => {
464
+ fs.opendirSync = originalOpendirSync;
465
+ syncBuiltinESMExports();
466
+ rmSync(cwd, { recursive: true, force: true });
467
+ });
468
+
469
+ writeTextFile(cwd, "src/index.ts", "export function main() { return 'ok'; }\n");
470
+ for (let index = 0; index < 205; index++) {
471
+ writeTextFile(cwd, `src/file-${String(index).padStart(3, "0")}.ts`, `export const value${index} = ${index};\n`);
472
+ }
473
+
474
+ for (const first of ["one", "two", "three"]) {
475
+ for (const second of ["one", "two", "three"]) {
476
+ for (const third of ["one", "two", "three"]) {
477
+ mkdirSync(join(cwd, "aaa", first, second, third), { recursive: true });
478
+ }
479
+ }
480
+ }
481
+ writeTextFile(cwd, "aaa/one/two/three/deep.ts", "export const deep = true;\n");
482
+
483
+ let opendirCalls = 0;
484
+ fs.opendirSync = (...args: any[]) => {
485
+ opendirCalls += 1;
486
+ return Reflect.apply(originalOpendirSync, fs, args);
487
+ };
488
+ syncBuiltinESMExports();
489
+
490
+ const context = assembleRepoContext(cwd, "Reverse engineer this app", "analysis", makeSignals({ topLevelDirs: ["aaa", "src"] }));
491
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
492
+
493
+ assert.ok(selectedPaths.includes("src/index.ts"));
494
+ assert.equal(selectedPaths[0], "src/index.ts");
495
+ assert.ok(opendirCalls <= 6, `expected bounded directory scan, saw ${opendirCalls} opendir calls`);
496
+ });
497
+
498
+ test("file loading reads only the configured byte limit", (t) => {
499
+ const cwd = createTempRepo();
500
+ const fs = createRequire(import.meta.url)("node:fs") as any;
501
+ const originalReadSync = fs.readSync;
502
+ t.after(() => {
503
+ fs.readSync = originalReadSync;
504
+ syncBuiltinESMExports();
505
+ rmSync(cwd, { recursive: true, force: true });
506
+ });
507
+
508
+ writeTextFile(cwd, "README.md", "x".repeat(9_500));
509
+
510
+ let readCalls = 0;
511
+ let maxRequestedLength = 0;
512
+ fs.readSync = (...args: any[]) => {
513
+ readCalls += 1;
514
+ if (typeof args[3] === "number") {
515
+ maxRequestedLength = Math.max(maxRequestedLength, args[3]);
516
+ }
517
+ return Reflect.apply(originalReadSync, fs, args);
518
+ };
519
+ syncBuiltinESMExports();
520
+
521
+ const context = assembleRepoContext(cwd, "Reverse engineer this app", "analysis", makeSignals({ topLevelFiles: ["README.md"] }));
522
+
523
+ assert.equal(context.selectedFiles.length, 1);
524
+ assert.equal(readCalls, 1);
525
+ assert.equal(maxRequestedLength, 8_000);
526
+ assert.equal(Buffer.byteLength(context.selectedFiles[0]?.content ?? "", "utf8"), 8_000);
527
+ });
528
+
529
+ test("secret-like files are excluded from selected files", (t) => {
530
+ const cwd = createTempRepo();
531
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
532
+
533
+ writeTextFile(cwd, "README.md", "# Demo\n");
534
+ writeTextFile(cwd, "src/index.ts", "export {};\n");
535
+ writeTextFile(cwd, ".env", "TOKEN=one\n");
536
+ writeTextFile(cwd, ".env.local", "TOKEN=two\n");
537
+ writeTextFile(cwd, ".npmrc", "registry=https://example.invalid\n");
538
+ writeTextFile(cwd, ".pypirc", "[distutils]\nindex-servers = test\n");
539
+ writeTextFile(cwd, ".netrc", "machine example.invalid login user password secret\n");
540
+ writeTextFile(cwd, "keys/server.pem", "pem\n");
541
+ writeTextFile(cwd, "keys/private.key", "key\n");
542
+ writeTextFile(cwd, "keys/release.asc", "asc\n");
543
+ writeTextFile(cwd, "ops-secrets/config.json", "{}\n");
544
+ writeTextFile(cwd, "credentials-prod/token.txt", "token\n");
545
+ writeTextFile(cwd, "config/secrets/prod.json", "{}\n");
546
+ writeTextFile(cwd, "config/credentials/service.json", "{}\n");
547
+ writeTextFile(cwd, ".aws/config", "[default]\nregion = us-west-2\n");
548
+ writeTextFile(cwd, ".ssh/id_rsa", "private-key\n");
549
+
550
+ const context = assembleRepoContext(
551
+ cwd,
552
+ "Reverse engineer this app",
553
+ "analysis",
554
+ makeSignals({
555
+ topLevelDirs: ["src", "keys", "ops-secrets", "credentials-prod", "config", ".aws", ".ssh"],
556
+ topLevelFiles: ["README.md", "ops-secrets/config.json", "credentials-prod/token.txt", ".env", ".env.local", ".npmrc", ".pypirc", ".netrc"],
557
+ }),
558
+ );
559
+
560
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
561
+ const secretLikePaths = [
562
+ ".env",
563
+ ".env.local",
564
+ ".npmrc",
565
+ ".pypirc",
566
+ ".netrc",
567
+ "keys/server.pem",
568
+ "keys/private.key",
569
+ "keys/release.asc",
570
+ "ops-secrets/config.json",
571
+ "credentials-prod/token.txt",
572
+ "config/secrets/prod.json",
573
+ "config/credentials/service.json",
574
+ ".aws/config",
575
+ ".ssh/id_rsa",
576
+ ];
577
+
578
+ assert.ok(selectedPaths.includes("README.md"));
579
+ assert.ok(selectedPaths.includes("src/index.ts"));
580
+ for (const path of secretLikePaths) {
581
+ assert.ok(!selectedPaths.includes(path), `unexpected secret-like file selected: ${path}`);
582
+ }
583
+ for (const token of [".env", ".npmrc", ".ssh", "secrets", "credentials", "ops-secrets", "credentials-prod", "release.asc"]) {
584
+ assert.ok(context.summaryLines.every((line) => !line.includes(token)), `unexpected secret-like token in summary lines: ${token}`);
585
+ }
586
+ });
587
+
588
+ test("similarly named source files stay visible while exact secret directories stay hidden", (t) => {
589
+ const cwd = createTempRepo();
590
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
591
+
592
+ writeTextFile(cwd, "README.md", "# Demo\n");
593
+ writeTextFile(cwd, "src/secretary.ts", "export const secretary = true;\n");
594
+ writeTextFile(cwd, "src/credential-form.tsx", "export const form = true;\n");
595
+ writeTextFile(cwd, "config/secrets/prod.json", "{}\n");
596
+ writeTextFile(cwd, "config/credentials/service.json", "{}\n");
597
+
598
+ const context = assembleRepoContext(
599
+ cwd,
600
+ "Reverse engineer this app",
601
+ "analysis",
602
+ makeSignals({
603
+ topLevelDirs: ["src", "config"],
604
+ topLevelFiles: ["README.md", "src/secretary.ts", "src/credential-form.tsx", "config/secrets/prod.json", "config/credentials/service.json"],
605
+ }),
606
+ );
607
+
608
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
609
+
610
+ assert.ok(selectedPaths.includes("README.md"));
611
+ assert.ok(selectedPaths.includes("src/secretary.ts"));
612
+ assert.ok(selectedPaths.includes("src/credential-form.tsx"));
613
+ for (const path of ["config/secrets/prod.json", "config/credentials/service.json"]) {
614
+ assert.ok(!selectedPaths.includes(path), `unexpected secret-like file selected: ${path}`);
615
+ }
616
+ });
617
+
618
+ test("basename .env* files are excluded from selected files and summaries", (t) => {
619
+ const cwd = createTempRepo();
620
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
621
+
622
+ writeTextFile(cwd, "README.md", "# Demo\n");
623
+ writeTextFile(cwd, "src/index.ts", "export {};\n");
624
+ writeTextFile(cwd, ".envrc", "export TOKEN=one\n");
625
+ writeTextFile(cwd, ".env.production", "TOKEN=two\n");
626
+ writeTextFile(cwd, ".envrc.local", "export TOKEN=three\n");
627
+
628
+ const context = assembleRepoContext(
629
+ cwd,
630
+ "Reverse engineer this app",
631
+ "analysis",
632
+ makeSignals({
633
+ topLevelDirs: ["src"],
634
+ topLevelFiles: ["README.md", ".envrc", ".env.production", ".envrc.local"],
635
+ }),
636
+ );
637
+
638
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
639
+
640
+ assert.ok(selectedPaths.includes("README.md"));
641
+ assert.ok(selectedPaths.includes("src/index.ts"));
642
+ for (const path of [".envrc", ".env.production", ".envrc.local"]) {
643
+ assert.ok(!selectedPaths.includes(path), `unexpected secret-like file selected: ${path}`);
644
+ }
645
+ for (const token of [".envrc", ".env.production", ".envrc.local"]) {
646
+ assert.ok(context.summaryLines.every((line) => !line.includes(token)), `unexpected secret-like token in summary lines: ${token}`);
647
+ }
648
+ });
649
+
650
+ test("excluded directories never contribute selected files", (t) => {
651
+ const cwd = createTempRepo();
652
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
653
+
654
+ writeTextFile(cwd, "README.md", "# Demo\n");
655
+ writeTextFile(cwd, "src/index.ts", "export {};\n");
656
+ writeTextFile(cwd, ".git/config", "ignored\n");
657
+ writeTextFile(cwd, "node_modules/ignored.js", "ignored\n");
658
+ writeTextFile(cwd, "dist/bundle.js", "ignored\n");
659
+ writeTextFile(cwd, "build/output.js", "ignored\n");
660
+ writeTextFile(cwd, "coverage/report.txt", "ignored\n");
661
+ writeTextFile(cwd, ".next/server.js", "ignored\n");
662
+
663
+ const context = assembleRepoContext(cwd, "Reverse engineer this app", "analysis", makeSignals({ topLevelDirs: ["src", "node_modules", "dist", "build", "coverage", ".next"], topLevelFiles: ["README.md"] }));
664
+ const selectedPaths = context.selectedFiles.map((file) => file.path);
665
+
666
+ assert.ok(selectedPaths.every((path) => !path.startsWith(".git/")));
667
+ assert.ok(selectedPaths.every((path) => !path.startsWith("node_modules/")));
668
+ assert.ok(selectedPaths.every((path) => !path.startsWith("dist/")));
669
+ assert.ok(selectedPaths.every((path) => !path.startsWith("build/")));
670
+ assert.ok(selectedPaths.every((path) => !path.startsWith("coverage/")));
671
+ assert.ok(selectedPaths.every((path) => !path.startsWith(".next/")));
672
+ });