@bastani/atomic 0.5.34 → 0.6.0-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 +329 -50
- package/dist/commands/cli/session.d.ts +67 -0
- package/dist/commands/cli/session.d.ts.map +1 -0
- package/dist/commands/cli/workflow-status.d.ts +63 -0
- package/dist/commands/cli/workflow-status.d.ts.map +1 -0
- package/dist/sdk/commander.d.ts +74 -0
- package/dist/sdk/commander.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts +18 -9
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/management-commands.d.ts +42 -0
- package/dist/sdk/management-commands.d.ts.map +1 -0
- package/dist/sdk/registry.d.ts +27 -0
- package/dist/sdk/registry.d.ts.map +1 -0
- package/dist/sdk/runtime/attached-footer.d.ts +1 -1
- package/dist/sdk/runtime/executor-env.d.ts +20 -0
- package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +61 -10
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +147 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/worker-shared.d.ts +42 -0
- package/dist/sdk/worker-shared.d.ts.map +1 -0
- package/dist/sdk/workflow-cli.d.ts +103 -0
- package/dist/sdk/workflow-cli.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
- package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +5 -5
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/cli.ts +85 -144
- package/src/commands/cli/chat/index.ts +10 -0
- package/src/commands/cli/workflow-command.test.ts +279 -938
- package/src/commands/cli/workflow-inputs.test.ts +41 -11
- package/src/commands/cli/workflow-inputs.ts +47 -12
- package/src/commands/cli/workflow-list.test.ts +234 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow.ts +11 -798
- package/src/scripts/constants.ts +2 -1
- package/src/sdk/commander.ts +161 -0
- package/src/sdk/components/workflow-picker-panel.tsx +78 -258
- package/src/sdk/define-workflow.test.ts +104 -11
- package/src/sdk/define-workflow.ts +47 -11
- package/src/sdk/errors.test.ts +16 -0
- package/src/sdk/index.ts +8 -8
- package/src/sdk/management-commands.ts +151 -0
- package/src/sdk/registry.ts +132 -0
- package/src/sdk/runtime/attached-footer.ts +1 -1
- package/src/sdk/runtime/executor-env.ts +45 -0
- package/src/sdk/runtime/executor.test.ts +37 -0
- package/src/sdk/runtime/executor.ts +147 -68
- package/src/sdk/types.ts +169 -4
- package/src/sdk/worker-shared.test.ts +163 -0
- package/src/sdk/worker-shared.ts +155 -0
- package/src/sdk/workflow-cli.ts +409 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin-registry.ts +23 -0
- package/src/sdk/workflows/index.ts +10 -20
- package/src/services/system/auth.test.ts +63 -1
- package/.agents/skills/workflow-creator/SKILL.md +0 -334
- package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
- package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
- package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
- package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
- package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
- package/.agents/skills/workflow-creator/references/session-config.md +0 -384
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
- package/.agents/skills/workflow-creator/references/user-input.md +0 -234
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
- package/dist/sdk/runtime/discovery.d.ts +0 -132
- package/dist/sdk/runtime/discovery.d.ts.map +0 -1
- package/dist/sdk/runtime/executor-entry.d.ts +0 -11
- package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
- package/dist/sdk/runtime/loader.d.ts +0 -70
- package/dist/sdk/runtime/loader.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/src/commands/cli/workflow.test.ts +0 -317
- package/src/sdk/runtime/discovery.ts +0 -368
- package/src/sdk/runtime/executor-entry.ts +0 -18
- package/src/sdk/runtime/loader.ts +0 -267
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Resolves the CLI entrypoint relative to this module (runtime/ lives at
|
|
11
11
|
* src/sdk/runtime/, so ../../cli.ts is the CLI). `process.argv[1]` points
|
|
12
|
-
* at the
|
|
12
|
+
* at the worker entrypoint when called from the orchestrator,
|
|
13
13
|
* so it can't be used here.
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator environment variable validation — extracted into its own
|
|
3
|
+
* module so test files can import it directly without touching the
|
|
4
|
+
* executor.ts module (which is mocked in worker.test.ts and
|
|
5
|
+
* workflow-command.test.ts). Importing from here bypasses those mocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentType } from "../types.ts";
|
|
9
|
+
import { isValidAgent } from "../../services/config/definitions.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read and validate the required orchestrator env vars, throwing on the
|
|
13
|
+
* first missing or invalid value.
|
|
14
|
+
*
|
|
15
|
+
* Required vars: ATOMIC_WF_ID, ATOMIC_WF_TMUX, ATOMIC_WF_AGENT, ATOMIC_WF_CWD.
|
|
16
|
+
*/
|
|
17
|
+
export function validateOrchestratorEnv(): {
|
|
18
|
+
workflowRunId: string;
|
|
19
|
+
tmuxSessionName: string;
|
|
20
|
+
agent: AgentType;
|
|
21
|
+
cwd: string;
|
|
22
|
+
} {
|
|
23
|
+
const requiredEnvVars = [
|
|
24
|
+
"ATOMIC_WF_ID",
|
|
25
|
+
"ATOMIC_WF_TMUX",
|
|
26
|
+
"ATOMIC_WF_AGENT",
|
|
27
|
+
"ATOMIC_WF_CWD",
|
|
28
|
+
] as const;
|
|
29
|
+
for (const key of requiredEnvVars) {
|
|
30
|
+
if (process.env[key] === undefined) {
|
|
31
|
+
throw new Error(`Missing required environment variable: ${key}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const workflowRunId = process.env.ATOMIC_WF_ID!;
|
|
36
|
+
const tmuxSessionName = process.env.ATOMIC_WF_TMUX!;
|
|
37
|
+
const rawAgent = process.env.ATOMIC_WF_AGENT!;
|
|
38
|
+
if (!isValidAgent(rawAgent)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Invalid ATOMIC_WF_AGENT: "${rawAgent}". Expected one of: copilot, opencode, claude`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const cwd = process.env.ATOMIC_WF_CWD!;
|
|
44
|
+
return { workflowRunId, tmuxSessionName, agent: rawAgent, cwd };
|
|
45
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
shouldOverrideCopilotCliPath,
|
|
13
13
|
discoverCopilotBinary,
|
|
14
14
|
applyContainerEnvDefaults,
|
|
15
|
+
normalizeExternalCopilotOptions,
|
|
15
16
|
type CopilotHILSessionSurface,
|
|
16
17
|
} from "./executor.ts";
|
|
17
18
|
import type { SavedMessage } from "../types.ts";
|
|
@@ -856,6 +857,42 @@ describe("watchCopilotSessionForElicitation", () => {
|
|
|
856
857
|
});
|
|
857
858
|
});
|
|
858
859
|
|
|
860
|
+
// ---------------------------------------------------------------------------
|
|
861
|
+
// Copilot SDK 0.3 external-server auth option normalization
|
|
862
|
+
// ---------------------------------------------------------------------------
|
|
863
|
+
|
|
864
|
+
describe("normalizeExternalCopilotOptions", () => {
|
|
865
|
+
test("moves client-level GitHub token to the session for cliUrl clients", () => {
|
|
866
|
+
const result = normalizeExternalCopilotOptions({
|
|
867
|
+
gitHubToken: "client-token",
|
|
868
|
+
logLevel: "error",
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
expect(result).toEqual({
|
|
872
|
+
clientOptions: { logLevel: "error" },
|
|
873
|
+
sessionGitHubToken: "client-token",
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
test("keeps an explicit session GitHub token when both levels are set", () => {
|
|
878
|
+
const result = normalizeExternalCopilotOptions(
|
|
879
|
+
{ gitHubToken: "client-token" },
|
|
880
|
+
"session-token",
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
expect(result).toEqual({
|
|
884
|
+
clientOptions: {},
|
|
885
|
+
sessionGitHubToken: "session-token",
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("rejects useLoggedInUser because external Copilot servers own auth", () => {
|
|
890
|
+
expect(() =>
|
|
891
|
+
normalizeExternalCopilotOptions({ useLoggedInUser: false }),
|
|
892
|
+
).toThrow("useLoggedInUser");
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
859
896
|
// ---------------------------------------------------------------------------
|
|
860
897
|
// Copilot CLI path discovery (Bun-without-node containers)
|
|
861
898
|
// ---------------------------------------------------------------------------
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* Workflow runtime executor.
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* 1. `executeWorkflow()` is called by the CLI command
|
|
6
|
-
* 2. It creates a tmux session with an orchestrator pane that
|
|
7
|
-
*
|
|
5
|
+
* 1. `executeWorkflow()` is called by the CLI command or worker
|
|
6
|
+
* 2. It creates a tmux session with an orchestrator pane that re-executes
|
|
7
|
+
* the user's entrypoint file with `ATOMIC_ORCHESTRATOR_MODE=1`
|
|
8
8
|
* 3. The CLI then attaches to the tmux session (user sees it live)
|
|
9
9
|
* 4. The orchestrator pane calls `definition.run(workflowCtx)` — the
|
|
10
10
|
* user's callback uses `ctx.stage()` to spawn agent sessions
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* In the new model the user's own file (e.g. `src/worker.ts`) is re-executed
|
|
13
|
+
* with env vars (`ATOMIC_ORCHESTRATOR_MODE`, `ATOMIC_WF_KEY`) that signal
|
|
14
|
+
* re-entry. The worker detects those vars and calls `runOrchestrator(definition)`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { join
|
|
17
|
+
import { join } from "node:path";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { writeFile } from "node:fs/promises";
|
|
20
20
|
import { statSync, accessSync, constants as fsConstants } from "node:fs";
|
|
@@ -36,7 +36,6 @@ import type {
|
|
|
36
36
|
ProviderSession,
|
|
37
37
|
} from "../types.ts";
|
|
38
38
|
import {
|
|
39
|
-
isValidAgent,
|
|
40
39
|
type ProviderOverrides,
|
|
41
40
|
} from "../../services/config/definitions.ts";
|
|
42
41
|
import { getProviderOverrides } from "../../services/config/atomic-config.ts";
|
|
@@ -48,7 +47,6 @@ import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
|
48
47
|
import * as tmux from "./tmux.ts";
|
|
49
48
|
import { spawnMuxAttach } from "./tmux.ts";
|
|
50
49
|
import { spawnAttachedFooter } from "./attached-footer.ts";
|
|
51
|
-
import { WorkflowLoader } from "./loader.ts";
|
|
52
50
|
import {
|
|
53
51
|
clearClaudeSession,
|
|
54
52
|
ClaudeClientWrapper,
|
|
@@ -133,8 +131,19 @@ export interface WorkflowRunOptions {
|
|
|
133
131
|
* whether the workflow declares a schema. Empty record is valid.
|
|
134
132
|
*/
|
|
135
133
|
inputs?: Record<string, string>;
|
|
136
|
-
/**
|
|
137
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Absolute path to the user's entrypoint file (e.g. `src/worker.ts`).
|
|
136
|
+
* The launcher re-executes this file with `ATOMIC_ORCHESTRATOR_MODE=1`
|
|
137
|
+
* so the worker can detect re-entry and call `runOrchestrator()`.
|
|
138
|
+
* Defaults to `process.argv[1]` at the call site.
|
|
139
|
+
*/
|
|
140
|
+
entrypointFile: string;
|
|
141
|
+
/**
|
|
142
|
+
* Registry key identifying this workflow, formatted as `"<agent>/<name>"`.
|
|
143
|
+
* Passed via `ATOMIC_WF_KEY` so the orchestrator can resolve the
|
|
144
|
+
* definition from the registry without a file-system scan.
|
|
145
|
+
*/
|
|
146
|
+
workflowKey: string;
|
|
138
147
|
/** Project root (defaults to cwd) */
|
|
139
148
|
projectRoot?: string;
|
|
140
149
|
/**
|
|
@@ -466,7 +475,8 @@ export async function executeWorkflow(
|
|
|
466
475
|
definition,
|
|
467
476
|
agent,
|
|
468
477
|
inputs = {},
|
|
469
|
-
|
|
478
|
+
entrypointFile,
|
|
479
|
+
workflowKey,
|
|
470
480
|
projectRoot = process.cwd(),
|
|
471
481
|
detach = false,
|
|
472
482
|
} = options;
|
|
@@ -477,10 +487,8 @@ export async function executeWorkflow(
|
|
|
477
487
|
await ensureDir(sessionsBaseDir);
|
|
478
488
|
|
|
479
489
|
// Write a launcher script for the orchestrator pane.
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
// are evaluated as separate module instances in Bun.
|
|
483
|
-
const thisFile = resolve(import.meta.dir, "executor-entry.ts");
|
|
490
|
+
// Re-executes the user's entrypoint file with ATOMIC_ORCHESTRATOR_MODE=1
|
|
491
|
+
// so the worker can detect re-entry and call runOrchestrator().
|
|
484
492
|
const isWin = process.platform === "win32";
|
|
485
493
|
const launcherExt = isWin ? "ps1" : "sh";
|
|
486
494
|
const launcherPath = join(sessionsBaseDir, `orchestrator.${launcherExt}`);
|
|
@@ -500,9 +508,10 @@ export async function executeWorkflow(
|
|
|
500
508
|
`$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
|
|
501
509
|
`$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
|
|
502
510
|
`$env:ATOMIC_WF_INPUTS = "${escPwsh(inputsB64)}"`,
|
|
503
|
-
`$env:
|
|
511
|
+
`$env:ATOMIC_ORCHESTRATOR_MODE = "1"`,
|
|
512
|
+
`$env:ATOMIC_WF_KEY = "${escPwsh(workflowKey)}"`,
|
|
504
513
|
`$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
|
|
505
|
-
`bun run "${escPwsh(
|
|
514
|
+
`bun run "${escPwsh(entrypointFile)}" 2>"${escPwsh(logPath)}"`,
|
|
506
515
|
].join("\n")
|
|
507
516
|
: [
|
|
508
517
|
"#!/bin/bash",
|
|
@@ -511,9 +520,10 @@ export async function executeWorkflow(
|
|
|
511
520
|
`export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
|
|
512
521
|
`export ATOMIC_WF_AGENT="${escBash(agent)}"`,
|
|
513
522
|
`export ATOMIC_WF_INPUTS="${escBash(inputsB64)}"`,
|
|
514
|
-
`export
|
|
523
|
+
`export ATOMIC_ORCHESTRATOR_MODE="1"`,
|
|
524
|
+
`export ATOMIC_WF_KEY="${escBash(workflowKey)}"`,
|
|
515
525
|
`export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
|
|
516
|
-
`bun run "${escBash(
|
|
526
|
+
`bun run "${escBash(entrypointFile)}" 2>"${escBash(logPath)}"`,
|
|
517
527
|
].join("\n");
|
|
518
528
|
|
|
519
529
|
await writeFile(launcherPath, launcherScript, { mode: 0o755 });
|
|
@@ -1204,6 +1214,48 @@ export function mergeExcludedTools(
|
|
|
1204
1214
|
return merged;
|
|
1205
1215
|
}
|
|
1206
1216
|
|
|
1217
|
+
type ExternalCopilotClientOptions = Omit<
|
|
1218
|
+
StageClientOptions<"copilot">,
|
|
1219
|
+
"gitHubToken" | "useLoggedInUser"
|
|
1220
|
+
>;
|
|
1221
|
+
|
|
1222
|
+
interface ExternalCopilotOptions {
|
|
1223
|
+
clientOptions: ExternalCopilotClientOptions;
|
|
1224
|
+
sessionGitHubToken?: string;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Copilot SDK 0.3.0 rejects client-level auth options when connecting to an
|
|
1229
|
+
* existing `cliUrl`. Visible stages use an already-running TUI server, so move
|
|
1230
|
+
* token auth to the session-level option that 0.3.0 introduced for this case.
|
|
1231
|
+
*/
|
|
1232
|
+
export function normalizeExternalCopilotOptions(
|
|
1233
|
+
clientOptions: StageClientOptions<"copilot">,
|
|
1234
|
+
sessionGitHubToken?: string,
|
|
1235
|
+
): ExternalCopilotOptions {
|
|
1236
|
+
const {
|
|
1237
|
+
gitHubToken: clientGitHubToken,
|
|
1238
|
+
useLoggedInUser,
|
|
1239
|
+
...externalClientOptions
|
|
1240
|
+
} = clientOptions;
|
|
1241
|
+
|
|
1242
|
+
if (useLoggedInUser !== undefined) {
|
|
1243
|
+
throw new Error(
|
|
1244
|
+
"Copilot client option `useLoggedInUser` cannot be used for visible stages because they connect to an existing Copilot CLI server. Configure authentication on the server process instead.",
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const normalized: ExternalCopilotOptions = {
|
|
1249
|
+
clientOptions: externalClientOptions,
|
|
1250
|
+
};
|
|
1251
|
+
if (sessionGitHubToken !== undefined) {
|
|
1252
|
+
normalized.sessionGitHubToken = sessionGitHubToken;
|
|
1253
|
+
} else if (clientGitHubToken !== undefined) {
|
|
1254
|
+
normalized.sessionGitHubToken = clientGitHubToken;
|
|
1255
|
+
}
|
|
1256
|
+
return normalized;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1207
1259
|
/**
|
|
1208
1260
|
* Create the provider-specific client and session for a stage.
|
|
1209
1261
|
* Called by the session runner after server readiness is confirmed.
|
|
@@ -1247,12 +1299,23 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
1247
1299
|
// when the caller didn't supply their own env keeps the
|
|
1248
1300
|
// SQLite `ExperimentalWarning` from leaking through the SDK's
|
|
1249
1301
|
// `[CLI subprocess]` stderr forwarder.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1302
|
+
let externalCopilotOptions: ExternalCopilotOptions | undefined;
|
|
1303
|
+
let client: InstanceType<typeof CopilotClient>;
|
|
1304
|
+
if (headless) {
|
|
1305
|
+
client = new CopilotClient({
|
|
1306
|
+
env: copilotSubprocessEnv(),
|
|
1307
|
+
...copilotClientOpts,
|
|
1308
|
+
});
|
|
1309
|
+
} else {
|
|
1310
|
+
externalCopilotOptions = normalizeExternalCopilotOptions(
|
|
1311
|
+
copilotClientOpts,
|
|
1312
|
+
copilotSessionOpts.gitHubToken,
|
|
1313
|
+
);
|
|
1314
|
+
client = new CopilotClient({
|
|
1315
|
+
...externalCopilotOptions.clientOptions,
|
|
1316
|
+
cliUrl: serverUrl,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1256
1319
|
await client.start();
|
|
1257
1320
|
// In headless stages, add `ask_user` to the session's excludedTools so
|
|
1258
1321
|
// the agent cannot call the interactive question tool — there is no
|
|
@@ -1260,6 +1323,9 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
1260
1323
|
const sessionConfig = {
|
|
1261
1324
|
onPermissionRequest: approveAll,
|
|
1262
1325
|
...copilotSessionOpts,
|
|
1326
|
+
...(externalCopilotOptions?.sessionGitHubToken !== undefined
|
|
1327
|
+
? { gitHubToken: externalCopilotOptions.sessionGitHubToken }
|
|
1328
|
+
: {}),
|
|
1263
1329
|
...(headless
|
|
1264
1330
|
? {
|
|
1265
1331
|
excludedTools: mergeExcludedTools(
|
|
@@ -1800,29 +1866,63 @@ function createSessionRunner(
|
|
|
1800
1866
|
// Orchestrator logic — runs inside a tmux pane
|
|
1801
1867
|
// ============================================================================
|
|
1802
1868
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Run the orchestrator inside a tmux pane.
|
|
1871
|
+
*
|
|
1872
|
+
* Called by the worker entrypoint when `ATOMIC_ORCHESTRATOR_MODE=1` is set.
|
|
1873
|
+
* The `definition` parameter is resolved by the caller (the worker) from the
|
|
1874
|
+
* registry using `ATOMIC_WF_KEY` — this function no longer performs any
|
|
1875
|
+
* file-path import or workflow discovery.
|
|
1876
|
+
*
|
|
1877
|
+
* @param definition - Resolved workflow definition from the registry.
|
|
1878
|
+
*/
|
|
1879
|
+
export { validateOrchestratorEnv } from "./executor-env.ts";
|
|
1880
|
+
import { validateOrchestratorEnv } from "./executor-env.ts";
|
|
1816
1881
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1882
|
+
/**
|
|
1883
|
+
* Orchestrator re-entry guard.
|
|
1884
|
+
*
|
|
1885
|
+
* When `executeWorkflow()` spawns a detached pane, it re-invokes the
|
|
1886
|
+
* composition root with `ATOMIC_ORCHESTRATOR_MODE=1` +
|
|
1887
|
+
* `ATOMIC_WF_KEY="<agent>/<name>"`. This helper detects that re-entry
|
|
1888
|
+
* and hands off to `runOrchestrator()` with the resolved definition.
|
|
1889
|
+
*
|
|
1890
|
+
* Returns `true` when re-entry was handled (caller should stop normal
|
|
1891
|
+
* CLI flow). Returns `false` when `ATOMIC_ORCHESTRATOR_MODE` is unset
|
|
1892
|
+
* — the caller should proceed with argv parsing.
|
|
1893
|
+
*
|
|
1894
|
+
* The `resolve` callback lets embedded workers pass a trivial lookup
|
|
1895
|
+
* (their single bound definition) while the dispatcher passes its
|
|
1896
|
+
* registry. Throws on malformed or unknown keys so authoring mistakes
|
|
1897
|
+
* surface loudly instead of silently hanging.
|
|
1898
|
+
*/
|
|
1899
|
+
export async function handleOrchestratorReEntry(
|
|
1900
|
+
resolve: (name: string, agent: AgentType) => WorkflowDefinition | undefined,
|
|
1901
|
+
): Promise<boolean> {
|
|
1902
|
+
if (process.env.ATOMIC_ORCHESTRATOR_MODE !== "1") {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
const key = process.env.ATOMIC_WF_KEY ?? "";
|
|
1906
|
+
const slashIdx = key.indexOf("/");
|
|
1907
|
+
if (slashIdx < 0) {
|
|
1821
1908
|
throw new Error(
|
|
1822
|
-
`
|
|
1909
|
+
`ATOMIC_ORCHESTRATOR_MODE=1 but ATOMIC_WF_KEY "${key}" is malformed — expected "<agent>/<name>"`,
|
|
1823
1910
|
);
|
|
1824
1911
|
}
|
|
1825
|
-
const agent
|
|
1912
|
+
const agent = key.slice(0, slashIdx) as AgentType;
|
|
1913
|
+
const name = key.slice(slashIdx + 1);
|
|
1914
|
+
const def = resolve(name, agent);
|
|
1915
|
+
if (!def) {
|
|
1916
|
+
throw new Error(`ATOMIC_WF_KEY "${key}" not found in registry`);
|
|
1917
|
+
}
|
|
1918
|
+
await runOrchestrator(def);
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
export async function runOrchestrator(
|
|
1923
|
+
definition: WorkflowDefinition,
|
|
1924
|
+
): Promise<void> {
|
|
1925
|
+
const { workflowRunId, tmuxSessionName, agent, cwd } = validateOrchestratorEnv();
|
|
1826
1926
|
// ATOMIC_WF_INPUTS carries the full input payload. Free-form
|
|
1827
1927
|
// workflows store their single positional prompt under the `prompt`
|
|
1828
1928
|
// key so workflow authors always read it via `ctx.inputs.prompt`.
|
|
@@ -1832,8 +1932,6 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1832
1932
|
// A bare prompt string is still useful for the panel header and the
|
|
1833
1933
|
// session-dir metadata.json — both just want something displayable.
|
|
1834
1934
|
const prompt = inputs.prompt ?? "";
|
|
1835
|
-
const workflowFile = process.env.ATOMIC_WF_FILE!;
|
|
1836
|
-
const cwd = process.env.ATOMIC_WF_CWD!;
|
|
1837
1935
|
|
|
1838
1936
|
process.chdir(cwd);
|
|
1839
1937
|
|
|
@@ -1915,28 +2013,9 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1915
2013
|
};
|
|
1916
2014
|
|
|
1917
2015
|
try {
|
|
1918
|
-
const plan: WorkflowLoader.Plan = {
|
|
1919
|
-
name: workflowFile.split("/").at(-3) ?? "unknown",
|
|
1920
|
-
agent,
|
|
1921
|
-
path: workflowFile,
|
|
1922
|
-
source: "local",
|
|
1923
|
-
};
|
|
1924
|
-
|
|
1925
|
-
const loaded = await WorkflowLoader.loadWorkflow(plan, {
|
|
1926
|
-
warn(warnings) {
|
|
1927
|
-
for (const w of warnings) {
|
|
1928
|
-
console.warn(`⚠ [${w.rule}] ${w.message}`);
|
|
1929
|
-
}
|
|
1930
|
-
},
|
|
1931
|
-
});
|
|
1932
|
-
if (!loaded.ok) {
|
|
1933
|
-
throw new Error(loaded.message);
|
|
1934
|
-
}
|
|
1935
|
-
const definition = loaded.value.definition;
|
|
1936
|
-
|
|
1937
2016
|
// Parse integer inputs to numbers so `ctx.inputs.<name>` matches the
|
|
1938
|
-
// declared type.
|
|
1939
|
-
//
|
|
2017
|
+
// declared type. Mutate shared.inputs so per-stage SessionContexts see
|
|
2018
|
+
// the same shape.
|
|
1940
2019
|
shared.inputs = coerceInputsBySchema(inputs, definition.inputs);
|
|
1941
2020
|
|
|
1942
2021
|
await Bun.write(
|
package/src/sdk/types.ts
CHANGED
|
@@ -412,7 +412,7 @@ export interface WorkflowOptions<
|
|
|
412
412
|
*
|
|
413
413
|
* When set, the CLI refuses to load the workflow on an older install
|
|
414
414
|
* and surfaces an actionable "update required" entry in the picker
|
|
415
|
-
* and `atomic workflow
|
|
415
|
+
* and `atomic workflow list` output instead of silently dropping it.
|
|
416
416
|
*
|
|
417
417
|
* Leave unset (the default) to opt out entirely — the workflow will
|
|
418
418
|
* be treated as compatible with every CLI version. Use this when you
|
|
@@ -425,6 +425,159 @@ export interface WorkflowOptions<
|
|
|
425
425
|
minSDKVersion?: string;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
// ─── Registry + WorkflowCli types ───────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Structural constraint for workflows accepted by `Registry.register()`.
|
|
432
|
+
*
|
|
433
|
+
* Uses `run: (...args: never[]) => Promise<void>` instead of the full
|
|
434
|
+
* `WorkflowDefinition<A, I>` constraint to avoid contravariance failures.
|
|
435
|
+
* A narrowly-typed `run(ctx: WorkflowContext<"claude">) => void` is not
|
|
436
|
+
* assignable to `run(ctx: WorkflowContext<AgentType>) => void` under
|
|
437
|
+
* `--strictFunctionTypes` (contravariant parameter position). Using
|
|
438
|
+
* `(...args: never[]) => Promise<void>` sidesteps this: any callable is
|
|
439
|
+
* assignable to a function that takes `never` args. Type narrowing on the
|
|
440
|
+
* accumulating `T` generic is still preserved via `W["agent"]`/`W["name"]`.
|
|
441
|
+
*/
|
|
442
|
+
export type RegistrableWorkflow = {
|
|
443
|
+
readonly __brand: "WorkflowDefinition";
|
|
444
|
+
readonly agent: AgentType;
|
|
445
|
+
readonly name: string;
|
|
446
|
+
readonly description: string;
|
|
447
|
+
readonly inputs: readonly WorkflowInput[];
|
|
448
|
+
readonly minSDKVersion: string | null;
|
|
449
|
+
readonly run: (...args: never[]) => Promise<void>;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Immutable, chainable registry of compiled workflow definitions.
|
|
454
|
+
*
|
|
455
|
+
* The generic parameter `T` accumulates the registered set as a
|
|
456
|
+
* `Record<"${agent}/${name}", WorkflowDefinition>` intersection, giving
|
|
457
|
+
* `get()` a typed return without casting.
|
|
458
|
+
*/
|
|
459
|
+
export type Registry<
|
|
460
|
+
T extends Record<string, WorkflowDefinition> = Record<string, WorkflowDefinition>,
|
|
461
|
+
> = {
|
|
462
|
+
/**
|
|
463
|
+
* Register a workflow definition. Returns a new Registry with the
|
|
464
|
+
* definition added. Throws if the same `${agent}/${name}` key is
|
|
465
|
+
* already registered.
|
|
466
|
+
*/
|
|
467
|
+
register<W extends RegistrableWorkflow>(
|
|
468
|
+
wf: W,
|
|
469
|
+
): Registry<T & Record<`${W["agent"]}/${W["name"]}`, W>>;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Retrieve a registered definition by its composite key.
|
|
473
|
+
* Compile-time typed based on the accumulated registry type.
|
|
474
|
+
*/
|
|
475
|
+
get<K extends keyof T>(key: K): T[K];
|
|
476
|
+
|
|
477
|
+
/** Return true if a workflow with the given composite key is registered. */
|
|
478
|
+
has(key: string): boolean;
|
|
479
|
+
|
|
480
|
+
/** Return all registered definitions as a readonly array. */
|
|
481
|
+
list(): readonly WorkflowDefinition[];
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Resolve a workflow by name + agent. Composes the composite key
|
|
485
|
+
* internally. Returns `undefined` when not found.
|
|
486
|
+
*/
|
|
487
|
+
resolve(name: string, agent: AgentType): WorkflowDefinition | undefined;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Argv control for `WorkflowCli.run`.
|
|
492
|
+
*
|
|
493
|
+
* - `undefined` — parse `process.argv` (default).
|
|
494
|
+
* - `string[]` — parse this explicit argv list (tests, embedding).
|
|
495
|
+
* - `false` — skip parsing; use `inputs` / `name` / `agent` as provided.
|
|
496
|
+
*/
|
|
497
|
+
export type ArgvMode = string[] | false;
|
|
498
|
+
|
|
499
|
+
/** Options for constructing a WorkflowCli via `createWorkflowCli()`. */
|
|
500
|
+
export interface CreateWorkflowCliOptions {
|
|
501
|
+
/** Programmatic inputs. CLI flags override these. */
|
|
502
|
+
inputs?: Record<string, string>;
|
|
503
|
+
/**
|
|
504
|
+
* Absolute path to the composition root file. The executor re-executes
|
|
505
|
+
* this path with `ATOMIC_ORCHESTRATOR_MODE=1` when detach is requested,
|
|
506
|
+
* so re-entry lands on the module that wired the dispatcher. Defaults
|
|
507
|
+
* to `process.argv[1]` — override when your composition root isn't
|
|
508
|
+
* argv[1] (test harnesses, bundled CLIs, embedded programs).
|
|
509
|
+
*/
|
|
510
|
+
entry?: string;
|
|
511
|
+
/**
|
|
512
|
+
* Hook to attach sibling commands to the standalone `run()` CLI. The
|
|
513
|
+
* callback runs once against the Commander program `run()` built
|
|
514
|
+
* internally. Not used by the `toCommand` adapter — if you're embedding,
|
|
515
|
+
* add your siblings to the parent directly.
|
|
516
|
+
*/
|
|
517
|
+
extend?: (program: import("@commander-js/extra-typings").Command) => void;
|
|
518
|
+
/**
|
|
519
|
+
* When `true` (the default), the generated CLI auto-registers the
|
|
520
|
+
* session + status management subcommands — `session list`,
|
|
521
|
+
* `session connect`, `session kill`, and `status` — so SDK users get
|
|
522
|
+
* the same monitoring UX as `atomic workflow …` without needing the
|
|
523
|
+
* global `atomic` binary.
|
|
524
|
+
*
|
|
525
|
+
* Every session spawned by the SDK lives on the shared `atomic` tmux
|
|
526
|
+
* socket, so these commands are pure pass-throughs to the same
|
|
527
|
+
* implementations the global CLI uses; there's no divergence. Set to
|
|
528
|
+
* `false` only when you want a minimal CLI (e.g. programmatic invocation
|
|
529
|
+
* or embedding under a parent Commander program where the parent owns
|
|
530
|
+
* session management).
|
|
531
|
+
*
|
|
532
|
+
* The `session` and `status` names are reserved — workflow inputs
|
|
533
|
+
* declared with those names will throw at `defineWorkflow` time to
|
|
534
|
+
* avoid flag collisions.
|
|
535
|
+
*/
|
|
536
|
+
includeManagementCommands?: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* A CLI program that resolves `--name` + `--agent` from argv and runs the
|
|
541
|
+
* matching workflow from a registry. Used by multi-workflow CLIs (e.g.
|
|
542
|
+
* the internal `atomic workflow` command) and single-workflow entry points.
|
|
543
|
+
*
|
|
544
|
+
* Framework-agnostic by design. To embed under a parent CLI, use the
|
|
545
|
+
* `toCommand` adapter in `@bastani/atomic/workflows/commander`.
|
|
546
|
+
*/
|
|
547
|
+
export interface WorkflowCli<
|
|
548
|
+
T extends Record<string, WorkflowDefinition> = Record<string, WorkflowDefinition>,
|
|
549
|
+
> {
|
|
550
|
+
/** Registry the CLI was constructed with. */
|
|
551
|
+
readonly registry: Registry<T>;
|
|
552
|
+
/**
|
|
553
|
+
* Absolute path the executor re-execs on `--detach`. Defaults to
|
|
554
|
+
* `process.argv[1]`; override via `createWorkflowCli(reg, { entry })`.
|
|
555
|
+
*/
|
|
556
|
+
readonly entry: string;
|
|
557
|
+
/**
|
|
558
|
+
* Input defaults supplied at construction. The adapter reads these so
|
|
559
|
+
* CLI-built Commands merge them identically to `run()`.
|
|
560
|
+
*/
|
|
561
|
+
readonly defaults: Record<string, string> | undefined;
|
|
562
|
+
/**
|
|
563
|
+
* Run the workflow CLI.
|
|
564
|
+
*
|
|
565
|
+
* - Default (`argv` unset): parses `process.argv` with `-n/--name`,
|
|
566
|
+
* `-a/--agent`, and the per-input union across the registry;
|
|
567
|
+
* `inputs`/`name`/`agent` layer in as defaults.
|
|
568
|
+
* - `argv: [...]`: parses the given argv list the same way.
|
|
569
|
+
* - `argv: false`: skip parsing; `name` and `agent` are required and
|
|
570
|
+
* `inputs` are used as-is.
|
|
571
|
+
*/
|
|
572
|
+
run(options?: {
|
|
573
|
+
name?: string;
|
|
574
|
+
agent?: AgentType;
|
|
575
|
+
inputs?: Record<string, string>;
|
|
576
|
+
argv?: ArgvMode;
|
|
577
|
+
detach?: boolean;
|
|
578
|
+
}): Promise<void>;
|
|
579
|
+
}
|
|
580
|
+
|
|
428
581
|
/**
|
|
429
582
|
* A compiled workflow definition — the sealed output of defineWorkflow().compile().
|
|
430
583
|
*/
|
|
@@ -434,14 +587,26 @@ export interface WorkflowDefinition<
|
|
|
434
587
|
> {
|
|
435
588
|
readonly __brand: "WorkflowDefinition";
|
|
436
589
|
readonly name: string;
|
|
590
|
+
/** The agent this workflow targets. Set via `.for(agent)` in the builder. */
|
|
591
|
+
readonly agent: A;
|
|
437
592
|
readonly description: string;
|
|
438
|
-
/**
|
|
439
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Declared input schema — empty tuple for free-form workflows.
|
|
595
|
+
* Typed as the builder-supplied `I` so consumers (e.g.
|
|
596
|
+
* `createWorkflowCli(def)`) can derive the narrow `InputsOf<I>` shape
|
|
597
|
+
* without carrying a second generic parameter.
|
|
598
|
+
*/
|
|
599
|
+
readonly inputs: I;
|
|
440
600
|
/**
|
|
441
601
|
* Minimum Atomic SDK version required. `null` when the workflow
|
|
442
602
|
* declared no requirement — treated as compatible with every CLI.
|
|
443
603
|
*/
|
|
444
604
|
readonly minSDKVersion: string | null;
|
|
445
605
|
/** The workflow's entry point. Called by the executor with a WorkflowContext. */
|
|
446
|
-
|
|
606
|
+
// Method signature (not a property) so TypeScript treats `run` as bivariant
|
|
607
|
+
// under --strictFunctionTypes — this allows a WorkflowDefinition<"claude">
|
|
608
|
+
// to be assigned to WorkflowDefinition<AgentType> even though `agent` is
|
|
609
|
+
// narrowed. Property function signatures would be contravariant and reject
|
|
610
|
+
// the assignment. See: https://www.typescriptlang.org/docs/handbook/2/functions.html
|
|
611
|
+
run(ctx: WorkflowContext<A, I>): Promise<void>;
|
|
447
612
|
}
|