@bastani/atomic 0.5.3 → 0.5.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.
- package/README.md +110 -11
- package/dist/{chunk-mn870nrv.js → chunk-xkxndz5g.js} +213 -154
- package/dist/sdk/components/workflow-picker-panel.d.ts +120 -0
- package/dist/sdk/define-workflow.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/runtime/discovery.d.ts +57 -3
- package/dist/sdk/runtime/executor.d.ts +15 -2
- package/dist/sdk/runtime/tmux.d.ts +9 -0
- package/dist/sdk/types.d.ts +63 -4
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +61 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +48 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +25 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +91 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +56 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +48 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +6 -5
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +6 -5
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +6 -5
- package/dist/sdk/workflows/index.d.ts +4 -4
- package/dist/sdk/workflows/index.js +7 -1
- package/package.json +1 -1
- package/src/cli.ts +25 -3
- package/src/commands/cli/chat/index.ts +5 -5
- package/src/commands/cli/init/index.ts +79 -77
- package/src/commands/cli/workflow-command.test.ts +757 -0
- package/src/commands/cli/workflow.test.ts +310 -0
- package/src/commands/cli/workflow.ts +445 -105
- package/src/sdk/components/workflow-picker-panel.tsx +1462 -0
- package/src/sdk/define-workflow.test.ts +101 -0
- package/src/sdk/define-workflow.ts +62 -2
- package/src/sdk/runtime/discovery.ts +111 -8
- package/src/sdk/runtime/executor.ts +89 -32
- package/src/sdk/runtime/tmux.conf +55 -0
- package/src/sdk/runtime/tmux.ts +34 -10
- package/src/sdk/types.ts +67 -4
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +294 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +276 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +38 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +816 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +334 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +284 -0
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +8 -4
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +10 -4
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +8 -4
- package/src/sdk/workflows/index.ts +9 -1
- package/src/services/system/auto-sync.ts +1 -1
- package/src/services/system/install-ui.ts +109 -39
- package/src/theme/colors.ts +65 -1
package/src/sdk/runtime/tmux.ts
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* sending keystrokes, and pane state detection.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { Subprocess } from "bun";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Dedicated tmux socket name — isolates Atomic sessions from the user's default server. */
|
|
17
|
+
export const SOCKET_NAME = "atomic";
|
|
18
|
+
|
|
19
|
+
/** Path to the bundled tmux config (shared by tmux and psmux). */
|
|
20
|
+
const CONFIG_PATH = join(import.meta.dir, "tmux.conf");
|
|
21
|
+
|
|
9
22
|
// ---------------------------------------------------------------------------
|
|
10
23
|
// Core tmux primitives
|
|
11
24
|
// ---------------------------------------------------------------------------
|
|
@@ -73,8 +86,9 @@ export function tmuxRun(args: string[]): { ok: true; stdout: string } | { ok: fa
|
|
|
73
86
|
if (!binary) {
|
|
74
87
|
return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
|
|
75
88
|
}
|
|
89
|
+
const fullArgs = ["-f", CONFIG_PATH, "-L", SOCKET_NAME, ...args];
|
|
76
90
|
const result = Bun.spawnSync({
|
|
77
|
-
cmd: [binary, ...
|
|
91
|
+
cmd: [binary, ...fullArgs],
|
|
78
92
|
stdout: "pipe",
|
|
79
93
|
stderr: "pipe",
|
|
80
94
|
});
|
|
@@ -318,14 +332,8 @@ export function killWindow(sessionName: string, windowName: string): void {
|
|
|
318
332
|
* Check if a tmux session exists.
|
|
319
333
|
*/
|
|
320
334
|
export function sessionExists(sessionName: string): boolean {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
const result = Bun.spawnSync({
|
|
324
|
-
cmd: [binary, "has-session", "-t", sessionName],
|
|
325
|
-
stdout: "pipe",
|
|
326
|
-
stderr: "pipe",
|
|
327
|
-
});
|
|
328
|
-
return result.success;
|
|
335
|
+
const result = tmuxRun(["has-session", "-t", sessionName]);
|
|
336
|
+
return result.ok;
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
/**
|
|
@@ -337,7 +345,7 @@ export function attachSession(sessionName: string): void {
|
|
|
337
345
|
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
338
346
|
}
|
|
339
347
|
const proc = Bun.spawnSync({
|
|
340
|
-
cmd: [binary, "attach-session", "-t", sessionName],
|
|
348
|
+
cmd: [binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName],
|
|
341
349
|
stdin: "inherit",
|
|
342
350
|
stdout: "inherit",
|
|
343
351
|
stderr: "pipe",
|
|
@@ -348,6 +356,22 @@ export function attachSession(sessionName: string): void {
|
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Spawn an interactive attach-session process.
|
|
361
|
+
* Encapsulates binary resolution, config injection, and socket isolation.
|
|
362
|
+
* Used by all async attach call sites (executor, chat).
|
|
363
|
+
*/
|
|
364
|
+
export function spawnMuxAttach(sessionName: string): Subprocess {
|
|
365
|
+
const binary = getMuxBinary();
|
|
366
|
+
if (!binary) {
|
|
367
|
+
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
368
|
+
}
|
|
369
|
+
return Bun.spawn(
|
|
370
|
+
[binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName],
|
|
371
|
+
{ stdio: ["inherit", "inherit", "inherit"] },
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
351
375
|
/**
|
|
352
376
|
* Switch the current tmux client to a different session.
|
|
353
377
|
* Use this instead of `attachSession` when already inside tmux to avoid
|
package/src/sdk/types.ts
CHANGED
|
@@ -95,6 +95,44 @@ export type {
|
|
|
95
95
|
ClaudeQueryDefaults,
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
// ─── Workflow input schemas ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Supported field types for a workflow's declared inputs.
|
|
102
|
+
*
|
|
103
|
+
* - `"string"` — single-line free-form input (short values, identifiers, paths)
|
|
104
|
+
* - `"text"` — multi-line free-form input (long prose, prompts, specs)
|
|
105
|
+
* - `"enum"` — one of a fixed list of allowed `values`
|
|
106
|
+
*/
|
|
107
|
+
export type WorkflowInputType = "string" | "text" | "enum";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A declared input for a workflow. When a workflow provides an `inputs`
|
|
111
|
+
* array, the CLI materialises one `--<name>` flag per input (and the
|
|
112
|
+
* interactive picker renders one field per input) so users can pass
|
|
113
|
+
* structured values rather than a single free-form prompt.
|
|
114
|
+
*
|
|
115
|
+
* Leaving `inputs` unset (or empty) signals that the workflow consumes a
|
|
116
|
+
* single free-form prompt instead — the legacy
|
|
117
|
+
* `atomic workflow -n <name> -a <agent> "prompt"` form.
|
|
118
|
+
*/
|
|
119
|
+
export interface WorkflowInput {
|
|
120
|
+
/** Field name — also the CLI flag (`--<name>`) and form field identifier. */
|
|
121
|
+
name: string;
|
|
122
|
+
/** Input kind — see {@link WorkflowInputType}. */
|
|
123
|
+
type: WorkflowInputType;
|
|
124
|
+
/** Whether the field must be non-empty before the workflow can run. */
|
|
125
|
+
required?: boolean;
|
|
126
|
+
/** Short human description shown as the field caption. */
|
|
127
|
+
description?: string;
|
|
128
|
+
/** Placeholder text shown when the field is empty. */
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
/** Default value pre-filled into the field. Enums use this to pick their initial value. */
|
|
131
|
+
default?: string;
|
|
132
|
+
/** Allowed values — required when `type` is `"enum"`. */
|
|
133
|
+
values?: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
// ─── Core types ─────────────────────────────────────────────────────────────
|
|
99
137
|
|
|
100
138
|
/**
|
|
@@ -169,8 +207,16 @@ export interface SessionContext<A extends AgentType = AgentType> {
|
|
|
169
207
|
client: ProviderClient<A>;
|
|
170
208
|
/** Provider-specific session (auto-created by runtime) */
|
|
171
209
|
session: ProviderSession<A>;
|
|
172
|
-
/**
|
|
173
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Structured inputs for this workflow run. Populated from CLI flags
|
|
212
|
+
* (`--<name>=<value>`) or the interactive picker.
|
|
213
|
+
*
|
|
214
|
+
* Free-form workflows (no declared `inputs` schema) receive their
|
|
215
|
+
* single positional prompt under the `prompt` key — so
|
|
216
|
+
* `s.inputs.prompt` is the canonical way to read the user's prompt
|
|
217
|
+
* regardless of whether the workflow is structured or free-form.
|
|
218
|
+
*/
|
|
219
|
+
inputs: Record<string, string>;
|
|
174
220
|
/** Which agent is running */
|
|
175
221
|
agent: A;
|
|
176
222
|
/**
|
|
@@ -212,8 +258,16 @@ export interface SessionContext<A extends AgentType = AgentType> {
|
|
|
212
258
|
* Does not have session-specific fields (paneId, save, etc.).
|
|
213
259
|
*/
|
|
214
260
|
export interface WorkflowContext<A extends AgentType = AgentType> {
|
|
215
|
-
/**
|
|
216
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Structured inputs for this workflow run. Populated from CLI flags
|
|
263
|
+
* (`--<name>=<value>`) or the interactive picker.
|
|
264
|
+
*
|
|
265
|
+
* Free-form workflows (no declared `inputs` schema) receive their
|
|
266
|
+
* single positional prompt under the `prompt` key — so
|
|
267
|
+
* `ctx.inputs.prompt` is the canonical way to read the user's prompt
|
|
268
|
+
* regardless of whether the workflow is structured or free-form.
|
|
269
|
+
*/
|
|
270
|
+
inputs: Record<string, string>;
|
|
217
271
|
/** Which agent is running */
|
|
218
272
|
agent: A;
|
|
219
273
|
/**
|
|
@@ -248,6 +302,13 @@ export interface WorkflowOptions {
|
|
|
248
302
|
name: string;
|
|
249
303
|
/** Human-readable description */
|
|
250
304
|
description?: string;
|
|
305
|
+
/**
|
|
306
|
+
* Optional declared inputs. When provided, the CLI materialises one
|
|
307
|
+
* `--<name>` flag per entry and the interactive picker renders one form
|
|
308
|
+
* field per entry. Leave unset to keep the workflow free-form (a single
|
|
309
|
+
* positional prompt argument).
|
|
310
|
+
*/
|
|
311
|
+
inputs?: WorkflowInput[];
|
|
251
312
|
}
|
|
252
313
|
|
|
253
314
|
/**
|
|
@@ -257,6 +318,8 @@ export interface WorkflowDefinition<A extends AgentType = AgentType> {
|
|
|
257
318
|
readonly __brand: "WorkflowDefinition";
|
|
258
319
|
readonly name: string;
|
|
259
320
|
readonly description: string;
|
|
321
|
+
/** Declared input schema — empty array for free-form workflows. */
|
|
322
|
+
readonly inputs: readonly WorkflowInput[];
|
|
260
323
|
/** The workflow's entry point. Called by the executor with a WorkflowContext. */
|
|
261
324
|
readonly run: (ctx: WorkflowContext<A>) => Promise<void>;
|
|
262
325
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deep-research-codebase / claude
|
|
3
|
+
*
|
|
4
|
+
* A deterministically-orchestrated, distributed version of the
|
|
5
|
+
* `research-codebase` skill. The research-codebase skill spawns
|
|
6
|
+
* codebase-locator / codebase-analyzer / codebase-pattern-finder /
|
|
7
|
+
* codebase-research-locator / codebase-research-analyzer /
|
|
8
|
+
* codebase-online-researcher sub-agents on the fly via LLM judgment;
|
|
9
|
+
* this workflow spawns the same agents on a deterministic schedule
|
|
10
|
+
* driven by the codebase's lines of code.
|
|
11
|
+
*
|
|
12
|
+
* Topology:
|
|
13
|
+
*
|
|
14
|
+
* ┌─→ codebase-scout
|
|
15
|
+
* parent ─┤
|
|
16
|
+
* └─→ research-history
|
|
17
|
+
* │
|
|
18
|
+
* ▼
|
|
19
|
+
* ┌──────────────────────────────────────────────────┐
|
|
20
|
+
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all)
|
|
21
|
+
* └──────────────────────────────────────────────────┘
|
|
22
|
+
* │
|
|
23
|
+
* ▼
|
|
24
|
+
* aggregator
|
|
25
|
+
*
|
|
26
|
+
* Stage 1a — codebase-scout
|
|
27
|
+
* Pure-TypeScript: lists files (git ls-files), counts LOC (batched wc -l),
|
|
28
|
+
* renders a depth-bounded ASCII tree, and bin-packs directories into N
|
|
29
|
+
* partitions where N is determined by the LOC heuristic. Then makes one
|
|
30
|
+
* short LLM call to produce an architectural orientation that primes the
|
|
31
|
+
* downstream explorers. Returns structured data via `handle.result` and
|
|
32
|
+
* the agent's prose via `ctx.transcript(handle)`.
|
|
33
|
+
*
|
|
34
|
+
* Stage 1b — research-history (parallel sibling of scout)
|
|
35
|
+
* Dispatches the codebase-research-locator and codebase-research-analyzer
|
|
36
|
+
* sub-agents over the project's existing research/ directory to surface
|
|
37
|
+
* prior decisions, completed investigations, and unresolved questions.
|
|
38
|
+
* Output is consumed via session transcript (≤400 words) and feeds into
|
|
39
|
+
* the aggregator as supplementary context.
|
|
40
|
+
*
|
|
41
|
+
* Stage 2 — explorer-1..N (parallel; depends on scout + history)
|
|
42
|
+
* Each explorer is a coordinator that dispatches specialized sub-agents
|
|
43
|
+
* over its assigned partition (single LOC-balanced slice of the codebase):
|
|
44
|
+
* - codebase-locator → finds relevant files in the partition
|
|
45
|
+
* - codebase-analyzer → documents how the most relevant files work
|
|
46
|
+
* - codebase-pattern-finder → finds existing pattern examples
|
|
47
|
+
* - codebase-online-researcher → (conditional) external library docs
|
|
48
|
+
* The explorer never reads files directly — it orchestrates specialists
|
|
49
|
+
* and writes a synthesized findings document to a known scratch path.
|
|
50
|
+
*
|
|
51
|
+
* Stage 3 — aggregator
|
|
52
|
+
* Reads each explorer's scratch file by path (file-based handoff to keep
|
|
53
|
+
* the aggregator's own context lean — we deliberately do NOT inline N
|
|
54
|
+
* transcripts into the prompt). Folds in the research-history overview
|
|
55
|
+
* as supplementary context. Synthesizes a single research document at
|
|
56
|
+
* research/docs/YYYY-MM-DD-<slug>.md.
|
|
57
|
+
*
|
|
58
|
+
* Context-engineering decisions are documented at each stage below.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import { defineWorkflow } from "../../../index.ts";
|
|
62
|
+
import { mkdir } from "node:fs/promises";
|
|
63
|
+
import path from "node:path";
|
|
64
|
+
|
|
65
|
+
import {
|
|
66
|
+
getCodebaseRoot,
|
|
67
|
+
partitionUnits,
|
|
68
|
+
scoutCodebase,
|
|
69
|
+
} from "../helpers/scout.ts";
|
|
70
|
+
import {
|
|
71
|
+
calculateExplorerCount,
|
|
72
|
+
explainHeuristic,
|
|
73
|
+
} from "../helpers/heuristic.ts";
|
|
74
|
+
import {
|
|
75
|
+
buildAggregatorPrompt,
|
|
76
|
+
buildExplorerPrompt,
|
|
77
|
+
buildHistoryPrompt,
|
|
78
|
+
buildScoutPrompt,
|
|
79
|
+
slugifyPrompt,
|
|
80
|
+
} from "../helpers/prompts.ts";
|
|
81
|
+
|
|
82
|
+
export default defineWorkflow<"claude">({
|
|
83
|
+
name: "deep-research-codebase",
|
|
84
|
+
description:
|
|
85
|
+
"Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
|
|
86
|
+
})
|
|
87
|
+
.run(async (ctx) => {
|
|
88
|
+
// Free-form workflows receive their positional prompt under
|
|
89
|
+
// `inputs.prompt`; destructure once so every stage below can close
|
|
90
|
+
// over a bare `prompt` string without re-reaching into ctx.inputs.
|
|
91
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
92
|
+
const root = getCodebaseRoot();
|
|
93
|
+
const startedAt = new Date();
|
|
94
|
+
const isoDate = startedAt.toISOString().slice(0, 10);
|
|
95
|
+
const slug = slugifyPrompt(prompt);
|
|
96
|
+
|
|
97
|
+
// ── Stages 1a + 1b: codebase-scout ∥ research-history ──────────────────
|
|
98
|
+
// Run the codebase scout (deterministic compute + brief LLM orientation)
|
|
99
|
+
// in parallel with the research-history scout (sub-agent dispatch over
|
|
100
|
+
// the project's prior research docs). Both must complete before any
|
|
101
|
+
// explorer starts, since:
|
|
102
|
+
// - explorers depend on `scout.result.partitions`
|
|
103
|
+
// - aggregator depends on the history transcript
|
|
104
|
+
// Promise.all gives us the cleanest auto-inferred graph topology:
|
|
105
|
+
// parent → [scout, history] → [explorer-1..N] → aggregator.
|
|
106
|
+
const [scout, history] = await Promise.all([
|
|
107
|
+
ctx.stage(
|
|
108
|
+
{
|
|
109
|
+
name: "codebase-scout",
|
|
110
|
+
description: "Map codebase, count LOC, partition for parallel explorers",
|
|
111
|
+
},
|
|
112
|
+
{},
|
|
113
|
+
{},
|
|
114
|
+
async (s) => {
|
|
115
|
+
// 1. Deterministic scouting.
|
|
116
|
+
const data = scoutCodebase(root);
|
|
117
|
+
if (data.units.length === 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`deep-research-codebase: scout found no source files under ${root}. ` +
|
|
120
|
+
`Run from inside a code repository or check the CODE_EXTENSIONS list.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Heuristic decides explorer count (capped by available units).
|
|
125
|
+
const targetCount = calculateExplorerCount(data.totalLoc);
|
|
126
|
+
const partitions = partitionUnits(data.units, targetCount);
|
|
127
|
+
const actualCount = partitions.length;
|
|
128
|
+
|
|
129
|
+
// 3. Scratch directory for explorer outputs (timestamped to avoid
|
|
130
|
+
// collisions across runs).
|
|
131
|
+
const scratchDir = path.join(
|
|
132
|
+
root,
|
|
133
|
+
"research",
|
|
134
|
+
"docs",
|
|
135
|
+
`.deep-research-${startedAt.getTime()}`,
|
|
136
|
+
);
|
|
137
|
+
await mkdir(scratchDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
// 4. Short LLM call: architectural orientation for downstream
|
|
140
|
+
// explorers. The prompt explicitly forbids the agent from
|
|
141
|
+
// answering the research question — its only job here is to
|
|
142
|
+
// orient.
|
|
143
|
+
await s.session.query(
|
|
144
|
+
buildScoutPrompt({
|
|
145
|
+
question: prompt,
|
|
146
|
+
tree: data.tree,
|
|
147
|
+
totalLoc: data.totalLoc,
|
|
148
|
+
totalFiles: data.totalFiles,
|
|
149
|
+
explorerCount: actualCount,
|
|
150
|
+
partitionPreview: partitions,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
s.save(s.sessionId);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
root,
|
|
157
|
+
totalLoc: data.totalLoc,
|
|
158
|
+
totalFiles: data.totalFiles,
|
|
159
|
+
tree: data.tree,
|
|
160
|
+
partitions,
|
|
161
|
+
explorerCount: actualCount,
|
|
162
|
+
scratchDir,
|
|
163
|
+
heuristicNote: explainHeuristic(data.totalLoc, actualCount),
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
ctx.stage(
|
|
168
|
+
{
|
|
169
|
+
name: "research-history",
|
|
170
|
+
description: "Surface prior research via research-locator + research-analyzer",
|
|
171
|
+
},
|
|
172
|
+
{},
|
|
173
|
+
{},
|
|
174
|
+
async (s) => {
|
|
175
|
+
// Dispatches codebase-research-locator → codebase-research-analyzer
|
|
176
|
+
// over the project's research/ directory and outputs a ≤400-word
|
|
177
|
+
// synthesis as prose (no file write — consumed via transcript).
|
|
178
|
+
await s.session.query(
|
|
179
|
+
buildHistoryPrompt({ question: prompt, root }),
|
|
180
|
+
);
|
|
181
|
+
s.save(s.sessionId);
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const {
|
|
187
|
+
partitions,
|
|
188
|
+
explorerCount,
|
|
189
|
+
scratchDir,
|
|
190
|
+
totalLoc,
|
|
191
|
+
totalFiles,
|
|
192
|
+
} = scout.result;
|
|
193
|
+
|
|
194
|
+
// Pull both scout transcripts ONCE at the workflow level so every
|
|
195
|
+
// explorer + the aggregator can embed them in their prompts. Both
|
|
196
|
+
// stages have already completed by this point (we're past Promise.all),
|
|
197
|
+
// so these reads are safe (F13).
|
|
198
|
+
const scoutOverview = (await ctx.transcript(scout)).content;
|
|
199
|
+
const historyOverview = (await ctx.transcript(history)).content;
|
|
200
|
+
|
|
201
|
+
// ── Stage 2: parallel explorers ────────────────────────────────────────
|
|
202
|
+
// Each explorer is a separate tmux pane / Claude session, running
|
|
203
|
+
// concurrently via Promise.all. Each one receives:
|
|
204
|
+
// - the original research question (top + bottom of prompt)
|
|
205
|
+
// - the scout's architectural overview
|
|
206
|
+
// - its OWN partition (never the full file list)
|
|
207
|
+
// - the absolute path to its scratch file
|
|
208
|
+
//
|
|
209
|
+
// Information flow choices:
|
|
210
|
+
// • We deliberately do not pass other explorers' work — they run in
|
|
211
|
+
// parallel and forward-only data flow is enforced by the runtime
|
|
212
|
+
// (F13). Cross-cutting happens in the aggregator.
|
|
213
|
+
// • We pass the partition via closure capture, not by parsing
|
|
214
|
+
// scout transcripts — strongly typed and lossless.
|
|
215
|
+
const explorerHandles = await Promise.all(
|
|
216
|
+
partitions.map((partition, idx) => {
|
|
217
|
+
const i = idx + 1;
|
|
218
|
+
const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
|
|
219
|
+
return ctx.stage(
|
|
220
|
+
{
|
|
221
|
+
name: `explorer-${i}`,
|
|
222
|
+
description: `Explore ${partition
|
|
223
|
+
.map((u) => u.path)
|
|
224
|
+
.join(", ")} (${partition.reduce((s, u) => s + u.fileCount, 0)} files)`,
|
|
225
|
+
},
|
|
226
|
+
{},
|
|
227
|
+
{},
|
|
228
|
+
async (s) => {
|
|
229
|
+
await s.session.query(
|
|
230
|
+
buildExplorerPrompt({
|
|
231
|
+
question: prompt,
|
|
232
|
+
index: i,
|
|
233
|
+
total: explorerCount,
|
|
234
|
+
partition,
|
|
235
|
+
scoutOverview,
|
|
236
|
+
scratchPath,
|
|
237
|
+
root,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
s.save(s.sessionId);
|
|
241
|
+
|
|
242
|
+
// Returning structured metadata lets the aggregator stage reach
|
|
243
|
+
// each explorer's scratch path without re-parsing transcripts.
|
|
244
|
+
return { index: i, scratchPath, partition };
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// ── Stage 3: aggregator ────────────────────────────────────────────────
|
|
251
|
+
// Synthesizes explorer findings into the final research document at
|
|
252
|
+
// research/docs/YYYY-MM-DD-<slug>.md.
|
|
253
|
+
//
|
|
254
|
+
// Information flow choice:
|
|
255
|
+
// • The aggregator reads explorer findings via FILE PATHS, not by
|
|
256
|
+
// embedding all N transcripts in its prompt. This keeps its
|
|
257
|
+
// context lean (filesystem-context skill) and lets the agent
|
|
258
|
+
// selectively re-read source files when explorers contradict
|
|
259
|
+
// each other.
|
|
260
|
+
// • The aggregator only sees the scout overview (short) plus a
|
|
261
|
+
// manifest of explorer scratch paths — token cost stays roughly
|
|
262
|
+
// constant in N rather than growing linearly.
|
|
263
|
+
const finalPath = path.join(
|
|
264
|
+
root,
|
|
265
|
+
"research",
|
|
266
|
+
"docs",
|
|
267
|
+
`${isoDate}-${slug}.md`,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
await ctx.stage(
|
|
271
|
+
{
|
|
272
|
+
name: "aggregator",
|
|
273
|
+
description: "Synthesize explorer findings + history into final research doc",
|
|
274
|
+
},
|
|
275
|
+
{},
|
|
276
|
+
{},
|
|
277
|
+
async (s) => {
|
|
278
|
+
await s.session.query(
|
|
279
|
+
buildAggregatorPrompt({
|
|
280
|
+
question: prompt,
|
|
281
|
+
totalLoc,
|
|
282
|
+
totalFiles,
|
|
283
|
+
explorerCount,
|
|
284
|
+
explorerFiles: explorerHandles.map((h) => h.result),
|
|
285
|
+
finalPath,
|
|
286
|
+
scoutOverview,
|
|
287
|
+
historyOverview,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
s.save(s.sessionId);
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
})
|
|
294
|
+
.compile();
|