@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
@@ -1,67 +1,63 @@
1
1
  /**
2
2
  * deep-research-codebase / claude
3
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.
4
+ * A deterministically-orchestrated, distributed codebase researcher built on
5
+ * the Claude Agent SDK's native sub-agent dispatch. Specialist sub-agents
6
+ * (codebase-locator / codebase-pattern-finder / codebase-analyzer /
7
+ * codebase-online-researcher / codebase-research-locator /
8
+ * codebase-research-analyzer) are spawned as separate headless `ctx.stage()`
9
+ * calls each binds the SDK's `agent` option to the desired specialist
10
+ * instead of relying on a coordinator agent that dispatches them via the
11
+ * `@"name (agent)"` prompt syntax.
11
12
  *
12
- * Topology:
13
+ * Why SDK primitives instead of in-prompt orchestration:
13
14
  *
14
- * ┌─→ codebase-scout
15
- * parent ─┤
16
- * └─→ research-history
17
- *
18
- * ▼
19
- * ┌──────────────────────────────────────────────────┐
20
- * │ explorer-1 explorer-2 ... explorer-N │ (Promise.all, headless)
21
- * └──────────────────────────────────────────────────┘
22
- * │
23
- * ▼
24
- * aggregator
15
+ * Each specialist runs in an ISOLATED conversation. The locator's giant
16
+ * file index doesn't pollute the analyzer's context window, and the
17
+ * online-researcher doesn't see the analyzer's reasoning at all. This is
18
+ * `multi-agent-patterns` swarm-style isolation, not orchestrator-style.
25
19
  *
26
- * Explorers run headless (in-process, no tmux window) they are transparent
27
- * to the graph, so the visible topology is: [scout, history] aggregator.
20
+ * There is no orchestrator turn whose context grows linearly with the
21
+ * number of specialists. Token cost per partition is bounded by the four
22
+ * specialists' independent prompts — adding more partitions scales
23
+ * cleanly because every fan-out is a fresh session.
28
24
  *
29
- * Stage 1a codebase-scout
30
- * Pure-TypeScript: lists files (git ls-files), counts LOC (batched wc -l),
31
- * renders a depth-bounded ASCII tree, and bin-packs directories into N
32
- * partitions where N is determined by the LOC heuristic. Then makes one
33
- * short LLM call to produce an architectural orientation that primes the
34
- * downstream explorers. Returns structured data via `handle.result` and
35
- * the agent's prose via `ctx.transcript(handle)`.
25
+ * Failure of one specialist does not abort the partition mid-thought —
26
+ * the runtime fails the stage, but its siblings' outputs are still on
27
+ * disk and the aggregator can continue with whatever completed.
36
28
  *
37
- * Stage 1b research-history (parallel sibling of scout)
38
- * Dispatches the codebase-research-locator and codebase-research-analyzer
39
- * sub-agents over the project's existing research/ directory to surface
40
- * prior decisions, completed investigations, and unresolved questions.
41
- * Output is consumed via session transcript (≤400 words) and feeds into
42
- * the aggregator as supplementary context.
29
+ * The synthesis step that combines specialist outputs is plain TypeScript
30
+ * (`renderExplorerMarkdown` in helpers/scratch.ts) no extra LLM call
31
+ * just to concatenate four markdown sections.
43
32
  *
44
- * Stage 2 — explorer-1..N (parallel; depends on scout + history)
45
- * Each explorer is a coordinator that dispatches specialized sub-agents
46
- * over its assigned partition (single LOC-balanced slice of the codebase):
47
- * - codebase-locator → finds relevant files in the partition
48
- * - codebase-analyzer → documents how the most relevant files work
49
- * - codebase-pattern-finder → finds existing pattern examples
50
- * - codebase-online-researcher → (conditional) external library docs
51
- * The explorer never reads files directly — it orchestrates specialists
52
- * and writes a synthesized findings document to a known scratch path.
33
+ * Topology:
53
34
  *
54
- * Stage 3 — aggregator
55
- * Reads each explorer's scratch file by path (file-based handoff to keep
56
- * the aggregator's own context lean — we deliberately do NOT inline N
57
- * transcripts into the prompt). Folds in the research-history overview
58
- * as supplementary context. Synthesizes a single research document at
59
- * research/docs/YYYY-MM-DD-<slug>.md.
35
+ * ┌─→ codebase-scout (visible)
36
+ * parent ─┤
37
+ * └─→ history-locator history-analyzer (headless)
38
+ *
39
+ *
40
+ * ┌──────────────────────────────────────────────────────────────────────┐
41
+ * │ Per-partition (Promise.all over partitions, all stages headless): │
42
+ * │ │
43
+ * │ locator-i ∥ pattern-finder-i (Layer 1, parallel) │
44
+ * │ │ │
45
+ * │ ▼ │
46
+ * │ analyzer-i ∥ online-researcher-i (Layer 2, parallel) │
47
+ * │ │ │
48
+ * │ ▼ │
49
+ * │ deterministic write to scratch file (TS helper, no LLM) │
50
+ * └──────────────────────────────────────────────────────────────────────┘
51
+ * │
52
+ * ▼
53
+ * aggregator (visible)
60
54
  *
61
- * Context-engineering decisions are documented at each stage below.
55
+ * Specialist stages run headless (in-process via the Agent SDK's `query()`),
56
+ * so they are transparent to the workflow graph. The visible nodes are just:
57
+ * parent → [codebase-scout] → aggregator
62
58
  */
63
59
 
64
- import { defineWorkflow } from "../../../index.ts";
60
+ import { defineWorkflow, extractAssistantText } from "../../../index.ts";
65
61
  import { mkdir } from "node:fs/promises";
66
62
  import path from "node:path";
67
63
 
@@ -76,21 +72,31 @@ import {
76
72
  } from "../helpers/heuristic.ts";
77
73
  import {
78
74
  buildAggregatorPrompt,
79
- buildExplorerPrompt,
80
- buildHistoryPrompt,
75
+ buildAnalyzerPrompt,
76
+ buildHistoryAnalyzerPrompt,
77
+ buildHistoryLocatorPrompt,
78
+ buildLocatorPrompt,
79
+ buildOnlineResearcherPrompt,
80
+ buildPatternFinderPrompt,
81
81
  buildScoutPrompt,
82
82
  slugifyPrompt,
83
83
  } from "../helpers/prompts.ts";
84
+ import { writeExplorerScratchFile } from "../helpers/scratch.ts";
84
85
 
85
- // ── Idle detection ─────────────────────────────────────────────────────────
86
- // Completion is detected by watching the session JSONL file for idle and result
87
- // events from Claude's own SDK no manual timeout is needed. The loop runs
88
- // until Claude reports idle or a result (success, error_max_turns, etc.).
86
+ /**
87
+ * Shared SDK options for every sub-agent dispatch. `permissionMode` +
88
+ * `allowDangerouslySkipPermissions` are required so the headless sub-agents
89
+ * can use Read/Grep/Glob/Bash without prompting (we are running unattended).
90
+ */
91
+ const SUBAGENT_OPTS = {
92
+ permissionMode: "bypassPermissions",
93
+ allowDangerouslySkipPermissions: true,
94
+ } as const;
89
95
 
90
96
  export default defineWorkflow({
91
97
  name: "deep-research-codebase",
92
98
  description:
93
- "Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
99
+ "Deterministic deep codebase research: scout → per-partition specialist sub-agents → aggregator",
94
100
  inputs: [
95
101
  {
96
102
  name: "prompt",
@@ -102,34 +108,29 @@ export default defineWorkflow({
102
108
  })
103
109
  .for<"claude">()
104
110
  .run(async (ctx) => {
105
- // Destructure once so every stage below can close over a bare
106
- // `prompt` string without re-reaching into ctx.inputs.
107
111
  const prompt = ctx.inputs.prompt ?? "";
108
112
  const root = getCodebaseRoot();
109
113
  const startedAt = new Date();
110
114
  const isoDate = startedAt.toISOString().slice(0, 10);
111
115
  const slug = slugifyPrompt(prompt);
112
116
 
113
- // ── Stages 1a + 1b: codebase-scout research-history ──────────────────
114
- // Run the codebase scout (deterministic compute + brief LLM orientation)
115
- // in parallel with the research-history scout (sub-agent dispatch over
116
- // the project's prior research docs). Both must complete before any
117
- // explorer starts, since:
118
- // - explorers depend on `scout.result.partitions`
119
- // - aggregator depends on the history transcript
120
- // Promise.all gives us the cleanest auto-inferred graph topology:
121
- // parent → [scout, history] → [explorer-1..N] → aggregator.
122
- const [scout, history] = await Promise.all([
117
+ // ── Stage 1a: codebase-scout (visible) ‖ Stage 1b: research-history pipeline (headless) ──
118
+ //
119
+ // Both pipelines are independent of each other and must complete before
120
+ // any explorer fan-out explorers depend on `scout.result.partitions`
121
+ // and the aggregator embeds the history overview as supplementary
122
+ // context. We wrap the history sub-pipeline (locator analyzer) in an
123
+ // IIFE so Promise.all sees it as a single awaitable.
124
+ const [scout, historyOverview] = await Promise.all([
123
125
  ctx.stage(
124
126
  {
125
127
  name: "codebase-scout",
126
128
  description:
127
- "Map codebase, count LOC, partition for parallel explorers",
129
+ "Map codebase, count LOC, partition for parallel specialists",
128
130
  },
129
131
  {},
130
132
  {},
131
133
  async (s) => {
132
- // 1. Deterministic scouting.
133
134
  const data = scoutCodebase(root);
134
135
  if (data.units.length === 0) {
135
136
  throw new Error(
@@ -138,13 +139,10 @@ export default defineWorkflow({
138
139
  );
139
140
  }
140
141
 
141
- // 2. Heuristic decides explorer count (capped by available units).
142
142
  const targetCount = calculateExplorerCount(data.totalLoc);
143
143
  const partitions = partitionUnits(data.units, targetCount);
144
144
  const actualCount = partitions.length;
145
145
 
146
- // 3. Scratch directory for explorer outputs (timestamped to avoid
147
- // collisions across runs).
148
146
  const scratchDir = path.join(
149
147
  root,
150
148
  "research",
@@ -153,10 +151,6 @@ export default defineWorkflow({
153
151
  );
154
152
  await mkdir(scratchDir, { recursive: true });
155
153
 
156
- // 4. Short LLM call: architectural orientation for downstream
157
- // explorers. The prompt explicitly forbids the agent from
158
- // answering the research question — its only job here is to
159
- // orient.
160
154
  await s.session.query(
161
155
  buildScoutPrompt({
162
156
  question: prompt,
@@ -181,100 +175,209 @@ export default defineWorkflow({
181
175
  };
182
176
  },
183
177
  ),
184
- ctx.stage(
185
- {
186
- name: "research-history",
187
- description:
188
- "Surface prior research via research-locator + research-analyzer",
189
- },
190
- {},
191
- {},
192
- async (s) => {
193
- // Dispatches codebase-research-locator → codebase-research-analyzer
194
- // over the project's research/ directory and outputs a ≤400-word
195
- // synthesis as prose (no file write — consumed via transcript).
196
- await s.session.query(buildHistoryPrompt({ question: prompt, root }));
197
- s.save(s.sessionId);
198
- },
199
- ),
200
- ]);
201
-
202
- const { partitions, explorerCount, scratchDir, totalLoc, totalFiles } =
203
- scout.result;
204
-
205
- // Pull both scout transcripts ONCE at the workflow level so every
206
- // explorer + the aggregator can embed them in their prompts. Both
207
- // stages have already completed by this point (we're past Promise.all),
208
- // so these reads are safe (F13).
209
- const scoutOverview = (await ctx.transcript(scout)).content;
210
- const historyOverview = (await ctx.transcript(history)).content;
178
+ // Research-history pipeline: locator → analyzer, both headless. The
179
+ // analyzer needs the locator's verbatim output, so this is sequential
180
+ // INSIDE the IIFE while remaining parallel TO the codebase scout.
181
+ (async (): Promise<string> => {
182
+ const historyLocator = await ctx.stage(
183
+ {
184
+ name: "history-locator",
185
+ headless: true,
186
+ description: "Locate prior research docs (codebase-research-locator)",
187
+ },
188
+ {},
189
+ {},
190
+ async (s) => {
191
+ const result = await s.session.query(
192
+ buildHistoryLocatorPrompt({ question: prompt, root }),
193
+ { agent: "codebase-research-locator", ...SUBAGENT_OPTS },
194
+ );
195
+ s.save(s.sessionId);
196
+ return extractAssistantText(result, 0);
197
+ },
198
+ );
211
199
 
212
- // ── Stage 2: parallel headless explorers ─────────────────────────────────
213
- // Each explorer runs headless (in-process, no tmux pane) via Promise.all.
214
- // They are invisible in the workflow graph but tracked by the background
215
- // task counter in the statusline. Each one receives:
216
- // - the original research question (top + bottom of prompt)
217
- // - the scout's architectural overview
218
- // - its OWN partition (never the full file list)
219
- // - the absolute path to its scratch file
220
- //
221
- // Information flow choices:
222
- // • We deliberately do not pass other explorers' work — they run in
223
- // parallel and forward-only data flow is enforced by the runtime
224
- // (F13). Cross-cutting happens in the aggregator.
225
- // • We pass the partition via closure capture, not by parsing
226
- // scout transcripts — strongly typed and lossless.
227
- const explorerHandles = await Promise.all(
228
- partitions.map((partition, idx) => {
229
- const i = idx + 1;
230
- const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
231
- return ctx.stage(
200
+ const historyAnalyzer = await ctx.stage(
232
201
  {
233
- name: `explorer-${i}`,
202
+ name: "history-analyzer",
234
203
  headless: true,
235
- description: `Explore ${partition
236
- .map((u) => u.path)
237
- .join(
238
- ", ",
239
- )} (${partition.reduce((s, u) => s + u.fileCount, 0)} files)`,
204
+ description: "Synthesize prior research (codebase-research-analyzer)",
240
205
  },
241
206
  {},
242
207
  {},
243
208
  async (s) => {
244
- await s.session.query(
245
- buildExplorerPrompt({
209
+ const result = await s.session.query(
210
+ buildHistoryAnalyzerPrompt({
246
211
  question: prompt,
247
- index: i,
248
- total: explorerCount,
249
- partition,
250
- scoutOverview,
251
- scratchPath,
212
+ locatorOutput: historyLocator.result,
252
213
  root,
253
214
  }),
215
+ { agent: "codebase-research-analyzer", ...SUBAGENT_OPTS },
254
216
  );
255
217
  s.save(s.sessionId);
256
-
257
- // Returning structured metadata lets the aggregator stage reach
258
- // each explorer's scratch path without re-parsing transcripts.
259
- return { index: i, scratchPath, partition };
218
+ return extractAssistantText(result, 0);
260
219
  },
261
220
  );
221
+
222
+ return historyAnalyzer.result;
223
+ })(),
224
+ ]);
225
+
226
+ const { partitions, explorerCount, scratchDir, totalLoc, totalFiles } =
227
+ scout.result;
228
+
229
+ // Pull the scout transcript ONCE so every per-partition specialist can
230
+ // embed the architectural orientation in its prompt. The scout has
231
+ // completed by the time we get here (we're past Promise.all), so this
232
+ // read is safe (failure-modes F13).
233
+ const scoutOverview = (await ctx.transcript(scout)).content;
234
+
235
+ // ── Stage 2: per-partition specialist fan-out ─────────────────────────
236
+ //
237
+ // Per partition i:
238
+ // Layer 1 (parallel): locator-i ∥ pattern-finder-i
239
+ // Layer 2 (parallel): analyzer-i ∥ online-researcher-i ← depend on locator-i
240
+ // Synthesis (deterministic TS): renderExplorerMarkdown → scratch file
241
+ //
242
+ // All N partitions run as parallel branches of the outer Promise.all.
243
+ // Sub-agent stages are headless: invisible in the graph and bounded only
244
+ // by SDK concurrency. Information flow is forward-only and all context
245
+ // each specialist needs (research question, scope, scout overview, and —
246
+ // for layer 2 — locator output) is injected into the first prompt.
247
+ const explorerHandles = await Promise.all(
248
+ partitions.map(async (partition, idx) => {
249
+ const i = idx + 1;
250
+ const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
251
+
252
+ // Layer 1: locator + pattern-finder run independently.
253
+ const [locator, patternFinder] = await Promise.all([
254
+ ctx.stage(
255
+ {
256
+ name: `locator-${i}`,
257
+ headless: true,
258
+ description: `codebase-locator over partition ${i}`,
259
+ },
260
+ {},
261
+ {},
262
+ async (s) => {
263
+ const result = await s.session.query(
264
+ buildLocatorPrompt({
265
+ question: prompt,
266
+ partition,
267
+ root,
268
+ scoutOverview,
269
+ index: i,
270
+ total: explorerCount,
271
+ }),
272
+ { agent: "codebase-locator", ...SUBAGENT_OPTS },
273
+ );
274
+ s.save(s.sessionId);
275
+ return extractAssistantText(result, 0);
276
+ },
277
+ ),
278
+ ctx.stage(
279
+ {
280
+ name: `pattern-finder-${i}`,
281
+ headless: true,
282
+ description: `codebase-pattern-finder over partition ${i}`,
283
+ },
284
+ {},
285
+ {},
286
+ async (s) => {
287
+ const result = await s.session.query(
288
+ buildPatternFinderPrompt({
289
+ question: prompt,
290
+ partition,
291
+ root,
292
+ scoutOverview,
293
+ index: i,
294
+ total: explorerCount,
295
+ }),
296
+ { agent: "codebase-pattern-finder", ...SUBAGENT_OPTS },
297
+ );
298
+ s.save(s.sessionId);
299
+ return extractAssistantText(result, 0);
300
+ },
301
+ ),
302
+ ]);
303
+
304
+ const locatorOutput = locator.result;
305
+ const patternsOutput = patternFinder.result;
306
+
307
+ // Layer 2: analyzer + online-researcher both consume locator output.
308
+ const [analyzer, onlineResearcher] = await Promise.all([
309
+ ctx.stage(
310
+ {
311
+ name: `analyzer-${i}`,
312
+ headless: true,
313
+ description: `codebase-analyzer over partition ${i}`,
314
+ },
315
+ {},
316
+ {},
317
+ async (s) => {
318
+ const result = await s.session.query(
319
+ buildAnalyzerPrompt({
320
+ question: prompt,
321
+ partition,
322
+ locatorOutput,
323
+ root,
324
+ scoutOverview,
325
+ index: i,
326
+ total: explorerCount,
327
+ }),
328
+ { agent: "codebase-analyzer", ...SUBAGENT_OPTS },
329
+ );
330
+ s.save(s.sessionId);
331
+ return extractAssistantText(result, 0);
332
+ },
333
+ ),
334
+ ctx.stage(
335
+ {
336
+ name: `online-researcher-${i}`,
337
+ headless: true,
338
+ description: `codebase-online-researcher over partition ${i}`,
339
+ },
340
+ {},
341
+ {},
342
+ async (s) => {
343
+ const result = await s.session.query(
344
+ buildOnlineResearcherPrompt({
345
+ question: prompt,
346
+ partition,
347
+ locatorOutput,
348
+ root,
349
+ index: i,
350
+ total: explorerCount,
351
+ }),
352
+ { agent: "codebase-online-researcher", ...SUBAGENT_OPTS },
353
+ );
354
+ s.save(s.sessionId);
355
+ return extractAssistantText(result, 0);
356
+ },
357
+ ),
358
+ ]);
359
+
360
+ // Deterministic synthesis — no fifth LLM call just to concatenate.
361
+ await writeExplorerScratchFile(scratchPath, {
362
+ index: i,
363
+ total: explorerCount,
364
+ partition,
365
+ locatorOutput,
366
+ patternsOutput,
367
+ analyzerOutput: analyzer.result,
368
+ onlineOutput: onlineResearcher.result,
369
+ });
370
+
371
+ return { index: i, scratchPath, partition };
262
372
  }),
263
373
  );
264
374
 
265
- // ── Stage 3: aggregator ────────────────────────────────────────────────
266
- // Synthesizes explorer findings into the final research document at
267
- // research/docs/YYYY-MM-DD-<slug>.md.
375
+ // ── Stage 3: aggregator (visible) ─────────────────────────────────────
268
376
  //
269
- // Information flow choice:
270
- // The aggregator reads explorer findings via FILE PATHS, not by
271
- // embedding all N transcripts in its prompt. This keeps its
272
- // context lean (filesystem-context skill) and lets the agent
273
- // selectively re-read source files when explorers contradict
274
- // each other.
275
- // • The aggregator only sees the scout overview (short) plus a
276
- // manifest of explorer scratch paths — token cost stays roughly
277
- // constant in N rather than growing linearly.
377
+ // Reads each partition's deterministic scratch file by PATH so the
378
+ // aggregator's own context stays bounded by N filenames + the short
379
+ // scout/history overviews not by N inlined transcripts (filesystem-
380
+ // context skill).
278
381
  const finalPath = path.join(
279
382
  root,
280
383
  "research",
@@ -286,7 +389,7 @@ export default defineWorkflow({
286
389
  {
287
390
  name: "aggregator",
288
391
  description:
289
- "Synthesize explorer findings + history into final research doc",
392
+ "Synthesize partition findings + history into final research doc",
290
393
  },
291
394
  {},
292
395
  {},
@@ -297,7 +400,7 @@ export default defineWorkflow({
297
400
  totalLoc,
298
401
  totalFiles,
299
402
  explorerCount,
300
- explorerFiles: explorerHandles.map((h) => h.result),
403
+ explorerFiles: explorerHandles,
301
404
  finalPath,
302
405
  scoutOverview,
303
406
  historyOverview,