@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/README.md
CHANGED
|
@@ -167,11 +167,16 @@ export default defineWorkflow<"claude">({
|
|
|
167
167
|
description: "Research -> Implement -> Review",
|
|
168
168
|
})
|
|
169
169
|
.run(async (ctx) => {
|
|
170
|
+
// Free-form workflows receive their positional prompt under
|
|
171
|
+
// `ctx.inputs.prompt`. Destructure it once so every stage below
|
|
172
|
+
// can close over a bare string.
|
|
173
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
174
|
+
|
|
170
175
|
const research = await ctx.stage(
|
|
171
176
|
{ name: "research", description: "Analyze the codebase" },
|
|
172
177
|
{}, {},
|
|
173
178
|
async (s) => {
|
|
174
|
-
await s.session.query(`/research-codebase ${
|
|
179
|
+
await s.session.query(`/research-codebase ${prompt}`);
|
|
175
180
|
s.save(s.sessionId);
|
|
176
181
|
},
|
|
177
182
|
);
|
|
@@ -250,11 +255,13 @@ export default defineWorkflow<"claude">({
|
|
|
250
255
|
description: "Two-session pipeline: describe -> summarize",
|
|
251
256
|
})
|
|
252
257
|
.run(async (ctx) => {
|
|
258
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
259
|
+
|
|
253
260
|
const describe = await ctx.stage(
|
|
254
261
|
{ name: "describe", description: "Ask Claude to describe the project" },
|
|
255
262
|
{}, {},
|
|
256
263
|
async (s) => {
|
|
257
|
-
await s.session.query(
|
|
264
|
+
await s.session.query(prompt);
|
|
258
265
|
s.save(s.sessionId);
|
|
259
266
|
},
|
|
260
267
|
);
|
|
@@ -287,10 +294,12 @@ export default defineWorkflow<"claude">({
|
|
|
287
294
|
description: "describe -> [summarize-a, summarize-b] -> merge",
|
|
288
295
|
})
|
|
289
296
|
.run(async (ctx) => {
|
|
297
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
298
|
+
|
|
290
299
|
const describe = await ctx.stage(
|
|
291
300
|
{ name: "describe" }, {}, {},
|
|
292
301
|
async (s) => {
|
|
293
|
-
await s.session.query(
|
|
302
|
+
await s.session.query(prompt);
|
|
294
303
|
s.save(s.sessionId);
|
|
295
304
|
},
|
|
296
305
|
);
|
|
@@ -322,6 +331,71 @@ export default defineWorkflow<"claude">({
|
|
|
322
331
|
|
|
323
332
|
</details>
|
|
324
333
|
|
|
334
|
+
<details>
|
|
335
|
+
<summary>Example: Structured-input workflow (declared schema + CLI flag validation)</summary>
|
|
336
|
+
|
|
337
|
+
Declare an `inputs` array on `defineWorkflow` and the CLI materialises one `--<field>=<value>` flag per entry. Required fields, enum membership, and unknown-flag rejection are all validated before any tmux session is spawned. The interactive picker (`atomic workflow -a <agent>`) renders the same schema as a form.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// .atomic/workflows/gen-spec/claude/index.ts
|
|
341
|
+
import { defineWorkflow } from "@bastani/atomic/workflows";
|
|
342
|
+
|
|
343
|
+
export default defineWorkflow<"claude">({
|
|
344
|
+
name: "gen-spec",
|
|
345
|
+
description: "Convert a research doc into an execution spec",
|
|
346
|
+
inputs: [
|
|
347
|
+
{
|
|
348
|
+
name: "research_doc",
|
|
349
|
+
type: "string",
|
|
350
|
+
required: true,
|
|
351
|
+
description: "path to the research doc",
|
|
352
|
+
placeholder: "research/docs/2026-04-11-auth.md",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "focus",
|
|
356
|
+
type: "enum",
|
|
357
|
+
required: true,
|
|
358
|
+
description: "how aggressively to scope the spec",
|
|
359
|
+
values: ["minimal", "standard", "exhaustive"],
|
|
360
|
+
default: "standard",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "notes",
|
|
364
|
+
type: "text",
|
|
365
|
+
description: "extra guidance for the spec writer (optional)",
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
})
|
|
369
|
+
.run(async (ctx) => {
|
|
370
|
+
// Read each declared field by name.
|
|
371
|
+
const { research_doc, focus } = ctx.inputs;
|
|
372
|
+
const notes = ctx.inputs.notes ?? "";
|
|
373
|
+
|
|
374
|
+
await ctx.stage({ name: "write-spec" }, {}, {}, async (s) => {
|
|
375
|
+
await s.session.query(
|
|
376
|
+
`Read ${research_doc} and produce a ${focus} spec.` +
|
|
377
|
+
(notes ? `\n\nExtra guidance:\n${notes}` : ""),
|
|
378
|
+
);
|
|
379
|
+
s.save(s.sessionId);
|
|
380
|
+
});
|
|
381
|
+
})
|
|
382
|
+
.compile();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Run it either way:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
# Named + flags (scriptable; CI-friendly)
|
|
389
|
+
atomic workflow -n gen-spec -a claude \
|
|
390
|
+
--research_doc=research/docs/2026-04-11-auth.md \
|
|
391
|
+
--focus=standard
|
|
392
|
+
|
|
393
|
+
# Picker (fuzzy-search the workflow list, then fill the form)
|
|
394
|
+
atomic workflow -a claude
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
</details>
|
|
398
|
+
|
|
325
399
|
**Key capabilities:**
|
|
326
400
|
|
|
327
401
|
| Capability | Description |
|
|
@@ -330,6 +404,8 @@ export default defineWorkflow<"claude">({
|
|
|
330
404
|
| **Native TypeScript control flow** | Use `for`, `if/else`, `Promise.all()`, `try/catch` — no framework DSL needed |
|
|
331
405
|
| **Session return values** | Session callbacks can return data: `const h = await ctx.stage(...); h.result` |
|
|
332
406
|
| **Transcript passing** | Access prior session output via handle (`s.transcript(handle)`) or name (`s.transcript("name")`) |
|
|
407
|
+
| **Declared input schemas** | Add an `inputs: [...]` array to `defineWorkflow()` and the CLI materialises `--<field>=<value>` flags with built-in validation (required fields, enum membership, unknown flags) |
|
|
408
|
+
| **Interactive picker** | `atomic workflow -a <agent>` launches a fuzzy-searchable picker that renders each workflow's input schema as a form — no flag-memorisation required |
|
|
333
409
|
| **Nested sub-sessions** | Call `s.stage()` inside a session callback to spawn child sessions — visible as nested nodes in the graph |
|
|
334
410
|
| **Auto-inferred graph** | Graph topology auto-inferred from `await`/`Promise.all` patterns — no annotations needed |
|
|
335
411
|
| **Provider-agnostic** | Write raw SDK code for Claude, Copilot, or OpenCode inside each session callback |
|
|
@@ -368,7 +444,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
|
|
|
368
444
|
|
|
369
445
|
| Property | Type | Description |
|
|
370
446
|
| ----------------------- | ------------------------- | -------------------------------------------------------------- |
|
|
371
|
-
| `ctx.
|
|
447
|
+
| `ctx.inputs` | `Record<string, string>` | Structured inputs for this run. Free-form workflows store their positional prompt under `ctx.inputs.prompt`; workflows with a declared `inputs` schema store one key per declared field |
|
|
372
448
|
| `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
|
|
373
449
|
| `ctx.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
|
|
374
450
|
| `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
|
|
@@ -380,7 +456,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
|
|
|
380
456
|
| ----------------------- | ------------------------- | -------------------------------------------------------------- |
|
|
381
457
|
| `s.client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
|
|
382
458
|
| `s.session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
|
|
383
|
-
| `s.
|
|
459
|
+
| `s.inputs` | `Record<string, string>` | Same inputs record as `ctx.inputs`, forwarded into every stage so session callbacks can read values without closing over the outer `ctx` |
|
|
384
460
|
| `s.agent` | `AgentType` | Which agent is running |
|
|
385
461
|
| `s.paneId` | `string` | tmux pane ID for this session |
|
|
386
462
|
| `s.sessionId` | `string` | Session UUID |
|
|
@@ -750,12 +826,35 @@ atomic chat -a claude --verbose # Forward --verbose to claude
|
|
|
750
826
|
|
|
751
827
|
#### `atomic workflow` Flags
|
|
752
828
|
|
|
753
|
-
| Flag
|
|
754
|
-
|
|
|
755
|
-
| `-n, --name <name>`
|
|
756
|
-
| `-a, --agent <name>`
|
|
757
|
-
| `-l, --list`
|
|
758
|
-
|
|
|
829
|
+
| Flag | Description |
|
|
830
|
+
| -------------------------- | ------------------------------------------------------------------- |
|
|
831
|
+
| `-n, --name <name>` | Workflow name (matches directory under `.atomic/workflows/<name>/`) |
|
|
832
|
+
| `-a, --agent <name>` | Agent: `claude`, `opencode`, `copilot` |
|
|
833
|
+
| `-l, --list` | List available workflows, grouped by source |
|
|
834
|
+
| `--<field>=<value>` | Structured input for workflows that declare an `inputs` schema (also accepts `--<field> <value>`) |
|
|
835
|
+
| `[prompt...]` | Positional prompt for free-form workflows (rejected on workflows with a declared schema) |
|
|
836
|
+
|
|
837
|
+
The workflow command supports four invocation shapes:
|
|
838
|
+
|
|
839
|
+
```bash
|
|
840
|
+
# 1. List every workflow available to you, grouped by source
|
|
841
|
+
atomic workflow -l
|
|
842
|
+
|
|
843
|
+
# 2. Launch the interactive picker for an agent (no -n) — fuzzy-search
|
|
844
|
+
# the list, fill the form rendered from the workflow's declared inputs,
|
|
845
|
+
# and confirm with y/n
|
|
846
|
+
atomic workflow -a claude
|
|
847
|
+
|
|
848
|
+
# 3. Run a free-form workflow with a positional prompt
|
|
849
|
+
atomic workflow -n ralph -a claude "build a REST API for user management"
|
|
850
|
+
|
|
851
|
+
# 4. Run a structured-input workflow with one --<field> flag per declared input
|
|
852
|
+
atomic workflow -n gen-spec -a claude \
|
|
853
|
+
--research_doc=research/docs/2026-04-11-auth.md \
|
|
854
|
+
--focus=standard
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Workflows that declare an `inputs: WorkflowInput[]` schema get CLI flag validation for free — missing required fields and invalid enum values are rejected before any tmux session is spawned, with error messages that spell out the expected flag set. Workflows that don't declare a schema still accept a single positional prompt, which the runtime stores under `ctx.inputs.prompt`. **Builtin workflows (like `ralph`) are reserved names** — a local or global workflow with the same name will not shadow a builtin at resolution time.
|
|
759
858
|
|
|
760
859
|
### Atomic-Provided Skills (invokable from any agent chat)
|
|
761
860
|
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/sdk/define-workflow.ts
|
|
3
|
+
function validateWorkflowInput(input, workflowName) {
|
|
4
|
+
if (!input.name || input.name.trim() === "") {
|
|
5
|
+
throw new Error(`Workflow "${workflowName}" has an input with an empty name.`);
|
|
6
|
+
}
|
|
7
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input.name)) {
|
|
8
|
+
throw new Error(`Workflow "${workflowName}" input "${input.name}" has an invalid ` + `name \u2014 must start with a letter and contain only letters, ` + `digits, underscores, and dashes (so it can be used as a ` + `\`--${input.name}\` CLI flag).`);
|
|
9
|
+
}
|
|
10
|
+
if (input.type === "enum") {
|
|
11
|
+
if (!Array.isArray(input.values) || input.values.length === 0) {
|
|
12
|
+
throw new Error(`Workflow "${workflowName}" input "${input.name}" is an enum but ` + `declares no \`values\`.`);
|
|
13
|
+
}
|
|
14
|
+
if (input.default !== undefined && !input.values.includes(input.default)) {
|
|
15
|
+
throw new Error(`Workflow "${workflowName}" input "${input.name}" has a default ` + `"${input.default}" that is not one of its declared values: ` + `${input.values.join(", ")}.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
class WorkflowBuilder {
|
|
4
21
|
__brand = "WorkflowBuilder";
|
|
5
22
|
options;
|
|
@@ -22,10 +39,21 @@ class WorkflowBuilder {
|
|
|
22
39
|
throw new Error(`Workflow "${this.options.name}" has no run callback. ` + `Add a .run(async (ctx) => { ... }) call before .compile().`);
|
|
23
40
|
}
|
|
24
41
|
const runFn = this.runFn;
|
|
42
|
+
const declaredInputs = this.options.inputs ?? [];
|
|
43
|
+
const seen = new Set;
|
|
44
|
+
for (const input of declaredInputs) {
|
|
45
|
+
validateWorkflowInput(input, this.options.name);
|
|
46
|
+
if (seen.has(input.name)) {
|
|
47
|
+
throw new Error(`Workflow "${this.options.name}" has duplicate input name "${input.name}".`);
|
|
48
|
+
}
|
|
49
|
+
seen.add(input.name);
|
|
50
|
+
}
|
|
51
|
+
const inputs = Object.freeze(declaredInputs.map((i) => Object.freeze({ ...i })));
|
|
25
52
|
return {
|
|
26
53
|
__brand: "WorkflowDefinition",
|
|
27
54
|
name: this.options.name,
|
|
28
55
|
description: this.options.description ?? "",
|
|
56
|
+
inputs,
|
|
29
57
|
run: runFn
|
|
30
58
|
};
|
|
31
59
|
}
|
|
@@ -37,133 +65,6 @@ function defineWorkflow(options) {
|
|
|
37
65
|
return new WorkflowBuilder(options);
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
// src/sdk/runtime/discovery.ts
|
|
41
|
-
import { join } from "path";
|
|
42
|
-
import { readdir, writeFile } from "fs/promises";
|
|
43
|
-
import { existsSync, readdirSync } from "fs";
|
|
44
|
-
import { homedir } from "os";
|
|
45
|
-
import ignore from "ignore";
|
|
46
|
-
function getLocalWorkflowsDir(projectRoot) {
|
|
47
|
-
return join(projectRoot, ".atomic", "workflows");
|
|
48
|
-
}
|
|
49
|
-
function getGlobalWorkflowsDir() {
|
|
50
|
-
return join(homedir(), ".atomic", "workflows");
|
|
51
|
-
}
|
|
52
|
-
var AGENTS = ["copilot", "opencode", "claude"];
|
|
53
|
-
var AGENT_SET = new Set(AGENTS);
|
|
54
|
-
var WORKFLOWS_GITIGNORE = [
|
|
55
|
-
"node_modules/",
|
|
56
|
-
"dist/",
|
|
57
|
-
"build/",
|
|
58
|
-
"coverage/",
|
|
59
|
-
".cache/",
|
|
60
|
-
"*.log",
|
|
61
|
-
"*.tsbuildinfo",
|
|
62
|
-
""
|
|
63
|
-
].join(`
|
|
64
|
-
`);
|
|
65
|
-
async function loadWorkflowsGitignore(workflowsDir) {
|
|
66
|
-
const gitignorePath = join(workflowsDir, ".gitignore");
|
|
67
|
-
let content;
|
|
68
|
-
try {
|
|
69
|
-
content = await Bun.file(gitignorePath).text();
|
|
70
|
-
} catch {
|
|
71
|
-
await writeFile(gitignorePath, WORKFLOWS_GITIGNORE);
|
|
72
|
-
content = WORKFLOWS_GITIGNORE;
|
|
73
|
-
}
|
|
74
|
-
return ignore().add(content);
|
|
75
|
-
}
|
|
76
|
-
async function discoverFromBaseDir(baseDir, source, agentFilter) {
|
|
77
|
-
const workflows = [];
|
|
78
|
-
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
79
|
-
const agentNames = new Set(agents);
|
|
80
|
-
let workflowEntries;
|
|
81
|
-
try {
|
|
82
|
-
workflowEntries = await readdir(baseDir, { withFileTypes: true });
|
|
83
|
-
} catch {
|
|
84
|
-
return workflows;
|
|
85
|
-
}
|
|
86
|
-
const ig = await loadWorkflowsGitignore(baseDir);
|
|
87
|
-
for (const wfEntry of workflowEntries) {
|
|
88
|
-
if (!wfEntry.isDirectory())
|
|
89
|
-
continue;
|
|
90
|
-
if (wfEntry.name.startsWith("."))
|
|
91
|
-
continue;
|
|
92
|
-
if (AGENT_SET.has(wfEntry.name))
|
|
93
|
-
continue;
|
|
94
|
-
if (ig.ignores(wfEntry.name + "/"))
|
|
95
|
-
continue;
|
|
96
|
-
const workflowDir = join(baseDir, wfEntry.name);
|
|
97
|
-
let agentEntries;
|
|
98
|
-
try {
|
|
99
|
-
agentEntries = await readdir(workflowDir, { withFileTypes: true });
|
|
100
|
-
} catch {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
for (const agentEntry of agentEntries) {
|
|
104
|
-
if (!agentEntry.isDirectory())
|
|
105
|
-
continue;
|
|
106
|
-
if (!agentNames.has(agentEntry.name))
|
|
107
|
-
continue;
|
|
108
|
-
const indexPath = join(workflowDir, agentEntry.name, "index.ts");
|
|
109
|
-
const file = Bun.file(indexPath);
|
|
110
|
-
if (await file.exists()) {
|
|
111
|
-
workflows.push({
|
|
112
|
-
name: wfEntry.name,
|
|
113
|
-
agent: agentEntry.name,
|
|
114
|
-
path: indexPath,
|
|
115
|
-
source
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return workflows;
|
|
121
|
-
}
|
|
122
|
-
var BUILTIN_WORKFLOWS_DIR = join(Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)));
|
|
123
|
-
function discoverBuiltinWorkflows(agentFilter) {
|
|
124
|
-
const results = [];
|
|
125
|
-
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
126
|
-
let workflowNames;
|
|
127
|
-
try {
|
|
128
|
-
workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
129
|
-
} catch {
|
|
130
|
-
return results;
|
|
131
|
-
}
|
|
132
|
-
for (const name of workflowNames) {
|
|
133
|
-
for (const agent of agents) {
|
|
134
|
-
const indexPath = join(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
|
|
135
|
-
if (existsSync(indexPath)) {
|
|
136
|
-
results.push({ name, agent, path: indexPath, source: "builtin" });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return results;
|
|
141
|
-
}
|
|
142
|
-
async function discoverWorkflows(projectRoot = process.cwd(), agentFilter) {
|
|
143
|
-
const localDir = getLocalWorkflowsDir(projectRoot);
|
|
144
|
-
const globalDir = getGlobalWorkflowsDir();
|
|
145
|
-
const builtinResults = discoverBuiltinWorkflows(agentFilter);
|
|
146
|
-
const [globalResults, localResults] = await Promise.all([
|
|
147
|
-
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
148
|
-
discoverFromBaseDir(localDir, "local", agentFilter)
|
|
149
|
-
]);
|
|
150
|
-
const byKey = new Map;
|
|
151
|
-
for (const wf of builtinResults) {
|
|
152
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
153
|
-
}
|
|
154
|
-
for (const wf of globalResults) {
|
|
155
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
156
|
-
}
|
|
157
|
-
for (const wf of localResults) {
|
|
158
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
159
|
-
}
|
|
160
|
-
return Array.from(byKey.values());
|
|
161
|
-
}
|
|
162
|
-
async function findWorkflow(name, agent, projectRoot = process.cwd()) {
|
|
163
|
-
const all = await discoverWorkflows(projectRoot, agent);
|
|
164
|
-
return all.find((w) => w.name === name) ?? null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
68
|
// src/sdk/providers/copilot.ts
|
|
168
69
|
function validateCopilotWorkflow(source) {
|
|
169
70
|
const warnings = [];
|
|
@@ -201,6 +102,9 @@ function validateOpenCodeWorkflow(source) {
|
|
|
201
102
|
}
|
|
202
103
|
|
|
203
104
|
// src/sdk/runtime/tmux.ts
|
|
105
|
+
import { join } from "path";
|
|
106
|
+
var SOCKET_NAME = "atomic";
|
|
107
|
+
var CONFIG_PATH = join(import.meta.dir, "tmux.conf");
|
|
204
108
|
var resolvedMuxBinary;
|
|
205
109
|
function getMuxBinary() {
|
|
206
110
|
if (resolvedMuxBinary !== undefined)
|
|
@@ -232,8 +136,9 @@ function tmuxRun(args) {
|
|
|
232
136
|
if (!binary) {
|
|
233
137
|
return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
|
|
234
138
|
}
|
|
139
|
+
const fullArgs = ["-f", CONFIG_PATH, "-L", SOCKET_NAME, ...args];
|
|
235
140
|
const result = Bun.spawnSync({
|
|
236
|
-
cmd: [binary, ...
|
|
141
|
+
cmd: [binary, ...fullArgs],
|
|
237
142
|
stdout: "pipe",
|
|
238
143
|
stderr: "pipe"
|
|
239
144
|
});
|
|
@@ -363,15 +268,8 @@ function killWindow(sessionName, windowName) {
|
|
|
363
268
|
} catch {}
|
|
364
269
|
}
|
|
365
270
|
function sessionExists(sessionName) {
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
return false;
|
|
369
|
-
const result = Bun.spawnSync({
|
|
370
|
-
cmd: [binary, "has-session", "-t", sessionName],
|
|
371
|
-
stdout: "pipe",
|
|
372
|
-
stderr: "pipe"
|
|
373
|
-
});
|
|
374
|
-
return result.success;
|
|
271
|
+
const result = tmuxRun(["has-session", "-t", sessionName]);
|
|
272
|
+
return result.ok;
|
|
375
273
|
}
|
|
376
274
|
function attachSession(sessionName) {
|
|
377
275
|
const binary = getMuxBinary();
|
|
@@ -379,7 +277,7 @@ function attachSession(sessionName) {
|
|
|
379
277
|
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
380
278
|
}
|
|
381
279
|
const proc = Bun.spawnSync({
|
|
382
|
-
cmd: [binary, "attach-session", "-t", sessionName],
|
|
280
|
+
cmd: [binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName],
|
|
383
281
|
stdin: "inherit",
|
|
384
282
|
stdout: "inherit",
|
|
385
283
|
stderr: "pipe"
|
|
@@ -389,6 +287,13 @@ function attachSession(sessionName) {
|
|
|
389
287
|
throw new Error(`Failed to attach to session: ${sessionName}${stderr ? ` (${stderr})` : ""}`);
|
|
390
288
|
}
|
|
391
289
|
}
|
|
290
|
+
function spawnMuxAttach(sessionName) {
|
|
291
|
+
const binary = getMuxBinary();
|
|
292
|
+
if (!binary) {
|
|
293
|
+
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
294
|
+
}
|
|
295
|
+
return Bun.spawn([binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName], { stdio: ["inherit", "inherit", "inherit"] });
|
|
296
|
+
}
|
|
392
297
|
function switchClient(sessionName) {
|
|
393
298
|
tmuxExec(["switch-client", "-t", sessionName]);
|
|
394
299
|
}
|
|
@@ -741,8 +646,156 @@ var WorkflowLoader;
|
|
|
741
646
|
WorkflowLoader.loadWorkflow = loadWorkflow;
|
|
742
647
|
})(WorkflowLoader ||= {});
|
|
743
648
|
|
|
649
|
+
// src/sdk/runtime/discovery.ts
|
|
650
|
+
import { join as join2 } from "path";
|
|
651
|
+
import { readdir, writeFile } from "fs/promises";
|
|
652
|
+
import { existsSync, readdirSync } from "fs";
|
|
653
|
+
import { homedir } from "os";
|
|
654
|
+
import ignore from "ignore";
|
|
655
|
+
function getLocalWorkflowsDir(projectRoot) {
|
|
656
|
+
return join2(projectRoot, ".atomic", "workflows");
|
|
657
|
+
}
|
|
658
|
+
function getGlobalWorkflowsDir() {
|
|
659
|
+
return join2(homedir(), ".atomic", "workflows");
|
|
660
|
+
}
|
|
661
|
+
var AGENTS = ["copilot", "opencode", "claude"];
|
|
662
|
+
var AGENT_SET = new Set(AGENTS);
|
|
663
|
+
var WORKFLOWS_GITIGNORE = [
|
|
664
|
+
"node_modules/",
|
|
665
|
+
"dist/",
|
|
666
|
+
"build/",
|
|
667
|
+
"coverage/",
|
|
668
|
+
".cache/",
|
|
669
|
+
"*.log",
|
|
670
|
+
"*.tsbuildinfo",
|
|
671
|
+
""
|
|
672
|
+
].join(`
|
|
673
|
+
`);
|
|
674
|
+
async function loadWorkflowsGitignore(workflowsDir) {
|
|
675
|
+
const gitignorePath = join2(workflowsDir, ".gitignore");
|
|
676
|
+
let content;
|
|
677
|
+
try {
|
|
678
|
+
content = await Bun.file(gitignorePath).text();
|
|
679
|
+
} catch {
|
|
680
|
+
await writeFile(gitignorePath, WORKFLOWS_GITIGNORE);
|
|
681
|
+
content = WORKFLOWS_GITIGNORE;
|
|
682
|
+
}
|
|
683
|
+
return ignore().add(content);
|
|
684
|
+
}
|
|
685
|
+
async function discoverFromBaseDir(baseDir, source, agentFilter) {
|
|
686
|
+
const workflows = [];
|
|
687
|
+
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
688
|
+
const agentNames = new Set(agents);
|
|
689
|
+
let workflowEntries;
|
|
690
|
+
try {
|
|
691
|
+
workflowEntries = await readdir(baseDir, { withFileTypes: true });
|
|
692
|
+
} catch {
|
|
693
|
+
return workflows;
|
|
694
|
+
}
|
|
695
|
+
const ig = await loadWorkflowsGitignore(baseDir);
|
|
696
|
+
for (const wfEntry of workflowEntries) {
|
|
697
|
+
if (!wfEntry.isDirectory())
|
|
698
|
+
continue;
|
|
699
|
+
if (wfEntry.name.startsWith("."))
|
|
700
|
+
continue;
|
|
701
|
+
if (AGENT_SET.has(wfEntry.name))
|
|
702
|
+
continue;
|
|
703
|
+
if (ig.ignores(wfEntry.name + "/"))
|
|
704
|
+
continue;
|
|
705
|
+
const workflowDir = join2(baseDir, wfEntry.name);
|
|
706
|
+
let agentEntries;
|
|
707
|
+
try {
|
|
708
|
+
agentEntries = await readdir(workflowDir, { withFileTypes: true });
|
|
709
|
+
} catch {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
for (const agentEntry of agentEntries) {
|
|
713
|
+
if (!agentEntry.isDirectory())
|
|
714
|
+
continue;
|
|
715
|
+
if (!agentNames.has(agentEntry.name))
|
|
716
|
+
continue;
|
|
717
|
+
const indexPath = join2(workflowDir, agentEntry.name, "index.ts");
|
|
718
|
+
const file = Bun.file(indexPath);
|
|
719
|
+
if (await file.exists()) {
|
|
720
|
+
workflows.push({
|
|
721
|
+
name: wfEntry.name,
|
|
722
|
+
agent: agentEntry.name,
|
|
723
|
+
path: indexPath,
|
|
724
|
+
source
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return workflows;
|
|
730
|
+
}
|
|
731
|
+
var BUILTIN_WORKFLOWS_DIR = join2(Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)));
|
|
732
|
+
function discoverBuiltinWorkflows(agentFilter) {
|
|
733
|
+
const results = [];
|
|
734
|
+
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
735
|
+
let workflowNames;
|
|
736
|
+
try {
|
|
737
|
+
workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
738
|
+
} catch {
|
|
739
|
+
return results;
|
|
740
|
+
}
|
|
741
|
+
for (const name of workflowNames) {
|
|
742
|
+
for (const agent of agents) {
|
|
743
|
+
const indexPath = join2(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
|
|
744
|
+
if (existsSync(indexPath)) {
|
|
745
|
+
results.push({ name, agent, path: indexPath, source: "builtin" });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return results;
|
|
750
|
+
}
|
|
751
|
+
async function discoverWorkflows(projectRoot = process.cwd(), agentFilter, options = {}) {
|
|
752
|
+
const { merge = true } = options;
|
|
753
|
+
const localDir = getLocalWorkflowsDir(projectRoot);
|
|
754
|
+
const globalDir = getGlobalWorkflowsDir();
|
|
755
|
+
const allBuiltins = discoverBuiltinWorkflows();
|
|
756
|
+
const reservedNames = new Set(allBuiltins.map((w) => w.name));
|
|
757
|
+
const builtinResults = agentFilter ? allBuiltins.filter((w) => w.agent === agentFilter) : allBuiltins;
|
|
758
|
+
const [globalResults, localResults] = await Promise.all([
|
|
759
|
+
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
760
|
+
discoverFromBaseDir(localDir, "local", agentFilter)
|
|
761
|
+
]);
|
|
762
|
+
const filteredGlobal = globalResults.filter((w) => !reservedNames.has(w.name));
|
|
763
|
+
const filteredLocal = localResults.filter((w) => !reservedNames.has(w.name));
|
|
764
|
+
if (!merge) {
|
|
765
|
+
return [...filteredGlobal, ...filteredLocal, ...builtinResults];
|
|
766
|
+
}
|
|
767
|
+
const byKey = new Map;
|
|
768
|
+
for (const wf of filteredGlobal) {
|
|
769
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
770
|
+
}
|
|
771
|
+
for (const wf of filteredLocal) {
|
|
772
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
773
|
+
}
|
|
774
|
+
for (const wf of builtinResults) {
|
|
775
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
776
|
+
}
|
|
777
|
+
return Array.from(byKey.values());
|
|
778
|
+
}
|
|
779
|
+
async function findWorkflow(name, agent, projectRoot = process.cwd()) {
|
|
780
|
+
const all = await discoverWorkflows(projectRoot, agent);
|
|
781
|
+
return all.find((w) => w.name === name) ?? null;
|
|
782
|
+
}
|
|
783
|
+
async function loadWorkflowsMetadata(discovered) {
|
|
784
|
+
const results = await Promise.all(discovered.map(async (wf) => {
|
|
785
|
+
const loaded = await WorkflowLoader.loadWorkflow(wf);
|
|
786
|
+
if (!loaded.ok)
|
|
787
|
+
return null;
|
|
788
|
+
return {
|
|
789
|
+
...wf,
|
|
790
|
+
description: loaded.value.definition.description,
|
|
791
|
+
inputs: loaded.value.definition.inputs
|
|
792
|
+
};
|
|
793
|
+
}));
|
|
794
|
+
return results.filter((r) => r !== null);
|
|
795
|
+
}
|
|
796
|
+
|
|
744
797
|
// src/sdk/runtime/executor.ts
|
|
745
|
-
import { join as
|
|
798
|
+
import { join as join3, resolve } from "path";
|
|
746
799
|
import { homedir as homedir2 } from "os";
|
|
747
800
|
import { mkdir, writeFile as writeFile2, readFile } from "fs/promises";
|
|
748
801
|
|
|
@@ -915,7 +968,7 @@ function generateId() {
|
|
|
915
968
|
return crypto.randomUUID().slice(0, 8);
|
|
916
969
|
}
|
|
917
970
|
function getSessionsBaseDir() {
|
|
918
|
-
return
|
|
971
|
+
return join3(homedir2(), ".atomic", "sessions");
|
|
919
972
|
}
|
|
920
973
|
async function ensureDir(dir) {
|
|
921
974
|
await mkdir(dir, { recursive: true });
|
|
@@ -930,25 +983,26 @@ async function executeWorkflow(options) {
|
|
|
930
983
|
const {
|
|
931
984
|
definition,
|
|
932
985
|
agent,
|
|
933
|
-
|
|
986
|
+
inputs = {},
|
|
934
987
|
workflowFile,
|
|
935
988
|
projectRoot = process.cwd()
|
|
936
989
|
} = options;
|
|
937
990
|
const workflowRunId = generateId();
|
|
938
991
|
const tmuxSessionName = `atomic-wf-${definition.name}-${workflowRunId}`;
|
|
939
|
-
const sessionsBaseDir =
|
|
992
|
+
const sessionsBaseDir = join3(getSessionsBaseDir(), workflowRunId);
|
|
940
993
|
await ensureDir(sessionsBaseDir);
|
|
941
994
|
const thisFile = resolve(import.meta.dir, "executor-entry.ts");
|
|
942
995
|
const isWin = process.platform === "win32";
|
|
943
996
|
const launcherExt = isWin ? "ps1" : "sh";
|
|
944
|
-
const launcherPath =
|
|
945
|
-
const logPath =
|
|
997
|
+
const launcherPath = join3(sessionsBaseDir, `orchestrator.${launcherExt}`);
|
|
998
|
+
const logPath = join3(sessionsBaseDir, "orchestrator.log");
|
|
999
|
+
const inputsB64 = Buffer.from(JSON.stringify(inputs)).toString("base64");
|
|
946
1000
|
const launcherScript = isWin ? [
|
|
947
1001
|
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
948
1002
|
`$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
|
|
949
1003
|
`$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
|
|
950
1004
|
`$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
|
|
951
|
-
`$env:
|
|
1005
|
+
`$env:ATOMIC_WF_INPUTS = "${escPwsh(inputsB64)}"`,
|
|
952
1006
|
`$env:ATOMIC_WF_FILE = "${escPwsh(workflowFile)}"`,
|
|
953
1007
|
`$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
|
|
954
1008
|
`bun run "${escPwsh(thisFile)}" 2>"${escPwsh(logPath)}"`
|
|
@@ -959,24 +1013,29 @@ async function executeWorkflow(options) {
|
|
|
959
1013
|
`export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
|
|
960
1014
|
`export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
|
|
961
1015
|
`export ATOMIC_WF_AGENT="${escBash(agent)}"`,
|
|
962
|
-
`export
|
|
1016
|
+
`export ATOMIC_WF_INPUTS="${escBash(inputsB64)}"`,
|
|
963
1017
|
`export ATOMIC_WF_FILE="${escBash(workflowFile)}"`,
|
|
964
1018
|
`export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
|
|
965
1019
|
`bun run "${escBash(thisFile)}" 2>"${escBash(logPath)}"`
|
|
966
1020
|
].join(`
|
|
967
1021
|
`);
|
|
968
1022
|
await writeFile2(launcherPath, launcherScript, { mode: 493 });
|
|
969
|
-
|
|
970
|
-
createSession(tmuxSessionName, shellCmd, "orchestrator");
|
|
1023
|
+
console.log(`[atomic] Session: ${tmuxSessionName} (FYI all atomic sessions run on tmux -L ${SOCKET_NAME})`);
|
|
971
1024
|
if (isInsideTmux()) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
const
|
|
976
|
-
stdio: ["inherit", "inherit", "inherit"]
|
|
1025
|
+
const defaultShell = process.env.SHELL || (isWin ? "pwsh" : "sh");
|
|
1026
|
+
createSession(tmuxSessionName, defaultShell, "orchestrator");
|
|
1027
|
+
const launcherCmd = isWin ? ["pwsh", "-NoProfile", "-File", launcherPath] : ["bash", launcherPath];
|
|
1028
|
+
const proc = Bun.spawn(launcherCmd, {
|
|
1029
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
1030
|
+
cwd: projectRoot
|
|
977
1031
|
});
|
|
1032
|
+
await proc.exited;
|
|
1033
|
+
} else {
|
|
1034
|
+
const shellCmd = isWin ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"` : `bash "${escBash(launcherPath)}"`;
|
|
1035
|
+
createSession(tmuxSessionName, shellCmd, "orchestrator");
|
|
1036
|
+
const attachProc = spawnMuxAttach(tmuxSessionName);
|
|
978
1037
|
await attachProc.exited;
|
|
979
1038
|
}
|
|
980
1039
|
}
|
|
981
1040
|
|
|
982
|
-
export { WorkflowBuilder, defineWorkflow,
|
|
1041
|
+
export { WorkflowBuilder, defineWorkflow, validateCopilotWorkflow, validateOpenCodeWorkflow, SOCKET_NAME, getMuxBinary, resetMuxBinaryCache, isTmuxInstalled, isInsideTmux, tmuxRun, createSession, createWindow, createPane, sendLiteralText, sendSpecialKey, sendKeysAndSubmit, capturePane, capturePaneVisible, capturePaneScrollback, killSession, killWindow, sessionExists, attachSession, spawnMuxAttach, switchClient, getCurrentSession, attachOrSwitch, selectWindow, normalizeTmuxCapture, normalizeTmuxLines, paneLooksReady, paneHasActiveTask, paneIsIdle, waitForPaneReady, attemptSubmitRounds, waitForOutput, clearClaudeSession, createClaudeSession, claudeQuery, validateClaudeWorkflow, WorkflowLoader, AGENTS, WORKFLOWS_GITIGNORE, discoverWorkflows, findWorkflow, loadWorkflowsMetadata, executeWorkflow };
|