@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.
Files changed (42) hide show
  1. package/.atomic/workflows/hello/claude/index.ts +22 -25
  2. package/.atomic/workflows/hello/copilot/index.ts +41 -31
  3. package/.atomic/workflows/hello/opencode/index.ts +40 -40
  4. package/.atomic/workflows/hello-parallel/claude/index.ts +54 -54
  5. package/.atomic/workflows/hello-parallel/copilot/index.ts +89 -70
  6. package/.atomic/workflows/hello-parallel/opencode/index.ts +77 -77
  7. package/.atomic/workflows/ralph/claude/index.ts +128 -93
  8. package/.atomic/workflows/ralph/copilot/index.ts +212 -112
  9. package/.atomic/workflows/ralph/helpers/prompts.ts +45 -2
  10. package/.atomic/workflows/ralph/opencode/index.ts +174 -111
  11. package/README.md +138 -59
  12. package/package.json +1 -1
  13. package/src/cli.ts +0 -2
  14. package/src/commands/cli/chat/index.ts +28 -8
  15. package/src/commands/cli/init/index.ts +7 -10
  16. package/src/commands/cli/init/scm.ts +27 -10
  17. package/src/sdk/components/connectors.test.ts +45 -0
  18. package/src/sdk/components/layout.test.ts +321 -0
  19. package/src/sdk/components/layout.ts +51 -15
  20. package/src/sdk/components/orchestrator-panel-contexts.ts +13 -4
  21. package/src/sdk/components/orchestrator-panel-store.test.ts +156 -0
  22. package/src/sdk/components/orchestrator-panel-store.ts +24 -0
  23. package/src/sdk/components/orchestrator-panel.tsx +21 -0
  24. package/src/sdk/components/session-graph-panel.tsx +8 -15
  25. package/src/sdk/components/statusline.tsx +4 -6
  26. package/src/sdk/define-workflow.test.ts +71 -0
  27. package/src/sdk/define-workflow.ts +42 -39
  28. package/src/sdk/errors.ts +1 -1
  29. package/src/sdk/index.ts +4 -1
  30. package/src/sdk/providers/claude.ts +1 -1
  31. package/src/sdk/providers/copilot.ts +5 -3
  32. package/src/sdk/providers/opencode.ts +5 -3
  33. package/src/sdk/runtime/executor.ts +512 -301
  34. package/src/sdk/runtime/loader.ts +2 -2
  35. package/src/sdk/runtime/tmux.ts +31 -2
  36. package/src/sdk/types.ts +93 -20
  37. package/src/sdk/workflows.ts +7 -4
  38. package/src/services/config/definitions.ts +39 -2
  39. package/src/services/config/settings.ts +0 -6
  40. package/src/services/system/skills.ts +3 -7
  41. package/.atomic/workflows/package-lock.json +0 -31
  42. 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
- * One CopilotClient backs every iteration; each loop step creates a fresh
5
- * sub-session bound to the appropriate sub-agent (planner, orchestrator,
6
- * reviewer, debugger). The loop terminates when:
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
- /** Concatenate the text content of every assistant message in an event stream. */
38
- function getLastAssistantText(messages: SessionEvent[]): string {
39
- const assistantMessages = messages.filter(
40
- (m): m is Extract<SessionEvent, { type: "assistant.message" }> =>
41
- m.type === "assistant.message",
42
- );
43
- const last = assistantMessages.at(-1);
44
- if (!last) return "";
45
- return last.data.content;
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
- .session({
54
- name: "ralph-loop",
55
- description:
56
- "Drive plan/orchestrate/review/debug iterations until clean or capped",
57
- run: async (ctx) => {
58
- const client = new CopilotClient({ cliUrl: ctx.serverUrl });
59
- await client.start();
60
-
61
- let lastMessages: SessionEvent[] = [];
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
- for (let iteration = 1; iteration <= MAX_LOOPS; iteration++) {
89
- // ── Plan ──────────────────────────────────────────────────────────
90
- await runAgent(
91
- "planner",
92
- buildPlannerPrompt(ctx.userPrompt, {
93
- iteration,
94
- debuggerReport: debuggerReport || undefined,
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
- // ── Orchestrate ───────────────────────────────────────────────────
99
- await runAgent("orchestrator", buildOrchestratorPrompt());
100
-
101
- // ── Review (first pass) ───────────────────────────────────────────
102
- let gitStatus = await safeGitStatusS();
103
- let reviewRaw = await runAgent(
104
- "reviewer",
105
- buildReviewPrompt(ctx.userPrompt, { gitStatus, iteration }),
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
- let parsed = parseReviewResult(reviewRaw);
138
+ s.save(await session.getMessages());
139
+ await session.disconnect();
140
+ await client.stop();
141
+ },
142
+ );
143
+ prevStage = orchName;
108
144
 
109
- if (!hasActionableFindings(parsed, reviewRaw)) {
110
- consecutiveClean += 1;
111
- if (consecutiveClean >= CONSECUTIVE_CLEAN_THRESHOLD) {
112
- break;
113
- }
114
-
115
- // Confirmation pass re-run reviewer only, NOT plan/orchestrate.
116
- gitStatus = await safeGitStatusS();
117
- reviewRaw = await runAgent(
118
- "reviewer",
119
- buildReviewPrompt(ctx.userPrompt, {
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
- parsed = parseReviewResult(reviewRaw);
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
- if (!hasActionableFindings(parsed, reviewRaw)) {
128
- consecutiveClean += 1;
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
- // ── Debug (only if findings remain AND another iteration is allowed)
141
- if (
142
- hasActionableFindings(parsed, reviewRaw) &&
143
- iteration < MAX_LOOPS
144
- ) {
145
- const debuggerRaw = await runAgent(
146
- "debugger",
147
- buildDebuggerReportPrompt(parsed, reviewRaw, {
148
- iteration,
149
- gitStatus,
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
- debuggerReport = extractMarkdownBlock(debuggerRaw);
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
- ctx.save(lastMessages);
157
- } finally {
158
- await client.stop();
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(): string {
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
- ## Retrieve Task List
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