@bastani/atomic 0.5.17 → 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.
- package/README.md +14 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +50 -54
- 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 +17 -36
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +64 -44
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts +43 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +17 -39
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +21 -2
- package/src/commands/cli/session.test.ts +223 -0
- package/src/commands/cli/session.ts +117 -1
- package/src/completions/bash.ts +3 -3
- package/src/completions/fish.ts +13 -7
- package/src/completions/powershell.ts +3 -0
- package/src/completions/zsh.ts +2 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +260 -157
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +224 -125
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +2 -2
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +428 -469
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.ts +115 -0
- 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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
9
|
+
* OpenCode-specific concerns baked in (see references/failure-modes.md):
|
|
14
10
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
19
|
+
* • F6 — every prompt explicitly requires trailing prose so transcripts and
|
|
20
|
+
* `extractResponseText()` reads are never empty.
|
|
31
21
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
// ──
|
|
92
|
-
const [scout,
|
|
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:
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
152
|
+
name: "history-locator",
|
|
216
153
|
headless: true,
|
|
217
|
-
description:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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:
|
|
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
|
|
393
|
+
explorerFiles: explorerHandles,
|
|
282
394
|
finalPath,
|
|
283
395
|
scoutOverview,
|
|
284
396
|
historyOverview,
|