@bastani/atomic 0.6.3-0 → 0.6.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.
Files changed (49) hide show
  1. package/.agents/skills/ast-grep/SKILL.md +323 -0
  2. package/.agents/skills/ast-grep/references/rule_reference.md +297 -0
  3. package/.agents/skills/ripgrep/SKILL.md +382 -0
  4. package/.mcp.json +5 -6
  5. package/dist/commands/cli/claude-inflight-hook.d.ts +100 -0
  6. package/dist/commands/cli/claude-inflight-hook.d.ts.map +1 -0
  7. package/dist/commands/cli/claude-stop-hook.d.ts +2 -0
  8. package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -1
  9. package/dist/lib/spawn.d.ts +1 -1
  10. package/dist/lib/spawn.d.ts.map +1 -1
  11. package/dist/sdk/providers/claude.d.ts +36 -0
  12. package/dist/sdk/providers/claude.d.ts.map +1 -1
  13. package/dist/sdk/providers/copilot.d.ts +17 -1
  14. package/dist/sdk/providers/copilot.d.ts.map +1 -1
  15. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  16. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +49 -34
  17. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  18. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +18 -16
  19. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  20. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts +43 -0
  21. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts.map +1 -0
  22. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +30 -0
  23. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
  24. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +2 -1
  25. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -1
  26. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +18 -16
  27. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  28. package/dist/services/config/additional-instructions.d.ts +67 -0
  29. package/dist/services/config/additional-instructions.d.ts.map +1 -0
  30. package/package.json +3 -1
  31. package/src/cli.ts +18 -1
  32. package/src/commands/cli/chat/index.ts +52 -2
  33. package/src/commands/cli/claude-inflight-hook.test.ts +598 -0
  34. package/src/commands/cli/claude-inflight-hook.ts +359 -0
  35. package/src/commands/cli/claude-stop-hook.ts +40 -4
  36. package/src/commands/cli/init/index.ts +9 -0
  37. package/src/lib/spawn.ts +6 -2
  38. package/src/sdk/providers/claude.ts +131 -0
  39. package/src/sdk/providers/copilot.ts +30 -1
  40. package/src/sdk/runtime/executor.ts +43 -2
  41. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +318 -158
  42. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +253 -129
  43. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/batching.ts +65 -0
  44. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/ignore-by-default.d.ts +8 -0
  45. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +203 -12
  46. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +248 -78
  47. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +258 -146
  48. package/src/services/config/additional-instructions.ts +273 -0
  49. package/src/services/system/auto-sync.ts +10 -1
@@ -1,33 +1,35 @@
1
1
  /**
2
2
  * deep-research-codebase / opencode
3
3
  *
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.
4
+ * OpenCode replica of the Claude deep-research-codebase workflow with the
5
+ * same **batched** Task-tool fan-out. Specialist sub-agents run inside batch
6
+ * sessions: each batch is a single `ctx.stage()` whose orchestrator turn
7
+ * dispatches up to MAX_TASKS_PER_BATCH (≈10) specialists in parallel via
8
+ * OpenCode's `task` tool. The default agent must have `task: "allow"` in
9
+ * its permission ruleset (see `.opencode/agents/worker.md` for the
10
+ * canonical example).
11
+ *
12
+ * See claude/index.ts for the full design rationale and topology diagram.
8
13
  *
9
14
  * OpenCode-specific concerns baked in (see references/failure-modes.md):
10
15
  *
11
- * • F5every `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.
16
+ * • F3 — `result.data!.parts` is heterogenous. Batch sessions don't read
17
+ * `extractResponseText` for orchestrator output (sub-agents write to
18
+ * disk; the orchestrator reply is just a short tally), but the history
19
+ * pipeline still uses it.
14
20
  *
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.
21
+ * • F5every `ctx.stage()` is a FRESH session. Batch session prompts
22
+ * embed everything the orchestrator needs in the first turn.
18
23
  *
19
- * • F6 — every prompt explicitly requires trailing prose so transcripts and
20
- * `extractResponseText()` reads are never empty.
24
+ * • F6 — orchestrator prompt requires a single-line tally as the trailing
25
+ * turn so transcripts are never empty.
21
26
  *
22
27
  * • 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.
25
- *
26
- * See claude/index.ts for the full design rationale and topology diagram.
28
+ * `result.data!`.
27
29
  */
28
30
 
29
31
  import { defineWorkflow } from "../../../index.ts";
30
- import { mkdir } from "node:fs/promises";
32
+ import { mkdir, readFile } from "node:fs/promises";
31
33
  import path from "node:path";
32
34
 
33
35
  import {
@@ -42,6 +44,7 @@ import {
42
44
  import {
43
45
  buildAggregatorPrompt,
44
46
  buildAnalyzerPrompt,
47
+ buildBatchOrchestratorPrompt,
45
48
  buildHistoryAnalyzerPrompt,
46
49
  buildHistoryLocatorPrompt,
47
50
  buildLocatorPrompt,
@@ -49,8 +52,16 @@ import {
49
52
  buildPatternFinderPrompt,
50
53
  buildScoutPrompt,
51
54
  slugifyPrompt,
55
+ wrapPromptForTaskDispatch,
52
56
  } from "../helpers/prompts.ts";
53
57
  import { writeExplorerScratchFile } from "../helpers/scratch.ts";
58
+ import {
59
+ chunkBatches,
60
+ MAX_TASKS_PER_BATCH,
61
+ SUBAGENT_TYPE,
62
+ type Layer1Task,
63
+ type Layer2Task,
64
+ } from "../helpers/batching.ts";
54
65
 
55
66
  /** Filter for text parts only — non-text parts produce [object Object]. */
56
67
  function extractResponseText(
@@ -62,6 +73,31 @@ function extractResponseText(
62
73
  .join("\n");
63
74
  }
64
75
 
76
+ /** Read a file as UTF-8, returning empty string if missing or unreadable. */
77
+ async function safeReadFile(absPath: string): Promise<string> {
78
+ try {
79
+ return await readFile(absPath, "utf8");
80
+ } catch {
81
+ return "";
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Log Promise.allSettled rejection reasons to stderr so an all-failed wave
87
+ * leaves a debugging trail instead of silently producing an empty report.
88
+ */
89
+ function logBatchRejections(
90
+ label: string,
91
+ results: PromiseSettledResult<unknown>[],
92
+ ): void {
93
+ for (let i = 0; i < results.length; i++) {
94
+ const r = results[i];
95
+ if (r?.status === "rejected") {
96
+ console.error(`[deep-research-codebase] ${label} batch ${i + 1} failed:`, r.reason);
97
+ }
98
+ }
99
+ }
100
+
65
101
  export default defineWorkflow({
66
102
  name: "deep-research-codebase",
67
103
  description:
@@ -98,7 +134,9 @@ export default defineWorkflow({
98
134
  if (data.units.length === 0) {
99
135
  throw new Error(
100
136
  `deep-research-codebase: scout found no source files under ${root}. ` +
101
- `Run from inside a code repository or check the CODE_EXTENSIONS list.`,
137
+ `Run from inside a code repository, or verify your files use a ` +
138
+ `recognized programming-language extension (sourced from GitHub ` +
139
+ `Linguist + sql/graphql/proto).`,
102
140
  );
103
141
  }
104
142
 
@@ -209,151 +247,225 @@ export default defineWorkflow({
209
247
 
210
248
  const scoutOverview = (await ctx.transcript(scout)).content;
211
249
 
212
- // ── Stage 2: per-partition specialist fan-out ─────────────────────────
213
- const explorerHandles = await Promise.all(
214
- partitions.map(async (partition, idx) => {
215
- const i = idx + 1;
216
- const scratchPath = path.join(scratchDir, `explorer-${i}.md`);
250
+ // ── Stage 2: batched specialist fan-out ───────────────────────────────
251
+ //
252
+ // Same two-wave batched design as claude/index.ts. Each batch session
253
+ // pins the dispatcher to the `orchestrator` agent (.opencode/agents/
254
+ // orchestrator.md) its system prompt is purpose-built to delegate
255
+ // everything via the `task` tool, so the dispatcher cannot wander off
256
+ // and start doing the specialists' work itself.
217
257
 
218
- // Layer 1: locator + pattern-finder run independently.
219
- const [locator, patternFinder] = await Promise.all([
220
- ctx.stage(
221
- {
222
- name: `locator-${i}`,
223
- headless: true,
224
- description: `codebase-locator over partition ${i}`,
225
- },
226
- {},
227
- { title: `locator-${i}` },
228
- async (s) => {
229
- const result = await s.client.session.prompt({
230
- sessionID: s.session.id,
231
- parts: [
232
- {
233
- type: "text",
234
- text: buildLocatorPrompt({
235
- question: prompt,
236
- partition,
237
- scoutOverview,
238
- index: i,
239
- total: explorerCount,
240
- }),
241
- },
242
- ],
243
- agent: "codebase-locator",
244
- });
245
- s.save(result.data!);
246
- return extractResponseText(result.data!.parts);
247
- },
248
- ),
249
- ctx.stage(
250
- {
251
- name: `pattern-finder-${i}`,
252
- headless: true,
253
- description: `codebase-pattern-finder over partition ${i}`,
254
- },
255
- {},
256
- { title: `pattern-finder-${i}` },
257
- async (s) => {
258
- const result = await s.client.session.prompt({
259
- sessionID: s.session.id,
260
- parts: [
261
- {
262
- type: "text",
263
- text: buildPatternFinderPrompt({
264
- question: prompt,
265
- partition,
266
- scoutOverview,
267
- index: i,
268
- total: explorerCount,
269
- }),
270
- },
271
- ],
272
- agent: "codebase-pattern-finder",
258
+ // Per-partition output paths, computed once and reused across both wave
259
+ // task-list construction and synthesis.
260
+ const partitionPaths = partitions.map((_, idx) => {
261
+ const i = idx + 1;
262
+ return {
263
+ locator: path.join(scratchDir, `locator-${i}.md`),
264
+ patternFinder: path.join(scratchDir, `pattern-finder-${i}.md`),
265
+ analyzer: path.join(scratchDir, `analyzer-${i}.md`),
266
+ online: path.join(scratchDir, `online-${i}.md`),
267
+ explorer: path.join(scratchDir, `explorer-${i}.md`),
268
+ };
269
+ });
270
+
271
+ const wave1Tasks: Layer1Task[] = partitions.flatMap((partition, idx) => {
272
+ const i = idx + 1;
273
+ const paths = partitionPaths[idx]!;
274
+ return [
275
+ {
276
+ kind: "locator" as const,
277
+ partitionIndex: i,
278
+ partition,
279
+ outputPath: paths.locator,
280
+ },
281
+ {
282
+ kind: "pattern-finder" as const,
283
+ partitionIndex: i,
284
+ partition,
285
+ outputPath: paths.patternFinder,
286
+ },
287
+ ];
288
+ });
289
+
290
+ const wave1Batches = chunkBatches(wave1Tasks, MAX_TASKS_PER_BATCH);
291
+
292
+ const wave1Results = await Promise.allSettled(
293
+ wave1Batches.map((batch, batchIdx) => {
294
+ const batchNumber = batchIdx + 1;
295
+ return ctx.stage(
296
+ {
297
+ name: `wave1-batch-${batchNumber}`,
298
+ headless: true,
299
+ description: `Layer 1 dispatch (${batch.length} tasks)`,
300
+ },
301
+ {},
302
+ { title: `wave1-batch-${batchNumber}` },
303
+ async (s) => {
304
+ const taskSpecs = batch.map((t) => {
305
+ const builder =
306
+ t.kind === "locator" ? buildLocatorPrompt : buildPatternFinderPrompt;
307
+ const specialistPrompt = builder({
308
+ question: prompt,
309
+ partition: t.partition,
310
+ scoutOverview,
311
+ index: t.partitionIndex,
312
+ total: explorerCount,
273
313
  });
274
- s.save(result.data!);
275
- return extractResponseText(result.data!.parts);
276
- },
277
- ),
278
- ]);
314
+ return {
315
+ subagentType: SUBAGENT_TYPE[t.kind],
316
+ outputPath: t.outputPath,
317
+ prompt: wrapPromptForTaskDispatch({
318
+ specialistPrompt,
319
+ outputPath: t.outputPath,
320
+ agentLabel: t.kind.toUpperCase().replaceAll("-", "_"),
321
+ }),
322
+ };
323
+ });
324
+
325
+ const result = await s.client.session.prompt({
326
+ sessionID: s.session.id,
327
+ parts: [
328
+ {
329
+ type: "text",
330
+ text: buildBatchOrchestratorPrompt({
331
+ wave: 1,
332
+ batchIndex: batchNumber,
333
+ totalBatches: wave1Batches.length,
334
+ tasks: taskSpecs,
335
+ }),
336
+ },
337
+ ],
338
+ agent: "orchestrator",
339
+ });
340
+ s.save(result.data!);
341
+ },
342
+ );
343
+ }),
344
+ );
345
+ logBatchRejections("wave1", wave1Results);
279
346
 
280
- const locatorOutput = locator.result;
281
- const patternsOutput = patternFinder.result;
347
+ const locatorOutputs: Map<number, string> = new Map();
348
+ await Promise.all(
349
+ partitions.map(async (_p, idx) => {
350
+ const i = idx + 1;
351
+ locatorOutputs.set(i, await safeReadFile(partitionPaths[idx]!.locator));
352
+ }),
353
+ );
282
354
 
283
- // Layer 2: analyzer + online-researcher consume locator output.
284
- const [analyzer, onlineResearcher] = await Promise.all([
285
- ctx.stage(
286
- {
287
- name: `analyzer-${i}`,
288
- headless: true,
289
- description: `codebase-analyzer over partition ${i}`,
290
- },
291
- {},
292
- { title: `analyzer-${i}` },
293
- async (s) => {
294
- const result = await s.client.session.prompt({
295
- sessionID: s.session.id,
296
- parts: [
297
- {
298
- type: "text",
299
- text: buildAnalyzerPrompt({
355
+ const wave2Tasks: Layer2Task[] = partitions.flatMap((partition, idx) => {
356
+ const i = idx + 1;
357
+ const paths = partitionPaths[idx]!;
358
+ const locatorOutput = locatorOutputs.get(i) ?? "";
359
+ return [
360
+ {
361
+ kind: "analyzer" as const,
362
+ partitionIndex: i,
363
+ partition,
364
+ outputPath: paths.analyzer,
365
+ locatorOutput,
366
+ },
367
+ {
368
+ kind: "online-researcher" as const,
369
+ partitionIndex: i,
370
+ partition,
371
+ outputPath: paths.online,
372
+ locatorOutput,
373
+ },
374
+ ];
375
+ });
376
+
377
+ const wave2Batches = chunkBatches(wave2Tasks, MAX_TASKS_PER_BATCH);
378
+
379
+ const wave2Results = await Promise.allSettled(
380
+ wave2Batches.map((batch, batchIdx) => {
381
+ const batchNumber = batchIdx + 1;
382
+ return ctx.stage(
383
+ {
384
+ name: `wave2-batch-${batchNumber}`,
385
+ headless: true,
386
+ description: `Layer 2 dispatch (${batch.length} tasks)`,
387
+ },
388
+ {},
389
+ { title: `wave2-batch-${batchNumber}` },
390
+ async (s) => {
391
+ const taskSpecs = batch.map((t) => {
392
+ const specialistPrompt =
393
+ t.kind === "analyzer"
394
+ ? buildAnalyzerPrompt({
300
395
  question: prompt,
301
- partition,
302
- locatorOutput,
396
+ partition: t.partition,
397
+ locatorOutput: t.locatorOutput,
303
398
  scoutOverview,
304
- index: i,
399
+ index: t.partitionIndex,
305
400
  total: explorerCount,
306
- }),
307
- },
308
- ],
309
- agent: "codebase-analyzer",
310
- });
311
- s.save(result.data!);
312
- return extractResponseText(result.data!.parts);
313
- },
314
- ),
315
- ctx.stage(
316
- {
317
- name: `online-researcher-${i}`,
318
- headless: true,
319
- description: `codebase-online-researcher over partition ${i}`,
320
- },
321
- {},
322
- { title: `online-researcher-${i}` },
323
- async (s) => {
324
- const result = await s.client.session.prompt({
325
- sessionID: s.session.id,
326
- parts: [
327
- {
328
- type: "text",
329
- text: buildOnlineResearcherPrompt({
401
+ })
402
+ : buildOnlineResearcherPrompt({
330
403
  question: prompt,
331
- partition,
332
- locatorOutput,
333
- index: i,
404
+ partition: t.partition,
405
+ locatorOutput: t.locatorOutput,
406
+ index: t.partitionIndex,
334
407
  total: explorerCount,
335
- }),
336
- },
337
- ],
338
- agent: "codebase-online-researcher",
339
- });
340
- s.save(result.data!);
341
- return extractResponseText(result.data!.parts);
342
- },
343
- ),
344
- ]);
408
+ });
409
+ return {
410
+ subagentType: SUBAGENT_TYPE[t.kind],
411
+ outputPath: t.outputPath,
412
+ prompt: wrapPromptForTaskDispatch({
413
+ specialistPrompt,
414
+ outputPath: t.outputPath,
415
+ agentLabel: t.kind.toUpperCase().replaceAll("-", "_"),
416
+ }),
417
+ };
418
+ });
419
+
420
+ const result = await s.client.session.prompt({
421
+ sessionID: s.session.id,
422
+ parts: [
423
+ {
424
+ type: "text",
425
+ text: buildBatchOrchestratorPrompt({
426
+ wave: 2,
427
+ batchIndex: batchNumber,
428
+ totalBatches: wave2Batches.length,
429
+ tasks: taskSpecs,
430
+ }),
431
+ },
432
+ ],
433
+ agent: "orchestrator",
434
+ });
435
+ s.save(result.data!);
436
+ },
437
+ );
438
+ }),
439
+ );
440
+ logBatchRejections("wave2", wave2Results);
441
+
442
+ // Synthesis: read all four specialist files per partition and write the
443
+ // consolidated explorer scratch file. Missing files fall back to "" so
444
+ // partial batch failures degrade gracefully.
445
+ const explorerHandles = await Promise.all(
446
+ partitions.map(async (partition, idx) => {
447
+ const i = idx + 1;
448
+ const paths = partitionPaths[idx]!;
449
+
450
+ const [locatorOutput, patternsOutput, analyzerOutput, onlineOutput] =
451
+ await Promise.all([
452
+ Promise.resolve(locatorOutputs.get(i) ?? ""),
453
+ safeReadFile(paths.patternFinder),
454
+ safeReadFile(paths.analyzer),
455
+ safeReadFile(paths.online),
456
+ ]);
345
457
 
346
- await writeExplorerScratchFile(scratchPath, {
458
+ await writeExplorerScratchFile(paths.explorer, {
347
459
  index: i,
348
460
  total: explorerCount,
349
461
  partition,
350
462
  locatorOutput,
351
463
  patternsOutput,
352
- analyzerOutput: analyzer.result,
353
- onlineOutput: onlineResearcher.result,
464
+ analyzerOutput,
465
+ onlineOutput,
354
466
  });
355
467
 
356
- return { index: i, scratchPath, partition };
468
+ return { index: i, scratchPath: paths.explorer, partition };
357
469
  }),
358
470
  );
359
471