@bastani/atomic 0.5.34 → 0.6.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.
Files changed (94) hide show
  1. package/README.md +329 -50
  2. package/dist/commands/cli/session.d.ts +67 -0
  3. package/dist/commands/cli/session.d.ts.map +1 -0
  4. package/dist/commands/cli/workflow-status.d.ts +63 -0
  5. package/dist/commands/cli/workflow-status.d.ts.map +1 -0
  6. package/dist/sdk/commander.d.ts +74 -0
  7. package/dist/sdk/commander.d.ts.map +1 -0
  8. package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
  9. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
  10. package/dist/sdk/define-workflow.d.ts +18 -9
  11. package/dist/sdk/define-workflow.d.ts.map +1 -1
  12. package/dist/sdk/index.d.ts +4 -3
  13. package/dist/sdk/index.d.ts.map +1 -1
  14. package/dist/sdk/management-commands.d.ts +42 -0
  15. package/dist/sdk/management-commands.d.ts.map +1 -0
  16. package/dist/sdk/registry.d.ts +27 -0
  17. package/dist/sdk/registry.d.ts.map +1 -0
  18. package/dist/sdk/runtime/attached-footer.d.ts +1 -1
  19. package/dist/sdk/runtime/executor-env.d.ts +20 -0
  20. package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
  21. package/dist/sdk/runtime/executor.d.ts +61 -10
  22. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  23. package/dist/sdk/types.d.ts +147 -4
  24. package/dist/sdk/types.d.ts.map +1 -1
  25. package/dist/sdk/worker-shared.d.ts +42 -0
  26. package/dist/sdk/worker-shared.d.ts.map +1 -0
  27. package/dist/sdk/workflow-cli.d.ts +103 -0
  28. package/dist/sdk/workflow-cli.d.ts.map +1 -0
  29. package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
  30. package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
  31. package/dist/sdk/workflows/index.d.ts +5 -5
  32. package/dist/sdk/workflows/index.d.ts.map +1 -1
  33. package/package.json +12 -8
  34. package/src/cli.ts +85 -144
  35. package/src/commands/cli/chat/index.ts +10 -0
  36. package/src/commands/cli/workflow-command.test.ts +279 -938
  37. package/src/commands/cli/workflow-inputs.test.ts +41 -11
  38. package/src/commands/cli/workflow-inputs.ts +47 -12
  39. package/src/commands/cli/workflow-list.test.ts +234 -0
  40. package/src/commands/cli/workflow-list.ts +0 -0
  41. package/src/commands/cli/workflow.ts +11 -798
  42. package/src/scripts/constants.ts +2 -1
  43. package/src/sdk/commander.ts +161 -0
  44. package/src/sdk/components/workflow-picker-panel.tsx +78 -258
  45. package/src/sdk/define-workflow.test.ts +104 -11
  46. package/src/sdk/define-workflow.ts +47 -11
  47. package/src/sdk/errors.test.ts +16 -0
  48. package/src/sdk/index.ts +8 -8
  49. package/src/sdk/management-commands.ts +151 -0
  50. package/src/sdk/registry.ts +132 -0
  51. package/src/sdk/runtime/attached-footer.ts +1 -1
  52. package/src/sdk/runtime/executor-env.ts +45 -0
  53. package/src/sdk/runtime/executor.test.ts +37 -0
  54. package/src/sdk/runtime/executor.ts +147 -68
  55. package/src/sdk/types.ts +169 -4
  56. package/src/sdk/worker-shared.test.ts +163 -0
  57. package/src/sdk/worker-shared.ts +155 -0
  58. package/src/sdk/workflow-cli.ts +409 -0
  59. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
  60. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
  61. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
  62. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
  63. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
  64. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
  65. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
  66. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
  67. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
  68. package/src/sdk/workflows/builtin-registry.ts +23 -0
  69. package/src/sdk/workflows/index.ts +10 -20
  70. package/src/services/system/auth.test.ts +63 -1
  71. package/.agents/skills/workflow-creator/SKILL.md +0 -334
  72. package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
  73. package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
  74. package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
  75. package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
  76. package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
  77. package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
  78. package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
  79. package/.agents/skills/workflow-creator/references/session-config.md +0 -384
  80. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
  81. package/.agents/skills/workflow-creator/references/user-input.md +0 -234
  82. package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
  83. package/dist/sdk/runtime/discovery.d.ts +0 -132
  84. package/dist/sdk/runtime/discovery.d.ts.map +0 -1
  85. package/dist/sdk/runtime/executor-entry.d.ts +0 -11
  86. package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
  87. package/dist/sdk/runtime/loader.d.ts +0 -70
  88. package/dist/sdk/runtime/loader.d.ts.map +0 -1
  89. package/dist/version.d.ts +0 -2
  90. package/dist/version.d.ts.map +0 -1
  91. package/src/commands/cli/workflow.test.ts +0 -317
  92. package/src/sdk/runtime/discovery.ts +0 -368
  93. package/src/sdk/runtime/executor-entry.ts +0 -18
  94. 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 orchestrator's executor-entry.ts when called from the executor,
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 runs
7
- * `bun run executor-entry.ts` (a thin wrapper that calls `runOrchestrator()`)
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
- * The entry point is in executor-entry.ts (not this file) to avoid Bun's
13
- * dual-module-identity issue: Bun evaluates a file twice when it is both
14
- * the entry point and reached through package.json `exports` self-referencing.
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, resolve } from "node:path";
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
- /** Absolute path to the workflow's index.ts file (from discovery) */
137
- workflowFile: string;
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
- workflowFile,
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
- // Points to executor-entry.ts (not executor.ts) to avoid Bun's
481
- // dual-module-identity issue: entry points and package self-references
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:ATOMIC_WF_FILE = "${escPwsh(workflowFile)}"`,
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(thisFile)}" 2>"${escPwsh(logPath)}"`,
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 ATOMIC_WF_FILE="${escBash(workflowFile)}"`,
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(thisFile)}" 2>"${escBash(logPath)}"`,
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
- const client = headless
1251
- ? new CopilotClient({
1252
- env: copilotSubprocessEnv(),
1253
- ...copilotClientOpts,
1254
- })
1255
- : new CopilotClient({ ...copilotClientOpts, cliUrl: serverUrl });
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
- export async function runOrchestrator(): Promise<void> {
1804
- const requiredEnvVars = [
1805
- "ATOMIC_WF_ID",
1806
- "ATOMIC_WF_TMUX",
1807
- "ATOMIC_WF_AGENT",
1808
- "ATOMIC_WF_FILE",
1809
- "ATOMIC_WF_CWD",
1810
- ] as const;
1811
- for (const key of requiredEnvVars) {
1812
- if (process.env[key] === undefined) {
1813
- throw new Error(`Missing required environment variable: ${key}`);
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
- const workflowRunId = process.env.ATOMIC_WF_ID!;
1818
- const tmuxSessionName = process.env.ATOMIC_WF_TMUX!;
1819
- const rawAgent = process.env.ATOMIC_WF_AGENT!;
1820
- if (!isValidAgent(rawAgent)) {
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
- `Invalid ATOMIC_WF_AGENT: "${rawAgent}". Expected one of: copilot, opencode, claude`,
1909
+ `ATOMIC_ORCHESTRATOR_MODE=1 but ATOMIC_WF_KEY "${key}" is malformed expected "<agent>/<name>"`,
1823
1910
  );
1824
1911
  }
1825
- const agent: AgentType = rawAgent;
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. Do this after loading (when the schema is known) and
1939
- // mutate shared.inputs so per-stage SessionContexts see the same shape.
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 -l` output instead of silently dropping it.
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
- /** Declared input schema — empty array for free-form workflows. */
439
- readonly inputs: readonly WorkflowInput[];
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
- readonly run: (ctx: WorkflowContext<A, I>) => Promise<void>;
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
  }