@bastani/atomic 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ast-grep/SKILL.md +323 -0
- package/.agents/skills/ast-grep/references/rule_reference.md +297 -0
- package/.agents/skills/ripgrep/SKILL.md +382 -0
- package/.mcp.json +5 -6
- package/dist/commands/cli/claude-inflight-hook.d.ts +100 -0
- package/dist/commands/cli/claude-inflight-hook.d.ts.map +1 -0
- package/dist/commands/cli/claude-stop-hook.d.ts +2 -0
- package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -1
- package/dist/lib/spawn.d.ts +1 -1
- package/dist/lib/spawn.d.ts.map +1 -1
- package/dist/sdk/providers/claude.d.ts +36 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +17 -1
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +49 -34
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +18 -16
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts +43 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +30 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +2 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +18 -16
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/services/config/additional-instructions.d.ts +67 -0
- package/dist/services/config/additional-instructions.d.ts.map +1 -0
- package/package.json +3 -1
- package/src/cli.ts +18 -1
- package/src/commands/cli/chat/index.ts +52 -2
- package/src/commands/cli/claude-inflight-hook.test.ts +598 -0
- package/src/commands/cli/claude-inflight-hook.ts +359 -0
- package/src/commands/cli/claude-stop-hook.ts +40 -4
- package/src/commands/cli/init/index.ts +9 -0
- package/src/lib/spawn.ts +6 -2
- package/src/sdk/providers/claude.ts +131 -0
- package/src/sdk/providers/copilot.ts +30 -1
- package/src/sdk/runtime/executor.ts +43 -2
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +318 -158
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +253 -129
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/batching.ts +65 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/ignore-by-default.d.ts +8 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +203 -12
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +248 -78
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +258 -146
- package/src/services/config/additional-instructions.ts +273 -0
- 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
|
|
6
|
-
* (codebase-locator / codebase-pattern-finder / codebase-analyzer
|
|
7
|
-
* codebase-online-researcher
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
14
|
+
* Why batched Task-tool dispatch instead of one ctx.stage per specialist:
|
|
14
15
|
*
|
|
15
|
-
* •
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
* •
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
* •
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
* just
|
|
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
|
-
* │
|
|
42
|
-
* │
|
|
43
|
-
* │
|
|
44
|
-
* │
|
|
45
|
-
* │
|
|
46
|
-
* │
|
|
47
|
-
* │ │
|
|
48
|
-
* │
|
|
49
|
-
* │
|
|
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
|
-
*
|
|
56
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
300
|
+
// ── Stage 2: batched specialist fan-out ───────────────────────────────
|
|
301
|
+
//
|
|
302
|
+
// Two waves, each chunked into batches of MAX_TASKS_PER_BATCH (≈10):
|
|
235
303
|
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
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
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
363
|
-
onlineOutput
|
|
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
|
|