@bastani/atomic 0.6.3-0 → 0.6.4-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 (49) hide show
  1. package/.agents/skills/ast-grep/SKILL.md +323 -0
  2. package/.agents/skills/ast-grep/references/rule_reference.md +297 -0
  3. package/.agents/skills/ripgrep/SKILL.md +382 -0
  4. package/.mcp.json +5 -6
  5. package/dist/commands/cli/claude-inflight-hook.d.ts +100 -0
  6. package/dist/commands/cli/claude-inflight-hook.d.ts.map +1 -0
  7. package/dist/commands/cli/claude-stop-hook.d.ts +2 -0
  8. package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -1
  9. package/dist/lib/spawn.d.ts +1 -1
  10. package/dist/lib/spawn.d.ts.map +1 -1
  11. package/dist/sdk/providers/claude.d.ts +36 -0
  12. package/dist/sdk/providers/claude.d.ts.map +1 -1
  13. package/dist/sdk/providers/copilot.d.ts +17 -1
  14. package/dist/sdk/providers/copilot.d.ts.map +1 -1
  15. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  16. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +49 -34
  17. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  18. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +18 -16
  19. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  20. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts +43 -0
  21. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts.map +1 -0
  22. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +30 -0
  23. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
  24. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +2 -1
  25. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -1
  26. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +18 -16
  27. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  28. package/dist/services/config/additional-instructions.d.ts +67 -0
  29. package/dist/services/config/additional-instructions.d.ts.map +1 -0
  30. package/package.json +3 -1
  31. package/src/cli.ts +18 -1
  32. package/src/commands/cli/chat/index.ts +52 -2
  33. package/src/commands/cli/claude-inflight-hook.test.ts +598 -0
  34. package/src/commands/cli/claude-inflight-hook.ts +359 -0
  35. package/src/commands/cli/claude-stop-hook.ts +40 -4
  36. package/src/commands/cli/init/index.ts +9 -0
  37. package/src/lib/spawn.ts +6 -2
  38. package/src/sdk/providers/claude.ts +131 -0
  39. package/src/sdk/providers/copilot.ts +30 -1
  40. package/src/sdk/runtime/executor.ts +43 -2
  41. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +318 -158
  42. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +253 -129
  43. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/batching.ts +65 -0
  44. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/ignore-by-default.d.ts +8 -0
  45. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +203 -12
  46. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +248 -78
  47. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +258 -146
  48. package/src/services/config/additional-instructions.ts +273 -0
  49. package/src/services/system/auto-sync.ts +10 -1
@@ -2,33 +2,44 @@
2
2
  * deep-research-codebase / claude
3
3
  *
4
4
  * A deterministically-orchestrated, distributed codebase researcher built on
5
- * the Claude Agent SDK's native sub-agent dispatch. Specialist sub-agents
6
- * (codebase-locator / codebase-pattern-finder / codebase-analyzer /
7
- * codebase-online-researcher / codebase-research-locator /
8
- * codebase-research-analyzer) are spawned as separate headless `ctx.stage()`
9
- * calls each binds the SDK's `agent` option to the desired specialist
10
- * instead of relying on a coordinator agent that dispatches them via the
11
- * `@"name (agent)"` prompt syntax.
5
+ * the Claude Agent SDK with **batched** Task-tool fan-out. Specialist
6
+ * sub-agents (codebase-locator / codebase-pattern-finder / codebase-analyzer
7
+ * / codebase-online-researcher) run inside batch sessions: each batch is a
8
+ * single `ctx.stage()` whose orchestrator turn dispatches up to
9
+ * MAX_TASKS_PER_BATCH (≈10) specialists in parallel via the Task tool.
10
+ * Research-history specialists (codebase-research-locator /
11
+ * codebase-research-analyzer) remain as their own small pipeline since they
12
+ * have a strict sequential dependency and run only twice per workflow.
12
13
  *
13
- * Why SDK primitives instead of in-prompt orchestration:
14
+ * Why batched Task-tool dispatch instead of one ctx.stage per specialist:
14
15
  *
15
- * • Each specialist runs in an ISOLATED conversation. The locator's giant
16
- * file index doesn't pollute the analyzer's context window, and the
17
- * online-researcher doesn't see the analyzer's reasoning at all. This is
18
- * `multi-agent-patterns` swarm-style isolation, not orchestrator-style.
16
+ * • SDK-level fan-out scales by codebase size at 5K LOC per partition
17
+ * and 4 specialists per partition, a 750K-LOC codebase would otherwise
18
+ * spawn 600 `claude` subprocesses. Batches of 10 cap that at ~60 SDK
19
+ * sessions, with each session internally fanning out via Task tool.
19
20
  *
20
- * • There is no orchestrator turn whose context grows linearly with the
21
- * number of specialists. Token cost per partition is bounded by the four
22
- * specialists' independent prompts adding more partitions scales
23
- * cleanly because every fan-out is a fresh session.
21
+ * • ~10 parallel Task tool sub-agents per single message is the practical
22
+ * ceiling before rate limits, context contention, and degraded
23
+ * coordination kick in (no documented hard cap; tunable in
24
+ * helpers/batching.ts).
24
25
  *
25
- * • Failure of one specialist does not abort the partition mid-thought
26
- * the runtime fails the stage, but its siblings' outputs are still on
27
- * disk and the aggregator can continue with whatever completed.
26
+ * • Sub-agents still run in ISOLATED contexts Task tool gives every
27
+ * sub-agent its own conversation window, so the locator's file index
28
+ * doesn't pollute the analyzer the way it would inside a shared
29
+ * conversation. (multi-agent-patterns swarm isolation.)
28
30
  *
29
- * • The synthesis step that combines specialist outputs is plain TypeScript
30
- * (`renderExplorerMarkdown` in helpers/scratch.ts) no extra LLM call
31
- * just to concatenate four markdown sections.
31
+ * • The orchestrator's turn does NOT grow linearly with sub-agent count:
32
+ * each Task sub-agent writes its verbatim findings to a per-task scratch
33
+ * file and returns just "DONE", so the orchestrator collects N short
34
+ * confirmations rather than N transcripts (filesystem-context skill).
35
+ *
36
+ * • Failure isolation is preserved at two levels: (1) Promise.allSettled
37
+ * around the batches means one failed batch doesn't abort siblings;
38
+ * (2) inside a batch, the orchestrator is instructed not to retry
39
+ * failed Task sub-agents — the synthesis step tolerates missing files.
40
+ *
41
+ * • Synthesis remains plain TypeScript (`renderExplorerMarkdown` in
42
+ * helpers/scratch.ts) — no extra LLM call just to concatenate sections.
32
43
  *
33
44
  * Topology:
34
45
  *
@@ -38,27 +49,31 @@
38
49
  * │
39
50
  * ▼
40
51
  * ┌──────────────────────────────────────────────────────────────────────┐
41
- * │ Per-partition (Promise.all over partitions, all stages headless):
42
- * │
43
- * │ locator-i ∥ pattern-finder-i (Layer 1, parallel)
44
- * │
45
- * │
46
- * │ analyzer-i ∥ online-researcher-i (Layer 2, parallel)
47
- * │ │
48
- * │
49
- * │ deterministic write to scratch file (TS helper, no LLM)
52
+ * │ Wave 1 (locator + pattern-finder, no inter-deps):
53
+ * │ wave1-batch-1 ∥ wave1-batch-2 ∥ ... (Promise.allSettled)
54
+ * │ └── each batch session: orchestrator dispatches ≤10 Task
55
+ * │ sub-agents in one assistant message; each writes to disk
56
+ * │
57
+ * │
58
+ * │ TS reads locator-i.md files from disk for Layer 2 prompts
59
+ * │
60
+ * │
61
+ * │ Wave 2 (analyzer + online-researcher, embed locator output): │
62
+ * │ wave2-batch-1 ∥ wave2-batch-2 ∥ ... (Promise.allSettled) │
63
+ * │ │ │
64
+ * │ ▼ │
65
+ * │ Per partition i: TS reads 4 specialist files + writes explorer-i.md │
50
66
  * └──────────────────────────────────────────────────────────────────────┘
51
67
  * │
52
68
  * ▼
53
69
  * aggregator (visible)
54
70
  *
55
- * Specialist stages run headless (in-process via the Agent SDK's `query()`),
56
- * so they are transparent to the workflow graph. The visible nodes are just:
57
- * parent → [codebase-scout] → aggregator
71
+ * Batch sessions are headless (transparent to the workflow graph). Visible
72
+ * nodes: parent [codebase-scout] aggregator.
58
73
  */
59
74
 
60
75
  import { defineWorkflow, extractAssistantText } from "../../../index.ts";
61
- import { mkdir } from "node:fs/promises";
76
+ import { mkdir, readFile } from "node:fs/promises";
62
77
  import path from "node:path";
63
78
 
64
79
  import {
@@ -73,6 +88,7 @@ import {
73
88
  import {
74
89
  buildAggregatorPrompt,
75
90
  buildAnalyzerPrompt,
91
+ buildBatchOrchestratorPrompt,
76
92
  buildHistoryAnalyzerPrompt,
77
93
  buildHistoryLocatorPrompt,
78
94
  buildLocatorPrompt,
@@ -80,19 +96,67 @@ import {
80
96
  buildPatternFinderPrompt,
81
97
  buildScoutPrompt,
82
98
  slugifyPrompt,
99
+ wrapPromptForTaskDispatch,
83
100
  } from "../helpers/prompts.ts";
84
101
  import { writeExplorerScratchFile } from "../helpers/scratch.ts";
102
+ import {
103
+ chunkBatches,
104
+ MAX_TASKS_PER_BATCH,
105
+ SUBAGENT_TYPE,
106
+ type Layer1Task,
107
+ type Layer2Task,
108
+ } from "../helpers/batching.ts";
85
109
 
86
110
  /**
87
111
  * Shared SDK options for every sub-agent dispatch. `permissionMode` +
88
112
  * `allowDangerouslySkipPermissions` are required so the headless sub-agents
89
- * can use Read/Grep/Glob/Bash without prompting (we are running unattended).
113
+ * can use Read/Grep/Glob/Bash/Write/Task without prompting (we are running
114
+ * unattended).
90
115
  */
91
116
  const SUBAGENT_OPTS = {
92
117
  permissionMode: "bypassPermissions",
93
118
  allowDangerouslySkipPermissions: true,
94
119
  } as const;
95
120
 
121
+ /**
122
+ * SDK options for batch sessions. Pin the dispatcher to the `orchestrator`
123
+ * agent (.claude/agents/orchestrator.md) — its system prompt is purpose-built
124
+ * to delegate everything to sub-agents via the Task tool, and its tool list
125
+ * (`Bash, Agent, Edit, Grep, Glob, Read, Task*`) excludes Write/etc., so the
126
+ * dispatcher cannot wander off and start doing the specialists' work itself.
127
+ * The orchestrator agent definition pins `model: opus`; override here to
128
+ * Sonnet if the per-batch dispatcher cost matters more than reliability.
129
+ */
130
+ const BATCH_DISPATCHER_OPTS = {
131
+ ...SUBAGENT_OPTS,
132
+ agent: "orchestrator",
133
+ } as const;
134
+
135
+ /** Read a file as UTF-8, returning empty string if missing or unreadable. */
136
+ async function safeReadFile(absPath: string): Promise<string> {
137
+ try {
138
+ return await readFile(absPath, "utf8");
139
+ } catch {
140
+ return "";
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Log Promise.allSettled rejection reasons to stderr so an all-failed wave
146
+ * leaves a debugging trail instead of silently producing an empty report.
147
+ */
148
+ function logBatchRejections(
149
+ label: string,
150
+ results: PromiseSettledResult<unknown>[],
151
+ ): void {
152
+ for (let i = 0; i < results.length; i++) {
153
+ const r = results[i];
154
+ if (r?.status === "rejected") {
155
+ console.error(`[deep-research-codebase] ${label} batch ${i + 1} failed:`, r.reason);
156
+ }
157
+ }
158
+ }
159
+
96
160
  export default defineWorkflow({
97
161
  name: "deep-research-codebase",
98
162
  description:
@@ -135,7 +199,9 @@ export default defineWorkflow({
135
199
  if (data.units.length === 0) {
136
200
  throw new Error(
137
201
  `deep-research-codebase: scout found no source files under ${root}. ` +
138
- `Run from inside a code repository or check the CODE_EXTENSIONS list.`,
202
+ `Run from inside a code repository, or verify your files use a ` +
203
+ `recognized programming-language extension (sourced from GitHub ` +
204
+ `Linguist + sql/graphql/proto).`,
139
205
  );
140
206
  }
141
207
 
@@ -231,139 +297,233 @@ export default defineWorkflow({
231
297
  // read is safe (failure-modes F13).
232
298
  const scoutOverview = (await ctx.transcript(scout)).content;
233
299
 
234
- // ── Stage 2: per-partition specialist fan-out ─────────────────────────
300
+ // ── Stage 2: batched specialist fan-out ───────────────────────────────
301
+ //
302
+ // Two waves, each chunked into batches of MAX_TASKS_PER_BATCH (≈10):
235
303
  //
236
- // Per partition i:
237
- // Layer 1 (parallel): locator-i ∥ pattern-finder-i
238
- // Layer 2 (parallel): analyzer-i ∥ online-researcher-i ← depend on locator-i
239
- // Synthesis (deterministic TS): renderExplorerMarkdown → scratch file
304
+ // Wave 1: locator + pattern-finder (no inter-task dependencies)
305
+ // Wave 2: analyzer + online-researcher (read locator output from disk)
240
306
  //
241
- // All N partitions run as parallel branches of the outer Promise.all.
242
- // Sub-agent stages are headless: invisible in the graph and bounded only
243
- // by SDK concurrency. Information flow is forward-only and all context
244
- // each specialist needs (research question, scope, scout overview, and —
245
- // for layer 2 locator output) is injected into the first prompt.
307
+ // Each batch is a single headless ctx.stage whose orchestrator turn
308
+ // dispatches all of its tasks via the Task tool in one assistant
309
+ // message. Specialists write their verbatim findings to per-task
310
+ // scratch files; the orchestrator only sees per-task "DONE" tokens, so
311
+ // batch session context stays bounded by O(tasks_per_batch). Synthesis
312
+ // reads the per-task files from disk.
313
+
314
+ // Per-partition output paths, computed once and reused across both wave
315
+ // task-list construction and synthesis. Specialists write these;
316
+ // synthesis reads them.
317
+ const partitionPaths = partitions.map((_, idx) => {
318
+ const i = idx + 1;
319
+ return {
320
+ locator: path.join(scratchDir, `locator-${i}.md`),
321
+ patternFinder: path.join(scratchDir, `pattern-finder-${i}.md`),
322
+ analyzer: path.join(scratchDir, `analyzer-${i}.md`),
323
+ online: path.join(scratchDir, `online-${i}.md`),
324
+ explorer: path.join(scratchDir, `explorer-${i}.md`),
325
+ };
326
+ });
327
+
328
+ // Wave 1 task list — flat across partitions and specialist kinds so the
329
+ // chunker can fill batches uniformly. Mixed-kind batches are fine: the
330
+ // Task tool's `subagent_type` is set per call inside the orchestrator.
331
+ const wave1Tasks: Layer1Task[] = partitions.flatMap((partition, idx) => {
332
+ const i = idx + 1;
333
+ const paths = partitionPaths[idx]!;
334
+ return [
335
+ {
336
+ kind: "locator" as const,
337
+ partitionIndex: i,
338
+ partition,
339
+ outputPath: paths.locator,
340
+ },
341
+ {
342
+ kind: "pattern-finder" as const,
343
+ partitionIndex: i,
344
+ partition,
345
+ outputPath: paths.patternFinder,
346
+ },
347
+ ];
348
+ });
349
+
350
+ const wave1Batches = chunkBatches(wave1Tasks, MAX_TASKS_PER_BATCH);
351
+
352
+ // Wave 1: dispatch all batches in parallel. allSettled so a single batch
353
+ // failure doesn't abort siblings — synthesis tolerates missing files.
354
+ // Rejection reasons are logged so an empty report is debuggable; without
355
+ // this, an all-failed wave would silently produce a confidently empty doc.
356
+ const wave1Results = await Promise.allSettled(
357
+ wave1Batches.map((batch, batchIdx) => {
358
+ const batchNumber = batchIdx + 1;
359
+ return ctx.stage(
360
+ {
361
+ name: `wave1-batch-${batchNumber}`,
362
+ headless: true,
363
+ description: `Layer 1 dispatch (${batch.length} tasks)`,
364
+ },
365
+ {},
366
+ {},
367
+ async (s) => {
368
+ const taskSpecs = batch.map((t) => {
369
+ const builder =
370
+ t.kind === "locator" ? buildLocatorPrompt : buildPatternFinderPrompt;
371
+ const specialistPrompt = builder({
372
+ question: prompt,
373
+ partition: t.partition,
374
+ scoutOverview,
375
+ index: t.partitionIndex,
376
+ total: explorerCount,
377
+ });
378
+ return {
379
+ subagentType: SUBAGENT_TYPE[t.kind],
380
+ outputPath: t.outputPath,
381
+ prompt: wrapPromptForTaskDispatch({
382
+ specialistPrompt,
383
+ outputPath: t.outputPath,
384
+ agentLabel: t.kind.toUpperCase().replaceAll("-", "_"),
385
+ }),
386
+ };
387
+ });
388
+
389
+ await s.session.query(
390
+ buildBatchOrchestratorPrompt({
391
+ wave: 1,
392
+ batchIndex: batchNumber,
393
+ totalBatches: wave1Batches.length,
394
+ tasks: taskSpecs,
395
+ }),
396
+ BATCH_DISPATCHER_OPTS,
397
+ );
398
+ s.save(s.sessionId);
399
+ },
400
+ );
401
+ }),
402
+ );
403
+ logBatchRejections("wave1", wave1Results);
404
+
405
+ // Read locator outputs from disk for Wave 2 prompts. Layer 2 specialists
406
+ // embed the locator's verbatim output rather than re-discovering it.
407
+ const locatorOutputs: Map<number, string> = new Map();
408
+ await Promise.all(
409
+ partitions.map(async (_p, idx) => {
410
+ const i = idx + 1;
411
+ locatorOutputs.set(i, await safeReadFile(partitionPaths[idx]!.locator));
412
+ }),
413
+ );
414
+
415
+ const wave2Tasks: Layer2Task[] = partitions.flatMap((partition, idx) => {
416
+ const i = idx + 1;
417
+ const paths = partitionPaths[idx]!;
418
+ const locatorOutput = locatorOutputs.get(i) ?? "";
419
+ return [
420
+ {
421
+ kind: "analyzer" as const,
422
+ partitionIndex: i,
423
+ partition,
424
+ outputPath: paths.analyzer,
425
+ locatorOutput,
426
+ },
427
+ {
428
+ kind: "online-researcher" as const,
429
+ partitionIndex: i,
430
+ partition,
431
+ outputPath: paths.online,
432
+ locatorOutput,
433
+ },
434
+ ];
435
+ });
436
+
437
+ const wave2Batches = chunkBatches(wave2Tasks, MAX_TASKS_PER_BATCH);
438
+
439
+ const wave2Results = await Promise.allSettled(
440
+ wave2Batches.map((batch, batchIdx) => {
441
+ const batchNumber = batchIdx + 1;
442
+ return ctx.stage(
443
+ {
444
+ name: `wave2-batch-${batchNumber}`,
445
+ headless: true,
446
+ description: `Layer 2 dispatch (${batch.length} tasks)`,
447
+ },
448
+ {},
449
+ {},
450
+ async (s) => {
451
+ const taskSpecs = batch.map((t) => {
452
+ const specialistPrompt =
453
+ t.kind === "analyzer"
454
+ ? buildAnalyzerPrompt({
455
+ question: prompt,
456
+ partition: t.partition,
457
+ locatorOutput: t.locatorOutput,
458
+ scoutOverview,
459
+ index: t.partitionIndex,
460
+ total: explorerCount,
461
+ })
462
+ : buildOnlineResearcherPrompt({
463
+ question: prompt,
464
+ partition: t.partition,
465
+ locatorOutput: t.locatorOutput,
466
+ index: t.partitionIndex,
467
+ total: explorerCount,
468
+ });
469
+ return {
470
+ subagentType: SUBAGENT_TYPE[t.kind],
471
+ outputPath: t.outputPath,
472
+ prompt: wrapPromptForTaskDispatch({
473
+ specialistPrompt,
474
+ outputPath: t.outputPath,
475
+ agentLabel: t.kind.toUpperCase().replaceAll("-", "_"),
476
+ }),
477
+ };
478
+ });
479
+
480
+ await s.session.query(
481
+ buildBatchOrchestratorPrompt({
482
+ wave: 2,
483
+ batchIndex: batchNumber,
484
+ totalBatches: wave2Batches.length,
485
+ tasks: taskSpecs,
486
+ }),
487
+ BATCH_DISPATCHER_OPTS,
488
+ );
489
+ s.save(s.sessionId);
490
+ },
491
+ );
492
+ }),
493
+ );
494
+ logBatchRejections("wave2", wave2Results);
495
+
496
+ // Synthesis: read all four specialist files per partition, then write
497
+ // the consolidated explorer scratch file the aggregator consumes.
498
+ // Missing files fall back to "" so the synthesis tolerates partial
499
+ // batch failures — the aggregator's prompt already handles empty
500
+ // sections via _(no … produced)_ placeholders in renderExplorerMarkdown.
246
501
  const explorerHandles = await Promise.all(
247
502
  partitions.map(async (partition, idx) => {
248
503
  const i = idx + 1;
249
- const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
250
-
251
- // Layer 1: locator + pattern-finder run independently.
252
- const [locator, patternFinder] = await Promise.all([
253
- ctx.stage(
254
- {
255
- name: `locator-${i}`,
256
- headless: true,
257
- description: `codebase-locator over partition ${i}`,
258
- },
259
- {},
260
- {},
261
- async (s) => {
262
- const result = await s.session.query(
263
- buildLocatorPrompt({
264
- question: prompt,
265
- partition,
266
- scoutOverview,
267
- index: i,
268
- total: explorerCount,
269
- }),
270
- { agent: "codebase-locator", ...SUBAGENT_OPTS },
271
- );
272
- s.save(s.sessionId);
273
- return extractAssistantText(result, 0);
274
- },
275
- ),
276
- ctx.stage(
277
- {
278
- name: `pattern-finder-${i}`,
279
- headless: true,
280
- description: `codebase-pattern-finder over partition ${i}`,
281
- },
282
- {},
283
- {},
284
- async (s) => {
285
- const result = await s.session.query(
286
- buildPatternFinderPrompt({
287
- question: prompt,
288
- partition,
289
- scoutOverview,
290
- index: i,
291
- total: explorerCount,
292
- }),
293
- { agent: "codebase-pattern-finder", ...SUBAGENT_OPTS },
294
- );
295
- s.save(s.sessionId);
296
- return extractAssistantText(result, 0);
297
- },
298
- ),
299
- ]);
300
-
301
- const locatorOutput = locator.result;
302
- const patternsOutput = patternFinder.result;
303
-
304
- // Layer 2: analyzer + online-researcher both consume locator output.
305
- const [analyzer, onlineResearcher] = await Promise.all([
306
- ctx.stage(
307
- {
308
- name: `analyzer-${i}`,
309
- headless: true,
310
- description: `codebase-analyzer over partition ${i}`,
311
- },
312
- {},
313
- {},
314
- async (s) => {
315
- const result = await s.session.query(
316
- buildAnalyzerPrompt({
317
- question: prompt,
318
- partition,
319
- locatorOutput,
320
- scoutOverview,
321
- index: i,
322
- total: explorerCount,
323
- }),
324
- { agent: "codebase-analyzer", ...SUBAGENT_OPTS },
325
- );
326
- s.save(s.sessionId);
327
- return extractAssistantText(result, 0);
328
- },
329
- ),
330
- ctx.stage(
331
- {
332
- name: `online-researcher-${i}`,
333
- headless: true,
334
- description: `codebase-online-researcher over partition ${i}`,
335
- },
336
- {},
337
- {},
338
- async (s) => {
339
- const result = await s.session.query(
340
- buildOnlineResearcherPrompt({
341
- question: prompt,
342
- partition,
343
- locatorOutput,
344
- index: i,
345
- total: explorerCount,
346
- }),
347
- { agent: "codebase-online-researcher", ...SUBAGENT_OPTS },
348
- );
349
- s.save(s.sessionId);
350
- return extractAssistantText(result, 0);
351
- },
352
- ),
353
- ]);
354
-
355
- // Deterministic synthesis — no fifth LLM call just to concatenate.
356
- await writeExplorerScratchFile(scratchPath, {
504
+ const paths = partitionPaths[idx]!;
505
+
506
+ const [locatorOutput, patternsOutput, analyzerOutput, onlineOutput] =
507
+ await Promise.all([
508
+ // Layer 1 locator was already read into locatorOutputs above —
509
+ // reuse it instead of re-reading from disk.
510
+ Promise.resolve(locatorOutputs.get(i) ?? ""),
511
+ safeReadFile(paths.patternFinder),
512
+ safeReadFile(paths.analyzer),
513
+ safeReadFile(paths.online),
514
+ ]);
515
+
516
+ await writeExplorerScratchFile(paths.explorer, {
357
517
  index: i,
358
518
  total: explorerCount,
359
519
  partition,
360
520
  locatorOutput,
361
521
  patternsOutput,
362
- analyzerOutput: analyzer.result,
363
- onlineOutput: onlineResearcher.result,
522
+ analyzerOutput,
523
+ onlineOutput,
364
524
  });
365
525
 
366
- return { index: i, scratchPath, partition };
526
+ return { index: i, scratchPath: paths.explorer, partition };
367
527
  }),
368
528
  );
369
529