@bastani/atomic 0.5.0-3 → 0.5.0-4
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/.atomic/workflows/hello/claude/index.ts +22 -25
- package/.atomic/workflows/hello/copilot/index.ts +41 -31
- package/.atomic/workflows/hello/opencode/index.ts +40 -40
- package/.atomic/workflows/hello-parallel/claude/index.ts +54 -54
- package/.atomic/workflows/hello-parallel/copilot/index.ts +89 -70
- package/.atomic/workflows/hello-parallel/opencode/index.ts +77 -77
- package/.atomic/workflows/ralph/claude/index.ts +128 -93
- package/.atomic/workflows/ralph/copilot/index.ts +212 -112
- package/.atomic/workflows/ralph/helpers/prompts.ts +45 -2
- package/.atomic/workflows/ralph/opencode/index.ts +174 -111
- package/README.md +62 -53
- package/package.json +1 -1
- package/src/commands/cli/chat/index.ts +28 -8
- package/src/commands/cli/init/index.ts +6 -4
- package/src/commands/cli/init/scm.ts +27 -10
- package/src/sdk/components/connectors.test.ts +45 -0
- package/src/sdk/components/layout.test.ts +321 -0
- package/src/sdk/components/layout.ts +51 -15
- package/src/sdk/components/orchestrator-panel-store.test.ts +156 -0
- package/src/sdk/components/orchestrator-panel-store.ts +24 -0
- package/src/sdk/components/orchestrator-panel.tsx +21 -0
- package/src/sdk/components/session-graph-panel.tsx +3 -9
- package/src/sdk/components/statusline.tsx +4 -6
- package/src/sdk/define-workflow.test.ts +71 -0
- package/src/sdk/define-workflow.ts +42 -39
- package/src/sdk/errors.ts +1 -1
- package/src/sdk/index.ts +4 -1
- package/src/sdk/providers/claude.ts +1 -1
- package/src/sdk/providers/copilot.ts +5 -3
- package/src/sdk/providers/opencode.ts +5 -3
- package/src/sdk/runtime/executor.ts +512 -301
- package/src/sdk/runtime/loader.ts +2 -2
- package/src/sdk/runtime/tmux.ts +31 -2
- package/src/sdk/types.ts +93 -20
- package/src/sdk/workflows.ts +7 -4
- package/src/services/config/definitions.ts +39 -2
- package/src/services/config/settings.ts +0 -6
- package/src/services/system/skills.ts +3 -7
- package/.atomic/workflows/package-lock.json +0 -31
- package/.atomic/workflows/package.json +0 -8
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ralph workflow for OpenCode — plan → orchestrate → review → debug loop.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Each sub-agent invocation spawns its own visible session in the graph,
|
|
5
|
+
* so users can see each iteration's progress in real time. The loop
|
|
6
|
+
* terminates when:
|
|
7
7
|
* - {@link MAX_LOOPS} iterations have completed, OR
|
|
8
8
|
* - Two consecutive reviewer passes return zero findings.
|
|
9
9
|
*
|
|
10
|
-
* A loop is one cycle of plan → orchestrate → review. When a review returns
|
|
11
|
-
* zero findings on the FIRST pass we re-run only the reviewer (still inside
|
|
12
|
-
* the same loop iteration) to confirm; if that confirmation pass is also
|
|
13
|
-
* clean we stop. The debugger only runs when findings remain, and its
|
|
14
|
-
* markdown report is fed back into the next iteration's planner.
|
|
15
|
-
*
|
|
16
10
|
* Run: atomic workflow -n ralph -a opencode "<your spec>"
|
|
17
11
|
*/
|
|
18
12
|
|
|
19
13
|
import { defineWorkflow } from "@bastani/atomic/workflows";
|
|
20
|
-
import {
|
|
21
|
-
createOpencodeClient,
|
|
22
|
-
type SessionPromptResponse,
|
|
23
|
-
} from "@opencode-ai/sdk/v2";
|
|
14
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
24
15
|
|
|
25
16
|
import {
|
|
26
17
|
buildPlannerPrompt,
|
|
@@ -51,114 +42,186 @@ export default defineWorkflow({
|
|
|
51
42
|
description:
|
|
52
43
|
"Plan → orchestrate → review → debug loop with bounded iteration",
|
|
53
44
|
})
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
45
|
+
.run(async (ctx) => {
|
|
46
|
+
let consecutiveClean = 0;
|
|
47
|
+
let debuggerReport = "";
|
|
48
|
+
// Track the most recent session so the next stage can declare it as a
|
|
49
|
+
// dependency — this chains planner → orchestrator → reviewer → [confirm]
|
|
50
|
+
// → [debugger] → next planner in the graph instead of showing every
|
|
51
|
+
// stage as an independent sibling under the root.
|
|
52
|
+
let prevStage: string | undefined;
|
|
53
|
+
const depsOn = (): string[] | undefined =>
|
|
54
|
+
prevStage ? [prevStage] : undefined;
|
|
55
|
+
|
|
56
|
+
for (let iteration = 1; iteration <= MAX_LOOPS; iteration++) {
|
|
57
|
+
// ── Plan ────────────────────────────────────────────────────────────
|
|
58
|
+
const plannerName = `planner-${iteration}`;
|
|
59
|
+
const planner = await ctx.session(
|
|
60
|
+
{ name: plannerName, dependsOn: depsOn() },
|
|
61
|
+
async (s) => {
|
|
62
|
+
const client = createOpencodeClient({ baseUrl: s.serverUrl });
|
|
63
|
+
const session = await client.session.create({
|
|
64
|
+
title: `planner-${iteration}`,
|
|
65
|
+
});
|
|
66
|
+
await client.tui.selectSession({ sessionID: session.data!.id });
|
|
67
|
+
const result = await client.session.prompt({
|
|
68
|
+
sessionID: session.data!.id,
|
|
69
|
+
parts: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: buildPlannerPrompt(s.userPrompt, {
|
|
73
|
+
iteration,
|
|
74
|
+
debuggerReport: debuggerReport || undefined,
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
agent: "planner",
|
|
79
|
+
});
|
|
80
|
+
s.save(result.data!);
|
|
81
|
+
return extractResponseText(result.data!.parts);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
prevStage = plannerName;
|
|
85
|
+
|
|
86
|
+
// ── Orchestrate ─────────────────────────────────────────────────────
|
|
87
|
+
const orchName = `orchestrator-${iteration}`;
|
|
88
|
+
await ctx.session(
|
|
89
|
+
{ name: orchName, dependsOn: depsOn() },
|
|
90
|
+
async (s) => {
|
|
91
|
+
const client = createOpencodeClient({ baseUrl: s.serverUrl });
|
|
92
|
+
const session = await client.session.create({
|
|
93
|
+
title: `orchestrator-${iteration}`,
|
|
94
|
+
});
|
|
95
|
+
await client.tui.selectSession({ sessionID: session.data!.id });
|
|
96
|
+
const result = await client.session.prompt({
|
|
97
|
+
sessionID: session.data!.id,
|
|
98
|
+
parts: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: buildOrchestratorPrompt(s.userPrompt, {
|
|
102
|
+
plannerNotes: planner.result,
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
agent: "orchestrator",
|
|
107
|
+
});
|
|
108
|
+
s.save(result.data!);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
prevStage = orchName;
|
|
112
|
+
|
|
113
|
+
// ── Review (first pass) ─────────────────────────────────────────────
|
|
114
|
+
let gitStatus = await safeGitStatusS();
|
|
115
|
+
const reviewerName = `reviewer-${iteration}`;
|
|
116
|
+
const review = await ctx.session(
|
|
117
|
+
{ name: reviewerName, dependsOn: depsOn() },
|
|
118
|
+
async (s) => {
|
|
119
|
+
const client = createOpencodeClient({ baseUrl: s.serverUrl });
|
|
120
|
+
const session = await client.session.create({
|
|
121
|
+
title: `reviewer-${iteration}`,
|
|
122
|
+
});
|
|
123
|
+
await client.tui.selectSession({ sessionID: session.data!.id });
|
|
124
|
+
const result = await client.session.prompt({
|
|
125
|
+
sessionID: session.data!.id,
|
|
126
|
+
parts: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: buildReviewPrompt(s.userPrompt, {
|
|
130
|
+
gitStatus,
|
|
131
|
+
iteration,
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
agent: "reviewer",
|
|
136
|
+
});
|
|
137
|
+
s.save(result.data!);
|
|
138
|
+
return extractResponseText(result.data!.parts);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
prevStage = reviewerName;
|
|
142
|
+
|
|
143
|
+
let reviewRaw = review.result;
|
|
144
|
+
let parsed = parseReviewResult(reviewRaw);
|
|
145
|
+
|
|
146
|
+
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
147
|
+
consecutiveClean += 1;
|
|
148
|
+
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) break;
|
|
149
|
+
|
|
150
|
+
// Confirmation pass — re-run reviewer only
|
|
151
|
+
gitStatus = await safeGitStatusS();
|
|
152
|
+
const confirmName = `reviewer-${iteration}-confirm`;
|
|
153
|
+
const confirm = await ctx.session(
|
|
154
|
+
{ name: confirmName, dependsOn: depsOn() },
|
|
155
|
+
async (s) => {
|
|
156
|
+
const client = createOpencodeClient({ baseUrl: s.serverUrl });
|
|
157
|
+
const session = await client.session.create({
|
|
158
|
+
title: `reviewer-${iteration}-confirm`,
|
|
159
|
+
});
|
|
160
|
+
await client.tui.selectSession({ sessionID: session.data!.id });
|
|
161
|
+
const result = await client.session.prompt({
|
|
162
|
+
sessionID: session.data!.id,
|
|
163
|
+
parts: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: buildReviewPrompt(s.userPrompt, {
|
|
167
|
+
gitStatus,
|
|
168
|
+
iteration,
|
|
169
|
+
isConfirmationPass: true,
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
agent: "reviewer",
|
|
174
|
+
});
|
|
175
|
+
s.save(result.data!);
|
|
176
|
+
return extractResponseText(result.data!.parts);
|
|
177
|
+
},
|
|
92
178
|
);
|
|
179
|
+
prevStage = confirmName;
|
|
93
180
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`orchestrator-${iteration}`,
|
|
97
|
-
"orchestrator",
|
|
98
|
-
buildOrchestratorPrompt(),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// ── Review (first pass) ─────────────────────────────────────────────
|
|
102
|
-
let gitStatus = await safeGitStatusS();
|
|
103
|
-
let reviewRaw = await runAgent(
|
|
104
|
-
`reviewer-${iteration}-1`,
|
|
105
|
-
"reviewer",
|
|
106
|
-
buildReviewPrompt(ctx.userPrompt, { gitStatus, iteration }),
|
|
107
|
-
);
|
|
108
|
-
let parsed = parseReviewResult(reviewRaw);
|
|
181
|
+
reviewRaw = confirm.result;
|
|
182
|
+
parsed = parseReviewResult(reviewRaw);
|
|
109
183
|
|
|
110
184
|
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
111
185
|
consecutiveClean += 1;
|
|
112
|
-
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD)
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Confirmation pass — re-run reviewer only, NOT plan/orchestrate.
|
|
117
|
-
gitStatus = await safeGitStatusS();
|
|
118
|
-
reviewRaw = await runAgent(
|
|
119
|
-
`reviewer-${iteration}-2`,
|
|
120
|
-
"reviewer",
|
|
121
|
-
buildReviewPrompt(ctx.userPrompt, {
|
|
122
|
-
gitStatus,
|
|
123
|
-
iteration,
|
|
124
|
-
isConfirmationPass: true,
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
parsed = parseReviewResult(reviewRaw);
|
|
128
|
-
|
|
129
|
-
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
130
|
-
consecutiveClean += 1;
|
|
131
|
-
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) {
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
consecutiveClean = 0;
|
|
136
|
-
// fall through to debugger
|
|
137
|
-
}
|
|
186
|
+
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) break;
|
|
138
187
|
} else {
|
|
139
188
|
consecutiveClean = 0;
|
|
140
189
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
hasActionableFindings(parsed, reviewRaw) &&
|
|
145
|
-
iteration < MAX_LOOPS
|
|
146
|
-
) {
|
|
147
|
-
const debuggerRaw = await runAgent(
|
|
148
|
-
`debugger-${iteration}`,
|
|
149
|
-
"debugger",
|
|
150
|
-
buildDebuggerReportPrompt(parsed, reviewRaw, {
|
|
151
|
-
iteration,
|
|
152
|
-
gitStatus,
|
|
153
|
-
}),
|
|
154
|
-
);
|
|
155
|
-
debuggerReport = extractMarkdownBlock(debuggerRaw);
|
|
156
|
-
}
|
|
190
|
+
} else {
|
|
191
|
+
consecutiveClean = 0;
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
// ── Debug (only if findings remain AND another iteration is allowed) ─
|
|
195
|
+
if (hasActionableFindings(parsed, reviewRaw) && iteration < MAX_LOOPS) {
|
|
196
|
+
const debuggerName = `debugger-${iteration}`;
|
|
197
|
+
const debugger_ = await ctx.session(
|
|
198
|
+
{ name: debuggerName, dependsOn: depsOn() },
|
|
199
|
+
async (s) => {
|
|
200
|
+
const client = createOpencodeClient({ baseUrl: s.serverUrl });
|
|
201
|
+
const session = await client.session.create({
|
|
202
|
+
title: `debugger-${iteration}`,
|
|
203
|
+
});
|
|
204
|
+
await client.tui.selectSession({ sessionID: session.data!.id });
|
|
205
|
+
const result = await client.session.prompt({
|
|
206
|
+
sessionID: session.data!.id,
|
|
207
|
+
parts: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: buildDebuggerReportPrompt(parsed, reviewRaw, {
|
|
211
|
+
iteration,
|
|
212
|
+
gitStatus,
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
agent: "debugger",
|
|
217
|
+
});
|
|
218
|
+
s.save(result.data!);
|
|
219
|
+
return extractResponseText(result.data!.parts);
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
prevStage = debuggerName;
|
|
223
|
+
debuggerReport = extractMarkdownBlock(debugger_.result);
|
|
161
224
|
}
|
|
162
|
-
}
|
|
225
|
+
}
|
|
163
226
|
})
|
|
164
227
|
.compile();
|
package/README.md
CHANGED
|
@@ -220,7 +220,7 @@ Each agent gets its own configuration directory (`.claude/`, `.opencode/`, `.git
|
|
|
220
220
|
|
|
221
221
|
### Workflow SDK — Build Your Own Harness
|
|
222
222
|
|
|
223
|
-
Every team has a process — triage bugs this way, ship features that way, review PRs with these checks. Most of it lives in a wiki nobody reads or in one senior engineer's head. The **Workflow SDK** (`@bastani/atomic/workflows`) lets you encode that process as
|
|
223
|
+
Every team has a process — triage bugs this way, ship features that way, review PRs with these checks. Most of it lives in a wiki nobody reads or in one senior engineer's head. The **Workflow SDK** (`@bastani/atomic/workflows`) lets you encode that process as TypeScript — spawn agent sessions dynamically with native control flow (`for`, `if`, `Promise.all()`), and watch them appear in a live graph as they execute.
|
|
224
224
|
|
|
225
225
|
Drop a `.ts` file in `.atomic/workflows/<name>/<agent>/index.ts` and run it:
|
|
226
226
|
|
|
@@ -239,31 +239,28 @@ export default defineWorkflow({
|
|
|
239
239
|
name: "hello",
|
|
240
240
|
description: "Two-session Claude demo: describe → summarize",
|
|
241
241
|
})
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
ctx.save(ctx.sessionId);
|
|
266
|
-
},
|
|
242
|
+
.run(async (ctx) => {
|
|
243
|
+
const describe = await ctx.session(
|
|
244
|
+
{ name: "describe", description: "Ask Claude to describe the project" },
|
|
245
|
+
async (s) => {
|
|
246
|
+
await createClaudeSession({ paneId: s.paneId });
|
|
247
|
+
await claudeQuery({ paneId: s.paneId, prompt: s.userPrompt });
|
|
248
|
+
s.save(s.sessionId);
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await ctx.session(
|
|
253
|
+
{ name: "summarize", description: "Summarize the previous session's output" },
|
|
254
|
+
async (s) => {
|
|
255
|
+
const research = await s.transcript(describe);
|
|
256
|
+
await createClaudeSession({ paneId: s.paneId });
|
|
257
|
+
await claudeQuery({
|
|
258
|
+
paneId: s.paneId,
|
|
259
|
+
prompt: `Read ${research.path} and summarize it in 2-3 bullet points.`,
|
|
260
|
+
});
|
|
261
|
+
s.save(s.sessionId);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
267
264
|
})
|
|
268
265
|
.compile();
|
|
269
266
|
```
|
|
@@ -272,13 +269,14 @@ export default defineWorkflow({
|
|
|
272
269
|
|
|
273
270
|
**Key capabilities:**
|
|
274
271
|
|
|
275
|
-
| Capability
|
|
276
|
-
|
|
|
277
|
-
| **
|
|
278
|
-
| **
|
|
279
|
-
| **
|
|
280
|
-
| **
|
|
281
|
-
| **
|
|
272
|
+
| Capability | Description |
|
|
273
|
+
| ---------------------------- | ------------------------------------------------------------------------------------ |
|
|
274
|
+
| **Dynamic session spawning** | Call `ctx.session()` to spawn sessions at runtime — each gets its own tmux window and graph node |
|
|
275
|
+
| **Native TypeScript control flow** | Use `for`, `if/else`, `Promise.all()`, `try/catch` — no framework DSL needed |
|
|
276
|
+
| **Session return values** | Session callbacks can return data: `const h = await ctx.session(...); h.result` |
|
|
277
|
+
| **Transcript passing** | Access prior session output via handle (`s.transcript(handle)`) or name (`s.transcript("name")`) |
|
|
278
|
+
| **Provider-agnostic** | Write raw SDK code for Claude, Copilot, or OpenCode inside each session callback |
|
|
279
|
+
| **Live graph visualization** | Sessions appear in the TUI graph as they're spawned — loops and conditionals are visible in real time |
|
|
282
280
|
|
|
283
281
|
Drop a `.ts` file in `.atomic/workflows/<name>/<agent>/` (project-local) or `~/.atomic/workflows/` (global). You can also ask Atomic to create workflows for you:
|
|
284
282
|
|
|
@@ -294,22 +292,33 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
|
|
|
294
292
|
| Method | Purpose |
|
|
295
293
|
| --------------------------------------- | ----------------------------------------------------------------- |
|
|
296
294
|
| `defineWorkflow({ name, description })` | Entry point — returns a `WorkflowBuilder` |
|
|
297
|
-
| `.
|
|
295
|
+
| `.run(async (ctx) => { ... })` | Set the workflow's entry point — `ctx` is a `WorkflowContext` |
|
|
298
296
|
| `.compile()` | **Required** — terminal method that seals the workflow definition |
|
|
299
297
|
|
|
300
|
-
####
|
|
298
|
+
#### WorkflowContext (`ctx`) — top-level orchestrator
|
|
301
299
|
|
|
302
300
|
| Property | Type | Description |
|
|
303
301
|
| ----------------------- | ------------------------- | -------------------------------------------------------------- |
|
|
304
302
|
| `ctx.userPrompt` | `string` | Original user prompt from the CLI invocation |
|
|
305
303
|
| `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
|
|
306
|
-
| `ctx.
|
|
307
|
-
| `ctx.
|
|
308
|
-
| `ctx.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
|
304
|
+
| `ctx.session(opts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
|
|
305
|
+
| `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
|
|
306
|
+
| `ctx.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
|
|
307
|
+
|
|
308
|
+
#### SessionContext (`s`) — inside each session callback
|
|
309
|
+
|
|
310
|
+
| Property | Type | Description |
|
|
311
|
+
| ----------------------- | ------------------------- | -------------------------------------------------------------- |
|
|
312
|
+
| `s.serverUrl` | `string` | The agent's server URL |
|
|
313
|
+
| `s.userPrompt` | `string` | Original user prompt from the CLI invocation |
|
|
314
|
+
| `s.agent` | `AgentType` | Which agent is running |
|
|
315
|
+
| `s.paneId` | `string` | tmux pane ID for this session |
|
|
316
|
+
| `s.sessionId` | `string` | Session UUID |
|
|
317
|
+
| `s.sessionDir` | `string` | Path to this session's storage directory on disk |
|
|
318
|
+
| `s.save(messages)` | `SaveTranscript` | Save this session's output for subsequent sessions |
|
|
319
|
+
| `s.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript |
|
|
320
|
+
| `s.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
|
|
321
|
+
| `s.session(opts, fn)` | `Promise<SessionHandle<T>>` | Spawn a nested sub-session (child in the graph) |
|
|
313
322
|
|
|
314
323
|
#### Saving Transcripts
|
|
315
324
|
|
|
@@ -317,9 +326,9 @@ Each provider saves transcripts differently:
|
|
|
317
326
|
|
|
318
327
|
| Provider | How to Save |
|
|
319
328
|
| ------------ | ------------------------------------------------------------------ |
|
|
320
|
-
| **Claude** | `
|
|
321
|
-
| **Copilot** | `
|
|
322
|
-
| **OpenCode** | `
|
|
329
|
+
| **Claude** | `s.save(s.sessionId)` — auto-reads via `getSessionMessages()` |
|
|
330
|
+
| **Copilot** | `s.save(await session.getMessages())` — pass `SessionEvent[]` |
|
|
331
|
+
| **OpenCode** | `s.save(result.data!)` — pass the full `{ info, parts }` response |
|
|
323
332
|
|
|
324
333
|
#### Provider Helpers
|
|
325
334
|
|
|
@@ -327,17 +336,17 @@ Each provider saves transcripts differently:
|
|
|
327
336
|
| --------------------------------- | --------------------------------------------------- |
|
|
328
337
|
| `createClaudeSession({ paneId })` | Start a Claude TUI in a tmux pane |
|
|
329
338
|
| `claudeQuery({ paneId, prompt })` | Send a prompt to Claude and wait for the response |
|
|
330
|
-
| `clearClaudeSession(
|
|
331
|
-
| `validateClaudeWorkflow()` | Validate a Claude workflow
|
|
332
|
-
| `validateCopilotWorkflow()` | Validate a Copilot workflow
|
|
333
|
-
| `validateOpenCodeWorkflow()` | Validate an OpenCode workflow
|
|
339
|
+
| `clearClaudeSession(paneId)` | Clear the current Claude session |
|
|
340
|
+
| `validateClaudeWorkflow()` | Validate a Claude workflow source before run |
|
|
341
|
+
| `validateCopilotWorkflow()` | Validate a Copilot workflow source before run |
|
|
342
|
+
| `validateOpenCodeWorkflow()` | Validate an OpenCode workflow source before run |
|
|
334
343
|
|
|
335
344
|
#### Key Rules
|
|
336
345
|
|
|
337
|
-
1. Every workflow file must use `export default` with `.
|
|
338
|
-
2. Session names must be unique within a workflow
|
|
339
|
-
3.
|
|
340
|
-
4. Each session runs in its own tmux
|
|
346
|
+
1. Every workflow file must use `export default` with `.run()` and `.compile()`
|
|
347
|
+
2. Session names must be unique within a workflow run
|
|
348
|
+
3. `transcript()` / `getMessages()` only access completed sessions (callback returned + saves flushed)
|
|
349
|
+
4. Each session runs in its own tmux window with the chosen agent
|
|
341
350
|
5. Workflows are organized per-workflow: `.atomic/workflows/<name>/<agent>/index.ts`
|
|
342
351
|
|
|
343
352
|
Workflow files need no `package.json` or `node_modules` of their own — the Atomic loader rewrites `@bastani/atomic/*` and atomic's transitive deps (`@github/copilot-sdk`, `@opencode-ai/sdk`, `@anthropic-ai/claude-agent-sdk`, `zod`, etc.) to absolute paths inside the installed atomic package at load time. Drop a `.ts` file and it runs.
|
package/package.json
CHANGED
|
@@ -92,14 +92,20 @@ function buildLauncherScript(
|
|
|
92
92
|
cmd: string,
|
|
93
93
|
args: string[],
|
|
94
94
|
projectRoot: string,
|
|
95
|
+
envVars: Record<string, string> = {},
|
|
95
96
|
): { script: string; ext: string } {
|
|
96
97
|
const isWin = process.platform === "win32";
|
|
98
|
+
const envEntries = Object.entries(envVars);
|
|
97
99
|
|
|
98
100
|
if (isWin) {
|
|
99
101
|
// PowerShell: use array splatting for safe arg passing
|
|
100
102
|
const argList = args.map((a) => `"${escPwsh(a)}"`).join(", ");
|
|
103
|
+
const envLines = envEntries.map(
|
|
104
|
+
([key, value]) => `$env:${key} = "${escPwsh(value)}"`,
|
|
105
|
+
);
|
|
101
106
|
const script = [
|
|
102
107
|
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
108
|
+
...envLines,
|
|
103
109
|
argList.length > 0
|
|
104
110
|
? `& "${escPwsh(cmd)}" @(${argList})`
|
|
105
111
|
: `& "${escPwsh(cmd)}"`,
|
|
@@ -111,9 +117,13 @@ function buildLauncherScript(
|
|
|
111
117
|
const quotedArgs = args
|
|
112
118
|
.map((a) => `"${escBash(a)}"`)
|
|
113
119
|
.join(" ");
|
|
120
|
+
const envLines = envEntries.map(
|
|
121
|
+
([key, value]) => `export ${key}="${escBash(value)}"`,
|
|
122
|
+
);
|
|
114
123
|
const script = [
|
|
115
124
|
"#!/bin/bash",
|
|
116
125
|
`cd "${escBash(projectRoot)}"`,
|
|
126
|
+
...envLines,
|
|
117
127
|
`exec "${escBash(cmd)}" ${quotedArgs}`,
|
|
118
128
|
].join("\n");
|
|
119
129
|
return { script, ext: "sh" };
|
|
@@ -162,15 +172,16 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
162
172
|
// ── Build argv ──
|
|
163
173
|
const args = buildAgentArgs(agentType, passthroughArgs);
|
|
164
174
|
const cmd = [config.cmd, ...args];
|
|
175
|
+
const envVars = config.env_vars;
|
|
165
176
|
|
|
166
177
|
// ── Inside tmux: spawn inline in the current pane ──
|
|
167
178
|
if (isInsideTmux()) {
|
|
168
|
-
return spawnDirect(cmd, projectRoot);
|
|
179
|
+
return spawnDirect(cmd, projectRoot, envVars);
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
// ── No TTY: tmux attach requires a real terminal ──
|
|
172
183
|
if (!process.stdin.isTTY) {
|
|
173
|
-
return spawnDirect(cmd, projectRoot);
|
|
184
|
+
return spawnDirect(cmd, projectRoot, envVars);
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
// ── Ensure tmux is available ──
|
|
@@ -184,7 +195,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
184
195
|
}
|
|
185
196
|
if (!isTmuxInstalled()) {
|
|
186
197
|
// No tmux available — fall back to direct spawn
|
|
187
|
-
return spawnDirect(cmd, projectRoot);
|
|
198
|
+
return spawnDirect(cmd, projectRoot, envVars);
|
|
188
199
|
}
|
|
189
200
|
}
|
|
190
201
|
|
|
@@ -194,7 +205,12 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
194
205
|
|
|
195
206
|
const sessionsDir = join(homedir(), ".atomic", "sessions", "chat");
|
|
196
207
|
await mkdir(sessionsDir, { recursive: true });
|
|
197
|
-
const { script, ext } = buildLauncherScript(
|
|
208
|
+
const { script, ext } = buildLauncherScript(
|
|
209
|
+
config.cmd,
|
|
210
|
+
args,
|
|
211
|
+
projectRoot,
|
|
212
|
+
envVars,
|
|
213
|
+
);
|
|
198
214
|
const launcherPath = join(sessionsDir, `${windowName}.${ext}`);
|
|
199
215
|
await writeFile(launcherPath, script, { mode: 0o755 });
|
|
200
216
|
|
|
@@ -218,7 +234,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
218
234
|
// If tmux attach itself failed (e.g. lost TTY), clean up and fall back
|
|
219
235
|
if (exitCode !== 0) {
|
|
220
236
|
try { killSession(windowName); } catch {}
|
|
221
|
-
return spawnDirect(cmd, projectRoot);
|
|
237
|
+
return spawnDirect(cmd, projectRoot, envVars);
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
return exitCode;
|
|
@@ -228,7 +244,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
228
244
|
console.error(
|
|
229
245
|
`${COLORS.yellow}Warning: Failed to create tmux session (${message}). Falling back to direct spawn.${COLORS.reset}`
|
|
230
246
|
);
|
|
231
|
-
return spawnDirect(cmd, projectRoot);
|
|
247
|
+
return spawnDirect(cmd, projectRoot, envVars);
|
|
232
248
|
}
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -236,11 +252,15 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
236
252
|
* Spawn the agent CLI directly with inherited stdio.
|
|
237
253
|
* Used when not inside tmux.
|
|
238
254
|
*/
|
|
239
|
-
async function spawnDirect(
|
|
255
|
+
async function spawnDirect(
|
|
256
|
+
cmd: string[],
|
|
257
|
+
projectRoot: string,
|
|
258
|
+
envVars: Record<string, string> = {},
|
|
259
|
+
): Promise<number> {
|
|
240
260
|
const proc = Bun.spawn(cmd, {
|
|
241
261
|
stdio: ["inherit", "inherit", "inherit"],
|
|
242
262
|
cwd: projectRoot,
|
|
243
|
-
env: { ...process.env },
|
|
263
|
+
env: { ...process.env, ...envVars },
|
|
244
264
|
});
|
|
245
265
|
|
|
246
266
|
return await proc.exited;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getAgentKeys,
|
|
22
22
|
isValidAgent,
|
|
23
23
|
SCM_CONFIG,
|
|
24
|
+
SCM_SKILLS_BY_TYPE,
|
|
24
25
|
type SourceControlType,
|
|
25
26
|
getScmKeys,
|
|
26
27
|
isValidScm,
|
|
@@ -35,7 +36,6 @@ import {
|
|
|
35
36
|
getTemplateAgentFolder,
|
|
36
37
|
} from "@/services/config/atomic-global-config.ts";
|
|
37
38
|
import {
|
|
38
|
-
getScmPrefix,
|
|
39
39
|
installLocalScmSkills,
|
|
40
40
|
reconcileScmVariants,
|
|
41
41
|
syncProjectScmSkills,
|
|
@@ -404,9 +404,11 @@ export async function initCommand(options: InitOptions = {}): Promise<void> {
|
|
|
404
404
|
// skip the network-backed skills CLI in that case to keep dev iteration
|
|
405
405
|
// fast and offline-friendly.
|
|
406
406
|
if (import.meta.dir.includes("node_modules")) {
|
|
407
|
+
const skillsToInstall = SCM_SKILLS_BY_TYPE[scmType];
|
|
408
|
+
const skillsLabel = skillsToInstall.join(", ");
|
|
407
409
|
const skillsSpinner = spinner();
|
|
408
410
|
skillsSpinner.start(
|
|
409
|
-
`Installing ${
|
|
411
|
+
`Installing ${skillsLabel} locally for ${agent.name}...`,
|
|
410
412
|
);
|
|
411
413
|
const skillsResult = await installLocalScmSkills({
|
|
412
414
|
scmType,
|
|
@@ -415,11 +417,11 @@ export async function initCommand(options: InitOptions = {}): Promise<void> {
|
|
|
415
417
|
});
|
|
416
418
|
if (skillsResult.success) {
|
|
417
419
|
skillsSpinner.stop(
|
|
418
|
-
`Installed ${
|
|
420
|
+
`Installed ${skillsLabel} locally for ${agent.name}`,
|
|
419
421
|
);
|
|
420
422
|
} else {
|
|
421
423
|
skillsSpinner.stop(
|
|
422
|
-
`Skipped local ${
|
|
424
|
+
`Skipped local ${skillsLabel} install (${skillsResult.details})`,
|
|
423
425
|
);
|
|
424
426
|
}
|
|
425
427
|
}
|