@bastani/atomic 0.5.17-0 → 0.5.18-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 (26) hide show
  1. package/README.md +14 -1
  2. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +50 -54
  3. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  4. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +17 -36
  5. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  6. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -1
  7. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +64 -44
  8. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
  9. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts +43 -0
  10. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts.map +1 -0
  11. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +17 -39
  12. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  13. package/package.json +1 -1
  14. package/src/cli.ts +21 -2
  15. package/src/commands/cli/session.test.ts +223 -0
  16. package/src/commands/cli/session.ts +117 -1
  17. package/src/completions/bash.ts +3 -3
  18. package/src/completions/fish.ts +13 -7
  19. package/src/completions/powershell.ts +3 -0
  20. package/src/completions/zsh.ts +2 -1
  21. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +260 -157
  22. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +224 -125
  23. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +2 -2
  24. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +428 -469
  25. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.ts +115 -0
  26. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +249 -137
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Deterministic synthesis of per-partition explorer scratch files.
3
+ *
4
+ * Each partition is investigated by four specialist sub-agents dispatched
5
+ * directly via the provider SDK's `agent` parameter:
6
+ *
7
+ * - codebase-locator → file index for the partition
8
+ * - codebase-pattern-finder → reusable code patterns in the partition
9
+ * - codebase-analyzer → how the most relevant impl files work
10
+ * - codebase-online-researcher → external library docs (when central)
11
+ *
12
+ * Rather than spawn a fifth "synthesizer" LLM stage just to concatenate four
13
+ * markdown sections, we do that synthesis in plain TypeScript here. This keeps
14
+ * the per-partition cost at exactly four LLM calls and avoids burning tokens
15
+ * on a step whose output is fully determined by its inputs.
16
+ *
17
+ * The file we write is the canonical handoff to the aggregator — it MUST keep
18
+ * the heading shape that buildAggregatorPrompt() promises ("Scope / Files in
19
+ * Scope / How It Works / Patterns / External References / Out-of-Partition
20
+ * References"), or the aggregator will look for sections that don't exist.
21
+ */
22
+
23
+ import { writeFile } from "node:fs/promises";
24
+ import path from "node:path";
25
+ import type { PartitionUnit } from "./scout.ts";
26
+
27
+ export type ExplorerSections = {
28
+ index: number;
29
+ total: number;
30
+ partition: PartitionUnit[];
31
+ /** Full assistant text from the codebase-locator sub-agent. */
32
+ locatorOutput: string;
33
+ /** Full assistant text from the codebase-pattern-finder sub-agent. */
34
+ patternsOutput: string;
35
+ /** Full assistant text from the codebase-analyzer sub-agent. */
36
+ analyzerOutput: string;
37
+ /** Full assistant text from the codebase-online-researcher sub-agent. */
38
+ onlineOutput: string;
39
+ };
40
+
41
+ /** Heuristic: detect the "no external research applicable" sentinel. */
42
+ function isOnlineSkip(output: string): boolean {
43
+ return /\(\s*no external research applicable\s*\)/i.test(output);
44
+ }
45
+
46
+ /** Render the markdown body deterministically. */
47
+ export function renderExplorerMarkdown(sections: ExplorerSections): string {
48
+ const scope = sections.partition
49
+ .map(
50
+ (u) =>
51
+ `\`${u.path}/\` (${u.fileCount} files, ${u.loc.toLocaleString()} LOC)`,
52
+ )
53
+ .join(", ");
54
+
55
+ const lines: string[] = [
56
+ `# Partition ${sections.index} of ${sections.total} — Findings`,
57
+ ``,
58
+ `## Scope`,
59
+ scope,
60
+ ``,
61
+ `## Files in Scope`,
62
+ `<!-- Source: codebase-locator sub-agent -->`,
63
+ sections.locatorOutput.trim() || "_(no files located)_",
64
+ ``,
65
+ `## How It Works`,
66
+ `<!-- Source: codebase-analyzer sub-agent -->`,
67
+ sections.analyzerOutput.trim() || "_(no analysis produced)_",
68
+ ``,
69
+ `## Patterns`,
70
+ `<!-- Source: codebase-pattern-finder sub-agent -->`,
71
+ sections.patternsOutput.trim() || "_(no patterns surfaced)_",
72
+ ``,
73
+ ];
74
+
75
+ // Only include the External References section when the online researcher
76
+ // actually returned external findings — its skip sentinel would otherwise
77
+ // pollute the aggregator's view of "evidence collected".
78
+ if (
79
+ sections.onlineOutput.trim().length > 0 &&
80
+ !isOnlineSkip(sections.onlineOutput)
81
+ ) {
82
+ lines.push(
83
+ `## External References`,
84
+ `<!-- Source: codebase-online-researcher sub-agent -->`,
85
+ sections.onlineOutput.trim(),
86
+ ``,
87
+ );
88
+ }
89
+
90
+ // Out-of-partition references live in the analyzer output already, but we
91
+ // surface a brief pointer for the aggregator's cross-stitching pass.
92
+ lines.push(
93
+ `## Out-of-Partition References`,
94
+ `Look for the **Out-of-Partition References** subsection inside the`,
95
+ `"How It Works" section above — that is where the analyzer flagged files`,
96
+ `outside this partition that other partitions should examine.`,
97
+ ``,
98
+ );
99
+
100
+ return lines.join("\n");
101
+ }
102
+
103
+ /**
104
+ * Write a partition's deterministic scratch file. Returns the absolute path so
105
+ * the caller can record it in the explorer manifest the aggregator reads.
106
+ */
107
+ export async function writeExplorerScratchFile(
108
+ scratchPath: string,
109
+ sections: ExplorerSections,
110
+ ): Promise<string> {
111
+ const abs = path.resolve(scratchPath);
112
+ const md = renderExplorerMarkdown(sections);
113
+ await writeFile(abs, md, "utf8");
114
+ return abs;
115
+ }
@@ -1,51 +1,29 @@
1
1
  /**
2
2
  * deep-research-codebase / opencode
3
3
  *
4
- * OpenCode replica of the Claude deep-research-codebase workflow. The Claude
5
- * version dispatches specialist sub-agents (codebase-locator, codebase-
6
- * analyzer, etc.) inside a single explorer session via `@"name (agent)"`
7
- * syntax a Claude-specific feature. OpenCode sessions are bound to a
8
- * single agent for their lifetime, so we keep the SAME graph topology
9
- * (scout ∥ history → explorer-1..N → aggregator) but drive each explorer
10
- * through the locate → analyze → patterns → synthesize sequence inline using
11
- * the default agent's built-in file tools.
4
+ * OpenCode replica of the Claude deep-research-codebase workflow. Specialist
5
+ * sub-agents are dispatched as separate headless `ctx.stage()` calls — each
6
+ * call passes `agent: "<name>"` to `s.client.session.prompt()` directly,
7
+ * which is OpenCode's SDK-native way to route a turn to a sub-agent.
12
8
  *
13
- * Topology (identical to Claude version):
9
+ * OpenCode-specific concerns baked in (see references/failure-modes.md):
14
10
  *
15
- * ┌─→ codebase-scout
16
- * parent ─┤
17
- * └─→ research-history
18
- * │
19
- * ▼
20
- * ┌──────────────────────────────────────────────────┐
21
- * │ explorer-1 explorer-2 ... explorer-N │ (Promise.all, headless)
22
- * └──────────────────────────────────────────────────┘
23
- * │
24
- * ▼
25
- * aggregator
11
+ * F5 — every `ctx.stage()` is a FRESH session. Each specialist receives
12
+ * everything it needs (research question, scope, scout overview, and —
13
+ * for layer-2 specialists — verbatim locator output) in its first prompt.
26
14
  *
27
- * Explorers run headless (in-process, no tmux window) — they are transparent
28
- * to the graph, so the visible topology is: [scout, history] → aggregator.
15
+ * F3 `result.data!.parts` is a heterogenous array (text/tool/reasoning/
16
+ * file parts). Use `extractResponseText()` to filter to text parts only;
17
+ * concatenating raw `parts` produces `[object Object]` strings.
29
18
  *
30
- * OpenCode-specific concerns baked in:
19
+ * F6 every prompt explicitly requires trailing prose so transcripts and
20
+ * `extractResponseText()` reads are never empty.
31
21
  *
32
- * F5every `ctx.stage()` call is a FRESH session with no memory of
33
- * prior stages. We forward the scout overview, history overview, and
34
- * partition assignment explicitly into each explorer's first prompt.
22
+ * F9 — `s.save()` receives the unwrapped `{ info, parts }` payload from
23
+ * `result.data!`; passing the full `result` or raw `result.data!.parts`
24
+ * breaks downstream `transcript()` reads.
35
25
  *
36
- * F9 — `s.save()` receives the `{ info, parts }` payload from
37
- * `s.client.session.prompt()` via `result.data!`. Passing the full
38
- * `result` (with its wrapping) or raw `result.data.parts` breaks
39
- * downstream `transcript()` reads.
40
- *
41
- * • F6 — every prompt explicitly requires trailing prose AFTER any tool
42
- * call so the rendered transcript has content. OpenCode's `parts` array
43
- * mixes text/tool/reasoning/file parts; without trailing text the
44
- * transcript extractor returns an empty string.
45
- *
46
- * • F3 — transcript extraction relies on the runtime's text-only rendering
47
- * of `result.data.parts`. The helpers call `ctx.transcript(handle)` which
48
- * returns `{ path, content }` where content is already text-filtered.
26
+ * See claude/index.ts for the full design rationale and topology diagram.
49
27
  */
50
28
 
51
29
  import { defineWorkflow } from "../../../index.ts";
@@ -63,42 +41,59 @@ import {
63
41
  } from "../helpers/heuristic.ts";
64
42
  import {
65
43
  buildAggregatorPrompt,
66
- buildExplorerPromptGeneric,
67
- buildHistoryPromptGeneric,
44
+ buildAnalyzerPrompt,
45
+ buildHistoryAnalyzerPrompt,
46
+ buildHistoryLocatorPrompt,
47
+ buildLocatorPrompt,
48
+ buildOnlineResearcherPrompt,
49
+ buildPatternFinderPrompt,
68
50
  buildScoutPrompt,
69
51
  slugifyPrompt,
70
52
  } from "../helpers/prompts.ts";
53
+ import { writeExplorerScratchFile } from "../helpers/scratch.ts";
54
+
55
+ /** Filter for text parts only — non-text parts produce [object Object]. */
56
+ function extractResponseText(
57
+ parts: Array<{ type: string; [key: string]: unknown }>,
58
+ ): string {
59
+ return parts
60
+ .filter((p) => p.type === "text")
61
+ .map((p) => (p as { type: string; text: string }).text)
62
+ .join("\n");
63
+ }
71
64
 
72
65
  export default defineWorkflow({
73
- name: "deep-research-codebase",
74
- description:
75
- "Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
76
- inputs: [
77
- { name: "prompt", type: "text", required: true, description: "research question" },
78
- ],
79
- })
66
+ name: "deep-research-codebase",
67
+ description:
68
+ "Deterministic deep codebase research: scout → per-partition specialist sub-agents → aggregator",
69
+ inputs: [
70
+ {
71
+ name: "prompt",
72
+ type: "text",
73
+ required: true,
74
+ description: "research question",
75
+ },
76
+ ],
77
+ })
80
78
  .for<"opencode">()
81
79
  .run(async (ctx) => {
82
- // Destructure once so every stage below can close over a bare
83
- // `inputs.prompt`; destructure once so every stage below can close
84
- // over a bare `prompt` string without re-reaching into ctx.inputs.
85
80
  const prompt = ctx.inputs.prompt ?? "";
86
81
  const root = getCodebaseRoot();
87
82
  const startedAt = new Date();
88
83
  const isoDate = startedAt.toISOString().slice(0, 10);
89
84
  const slug = slugifyPrompt(prompt);
90
85
 
91
- // ── Stages 1a + 1b: codebase-scout research-history ──────────────────
92
- const [scout, history] = await Promise.all([
86
+ // ── Stage 1a: codebase-scout Stage 1b: research-history pipeline ────
87
+ const [scout, historyOverview] = await Promise.all([
93
88
  ctx.stage(
94
89
  {
95
90
  name: "codebase-scout",
96
- description: "Map codebase, count LOC, partition for parallel explorers",
91
+ description:
92
+ "Map codebase, count LOC, partition for parallel specialists",
97
93
  },
98
94
  {},
99
95
  { title: "codebase-scout" },
100
96
  async (s) => {
101
- // 1. Deterministic scouting (pure TypeScript — no LLM).
102
97
  const data = scoutCodebase(root);
103
98
  if (data.units.length === 0) {
104
99
  throw new Error(
@@ -107,13 +102,10 @@ export default defineWorkflow({
107
102
  );
108
103
  }
109
104
 
110
- // 2. Heuristic decides explorer count (capped by available units).
111
105
  const targetCount = calculateExplorerCount(data.totalLoc);
112
106
  const partitions = partitionUnits(data.units, targetCount);
113
107
  const actualCount = partitions.length;
114
108
 
115
- // 3. Scratch directory for explorer outputs (timestamped to avoid
116
- // collisions across runs).
117
109
  const scratchDir = path.join(
118
110
  root,
119
111
  "research",
@@ -122,9 +114,6 @@ export default defineWorkflow({
122
114
  );
123
115
  await mkdir(scratchDir, { recursive: true });
124
116
 
125
- // 4. Short LLM call: architectural orientation for downstream
126
- // explorers. The prompt forbids the agent from answering the
127
- // research question — its only job here is to orient.
128
117
  const result = await s.client.session.prompt({
129
118
  sessionID: s.session.id,
130
119
  parts: [
@@ -156,103 +145,225 @@ export default defineWorkflow({
156
145
  };
157
146
  },
158
147
  ),
159
- ctx.stage(
160
- {
161
- name: "research-history",
162
- description: "Surface prior research from research/ directory",
163
- },
164
- {},
165
- { title: "research-history" },
166
- async (s) => {
167
- // The generic history prompt drives a single default-agent session
168
- // through locate → analyze → synthesize inline, instead of Claude's
169
- // sub-agent dispatch.
170
- const result = await s.client.session.prompt({
171
- sessionID: s.session.id,
172
- parts: [
173
- {
174
- type: "text",
175
- text: buildHistoryPromptGeneric({
176
- question: prompt,
177
- root,
178
- }),
179
- },
180
- ],
181
- });
182
- s.save(result.data!);
183
- },
184
- ),
185
- ]);
186
-
187
- const {
188
- partitions,
189
- explorerCount,
190
- scratchDir,
191
- totalLoc,
192
- totalFiles,
193
- } = scout.result;
194
-
195
- // Pull both scout transcripts ONCE at the workflow level so every
196
- // explorer + the aggregator can embed them in their prompts (F5). Both
197
- // stages have completed here (we're past Promise.all), so these reads
198
- // are safe (F13).
199
- const scoutOverview = (await ctx.transcript(scout)).content;
200
- const historyOverview = (await ctx.transcript(history)).content;
201
-
202
- // ── Stage 2: parallel headless explorers ─────────────────────────────────
203
- // Each explorer runs headless (in-process, no tmux pane) via Promise.all.
204
- // They are invisible in the workflow graph but tracked by the background
205
- // task counter in the statusline. Because each session is fresh (F5),
206
- // every piece of context it needs — question, architectural orientation,
207
- // historical context, partition assignment, scratch path — is injected
208
- // into the first prompt via buildExplorerPromptGeneric.
209
- const explorerHandles = await Promise.all(
210
- partitions.map((partition, idx) => {
211
- const i = idx + 1;
212
- const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
213
- return ctx.stage(
148
+ // research-history pipeline: sequential locator → analyzer, both headless.
149
+ (async (): Promise<string> => {
150
+ const historyLocator = await ctx.stage(
214
151
  {
215
- name: `explorer-${i}`,
152
+ name: "history-locator",
216
153
  headless: true,
217
- description: `Explore ${partition
218
- .map((u) => u.path)
219
- .join(", ")} (${partition.reduce((s, u) => s + u.fileCount, 0)} files)`,
154
+ description: "Locate prior research docs (codebase-research-locator)",
220
155
  },
221
156
  {},
222
- { title: `explorer-${i}` },
157
+ { title: "history-locator" },
223
158
  async (s) => {
224
159
  const result = await s.client.session.prompt({
225
160
  sessionID: s.session.id,
226
161
  parts: [
227
162
  {
228
163
  type: "text",
229
- text: buildExplorerPromptGeneric({
164
+ text: buildHistoryLocatorPrompt({
230
165
  question: prompt,
231
- index: i,
232
- total: explorerCount,
233
- partition,
234
- scoutOverview,
235
- historyOverview,
236
- scratchPath,
237
166
  root,
238
167
  }),
239
168
  },
240
169
  ],
170
+ agent: "codebase-research-locator",
241
171
  });
242
172
  s.save(result.data!);
173
+ return extractResponseText(result.data!.parts);
174
+ },
175
+ );
243
176
 
244
- // Returning structured metadata lets the aggregator stage reach
245
- // each explorer's scratch path without re-parsing transcripts.
246
- return { index: i, scratchPath, partition };
177
+ const historyAnalyzer = await ctx.stage(
178
+ {
179
+ name: "history-analyzer",
180
+ headless: true,
181
+ description: "Synthesize prior research (codebase-research-analyzer)",
182
+ },
183
+ {},
184
+ { title: "history-analyzer" },
185
+ async (s) => {
186
+ const result = await s.client.session.prompt({
187
+ sessionID: s.session.id,
188
+ parts: [
189
+ {
190
+ type: "text",
191
+ text: buildHistoryAnalyzerPrompt({
192
+ question: prompt,
193
+ locatorOutput: historyLocator.result,
194
+ root,
195
+ }),
196
+ },
197
+ ],
198
+ agent: "codebase-research-analyzer",
199
+ });
200
+ s.save(result.data!);
201
+ return extractResponseText(result.data!.parts);
247
202
  },
248
203
  );
204
+
205
+ return historyAnalyzer.result;
206
+ })(),
207
+ ]);
208
+
209
+ const { partitions, explorerCount, scratchDir, totalLoc, totalFiles } =
210
+ scout.result;
211
+
212
+ const scoutOverview = (await ctx.transcript(scout)).content;
213
+
214
+ // ── Stage 2: per-partition specialist fan-out ─────────────────────────
215
+ const explorerHandles = await Promise.all(
216
+ partitions.map(async (partition, idx) => {
217
+ const i = idx + 1;
218
+ const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
219
+
220
+ // Layer 1: locator + pattern-finder run independently.
221
+ const [locator, patternFinder] = await Promise.all([
222
+ ctx.stage(
223
+ {
224
+ name: `locator-${i}`,
225
+ headless: true,
226
+ description: `codebase-locator over partition ${i}`,
227
+ },
228
+ {},
229
+ { title: `locator-${i}` },
230
+ async (s) => {
231
+ const result = await s.client.session.prompt({
232
+ sessionID: s.session.id,
233
+ parts: [
234
+ {
235
+ type: "text",
236
+ text: buildLocatorPrompt({
237
+ question: prompt,
238
+ partition,
239
+ root,
240
+ scoutOverview,
241
+ index: i,
242
+ total: explorerCount,
243
+ }),
244
+ },
245
+ ],
246
+ agent: "codebase-locator",
247
+ });
248
+ s.save(result.data!);
249
+ return extractResponseText(result.data!.parts);
250
+ },
251
+ ),
252
+ ctx.stage(
253
+ {
254
+ name: `pattern-finder-${i}`,
255
+ headless: true,
256
+ description: `codebase-pattern-finder over partition ${i}`,
257
+ },
258
+ {},
259
+ { title: `pattern-finder-${i}` },
260
+ async (s) => {
261
+ const result = await s.client.session.prompt({
262
+ sessionID: s.session.id,
263
+ parts: [
264
+ {
265
+ type: "text",
266
+ text: buildPatternFinderPrompt({
267
+ question: prompt,
268
+ partition,
269
+ root,
270
+ scoutOverview,
271
+ index: i,
272
+ total: explorerCount,
273
+ }),
274
+ },
275
+ ],
276
+ agent: "codebase-pattern-finder",
277
+ });
278
+ s.save(result.data!);
279
+ return extractResponseText(result.data!.parts);
280
+ },
281
+ ),
282
+ ]);
283
+
284
+ const locatorOutput = locator.result;
285
+ const patternsOutput = patternFinder.result;
286
+
287
+ // Layer 2: analyzer + online-researcher consume locator output.
288
+ const [analyzer, onlineResearcher] = await Promise.all([
289
+ ctx.stage(
290
+ {
291
+ name: `analyzer-${i}`,
292
+ headless: true,
293
+ description: `codebase-analyzer over partition ${i}`,
294
+ },
295
+ {},
296
+ { title: `analyzer-${i}` },
297
+ async (s) => {
298
+ const result = await s.client.session.prompt({
299
+ sessionID: s.session.id,
300
+ parts: [
301
+ {
302
+ type: "text",
303
+ text: buildAnalyzerPrompt({
304
+ question: prompt,
305
+ partition,
306
+ locatorOutput,
307
+ root,
308
+ scoutOverview,
309
+ index: i,
310
+ total: explorerCount,
311
+ }),
312
+ },
313
+ ],
314
+ agent: "codebase-analyzer",
315
+ });
316
+ s.save(result.data!);
317
+ return extractResponseText(result.data!.parts);
318
+ },
319
+ ),
320
+ ctx.stage(
321
+ {
322
+ name: `online-researcher-${i}`,
323
+ headless: true,
324
+ description: `codebase-online-researcher over partition ${i}`,
325
+ },
326
+ {},
327
+ { title: `online-researcher-${i}` },
328
+ async (s) => {
329
+ const result = await s.client.session.prompt({
330
+ sessionID: s.session.id,
331
+ parts: [
332
+ {
333
+ type: "text",
334
+ text: buildOnlineResearcherPrompt({
335
+ question: prompt,
336
+ partition,
337
+ locatorOutput,
338
+ root,
339
+ index: i,
340
+ total: explorerCount,
341
+ }),
342
+ },
343
+ ],
344
+ agent: "codebase-online-researcher",
345
+ });
346
+ s.save(result.data!);
347
+ return extractResponseText(result.data!.parts);
348
+ },
349
+ ),
350
+ ]);
351
+
352
+ await writeExplorerScratchFile(scratchPath, {
353
+ index: i,
354
+ total: explorerCount,
355
+ partition,
356
+ locatorOutput,
357
+ patternsOutput,
358
+ analyzerOutput: analyzer.result,
359
+ onlineOutput: onlineResearcher.result,
360
+ });
361
+
362
+ return { index: i, scratchPath, partition };
249
363
  }),
250
364
  );
251
365
 
252
- // ── Stage 3: aggregator ────────────────────────────────────────────────
253
- // Reads explorer findings via FILE PATHS (filesystem-context skill) to
254
- // keep the aggregator's own context lean — we deliberately do NOT inline
255
- // N transcripts into the prompt. Token cost stays roughly constant in N.
366
+ // ── Stage 3: aggregator ───────────────────────────────────────────────
256
367
  const finalPath = path.join(
257
368
  root,
258
369
  "research",
@@ -263,7 +374,8 @@ export default defineWorkflow({
263
374
  await ctx.stage(
264
375
  {
265
376
  name: "aggregator",
266
- description: "Synthesize explorer findings + history into final research doc",
377
+ description:
378
+ "Synthesize partition findings + history into final research doc",
267
379
  },
268
380
  {},
269
381
  { title: "aggregator" },
@@ -278,7 +390,7 @@ export default defineWorkflow({
278
390
  totalLoc,
279
391
  totalFiles,
280
392
  explorerCount,
281
- explorerFiles: explorerHandles.map((h) => h.result),
393
+ explorerFiles: explorerHandles,
282
394
  finalPath,
283
395
  scoutOverview,
284
396
  historyOverview,