@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.
Files changed (48) hide show
  1. package/README.md +110 -11
  2. package/dist/{chunk-mn870nrv.js → chunk-xkxndz5g.js} +213 -154
  3. package/dist/sdk/components/workflow-picker-panel.d.ts +120 -0
  4. package/dist/sdk/define-workflow.d.ts +1 -1
  5. package/dist/sdk/index.js +1 -1
  6. package/dist/sdk/runtime/discovery.d.ts +57 -3
  7. package/dist/sdk/runtime/executor.d.ts +15 -2
  8. package/dist/sdk/runtime/tmux.d.ts +9 -0
  9. package/dist/sdk/types.d.ts +63 -4
  10. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +61 -0
  11. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +48 -0
  12. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +25 -0
  13. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +91 -0
  14. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +56 -0
  15. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +48 -0
  16. package/dist/sdk/workflows/builtin/ralph/claude/index.js +6 -5
  17. package/dist/sdk/workflows/builtin/ralph/copilot/index.js +6 -5
  18. package/dist/sdk/workflows/builtin/ralph/opencode/index.js +6 -5
  19. package/dist/sdk/workflows/index.d.ts +4 -4
  20. package/dist/sdk/workflows/index.js +7 -1
  21. package/package.json +1 -1
  22. package/src/cli.ts +25 -3
  23. package/src/commands/cli/chat/index.ts +5 -5
  24. package/src/commands/cli/init/index.ts +79 -77
  25. package/src/commands/cli/workflow-command.test.ts +757 -0
  26. package/src/commands/cli/workflow.test.ts +310 -0
  27. package/src/commands/cli/workflow.ts +445 -105
  28. package/src/sdk/components/workflow-picker-panel.tsx +1462 -0
  29. package/src/sdk/define-workflow.test.ts +101 -0
  30. package/src/sdk/define-workflow.ts +62 -2
  31. package/src/sdk/runtime/discovery.ts +111 -8
  32. package/src/sdk/runtime/executor.ts +89 -32
  33. package/src/sdk/runtime/tmux.conf +55 -0
  34. package/src/sdk/runtime/tmux.ts +34 -10
  35. package/src/sdk/types.ts +67 -4
  36. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +294 -0
  37. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +276 -0
  38. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +38 -0
  39. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +816 -0
  40. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +334 -0
  41. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +284 -0
  42. package/src/sdk/workflows/builtin/ralph/claude/index.ts +8 -4
  43. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +10 -4
  44. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +8 -4
  45. package/src/sdk/workflows/index.ts +9 -1
  46. package/src/services/system/auto-sync.ts +1 -1
  47. package/src/services/system/install-ui.ts +109 -39
  48. package/src/theme/colors.ts +65 -1
@@ -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, ...args],
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 binary = getMuxBinary();
322
- if (!binary) return false;
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
- /** The original user prompt from the CLI invocation */
173
- userPrompt: string;
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
- /** The original user prompt from the CLI invocation */
216
- userPrompt: string;
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();