@bastani/atomic 0.5.4 → 0.5.5-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 +44 -1
- package/dist/lib/path-root-guard.d.ts +4 -0
- package/dist/lib/path-root-guard.d.ts.map +1 -0
- package/dist/sdk/components/color-utils.d.ts +1 -0
- package/dist/sdk/components/color-utils.d.ts.map +1 -0
- package/dist/sdk/components/connectors.d.ts +3 -2
- package/dist/sdk/components/connectors.d.ts.map +1 -0
- package/dist/sdk/components/connectors.test.d.ts +1 -0
- package/dist/sdk/components/connectors.test.d.ts.map +1 -0
- package/dist/sdk/components/edge.d.ts +2 -1
- package/dist/sdk/components/edge.d.ts.map +1 -0
- package/dist/sdk/components/error-boundary.d.ts +1 -0
- package/dist/sdk/components/error-boundary.d.ts.map +1 -0
- package/dist/sdk/components/graph-theme.d.ts +2 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -0
- package/dist/sdk/components/hooks.d.ts +15 -0
- package/dist/sdk/components/hooks.d.ts.map +1 -0
- package/dist/sdk/components/layout.d.ts +2 -1
- package/dist/sdk/components/layout.d.ts.map +1 -0
- package/dist/sdk/components/layout.test.d.ts +1 -0
- package/dist/sdk/components/layout.test.d.ts.map +1 -0
- package/dist/sdk/components/node-card.d.ts +5 -3
- package/dist/sdk/components/node-card.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts +3 -2
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -0
- package/dist/sdk/components/status-helpers.d.ts +2 -1
- package/dist/sdk/components/status-helpers.d.ts.map +1 -0
- package/dist/sdk/components/statusline.d.ts +2 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +11 -8
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -0
- package/dist/sdk/define-workflow.d.ts +2 -1
- package/dist/sdk/define-workflow.d.ts.map +1 -0
- package/dist/sdk/define-workflow.test.d.ts +1 -0
- package/dist/sdk/define-workflow.test.d.ts.map +1 -0
- package/dist/sdk/errors.d.ts +3 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.test.d.ts +2 -0
- package/dist/sdk/errors.test.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +7 -6
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts +17 -6
- package/dist/sdk/providers/claude.d.ts.map +1 -0
- package/dist/sdk/providers/copilot.d.ts +2 -5
- package/dist/sdk/providers/copilot.d.ts.map +1 -0
- package/dist/sdk/providers/opencode.d.ts +2 -5
- package/dist/sdk/providers/opencode.d.ts.map +1 -0
- package/dist/sdk/runtime/discovery.d.ts +2 -1
- package/dist/sdk/runtime/discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +3 -6
- package/dist/sdk/runtime/executor.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.test.d.ts +1 -0
- package/dist/sdk/runtime/executor.test.d.ts.map +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts.map +1 -0
- package/dist/sdk/runtime/loader.d.ts +5 -7
- package/dist/sdk/runtime/loader.d.ts.map +1 -0
- package/dist/sdk/runtime/panel.d.ts +3 -2
- package/dist/sdk/runtime/panel.d.ts.map +1 -0
- package/dist/sdk/runtime/theme.d.ts +1 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +26 -8
- package/dist/sdk/runtime/tmux.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +23 -1
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +2 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +2 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +14 -14
- package/dist/sdk/workflows/index.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +85 -0
- package/dist/services/config/definitions.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +77 -0
- package/dist/services/system/copy.d.ts.map +1 -0
- package/dist/services/system/detect.d.ts +75 -0
- package/dist/services/system/detect.d.ts.map +1 -0
- package/package.json +13 -34
- package/src/cli.ts +11 -10
- package/src/commands/cli/chat/index.ts +11 -11
- package/src/commands/cli/chat.ts +1 -1
- package/src/commands/cli/config.ts +10 -9
- package/src/commands/cli/init/index.ts +11 -11
- package/src/commands/cli/init/onboarding.ts +4 -4
- package/src/commands/cli/init/scm.ts +5 -5
- package/src/commands/cli/init.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +19 -11
- package/src/commands/cli/workflow.test.ts +2 -2
- package/src/commands/cli/workflow.ts +6 -6
- package/src/lib/merge.ts +17 -31
- package/src/lib/path-root-guard.ts +2 -2
- package/src/lib/spawn.ts +13 -7
- package/src/scripts/bump-version.ts +1 -1
- package/src/scripts/constants.ts +2 -2
- package/src/sdk/components/header.tsx +21 -23
- package/src/sdk/components/hooks.ts +21 -0
- package/src/sdk/components/node-card.tsx +3 -2
- package/src/sdk/components/session-graph-panel.tsx +14 -18
- package/src/sdk/components/workflow-picker-panel.tsx +201 -216
- package/src/sdk/errors.test.ts +56 -0
- package/src/sdk/errors.ts +5 -0
- package/src/sdk/providers/claude.ts +279 -70
- package/src/sdk/providers/copilot.ts +17 -27
- package/src/sdk/providers/opencode.ts +17 -27
- package/src/sdk/runtime/discovery.ts +18 -18
- package/src/sdk/runtime/executor.test.ts +15 -48
- package/src/sdk/runtime/executor.ts +152 -121
- package/src/sdk/runtime/loader.ts +16 -21
- package/src/sdk/runtime/tmux.ts +95 -32
- package/src/sdk/types.ts +45 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +27 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +25 -16
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +25 -24
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +5 -0
- package/src/sdk/workflows/index.ts +3 -3
- package/src/services/config/atomic-config.ts +7 -8
- package/src/services/config/atomic-global-config.ts +9 -9
- package/src/services/config/config-path.ts +1 -1
- package/src/services/config/definitions.ts +3 -4
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +30 -36
- package/src/services/system/agents.ts +3 -3
- package/src/services/system/auto-sync.ts +9 -9
- package/src/services/system/copy.ts +9 -9
- package/src/services/system/file-lock.ts +2 -2
- package/src/services/system/install-ui.ts +2 -2
- package/src/services/system/skills.ts +1 -1
- package/src/theme/colors.ts +1 -1
- package/src/theme/logo.ts +1 -1
- package/tsconfig.json +3 -4
- package/dist/chunk-1gb5qxz9.js +0 -1
- package/dist/chunk-fdk7tact.js +0 -417
- package/dist/chunk-xkxndz5g.js +0 -1041
- package/dist/sdk/index.js +0 -52
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +0 -96
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +0 -119
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +0 -148
- package/dist/sdk/workflows/index.js +0 -100
- package/src/commands/cli/chat/client.ts +0 -18
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* Project-local workflows take precedence over global ones with the same name.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { readdir
|
|
13
|
-
import {
|
|
14
|
-
import { homedir } from "os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { readdir } from "node:fs/promises";
|
|
13
|
+
import { homedir } from "node:os";
|
|
15
14
|
import ignore from "ignore";
|
|
16
15
|
import type { AgentType, WorkflowInput } from "../types.ts";
|
|
17
16
|
import { WorkflowLoader } from "./loader.ts";
|
|
@@ -61,7 +60,7 @@ async function loadWorkflowsGitignore(workflowsDir: string): Promise<ignore.Igno
|
|
|
61
60
|
content = await Bun.file(gitignorePath).text();
|
|
62
61
|
} catch {
|
|
63
62
|
// Missing — regenerate from the canonical template
|
|
64
|
-
await
|
|
63
|
+
await Bun.write(gitignorePath, WORKFLOWS_GITIGNORE);
|
|
65
64
|
content = WORKFLOWS_GITIGNORE;
|
|
66
65
|
}
|
|
67
66
|
return ignore().add(content);
|
|
@@ -147,25 +146,27 @@ const BUILTIN_WORKFLOWS_DIR = join(
|
|
|
147
146
|
* Scans `src/sdk/workflows/builtin/<name>/<agent>/index.ts` for known
|
|
148
147
|
* workflow directories. Returns entries with `source: "builtin"`.
|
|
149
148
|
*/
|
|
150
|
-
function discoverBuiltinWorkflows(
|
|
149
|
+
async function discoverBuiltinWorkflows(
|
|
151
150
|
agentFilter?: AgentType,
|
|
152
|
-
): DiscoveredWorkflow[] {
|
|
151
|
+
): Promise<DiscoveredWorkflow[]> {
|
|
153
152
|
const results: DiscoveredWorkflow[] = [];
|
|
154
153
|
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
155
154
|
|
|
156
|
-
let
|
|
155
|
+
let workflowEntries;
|
|
157
156
|
try {
|
|
158
|
-
|
|
159
|
-
.filter((d) => d.isDirectory())
|
|
160
|
-
.map((d) => d.name);
|
|
157
|
+
workflowEntries = await readdir(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true });
|
|
161
158
|
} catch {
|
|
162
159
|
return results;
|
|
163
160
|
}
|
|
164
161
|
|
|
162
|
+
const workflowNames = workflowEntries
|
|
163
|
+
.filter((d) => d.isDirectory())
|
|
164
|
+
.map((d) => d.name);
|
|
165
|
+
|
|
165
166
|
for (const name of workflowNames) {
|
|
166
167
|
for (const agent of agents) {
|
|
167
168
|
const indexPath = join(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
|
|
168
|
-
if (
|
|
169
|
+
if (await Bun.file(indexPath).exists()) {
|
|
169
170
|
results.push({ name, agent, path: indexPath, source: "builtin" });
|
|
170
171
|
}
|
|
171
172
|
}
|
|
@@ -219,17 +220,16 @@ export async function discoverWorkflows(
|
|
|
219
220
|
// name-based across every agent: a local `ralph` for copilot is still
|
|
220
221
|
// reserved by a builtin `ralph` for claude, even when the discovery
|
|
221
222
|
// call was filtered to copilot.
|
|
222
|
-
const allBuiltins =
|
|
223
|
+
const [allBuiltins, globalResults, localResults] = await Promise.all([
|
|
224
|
+
discoverBuiltinWorkflows(),
|
|
225
|
+
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
226
|
+
discoverFromBaseDir(localDir, "local", agentFilter),
|
|
227
|
+
]);
|
|
223
228
|
const reservedNames = new Set<string>(allBuiltins.map((w) => w.name));
|
|
224
229
|
const builtinResults = agentFilter
|
|
225
230
|
? allBuiltins.filter((w) => w.agent === agentFilter)
|
|
226
231
|
: allBuiltins;
|
|
227
232
|
|
|
228
|
-
const [globalResults, localResults] = await Promise.all([
|
|
229
|
-
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
230
|
-
discoverFromBaseDir(localDir, "local", agentFilter),
|
|
231
|
-
]);
|
|
232
|
-
|
|
233
233
|
// Drop any local/global workflow whose name matches a reserved
|
|
234
234
|
// builtin. This happens BEFORE both merge and unmerged code paths so
|
|
235
235
|
// reserved names never leak into `findWorkflow`, the picker, or
|
|
@@ -2,7 +2,6 @@ import { test, expect, describe } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
renderMessagesToText,
|
|
4
4
|
hasContent,
|
|
5
|
-
isTextBlockArray,
|
|
6
5
|
escBash,
|
|
7
6
|
escPwsh,
|
|
8
7
|
} from "./executor.ts";
|
|
@@ -193,10 +192,23 @@ describe("renderMessagesToText", () => {
|
|
|
193
192
|
expect(renderMessagesToText(messages)).toBe("");
|
|
194
193
|
});
|
|
195
194
|
|
|
196
|
-
test("
|
|
195
|
+
test("returns empty string for claude assistant with unknown message shape", () => {
|
|
197
196
|
const unknownMsg = { weird: "shape", count: 99 };
|
|
198
197
|
const messages: SavedMessage[] = [makeClaudeMessage("assistant", unknownMsg)];
|
|
199
|
-
expect(renderMessagesToText(messages)).toBe(
|
|
198
|
+
expect(renderMessagesToText(messages)).toBe("");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("extracts text blocks from mixed claude content array (text + tool_use)", () => {
|
|
202
|
+
const messages: SavedMessage[] = [
|
|
203
|
+
makeClaudeMessage("assistant", {
|
|
204
|
+
content: [
|
|
205
|
+
{ type: "text", text: "I'll read the file" },
|
|
206
|
+
{ type: "tool_use", id: "tu-1", name: "Read", input: { path: "/tmp/foo" } },
|
|
207
|
+
{ type: "text", text: "Here's what I found" },
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
expect(renderMessagesToText(messages)).toBe("I'll read the file\nHere's what I found");
|
|
200
212
|
});
|
|
201
213
|
|
|
202
214
|
// --- Mixed providers ---
|
|
@@ -252,51 +264,6 @@ describe("hasContent", () => {
|
|
|
252
264
|
});
|
|
253
265
|
});
|
|
254
266
|
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// isTextBlockArray type guard
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
describe("isTextBlockArray", () => {
|
|
260
|
-
test("returns true for a valid array of text blocks", () => {
|
|
261
|
-
expect(isTextBlockArray([{ type: "text", text: "hi" }])).toBe(true);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("returns true for an array with multiple text blocks", () => {
|
|
265
|
-
expect(
|
|
266
|
-
isTextBlockArray([
|
|
267
|
-
{ type: "text", text: "first" },
|
|
268
|
-
{ type: "text", text: "second" },
|
|
269
|
-
]),
|
|
270
|
-
).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("returns true for an empty array (vacuously satisfies every element check)", () => {
|
|
274
|
-
// Array.prototype.every returns true on empty arrays — the empty array
|
|
275
|
-
// satisfies the type guard because there are no elements that violate it.
|
|
276
|
-
expect(isTextBlockArray([])).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("returns false for array with wrong block shape (missing text)", () => {
|
|
280
|
-
expect(isTextBlockArray([{ type: "text" }])).toBe(false);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("returns false for array with wrong type value", () => {
|
|
284
|
-
expect(isTextBlockArray([{ type: "tool_use", text: "hi" }])).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("returns false for non-array value", () => {
|
|
288
|
-
expect(isTextBlockArray("not an array")).toBe(false);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("returns false for null", () => {
|
|
292
|
-
expect(isTextBlockArray(null)).toBe(false);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("returns false when array elements are not objects", () => {
|
|
296
|
-
expect(isTextBlockArray(["text"])).toBe(false);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
267
|
// ---------------------------------------------------------------------------
|
|
301
268
|
// escBash — shell escaping for bash double-quoted strings
|
|
302
269
|
// ---------------------------------------------------------------------------
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* the entry point and reached through package.json `exports` self-referencing.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { join, resolve } from "path";
|
|
18
|
-
import { homedir } from "os";
|
|
19
|
-
import {
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { writeFile } from "node:fs/promises";
|
|
20
20
|
import type {
|
|
21
21
|
WorkflowDefinition,
|
|
22
22
|
WorkflowContext,
|
|
@@ -30,7 +30,11 @@ import type {
|
|
|
30
30
|
SaveTranscript,
|
|
31
31
|
StageClientOptions,
|
|
32
32
|
StageSessionOptions,
|
|
33
|
+
ProviderClient,
|
|
34
|
+
ProviderSession,
|
|
33
35
|
} from "../types.ts";
|
|
36
|
+
import { isValidAgent } from "../../services/config/definitions.ts";
|
|
37
|
+
import { ensureDir } from "../../services/system/copy.ts";
|
|
34
38
|
import type { SessionEvent } from "@github/copilot-sdk";
|
|
35
39
|
import type { SessionPromptResponse } from "@opencode-ai/sdk/v2";
|
|
36
40
|
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -44,6 +48,7 @@ import {
|
|
|
44
48
|
} from "../providers/claude.ts";
|
|
45
49
|
import { OrchestratorPanel } from "./panel.tsx";
|
|
46
50
|
import { GraphFrontierTracker } from "./graph-inference.ts";
|
|
51
|
+
import { errorMessage } from "../errors.ts";
|
|
47
52
|
|
|
48
53
|
/** Maximum time (ms) to wait for an agent's server to become reachable. */
|
|
49
54
|
const SERVER_WAIT_TIMEOUT_MS = 60_000;
|
|
@@ -85,6 +90,21 @@ class WorkflowAbortError extends Error {
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
/** Compile-time exhaustiveness guard for discriminated unions. */
|
|
94
|
+
function assertNever(value: never): never {
|
|
95
|
+
throw new Error(`Unhandled agent type: ${String(value)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Re-export for backward compatibility (tests import from here)
|
|
99
|
+
export { errorMessage } from "../errors.ts";
|
|
100
|
+
|
|
101
|
+
/** Runtime guard for deserialized SavedMessage objects. */
|
|
102
|
+
function isValidSavedMessage(msg: unknown): msg is SavedMessage {
|
|
103
|
+
if (!msg || typeof msg !== "object") return false;
|
|
104
|
+
const m = msg as Record<string, unknown>;
|
|
105
|
+
return m.provider === "copilot" || m.provider === "opencode" || m.provider === "claude";
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
export interface WorkflowRunOptions {
|
|
89
109
|
/** The compiled workflow definition */
|
|
90
110
|
definition: WorkflowDefinition;
|
|
@@ -180,10 +200,7 @@ function buildPaneCommand(
|
|
|
180
200
|
envVars,
|
|
181
201
|
};
|
|
182
202
|
default:
|
|
183
|
-
return
|
|
184
|
-
command: [cmd, ...chatFlags].join(" "),
|
|
185
|
-
envVars,
|
|
186
|
-
};
|
|
203
|
+
return assertNever(agent);
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
206
|
|
|
@@ -226,10 +243,6 @@ async function waitForServer(
|
|
|
226
243
|
return serverUrl;
|
|
227
244
|
}
|
|
228
245
|
|
|
229
|
-
async function ensureDir(dir: string): Promise<void> {
|
|
230
|
-
await mkdir(dir, { recursive: true });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
246
|
/**
|
|
234
247
|
* Escape a string for safe interpolation inside a bash double-quoted string.
|
|
235
248
|
*
|
|
@@ -274,7 +287,7 @@ export function parseInputsEnv(raw: string | undefined): Record<string, string>
|
|
|
274
287
|
return {};
|
|
275
288
|
}
|
|
276
289
|
const out: Record<string, string> = {};
|
|
277
|
-
for (const [k, v] of Object.entries(parsed
|
|
290
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
278
291
|
if (typeof v === "string") out[k] = v;
|
|
279
292
|
}
|
|
280
293
|
return out;
|
|
@@ -402,22 +415,6 @@ export function hasContent(value: unknown): value is { content: string } {
|
|
|
402
415
|
);
|
|
403
416
|
}
|
|
404
417
|
|
|
405
|
-
/** Type guard for Claude message objects whose `content` is an array of text blocks. */
|
|
406
|
-
export function isTextBlockArray(
|
|
407
|
-
value: unknown,
|
|
408
|
-
): value is Array<{ type: "text"; text: string }> {
|
|
409
|
-
return (
|
|
410
|
-
Array.isArray(value) &&
|
|
411
|
-
value.every(
|
|
412
|
-
(b) =>
|
|
413
|
-
typeof b === "object" &&
|
|
414
|
-
b !== null &&
|
|
415
|
-
b.type === "text" &&
|
|
416
|
-
typeof b.text === "string",
|
|
417
|
-
)
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
418
|
export function renderMessagesToText(messages: SavedMessage[]): string {
|
|
422
419
|
return messages
|
|
423
420
|
.map((m) => {
|
|
@@ -444,11 +441,25 @@ export function renderMessagesToText(messages: SavedMessage[]): string {
|
|
|
444
441
|
if (msg && typeof msg === "object" && "content" in msg) {
|
|
445
442
|
const { content } = msg as { content: unknown };
|
|
446
443
|
if (typeof content === "string") return content;
|
|
447
|
-
|
|
448
|
-
|
|
444
|
+
// Claude messages often have mixed content arrays (text +
|
|
445
|
+
// tool_use + thinking blocks). Filter for text blocks instead
|
|
446
|
+
// of requiring ALL blocks to be text — the old isTextBlockArray
|
|
447
|
+
// check caused a JSON.stringify fallback that embedded raw
|
|
448
|
+
// message objects into downstream prompts.
|
|
449
|
+
if (Array.isArray(content)) {
|
|
450
|
+
const textParts = content
|
|
451
|
+
.filter(
|
|
452
|
+
(b): b is { type: "text"; text: string } =>
|
|
453
|
+
typeof b === "object" &&
|
|
454
|
+
b !== null &&
|
|
455
|
+
b.type === "text" &&
|
|
456
|
+
typeof b.text === "string",
|
|
457
|
+
)
|
|
458
|
+
.map((b) => b.text);
|
|
459
|
+
if (textParts.length > 0) return textParts.join("\n");
|
|
449
460
|
}
|
|
450
461
|
}
|
|
451
|
-
return
|
|
462
|
+
return "";
|
|
452
463
|
}
|
|
453
464
|
}
|
|
454
465
|
})
|
|
@@ -461,6 +472,61 @@ function resolveRef(ref: SessionRef): string {
|
|
|
461
472
|
return typeof ref === "string" ? ref : ref.name;
|
|
462
473
|
}
|
|
463
474
|
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// Shared transcript / message readers
|
|
477
|
+
// ============================================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a `transcript(ref)` function bound to a completed-session registry.
|
|
481
|
+
* Used by both the top-level WorkflowContext and per-session SessionContext
|
|
482
|
+
* so the implementation is defined once.
|
|
483
|
+
*/
|
|
484
|
+
function createTranscriptReader(
|
|
485
|
+
completedRegistry: Map<string, SessionResult>,
|
|
486
|
+
): (ref: SessionRef) => Promise<Transcript> {
|
|
487
|
+
return async (ref) => {
|
|
488
|
+
const refName = resolveRef(ref);
|
|
489
|
+
const prev = completedRegistry.get(refName);
|
|
490
|
+
if (!prev) {
|
|
491
|
+
const available =
|
|
492
|
+
[...completedRegistry.keys()].join(", ") || "(none)";
|
|
493
|
+
throw new Error(
|
|
494
|
+
`No transcript for "${refName}". Available: ${available}`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
const filePath = join(prev.sessionDir, "inbox.md");
|
|
498
|
+
const content = await Bun.file(filePath).text();
|
|
499
|
+
return { path: filePath, content };
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Create a `getMessages(ref)` function bound to a completed-session registry.
|
|
505
|
+
* Used by both the top-level WorkflowContext and per-session SessionContext.
|
|
506
|
+
*/
|
|
507
|
+
function createMessagesReader(
|
|
508
|
+
completedRegistry: Map<string, SessionResult>,
|
|
509
|
+
): (ref: SessionRef) => Promise<SavedMessage[]> {
|
|
510
|
+
return async (ref) => {
|
|
511
|
+
const refName = resolveRef(ref);
|
|
512
|
+
const prev = completedRegistry.get(refName);
|
|
513
|
+
if (!prev) {
|
|
514
|
+
const available =
|
|
515
|
+
[...completedRegistry.keys()].join(", ") || "(none)";
|
|
516
|
+
throw new Error(
|
|
517
|
+
`No messages for "${refName}". Available: ${available}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const filePath = join(prev.sessionDir, "messages.json");
|
|
521
|
+
const raw = await Bun.file(filePath).text();
|
|
522
|
+
const parsed: unknown = JSON.parse(raw);
|
|
523
|
+
if (!Array.isArray(parsed)) {
|
|
524
|
+
throw new Error(`Invalid messages file for "${refName}": expected array`);
|
|
525
|
+
}
|
|
526
|
+
return parsed.filter(isValidSavedMessage);
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
464
530
|
// ============================================================================
|
|
465
531
|
// Session runner — implements ctx.stage() lifecycle
|
|
466
532
|
// ============================================================================
|
|
@@ -489,15 +555,23 @@ interface SharedRunnerState {
|
|
|
489
555
|
/**
|
|
490
556
|
* Create the provider-specific client and session for a stage.
|
|
491
557
|
* Called by the session runner after server readiness is confirmed.
|
|
558
|
+
*
|
|
559
|
+
* Generic over `A` so callers receive typed `ProviderClient<A>` /
|
|
560
|
+
* `ProviderSession<A>` without unsafe casts. The internal `switch`
|
|
561
|
+
* branches know the concrete types being constructed, so the `as`
|
|
562
|
+
* assertions here are producer-side (correct by construction) rather
|
|
563
|
+
* than consumer-side (trusting the caller to guess right).
|
|
492
564
|
*/
|
|
493
|
-
async function initProviderClientAndSession(
|
|
494
|
-
agent:
|
|
565
|
+
async function initProviderClientAndSession<A extends AgentType>(
|
|
566
|
+
agent: A,
|
|
495
567
|
serverUrl: string,
|
|
496
568
|
paneId: string,
|
|
497
569
|
sessionId: string,
|
|
498
|
-
clientOpts: StageClientOptions<
|
|
499
|
-
sessionOpts: StageSessionOptions<
|
|
500
|
-
): Promise<{ client:
|
|
570
|
+
clientOpts: StageClientOptions<A>,
|
|
571
|
+
sessionOpts: StageSessionOptions<A>,
|
|
572
|
+
): Promise<{ client: ProviderClient<A>; session: ProviderSession<A> }> {
|
|
573
|
+
type Result = { client: ProviderClient<A>; session: ProviderSession<A> };
|
|
574
|
+
|
|
501
575
|
switch (agent) {
|
|
502
576
|
case "copilot": {
|
|
503
577
|
const { CopilotClient, approveAll } = await import("@github/copilot-sdk");
|
|
@@ -510,7 +584,7 @@ async function initProviderClientAndSession(
|
|
|
510
584
|
...copilotSessionOpts,
|
|
511
585
|
});
|
|
512
586
|
await client.setForegroundSessionId(session.sessionId);
|
|
513
|
-
return { client, session };
|
|
587
|
+
return { client, session } as Result;
|
|
514
588
|
}
|
|
515
589
|
case "opencode": {
|
|
516
590
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
@@ -519,7 +593,7 @@ async function initProviderClientAndSession(
|
|
|
519
593
|
const client = createOpencodeClient({ ...ocClientOpts, baseUrl: serverUrl });
|
|
520
594
|
const sessionResult = await client.session.create(ocSessionOpts);
|
|
521
595
|
await client.tui.selectSession({ sessionID: sessionResult.data!.id });
|
|
522
|
-
return { client, session: sessionResult.data! };
|
|
596
|
+
return { client, session: sessionResult.data! } as Result;
|
|
523
597
|
}
|
|
524
598
|
case "claude": {
|
|
525
599
|
const claudeClientOpts = clientOpts as StageClientOptions<"claude">;
|
|
@@ -527,35 +601,41 @@ async function initProviderClientAndSession(
|
|
|
527
601
|
const client = new ClaudeClientWrapper(paneId, claudeClientOpts);
|
|
528
602
|
await client.start();
|
|
529
603
|
const session = new ClaudeSessionWrapper(paneId, sessionId, claudeSessionOpts);
|
|
530
|
-
return { client, session };
|
|
604
|
+
return { client, session } as Result;
|
|
531
605
|
}
|
|
606
|
+
default:
|
|
607
|
+
return assertNever(agent);
|
|
532
608
|
}
|
|
533
609
|
}
|
|
534
610
|
|
|
535
611
|
/**
|
|
536
612
|
* Clean up provider-specific resources after a stage callback completes.
|
|
537
613
|
* Errors are silently caught — cleanup must not mask callback errors.
|
|
614
|
+
*
|
|
615
|
+
* The `switch (agent)` already narrows the type, so we call
|
|
616
|
+
* disconnect/stop directly without redundant `instanceof` checks or
|
|
617
|
+
* dynamic imports.
|
|
538
618
|
*/
|
|
539
|
-
async function cleanupProvider(
|
|
540
|
-
agent:
|
|
541
|
-
providerClient:
|
|
542
|
-
providerSession:
|
|
619
|
+
async function cleanupProvider<A extends AgentType>(
|
|
620
|
+
agent: A,
|
|
621
|
+
providerClient: ProviderClient<A>,
|
|
622
|
+
providerSession: ProviderSession<A>,
|
|
543
623
|
paneId: string,
|
|
544
624
|
): Promise<void> {
|
|
545
625
|
switch (agent) {
|
|
546
626
|
case "copilot": {
|
|
547
|
-
const
|
|
627
|
+
const session = providerSession as ProviderSession<"copilot">;
|
|
628
|
+
const client = providerClient as ProviderClient<"copilot">;
|
|
548
629
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
}
|
|
630
|
+
await session.disconnect();
|
|
631
|
+
} catch (e) {
|
|
632
|
+
console.warn(`[cleanup] copilot session disconnect failed: ${errorMessage(e)}`);
|
|
633
|
+
}
|
|
553
634
|
try {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
} catch {}
|
|
635
|
+
await client.stop();
|
|
636
|
+
} catch (e) {
|
|
637
|
+
console.warn(`[cleanup] copilot client stop failed: ${errorMessage(e)}`);
|
|
638
|
+
}
|
|
559
639
|
break;
|
|
560
640
|
}
|
|
561
641
|
case "opencode":
|
|
@@ -564,6 +644,8 @@ async function cleanupProvider(
|
|
|
564
644
|
case "claude":
|
|
565
645
|
clearClaudeSession(paneId);
|
|
566
646
|
break;
|
|
647
|
+
default:
|
|
648
|
+
assertNever(agent);
|
|
567
649
|
}
|
|
568
650
|
}
|
|
569
651
|
|
|
@@ -752,37 +834,8 @@ function createSessionRunner(
|
|
|
752
834
|
}) as SaveTranscript;
|
|
753
835
|
|
|
754
836
|
// ── Transcript/messages access (reads only from completedRegistry) ──
|
|
755
|
-
const transcriptFn =
|
|
756
|
-
|
|
757
|
-
const prev = shared.completedRegistry.get(refName);
|
|
758
|
-
if (!prev) {
|
|
759
|
-
const available =
|
|
760
|
-
[...shared.completedRegistry.keys()].join(", ") || "(none)";
|
|
761
|
-
throw new Error(
|
|
762
|
-
`No transcript for "${refName}". Available: ${available}`,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
const filePath = join(prev.sessionDir, "inbox.md");
|
|
766
|
-
const content = await readFile(filePath, "utf-8");
|
|
767
|
-
return { path: filePath, content };
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
const getMessagesFn = async (
|
|
771
|
-
ref: SessionRef,
|
|
772
|
-
): Promise<SavedMessage[]> => {
|
|
773
|
-
const refName = resolveRef(ref);
|
|
774
|
-
const prev = shared.completedRegistry.get(refName);
|
|
775
|
-
if (!prev) {
|
|
776
|
-
const available =
|
|
777
|
-
[...shared.completedRegistry.keys()].join(", ") || "(none)";
|
|
778
|
-
throw new Error(
|
|
779
|
-
`No messages for "${refName}". Available: ${available}`,
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
const filePath = join(prev.sessionDir, "messages.json");
|
|
783
|
-
const raw = await readFile(filePath, "utf-8");
|
|
784
|
-
return JSON.parse(raw) as SavedMessage[];
|
|
785
|
-
};
|
|
837
|
+
const transcriptFn = createTranscriptReader(shared.completedRegistry);
|
|
838
|
+
const getMessagesFn = createMessagesReader(shared.completedRegistry);
|
|
786
839
|
|
|
787
840
|
// ── 12. Auto-create provider client and session ──
|
|
788
841
|
const { client: providerClient, session: providerSession } =
|
|
@@ -801,8 +854,8 @@ function createSessionRunner(
|
|
|
801
854
|
// A single uniform access pattern means workflow code never has
|
|
802
855
|
// to branch on "is this workflow structured or free-form".
|
|
803
856
|
const ctx: SessionContext = {
|
|
804
|
-
client: providerClient
|
|
805
|
-
session: providerSession
|
|
857
|
+
client: providerClient,
|
|
858
|
+
session: providerSession,
|
|
806
859
|
inputs: shared.inputs,
|
|
807
860
|
agent: shared.agent,
|
|
808
861
|
sessionDir,
|
|
@@ -815,7 +868,7 @@ function createSessionRunner(
|
|
|
815
868
|
};
|
|
816
869
|
|
|
817
870
|
// ── Write session metadata ──
|
|
818
|
-
await
|
|
871
|
+
await Bun.write(
|
|
819
872
|
join(sessionDir, "metadata.json"),
|
|
820
873
|
JSON.stringify(
|
|
821
874
|
{
|
|
@@ -839,8 +892,8 @@ function createSessionRunner(
|
|
|
839
892
|
if (pendingSaves.length > 0) await Promise.all(pendingSaves);
|
|
840
893
|
} catch (error) {
|
|
841
894
|
const message =
|
|
842
|
-
|
|
843
|
-
await
|
|
895
|
+
errorMessage(error);
|
|
896
|
+
await Bun.write(join(sessionDir, "error.txt"), message).catch(
|
|
844
897
|
() => {},
|
|
845
898
|
);
|
|
846
899
|
shared.panel.sessionError(name, message);
|
|
@@ -861,7 +914,7 @@ function createSessionRunner(
|
|
|
861
914
|
graphTracker.onSettle(name);
|
|
862
915
|
return { name, id: sessionId, result: callbackResult! };
|
|
863
916
|
} catch (error) {
|
|
864
|
-
const message =
|
|
917
|
+
const message = errorMessage(error);
|
|
865
918
|
if (panelSessionAdded) {
|
|
866
919
|
shared.panel.sessionError(name, message);
|
|
867
920
|
}
|
|
@@ -900,7 +953,11 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
900
953
|
|
|
901
954
|
const workflowRunId = process.env.ATOMIC_WF_ID!;
|
|
902
955
|
const tmuxSessionName = process.env.ATOMIC_WF_TMUX!;
|
|
903
|
-
const
|
|
956
|
+
const rawAgent = process.env.ATOMIC_WF_AGENT!;
|
|
957
|
+
if (!isValidAgent(rawAgent)) {
|
|
958
|
+
throw new Error(`Invalid ATOMIC_WF_AGENT: "${rawAgent}". Expected one of: copilot, opencode, claude`);
|
|
959
|
+
}
|
|
960
|
+
const agent: AgentType = rawAgent;
|
|
904
961
|
// ATOMIC_WF_INPUTS carries the full input payload. Free-form
|
|
905
962
|
// workflows store their single positional prompt under the `prompt`
|
|
906
963
|
// key so workflow authors always read it via `ctx.inputs.prompt`.
|
|
@@ -971,7 +1028,7 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
971
1028
|
}
|
|
972
1029
|
const definition = loaded.value.definition;
|
|
973
1030
|
|
|
974
|
-
await
|
|
1031
|
+
await Bun.write(
|
|
975
1032
|
join(sessionsBaseDir, "metadata.json"),
|
|
976
1033
|
JSON.stringify(
|
|
977
1034
|
{
|
|
@@ -996,34 +1053,8 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
996
1053
|
inputs,
|
|
997
1054
|
agent,
|
|
998
1055
|
stage: sessionRunner as WorkflowContext["stage"],
|
|
999
|
-
transcript:
|
|
1000
|
-
|
|
1001
|
-
const prev = shared.completedRegistry.get(refName);
|
|
1002
|
-
if (!prev) {
|
|
1003
|
-
const available =
|
|
1004
|
-
[...shared.completedRegistry.keys()].join(", ") || "(none)";
|
|
1005
|
-
throw new Error(
|
|
1006
|
-
`No transcript for "${refName}". Available: ${available}`,
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
const filePath = join(prev.sessionDir, "inbox.md");
|
|
1010
|
-
const content = await readFile(filePath, "utf-8");
|
|
1011
|
-
return { path: filePath, content };
|
|
1012
|
-
},
|
|
1013
|
-
getMessages: async (ref: SessionRef): Promise<SavedMessage[]> => {
|
|
1014
|
-
const refName = resolveRef(ref);
|
|
1015
|
-
const prev = shared.completedRegistry.get(refName);
|
|
1016
|
-
if (!prev) {
|
|
1017
|
-
const available =
|
|
1018
|
-
[...shared.completedRegistry.keys()].join(", ") || "(none)";
|
|
1019
|
-
throw new Error(
|
|
1020
|
-
`No messages for "${refName}". Available: ${available}`,
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
const filePath = join(prev.sessionDir, "messages.json");
|
|
1024
|
-
const raw = await readFile(filePath, "utf-8");
|
|
1025
|
-
return JSON.parse(raw) as SavedMessage[];
|
|
1026
|
-
},
|
|
1056
|
+
transcript: createTranscriptReader(shared.completedRegistry),
|
|
1057
|
+
getMessages: createMessagesReader(shared.completedRegistry),
|
|
1027
1058
|
};
|
|
1028
1059
|
|
|
1029
1060
|
// Run the workflow, racing against user abort (q / Ctrl+C)
|
|
@@ -1046,7 +1077,7 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1046
1077
|
if (error instanceof WorkflowAbortError) {
|
|
1047
1078
|
shutdown(0);
|
|
1048
1079
|
} else {
|
|
1049
|
-
const message =
|
|
1080
|
+
const message = errorMessage(error);
|
|
1050
1081
|
try {
|
|
1051
1082
|
panel.showFatalError(message);
|
|
1052
1083
|
await panel.waitForExit();
|