@bastani/atomic 0.5.0-3 → 0.5.0-5
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 +138 -59
- package/package.json +1 -1
- package/src/cli.ts +0 -2
- package/src/commands/cli/chat/index.ts +28 -8
- package/src/commands/cli/init/index.ts +7 -10
- 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-contexts.ts +13 -4
- 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 +8 -15
- 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,18 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ralph workflow for Copilot — 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 copilot "<your spec>"
|
|
17
11
|
*/
|
|
18
12
|
|
|
@@ -33,16 +27,45 @@ import { safeGitStatusS } from "../helpers/git.ts";
|
|
|
33
27
|
|
|
34
28
|
const MAX_LOOPS = 10;
|
|
35
29
|
const CONSECUTIVE_CLEAN_THRESHOLD = 2;
|
|
30
|
+
/**
|
|
31
|
+
* Per-agent send timeout. `CopilotSession.sendAndWait` defaults to 60s, which
|
|
32
|
+
* is far too short for real planner/orchestrator/reviewer/debugger work — a
|
|
33
|
+
* timeout there throws and aborts the whole workflow before the next stage
|
|
34
|
+
* can run. 30 minutes gives each sub-agent ample headroom while still
|
|
35
|
+
* surfacing truly hung sessions.
|
|
36
|
+
*/
|
|
37
|
+
const AGENT_SEND_TIMEOUT_MS = 30 * 60 * 1000;
|
|
36
38
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Concatenate the text content of every top-level assistant message in the
|
|
41
|
+
* event stream.
|
|
42
|
+
*
|
|
43
|
+
* Why not just `.at(-1)`? Two traps:
|
|
44
|
+
*
|
|
45
|
+
* 1. A single Copilot turn is one `assistant.message` event that carries BOTH
|
|
46
|
+
* prose AND a `toolRequests[]` array. When the model ends a turn with
|
|
47
|
+
* tool-calls-only (e.g. the planner's final `TaskList` verification call),
|
|
48
|
+
* `content` is an empty string — picking the final message drops the
|
|
49
|
+
* planner's actual reasoning from the earlier turns.
|
|
50
|
+
* 2. `assistant.message` events have a `parentToolCallId` field populated when
|
|
51
|
+
* they originate from a sub-agent spawned by the parent. `getMessages()`
|
|
52
|
+
* returns the complete history including those, so `.at(-1)` can land on a
|
|
53
|
+
* sub-agent's final message instead of the top-level agent's. Filter them
|
|
54
|
+
* out to get only the agent's own turns.
|
|
55
|
+
*
|
|
56
|
+
* Joining every non-empty top-level content string preserves the full
|
|
57
|
+
* commentary across all turns, which is what downstream stages (e.g. the
|
|
58
|
+
* orchestrator reading the planner's handoff) actually need.
|
|
59
|
+
*/
|
|
60
|
+
function getAssistantText(messages: SessionEvent[]): string {
|
|
61
|
+
return messages
|
|
62
|
+
.filter(
|
|
63
|
+
(m): m is Extract<SessionEvent, { type: "assistant.message" }> =>
|
|
64
|
+
m.type === "assistant.message" && !m.data.parentToolCallId,
|
|
65
|
+
)
|
|
66
|
+
.map((m) => m.data.content)
|
|
67
|
+
.filter((c) => c.length > 0)
|
|
68
|
+
.join("\n\n");
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
export default defineWorkflow({
|
|
@@ -50,113 +73,190 @@ export default defineWorkflow({
|
|
|
50
73
|
description:
|
|
51
74
|
"Plan → orchestrate → review → debug loop with bounded iteration",
|
|
52
75
|
})
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Spin up a fresh sub-session bound to the named agent, send the
|
|
65
|
-
* prompt, await the response, then disconnect. Returns the text of the
|
|
66
|
-
* last assistant message so the caller can parse it.
|
|
67
|
-
*/
|
|
68
|
-
async function runAgent(agent: string, prompt: string): Promise<string> {
|
|
69
|
-
const session = await client.createSession({
|
|
70
|
-
agent,
|
|
71
|
-
onPermissionRequest: approveAll,
|
|
72
|
-
});
|
|
73
|
-
await client.setForegroundSessionId(session.sessionId);
|
|
74
|
-
|
|
75
|
-
await session.sendAndWait({ prompt });
|
|
76
|
-
|
|
77
|
-
const messages = await session.getMessages();
|
|
78
|
-
lastMessages = messages;
|
|
79
|
-
|
|
80
|
-
await session.disconnect();
|
|
81
|
-
return getLastAssistantText(messages);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
let consecutiveClean = 0;
|
|
86
|
-
let debuggerReport = "";
|
|
76
|
+
.run(async (ctx) => {
|
|
77
|
+
let consecutiveClean = 0;
|
|
78
|
+
let debuggerReport = "";
|
|
79
|
+
// Track the most recent session so the next stage can declare it as a
|
|
80
|
+
// dependency — this chains planner → orchestrator → reviewer → [confirm]
|
|
81
|
+
// → [debugger] → next planner in the graph instead of showing every
|
|
82
|
+
// stage as an independent sibling under the root.
|
|
83
|
+
let prevStage: string | undefined;
|
|
84
|
+
const depsOn = (): string[] | undefined =>
|
|
85
|
+
prevStage ? [prevStage] : undefined;
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
for (let iteration = 1; iteration <= MAX_LOOPS; iteration++) {
|
|
88
|
+
// ── Plan ──────────────────────────────────────────────────────────
|
|
89
|
+
const plannerName = `planner-${iteration}`;
|
|
90
|
+
const planner = await ctx.session(
|
|
91
|
+
{ name: plannerName, dependsOn: depsOn() },
|
|
92
|
+
async (s) => {
|
|
93
|
+
const client = new CopilotClient({ cliUrl: s.serverUrl });
|
|
94
|
+
await client.start();
|
|
95
|
+
const session = await client.createSession({
|
|
96
|
+
agent: "planner",
|
|
97
|
+
onPermissionRequest: approveAll,
|
|
98
|
+
});
|
|
99
|
+
await client.setForegroundSessionId(session.sessionId);
|
|
100
|
+
await session.sendAndWait(
|
|
101
|
+
{
|
|
102
|
+
prompt: buildPlannerPrompt(s.userPrompt, {
|
|
103
|
+
iteration,
|
|
104
|
+
debuggerReport: debuggerReport || undefined,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
AGENT_SEND_TIMEOUT_MS,
|
|
96
108
|
);
|
|
109
|
+
const messages = await session.getMessages();
|
|
110
|
+
s.save(messages);
|
|
111
|
+
await session.disconnect();
|
|
112
|
+
await client.stop();
|
|
113
|
+
return getAssistantText(messages);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
prevStage = plannerName;
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
// ── Orchestrate ───────────────────────────────────────────────────
|
|
119
|
+
const orchName = `orchestrator-${iteration}`;
|
|
120
|
+
await ctx.session(
|
|
121
|
+
{ name: orchName, dependsOn: depsOn() },
|
|
122
|
+
async (s) => {
|
|
123
|
+
const client = new CopilotClient({ cliUrl: s.serverUrl });
|
|
124
|
+
await client.start();
|
|
125
|
+
const session = await client.createSession({
|
|
126
|
+
agent: "orchestrator",
|
|
127
|
+
onPermissionRequest: approveAll,
|
|
128
|
+
});
|
|
129
|
+
await client.setForegroundSessionId(session.sessionId);
|
|
130
|
+
await session.sendAndWait(
|
|
131
|
+
{
|
|
132
|
+
prompt: buildOrchestratorPrompt(s.userPrompt, {
|
|
133
|
+
plannerNotes: planner.result,
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
AGENT_SEND_TIMEOUT_MS,
|
|
106
137
|
);
|
|
107
|
-
|
|
138
|
+
s.save(await session.getMessages());
|
|
139
|
+
await session.disconnect();
|
|
140
|
+
await client.stop();
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
prevStage = orchName;
|
|
108
144
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
// ── Review (first pass) ───────────────────────────────────────────
|
|
146
|
+
let gitStatus = await safeGitStatusS();
|
|
147
|
+
const reviewerName = `reviewer-${iteration}`;
|
|
148
|
+
const review = await ctx.session(
|
|
149
|
+
{ name: reviewerName, dependsOn: depsOn() },
|
|
150
|
+
async (s) => {
|
|
151
|
+
const client = new CopilotClient({ cliUrl: s.serverUrl });
|
|
152
|
+
await client.start();
|
|
153
|
+
const session = await client.createSession({
|
|
154
|
+
agent: "reviewer",
|
|
155
|
+
onPermissionRequest: approveAll,
|
|
156
|
+
});
|
|
157
|
+
await client.setForegroundSessionId(session.sessionId);
|
|
158
|
+
await session.sendAndWait(
|
|
159
|
+
{
|
|
160
|
+
prompt: buildReviewPrompt(s.userPrompt, {
|
|
120
161
|
gitStatus,
|
|
121
162
|
iteration,
|
|
122
|
-
isConfirmationPass: true,
|
|
123
163
|
}),
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
},
|
|
165
|
+
AGENT_SEND_TIMEOUT_MS,
|
|
166
|
+
);
|
|
167
|
+
const messages = await session.getMessages();
|
|
168
|
+
s.save(messages);
|
|
169
|
+
await session.disconnect();
|
|
170
|
+
await client.stop();
|
|
171
|
+
return getAssistantText(messages);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
prevStage = reviewerName;
|
|
126
175
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) {
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
} else {
|
|
133
|
-
consecutiveClean = 0;
|
|
134
|
-
// fall through to debugger
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
consecutiveClean = 0;
|
|
138
|
-
}
|
|
176
|
+
let reviewRaw = review.result;
|
|
177
|
+
let parsed = parseReviewResult(reviewRaw);
|
|
139
178
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
179
|
+
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
180
|
+
consecutiveClean += 1;
|
|
181
|
+
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) break;
|
|
182
|
+
|
|
183
|
+
// Confirmation pass — re-run reviewer only
|
|
184
|
+
gitStatus = await safeGitStatusS();
|
|
185
|
+
const confirmName = `reviewer-${iteration}-confirm`;
|
|
186
|
+
const confirm = await ctx.session(
|
|
187
|
+
{ name: confirmName, dependsOn: depsOn() },
|
|
188
|
+
async (s) => {
|
|
189
|
+
const client = new CopilotClient({ cliUrl: s.serverUrl });
|
|
190
|
+
await client.start();
|
|
191
|
+
const session = await client.createSession({
|
|
192
|
+
agent: "reviewer",
|
|
193
|
+
onPermissionRequest: approveAll,
|
|
194
|
+
});
|
|
195
|
+
await client.setForegroundSessionId(session.sessionId);
|
|
196
|
+
await session.sendAndWait(
|
|
197
|
+
{
|
|
198
|
+
prompt: buildReviewPrompt(s.userPrompt, {
|
|
199
|
+
gitStatus,
|
|
200
|
+
iteration,
|
|
201
|
+
isConfirmationPass: true,
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
AGENT_SEND_TIMEOUT_MS,
|
|
151
205
|
);
|
|
152
|
-
|
|
153
|
-
|
|
206
|
+
const messages = await session.getMessages();
|
|
207
|
+
s.save(messages);
|
|
208
|
+
await session.disconnect();
|
|
209
|
+
await client.stop();
|
|
210
|
+
return getAssistantText(messages);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
prevStage = confirmName;
|
|
214
|
+
|
|
215
|
+
reviewRaw = confirm.result;
|
|
216
|
+
parsed = parseReviewResult(reviewRaw);
|
|
217
|
+
|
|
218
|
+
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
219
|
+
consecutiveClean += 1;
|
|
220
|
+
if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) break;
|
|
221
|
+
} else {
|
|
222
|
+
consecutiveClean = 0;
|
|
154
223
|
}
|
|
224
|
+
} else {
|
|
225
|
+
consecutiveClean = 0;
|
|
226
|
+
}
|
|
155
227
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
228
|
+
// ── Debug (only if findings remain AND another iteration is allowed) ─
|
|
229
|
+
if (hasActionableFindings(parsed, reviewRaw) && iteration < MAX_LOOPS) {
|
|
230
|
+
const debuggerName = `debugger-${iteration}`;
|
|
231
|
+
const debugger_ = await ctx.session(
|
|
232
|
+
{ name: debuggerName, dependsOn: depsOn() },
|
|
233
|
+
async (s) => {
|
|
234
|
+
const client = new CopilotClient({ cliUrl: s.serverUrl });
|
|
235
|
+
await client.start();
|
|
236
|
+
const session = await client.createSession({
|
|
237
|
+
agent: "debugger",
|
|
238
|
+
onPermissionRequest: approveAll,
|
|
239
|
+
});
|
|
240
|
+
await client.setForegroundSessionId(session.sessionId);
|
|
241
|
+
await session.sendAndWait(
|
|
242
|
+
{
|
|
243
|
+
prompt: buildDebuggerReportPrompt(parsed, reviewRaw, {
|
|
244
|
+
iteration,
|
|
245
|
+
gitStatus,
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
AGENT_SEND_TIMEOUT_MS,
|
|
249
|
+
);
|
|
250
|
+
const messages = await session.getMessages();
|
|
251
|
+
s.save(messages);
|
|
252
|
+
await session.disconnect();
|
|
253
|
+
await client.stop();
|
|
254
|
+
return getAssistantText(messages);
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
prevStage = debuggerName;
|
|
258
|
+
debuggerReport = extractMarkdownBlock(debugger_.result);
|
|
159
259
|
}
|
|
160
|
-
}
|
|
260
|
+
}
|
|
161
261
|
})
|
|
162
262
|
.compile();
|
|
@@ -110,14 +110,57 @@ and persist them via TaskCreate.
|
|
|
110
110
|
// ORCHESTRATOR
|
|
111
111
|
// ============================================================================
|
|
112
112
|
|
|
113
|
+
export interface OrchestratorContext {
|
|
114
|
+
/**
|
|
115
|
+
* Trailing commentary from the planner's last assistant message, if any.
|
|
116
|
+
* The Copilot and OpenCode workflows create a fresh session for each
|
|
117
|
+
* sub-agent, so the planner's in-session output is NOT automatically
|
|
118
|
+
* visible to the orchestrator — only what the planner persisted via
|
|
119
|
+
* `TaskCreate`. Forward the planner's final text here so the orchestrator
|
|
120
|
+
* sees any caveats, risks, or execution hints that didn't fit into task
|
|
121
|
+
* bodies.
|
|
122
|
+
*/
|
|
123
|
+
plannerNotes?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Build the orchestrator prompt. The orchestrator retrieves the planner's
|
|
115
128
|
* task list, validates the dependency graph, and spawns parallel workers.
|
|
129
|
+
*
|
|
130
|
+
* @param spec - The original user specification. Required because the
|
|
131
|
+
* orchestrator runs in a fresh session on Copilot/OpenCode and needs the
|
|
132
|
+
* end-user goal to resolve ambiguous tasks.
|
|
133
|
+
* @param context - Optional planner handoff context (trailing commentary).
|
|
116
134
|
*/
|
|
117
|
-
export function buildOrchestratorPrompt(
|
|
135
|
+
export function buildOrchestratorPrompt(
|
|
136
|
+
spec: string,
|
|
137
|
+
context: OrchestratorContext = {},
|
|
138
|
+
): string {
|
|
139
|
+
const plannerNotes = context.plannerNotes?.trim() ?? "";
|
|
140
|
+
const plannerSection =
|
|
141
|
+
plannerNotes.length > 0
|
|
142
|
+
? `## Planner Notes (trailing commentary)
|
|
143
|
+
|
|
144
|
+
The planner produced the notes below alongside the task list. They capture
|
|
145
|
+
caveats, risks, or execution hints that did not fit into individual task
|
|
146
|
+
bodies. Treat them as guidance, not as task definitions.
|
|
147
|
+
|
|
148
|
+
<planner_notes>
|
|
149
|
+
${plannerNotes}
|
|
150
|
+
</planner_notes>
|
|
151
|
+
|
|
152
|
+
`
|
|
153
|
+
: "";
|
|
154
|
+
|
|
118
155
|
return `You are an orchestrator managing a set of implementation tasks.
|
|
119
156
|
|
|
120
|
-
##
|
|
157
|
+
## Original User Specification
|
|
158
|
+
|
|
159
|
+
<specification>
|
|
160
|
+
${spec}
|
|
161
|
+
</specification>
|
|
162
|
+
|
|
163
|
+
${plannerSection}## Retrieve Task List
|
|
121
164
|
|
|
122
165
|
Start by retrieving the current task list using your TaskList tool. The
|
|
123
166
|
planner has already created all tasks; you MUST retrieve them before any
|