@bastani/atomic 0.6.5 → 0.6.6-1

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 (148) hide show
  1. package/.agents/skills/ado-commit/SKILL.md +2 -0
  2. package/.agents/skills/ado-create-pr/SKILL.md +2 -0
  3. package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
  4. package/.agents/skills/ast-grep/SKILL.md +2 -0
  5. package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
  6. package/.agents/skills/bun/SKILL.md +156 -122
  7. package/.agents/skills/context-compression/SKILL.md +2 -0
  8. package/.agents/skills/context-degradation/SKILL.md +2 -0
  9. package/.agents/skills/context-fundamentals/SKILL.md +2 -0
  10. package/.agents/skills/context-optimization/SKILL.md +2 -0
  11. package/.agents/skills/create-spec/SKILL.md +2 -0
  12. package/.agents/skills/docx/SKILL.md +2 -0
  13. package/.agents/skills/evaluation/SKILL.md +2 -0
  14. package/.agents/skills/explain-code/SKILL.md +2 -0
  15. package/.agents/skills/filesystem-context/SKILL.md +2 -0
  16. package/.agents/skills/find-skills/SKILL.md +2 -0
  17. package/.agents/skills/gh-commit/SKILL.md +2 -0
  18. package/.agents/skills/gh-create-pr/SKILL.md +2 -0
  19. package/.agents/skills/hosted-agents/SKILL.md +2 -0
  20. package/.agents/skills/impeccable/SKILL.md +117 -304
  21. package/.agents/skills/impeccable/agents/openai.yaml +4 -0
  22. package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
  23. package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
  24. package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
  25. package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
  26. package/.agents/skills/impeccable/reference/brand.md +114 -0
  27. package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
  28. package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
  29. package/.agents/skills/impeccable/reference/craft.md +152 -29
  30. package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
  31. package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
  32. package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
  33. package/.agents/skills/impeccable/reference/document.md +427 -0
  34. package/.agents/skills/impeccable/reference/extract.md +1 -1
  35. package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
  36. package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
  37. package/.agents/skills/impeccable/reference/live.md +594 -0
  38. package/.agents/skills/impeccable/reference/motion-design.md +12 -2
  39. package/.agents/skills/impeccable/reference/onboard.md +234 -0
  40. package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
  41. package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
  42. package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
  43. package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
  44. package/.agents/skills/impeccable/reference/product.md +62 -0
  45. package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
  46. package/.agents/skills/impeccable/reference/shape.md +151 -0
  47. package/.agents/skills/impeccable/reference/teach.md +156 -0
  48. package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
  49. package/.agents/skills/impeccable/reference/typography.md +31 -14
  50. package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
  51. package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
  52. package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
  53. package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
  54. package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
  55. package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
  56. package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
  57. package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
  58. package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
  59. package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
  60. package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
  61. package/.agents/skills/impeccable/scripts/live.mjs +247 -0
  62. package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
  63. package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  64. package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
  65. package/.agents/skills/init/SKILL.md +2 -0
  66. package/.agents/skills/liteparse/SKILL.md +1 -0
  67. package/.agents/skills/memory-systems/SKILL.md +2 -0
  68. package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
  69. package/.agents/skills/opentui/SKILL.md +1 -0
  70. package/.agents/skills/pdf/SKILL.md +2 -0
  71. package/.agents/skills/playwright-cli/SKILL.md +51 -5
  72. package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
  73. package/.agents/skills/playwright-cli/references/running-code.md +10 -0
  74. package/.agents/skills/playwright-cli/references/session-management.md +56 -0
  75. package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
  76. package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
  77. package/.agents/skills/pptx/SKILL.md +2 -0
  78. package/.agents/skills/project-development/SKILL.md +2 -0
  79. package/.agents/skills/prompt-engineer/SKILL.md +2 -0
  80. package/.agents/skills/research-codebase/SKILL.md +2 -0
  81. package/.agents/skills/ripgrep/SKILL.md +2 -0
  82. package/.agents/skills/skill-creator/LICENSE.txt +1 -1
  83. package/.agents/skills/skill-creator/SKILL.md +2 -0
  84. package/.agents/skills/sl-commit/SKILL.md +2 -0
  85. package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
  86. package/.agents/skills/tdd/SKILL.md +4 -0
  87. package/.agents/skills/tool-design/SKILL.md +2 -0
  88. package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
  89. package/.agents/skills/typescript-expert/SKILL.md +7 -1
  90. package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
  91. package/.agents/skills/workflow-creator/SKILL.md +75 -72
  92. package/.agents/skills/workflow-creator/references/session-config.md +48 -1
  93. package/.agents/skills/xlsx/SKILL.md +2 -0
  94. package/.opencode/opencode.json +6 -2
  95. package/README.md +39 -38
  96. package/dist/lib/atomic-temp.d.ts +8 -0
  97. package/dist/lib/atomic-temp.d.ts.map +1 -0
  98. package/dist/lib/terminal-env.d.ts +9 -0
  99. package/dist/lib/terminal-env.d.ts.map +1 -0
  100. package/dist/sdk/providers/claude.d.ts.map +1 -1
  101. package/dist/sdk/providers/copilot.d.ts +24 -14
  102. package/dist/sdk/providers/copilot.d.ts.map +1 -1
  103. package/dist/sdk/runtime/executor.d.ts +8 -0
  104. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  105. package/dist/sdk/runtime/port-discovery.d.ts +71 -0
  106. package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
  107. package/dist/sdk/runtime/tmux.d.ts +10 -0
  108. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  109. package/dist/sdk/types.d.ts +1 -0
  110. package/dist/sdk/types.d.ts.map +1 -1
  111. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  112. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  113. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  114. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  115. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
  116. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
  117. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  118. package/package.json +10 -10
  119. package/src/commands/cli/chat/index.test.ts +194 -2
  120. package/src/commands/cli/chat/index.ts +83 -28
  121. package/src/lib/atomic-temp.test.ts +86 -0
  122. package/src/lib/atomic-temp.ts +62 -0
  123. package/src/lib/terminal-env.test.ts +343 -0
  124. package/src/lib/terminal-env.ts +100 -0
  125. package/src/scripts/clean-dist.test.ts +53 -0
  126. package/src/scripts/clean-dist.ts +37 -0
  127. package/src/sdk/providers/claude.ts +42 -20
  128. package/src/sdk/providers/copilot.test.ts +365 -0
  129. package/src/sdk/providers/copilot.ts +117 -15
  130. package/src/sdk/runtime/cc-debounce.ts +2 -2
  131. package/src/sdk/runtime/executor.test.ts +322 -1
  132. package/src/sdk/runtime/executor.ts +159 -96
  133. package/src/sdk/runtime/port-discovery.test.ts +573 -0
  134. package/src/sdk/runtime/port-discovery.ts +496 -0
  135. package/src/sdk/runtime/tmux.ts +22 -2
  136. package/src/sdk/types.ts +1 -0
  137. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
  138. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
  139. package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
  140. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
  141. package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
  142. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
  143. package/src/services/system/auth.test.ts +53 -0
  144. package/src/services/system/auth.ts +31 -28
  145. package/src/services/system/detect.ts +1 -1
  146. package/.agents/skills/shape/SKILL.md +0 -96
  147. /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
  148. /package/.agents/skills/{critique → impeccable}/reference/heuristics-scoring.md +0 -0
@@ -39,9 +39,7 @@ import type {
39
39
  ProviderClient,
40
40
  ProviderSession,
41
41
  } from "../types.ts";
42
- import {
43
- type ProviderOverrides,
44
- } from "../../services/config/definitions.ts";
42
+ import { type ProviderOverrides } from "../../services/config/definitions.ts";
45
43
  import { getProviderOverrides } from "../../services/config/atomic-config.ts";
46
44
  import { getCopilotScmDisableFlags } from "../../services/config/scm-sync.ts";
47
45
  import { reconcileOpencodeInstructions } from "../../services/config/additional-instructions.ts";
@@ -51,6 +49,10 @@ import type { SessionPromptResponse } from "@opencode-ai/sdk/v2";
51
49
  import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
52
50
  import * as tmux from "./tmux.ts";
53
51
  import { spawnMuxAttach } from "./tmux.ts";
52
+ import {
53
+ getListeningPortForPid,
54
+ PORT_DISCOVERY_TIMEOUT_MS,
55
+ } from "./port-discovery.ts";
54
56
  import { spawnAttachedFooter } from "./attached-footer.ts";
55
57
  import {
56
58
  clearClaudeSession,
@@ -60,14 +62,16 @@ import {
60
62
  HeadlessClaudeSessionWrapper,
61
63
  } from "../providers/claude.ts";
62
64
  import { withHeadlessOpencodeEnv } from "../providers/opencode.ts";
65
+ import { resolveCopilotCliPath } from "../providers/copilot.ts";
63
66
  import { OrchestratorPanel } from "./panel.tsx";
64
67
  import { GraphFrontierTracker } from "./graph-inference.ts";
65
68
  import { buildSnapshot, writeSnapshot } from "./status-writer.ts";
66
69
  import { errorMessage } from "../errors.ts";
67
70
  import { createPainter } from "../../theme/colors.ts";
71
+ import { atomicTempEnv } from "../../lib/atomic-temp.ts";
68
72
 
69
- /** Maximum time (ms) to wait for an agent's server to become reachable. */
70
- const SERVER_WAIT_TIMEOUT_MS = 60_000;
73
+ /** Maximum time (ms) for the SDK probe to succeed after port is discovered. */
74
+ export const SERVER_PROBE_TIMEOUT_MS = 60_000;
71
75
 
72
76
  /** Agent CLI configuration for spawning in tmux panes. */
73
77
  const AGENT_CLI: Record<
@@ -170,33 +174,6 @@ function getSessionsBaseDir(): string {
170
174
  return join(homedir(), ".atomic", "sessions");
171
175
  }
172
176
 
173
- async function getRandomPort(): Promise<number> {
174
- const net = await import("node:net");
175
-
176
- const MAX_RETRIES = 3;
177
- let lastPort = 0;
178
-
179
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
180
- const port = await new Promise<number>((resolve, reject) => {
181
- const server = net.createServer();
182
- server.listen(0, () => {
183
- const addr = server.address();
184
- const p = typeof addr === "object" && addr ? addr.port : 0;
185
- server.close(() => resolve(p));
186
- });
187
- server.on("error", reject);
188
- });
189
-
190
- if (port > 0) return port;
191
- lastPort = port;
192
- await Bun.sleep(50);
193
- }
194
-
195
- throw new Error(
196
- `Failed to acquire a random port after ${MAX_RETRIES} attempts (last: ${lastPort})`,
197
- );
198
- }
199
-
200
177
  /**
201
178
  * Resolve a non-JS Copilot CLI binary on PATH.
202
179
  *
@@ -280,9 +257,29 @@ export function applyContainerEnvDefaults(): void {
280
257
  if (bin) process.env.COPILOT_CLI_PATH = bin;
281
258
  }
282
259
 
283
- function buildPaneCommand(
260
+ /**
261
+ * Resolve a CLI binary name to its absolute path using the parent atomic
262
+ * process's PATH. tmux's child shell can have a stripped or differently
263
+ * ordered PATH from the user's interactive shell — most visibly when atomic
264
+ * is launched from a globally-installed bin wrapper rather than `bun run dev`.
265
+ * Resolving here, where we still have the full interactive PATH, mirrors
266
+ * how `attached-footer.ts` injects `process.execPath` + an absolute cli.ts
267
+ * path so the footer always spawns regardless of the child shell's PATH.
268
+ *
269
+ * Falls back to the bare name when the binary isn't found on PATH so behavior
270
+ * stays unchanged for callers running entirely inside a normal interactive shell.
271
+ */
272
+ function resolveCliBinary(cmd: string): string {
273
+ return Bun.which(cmd, { PATH: process.env.PATH ?? "" }) ?? cmd;
274
+ }
275
+
276
+ /** Wrap a path in bash double quotes only when it contains shell-significant characters. */
277
+ function quotePathIfNeeded(path: string): string {
278
+ return /[\s'"$`!\\]/.test(path) ? `"${escBash(path)}"` : path;
279
+ }
280
+
281
+ export function buildPaneCommand(
284
282
  agent: AgentType,
285
- port: number,
286
283
  overrides: ProviderOverrides = {},
287
284
  extraChatFlags: string[] = [],
288
285
  ): { command: string; envVars: Record<string, string> } {
@@ -292,62 +289,96 @@ function buildPaneCommand(
292
289
  envVars: defaultEnvVars,
293
290
  } = AGENT_CLI[agent];
294
291
  const chatFlags = overrides.chatFlags ?? defaultFlags;
292
+ const claudeTempEnv = agent === "claude" ? atomicTempEnv() : {};
295
293
  const envVars = overrides.envVars
296
294
  ? { ...defaultEnvVars, ...overrides.envVars }
297
295
  : defaultEnvVars;
296
+ const mergedEnvVars = { ...envVars, ...claudeTempEnv, ...overrides.envVars };
297
+
298
+ const resolvedCmd = quotePathIfNeeded(resolveCliBinary(cmd));
298
299
 
299
300
  switch (agent) {
300
- case "copilot":
301
+ case "copilot": {
302
+ // Prefer the copilot binary resolved via resolveCopilotCliPath so that
303
+ // COPILOT_CLI_PATH (set by applyContainerEnvDefaults in Bun-without-node
304
+ // environments) is honoured in the tmux pane command, keeping the pane
305
+ // binary consistent with the SDK subprocess binary.
306
+ const copilotBin = resolveCopilotCliPath() ?? resolveCliBinary(cmd);
301
307
  return {
302
308
  command: [
303
- cmd,
309
+ quotePathIfNeeded(copilotBin),
304
310
  "--ui-server",
305
311
  "--port",
306
- String(port),
312
+ "0",
307
313
  ...chatFlags,
308
314
  ...extraChatFlags,
309
315
  ].join(" "),
310
- envVars,
316
+ envVars: mergedEnvVars,
311
317
  };
318
+ }
312
319
  case "opencode":
313
320
  return {
314
- command: [cmd, "--port", String(port), ...chatFlags].join(" "),
315
- envVars,
321
+ command: [resolvedCmd, "--port", "0", ...chatFlags].join(" "),
322
+ envVars: mergedEnvVars,
316
323
  };
317
- case "claude":
318
- // Claude is started via createClaudeSession() in the workflow's run()
324
+ case "claude": {
325
+ // Claude is started via createClaudeSession() in the workflow's run().
326
+ // Resolve $SHELL (or the platform default) to an absolute path for the
327
+ // same reason the agent CLIs are resolved above.
328
+ const fallback = process.platform === "win32" ? "pwsh" : "sh";
329
+ const shellCandidate = process.env.SHELL || fallback;
330
+ const resolvedShell =
331
+ shellCandidate.includes("/") || shellCandidate.includes("\\")
332
+ ? shellCandidate
333
+ : resolveCliBinary(shellCandidate);
319
334
  return {
320
- command:
321
- process.env.SHELL || (process.platform === "win32" ? "pwsh" : "sh"),
322
- envVars,
335
+ command: quotePathIfNeeded(resolvedShell),
336
+ envVars: mergedEnvVars,
323
337
  };
338
+ }
324
339
  default:
325
340
  return assertNever(agent);
326
341
  }
327
342
  }
328
343
 
329
- async function waitForServer(
344
+ export async function waitForServer(
330
345
  agent: AgentType,
331
- port: number,
332
346
  paneId: string,
333
347
  ): Promise<string> {
334
348
  if (agent === "claude") return "";
335
349
 
336
- const serverUrl = `localhost:${port}`;
337
- const deadline = Date.now() + SERVER_WAIT_TIMEOUT_MS;
350
+ const portDeadline = Date.now() + PORT_DISCOVERY_TIMEOUT_MS;
338
351
 
339
- // Wait for the TUI to render first
340
- while (Date.now() < deadline) {
352
+ // 1. Wait for the agent process to start and the TUI to render.
353
+ while (Date.now() < portDeadline) {
341
354
  const content = tmux.capturePane(paneId);
342
355
  const lines = content.split("\n").filter((l) => l.trim().length > 0);
343
356
  if (lines.length >= 3) break;
344
357
  await Bun.sleep(1_000);
345
358
  }
346
359
 
347
- // Then verify the SDK can actually connect and list sessions
360
+ // 2. Discover the listening port via the agent's PID.
361
+ const panePid = tmux.getPanePid(paneId);
362
+ if (!panePid) {
363
+ throw new Error(`failed to resolve agent PID for pane ${paneId}`);
364
+ }
365
+ const remainingMs = Math.max(0, portDeadline - Date.now());
366
+ const port = await getListeningPortForPid(panePid, {
367
+ timeoutMs: remainingMs,
368
+ });
369
+ if (port === null) {
370
+ throw new Error(
371
+ `agent (${agent}) did not bind a TCP port within ${PORT_DISCOVERY_TIMEOUT_MS}ms ` +
372
+ `(pane ${paneId}, pid ${panePid})`,
373
+ );
374
+ }
375
+ const serverUrl = `localhost:${port}`;
376
+
377
+ // 3. Verify the SDK can actually connect.
348
378
  if (agent === "copilot") {
379
+ const probeDeadline = Date.now() + SERVER_PROBE_TIMEOUT_MS;
349
380
  const { CopilotClient } = await import("@github/copilot-sdk");
350
- while (Date.now() < deadline) {
381
+ while (Date.now() < probeDeadline) {
351
382
  try {
352
383
  const probe = new CopilotClient({ cliUrl: serverUrl });
353
384
  await probe.start();
@@ -358,10 +389,13 @@ async function waitForServer(
358
389
  await Bun.sleep(1_000);
359
390
  }
360
391
  }
392
+ throw new Error(
393
+ `copilot SDK probe did not respond at ${serverUrl} within ${SERVER_PROBE_TIMEOUT_MS}ms`,
394
+ );
361
395
  }
362
396
 
363
- // For OpenCode, give it extra time after TUI renders
364
- await Bun.sleep(3_000);
397
+ // OpenCode: short settle delay, then return.
398
+ await Bun.sleep(1_000);
365
399
  return serverUrl;
366
400
  }
367
401
 
@@ -470,6 +504,7 @@ export async function executeWorkflow(
470
504
  const launcherExt = isWin ? "ps1" : "sh";
471
505
  const launcherPath = join(sessionsBaseDir, `orchestrator.${launcherExt}`);
472
506
  const logPath = join(sessionsBaseDir, "orchestrator.log");
507
+ const launcherEnvVars = agent === "claude" ? atomicTempEnv() : {};
473
508
 
474
509
  // Inputs are passed through as base64-encoded JSON so long multiline
475
510
  // text values survive shell quoting without any further escaping.
@@ -482,23 +517,35 @@ export async function executeWorkflow(
482
517
  const orchestratorEntry = join(import.meta.dir, "orchestrator-entry.ts");
483
518
  const workflowSource = definition.source;
484
519
 
520
+ // Resolve the bun binary to an absolute path here — `process.execPath` is
521
+ // the exact bun interpreter currently running atomic, so we don't depend on
522
+ // bare `bun` being on the tmux child shell's PATH (the same reason
523
+ // `attached-footer.ts` uses it).
524
+ const bunBinary = process.execPath;
525
+
485
526
  const launcherScript = isWin
486
527
  ? [
487
528
  `Set-Location "${escPwsh(projectRoot)}"`,
529
+ ...Object.entries(launcherEnvVars).map(
530
+ ([key, value]) => `$env:${key} = "${escPwsh(value)}"`,
531
+ ),
488
532
  `$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
489
533
  `$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
490
534
  `$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
491
535
  `$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
492
- `bun run "${escPwsh(orchestratorEntry)}" "${escPwsh(workflowSource)}" "${escPwsh(agent)}" "${escPwsh(inputsB64)}" 2>"${escPwsh(logPath)}"`,
536
+ `& "${escPwsh(bunBinary)}" run "${escPwsh(orchestratorEntry)}" "${escPwsh(workflowSource)}" "${escPwsh(agent)}" "${escPwsh(inputsB64)}" 2>"${escPwsh(logPath)}"`,
493
537
  ].join("\n")
494
538
  : [
495
539
  "#!/bin/bash",
496
540
  `cd "${escBash(projectRoot)}"`,
541
+ ...Object.entries(launcherEnvVars).map(
542
+ ([key, value]) => `export ${key}="${escBash(value)}"`,
543
+ ),
497
544
  `export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
498
545
  `export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
499
546
  `export ATOMIC_WF_AGENT="${escBash(agent)}"`,
500
547
  `export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
501
- `bun run "${escBash(orchestratorEntry)}" "${escBash(workflowSource)}" "${escBash(agent)}" "${escBash(inputsB64)}" 2>"${escBash(logPath)}"`,
548
+ `"${escBash(bunBinary)}" run "${escBash(orchestratorEntry)}" "${escBash(workflowSource)}" "${escBash(agent)}" "${escBash(inputsB64)}" 2>"${escBash(logPath)}"`,
502
549
  ].join("\n");
503
550
 
504
551
  await writeFile(launcherPath, launcherScript, { mode: 0o755 });
@@ -506,7 +553,7 @@ export async function executeWorkflow(
506
553
  const shellCmd = isWin
507
554
  ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"`
508
555
  : `bash "${escBash(launcherPath)}"`;
509
- tmux.createSession(tmuxSessionName, shellCmd, "orchestrator");
556
+ tmux.createSession(tmuxSessionName, shellCmd, "orchestrator", undefined, launcherEnvVars);
510
557
  tmux.setSessionEnv(tmuxSessionName, "ATOMIC_AGENT", agent);
511
558
 
512
559
  if (detach) {
@@ -541,12 +588,28 @@ function printDetachedBanner(tmuxSessionName: string): void {
541
588
  const paint = createPainter();
542
589
  process.stdout.write(
543
590
  "\n" +
544
- " " + paint("success", "✓") + " " + paint("text", "workflow started in background", { bold: true }) + "\n" +
545
- " " + paint("dim", "session: ") + paint("accent", tmuxSessionName) + "\n" +
591
+ " " +
592
+ paint("success", "") +
593
+ " " +
594
+ paint("text", "workflow started in background", { bold: true }) +
595
+ "\n" +
596
+ " " +
597
+ paint("dim", "session: ") +
598
+ paint("accent", tmuxSessionName) +
599
+ "\n" +
600
+ "\n" +
601
+ " " +
602
+ paint("dim", "attach: ") +
603
+ paint("accent", `atomic workflow session connect ${tmuxSessionName}`) +
604
+ "\n" +
605
+ " " +
606
+ paint("dim", "list: ") +
607
+ paint("accent", "atomic workflow session list") +
608
+ "\n" +
609
+ " " +
610
+ paint("dim", "kill: ") +
611
+ paint("accent", `atomic workflow session kill ${tmuxSessionName}`) +
546
612
  "\n" +
547
- " " + paint("dim", "attach: ") + paint("accent", `atomic workflow session connect ${tmuxSessionName}`) + "\n" +
548
- " " + paint("dim", "list: ") + paint("accent", "atomic workflow session list") + "\n" +
549
- " " + paint("dim", "kill: ") + paint("accent", `atomic workflow session kill ${tmuxSessionName}`) + "\n" +
550
613
  "\n",
551
614
  );
552
615
  }
@@ -579,7 +642,9 @@ function resolveProviderSessionId(
579
642
  return typeof obj["id"] === "string" ? (obj["id"] as string) : "";
580
643
  }
581
644
  // claude and copilot both expose `sessionId` as a string.
582
- return typeof obj["sessionId"] === "string" ? (obj["sessionId"] as string) : "";
645
+ return typeof obj["sessionId"] === "string"
646
+ ? (obj["sessionId"] as string)
647
+ : "";
583
648
  }
584
649
 
585
650
  /** Type guard for objects with a string `content` property (Copilot assistant.message data). */
@@ -792,7 +857,9 @@ function renderCopilotTranscript(
792
857
  * invocations. `reasoning` and `subtask` parts are internal and omitted.
793
858
  */
794
859
  function renderOpencodeTranscript(response: {
795
- parts?: ReadonlyArray<{ type?: unknown; text?: unknown } & Record<string, unknown>>;
860
+ parts?: ReadonlyArray<
861
+ { type?: unknown; text?: unknown } & Record<string, unknown>
862
+ >;
796
863
  }): string {
797
864
  if (!response.parts) return "";
798
865
  const parts: string[] = [];
@@ -811,8 +878,8 @@ function renderOpencodeTranscript(response: {
811
878
  const state = part["state"];
812
879
  const args =
813
880
  state && typeof state === "object"
814
- ? (state as Record<string, unknown>)["input"] ??
815
- (state as Record<string, unknown>)["args"]
881
+ ? ((state as Record<string, unknown>)["input"] ??
882
+ (state as Record<string, unknown>)["args"])
816
883
  : undefined;
817
884
  parts.push(
818
885
  `**→ \`${name}\`**\n\n\`\`\`json\n${renderToolInput(args)}\n\`\`\``,
@@ -842,9 +909,7 @@ export function renderMessagesToText(messages: SavedMessage[]): string {
842
909
 
843
910
  for (const m of messages) {
844
911
  if (m.provider === "claude") {
845
- claudeBatch.push(
846
- m.data as unknown as { type: string; message: unknown },
847
- );
912
+ claudeBatch.push(m.data as unknown as { type: string; message: unknown });
848
913
  continue;
849
914
  }
850
915
  flushClaude();
@@ -880,7 +945,10 @@ function resolveRef(ref: SessionRef): string {
880
945
  * CopilotSession and lightweight test mocks.
881
946
  */
882
947
  export interface CopilotSendSessionSurface {
883
- on(eventType: string, handler: (event: { data?: unknown }) => void): () => void;
948
+ on(
949
+ eventType: string,
950
+ handler: (event: { data?: unknown }) => void,
951
+ ): () => void;
884
952
  }
885
953
 
886
954
  /**
@@ -1078,11 +1146,7 @@ export function watchCopilotSessionForElicitation(
1078
1146
  });
1079
1147
  const unsubCompleted = session.on("elicitation.completed", (event) => {
1080
1148
  const data = event.data as { requestId?: string } | undefined;
1081
- if (
1082
- data?.requestId &&
1083
- active.delete(data.requestId) &&
1084
- active.size === 0
1085
- ) {
1149
+ if (data?.requestId && active.delete(data.requestId) && active.size === 0) {
1086
1150
  onHIL(false);
1087
1151
  }
1088
1152
  });
@@ -1275,18 +1339,16 @@ async function initProviderClientAndSession<A extends AgentType>(
1275
1339
  switch (agent) {
1276
1340
  case "copilot": {
1277
1341
  const { CopilotClient, approveAll } = await import("@github/copilot-sdk");
1278
- const { copilotSubprocessEnv, mergeCopilotSystemMessage } = await import(
1279
- "../providers/copilot.ts"
1280
- );
1281
- const { resolveAdditionalInstructionsContent } = await import(
1282
- "../../services/config/additional-instructions.ts"
1283
- );
1342
+ const { copilotSdkLaunchOptions, mergeCopilotSystemMessage } =
1343
+ await import("../providers/copilot.ts");
1344
+ const { resolveAdditionalInstructionsContent } =
1345
+ await import("../../services/config/additional-instructions.ts");
1284
1346
  const copilotClientOpts = clientOpts as StageClientOptions<"copilot">;
1285
1347
  const copilotSessionOpts = sessionOpts as StageSessionOptions<"copilot">;
1286
1348
  // Headless: let the SDK spawn its own CLI process (no cliUrl).
1287
1349
  // Non-headless: connect to the CLI server running in a tmux pane.
1288
1350
  // `env` is only meaningful in the headless path — the SDK ignores
1289
- // it when `cliUrl` is set — but layering in `copilotSubprocessEnv`
1351
+ // it when `cliUrl` is set — but layering in `copilotSdkLaunchOptions`
1290
1352
  // when the caller didn't supply their own env keeps the
1291
1353
  // SQLite `ExperimentalWarning` from leaking through the SDK's
1292
1354
  // `[CLI subprocess]` stderr forwarder.
@@ -1294,7 +1356,7 @@ async function initProviderClientAndSession<A extends AgentType>(
1294
1356
  let client: InstanceType<typeof CopilotClient>;
1295
1357
  if (headless) {
1296
1358
  client = new CopilotClient({
1297
- env: copilotSubprocessEnv(),
1359
+ ...copilotSdkLaunchOptions(),
1298
1360
  ...copilotClientOpts,
1299
1361
  });
1300
1362
  } else {
@@ -1311,9 +1373,8 @@ async function initProviderClientAndSession<A extends AgentType>(
1311
1373
  // In headless stages, add `ask_user` to the session's excludedTools so
1312
1374
  // the agent cannot call the interactive question tool — there is no
1313
1375
  // human attached to answer and the SDK would otherwise sit blocked.
1314
- const additionalInstructions = await resolveAdditionalInstructionsContent(
1315
- projectRoot,
1316
- );
1376
+ const additionalInstructions =
1377
+ await resolveAdditionalInstructionsContent(projectRoot);
1317
1378
  const sessionConfig = {
1318
1379
  onPermissionRequest: approveAll,
1319
1380
  ...copilotSessionOpts,
@@ -1356,7 +1417,10 @@ async function initProviderClientAndSession<A extends AgentType>(
1356
1417
  // the session permission ruleset).
1357
1418
  return await withHeadlessOpencodeEnv(async () => {
1358
1419
  const oc = await createOpencode({ port: 0 });
1359
- const sessionResult = await oc.client.session.create(ocSessionOpts);
1420
+ const sessionResult = await oc.client.session.create({
1421
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1422
+ ...ocSessionOpts,
1423
+ });
1360
1424
  return {
1361
1425
  client: oc.client,
1362
1426
  session: sessionResult.data!,
@@ -1530,11 +1594,9 @@ function createSessionRunner(
1530
1594
  let panelSessionAdded = false;
1531
1595
 
1532
1596
  try {
1533
- // ── 6. Allocate port ──
1534
- const port = await getRandomPort();
1597
+ // ── 6. Build pane command (OS allocates port via --port 0) ──
1535
1598
  const { command: paneCmd, envVars: paneEnvVars } = buildPaneCommand(
1536
1599
  shared.agent,
1537
- port,
1538
1600
  shared.providerOverrides,
1539
1601
  shared.extraChatFlags,
1540
1602
  );
@@ -1564,7 +1626,7 @@ function createSessionRunner(
1564
1626
 
1565
1627
  spawnAttachedFooter(name, paneId);
1566
1628
 
1567
- serverUrl = await waitForServer(shared.agent, port, paneId);
1629
+ serverUrl = await waitForServer(shared.agent, paneId);
1568
1630
 
1569
1631
  shared.panel.addSession(name, graphParents);
1570
1632
  panelSessionAdded = true;
@@ -1592,8 +1654,8 @@ function createSessionRunner(
1592
1654
  if (!arg) {
1593
1655
  throw new Error(
1594
1656
  "wrapMessages: empty Claude session id. Call s.save(s.sessionId) " +
1595
- "only after a successful s.session.query() (headless wrappers " +
1596
- "only know their session_id once a query completes).",
1657
+ "only after a successful s.session.query() (headless wrappers " +
1658
+ "only know their session_id once a query completes).",
1597
1659
  );
1598
1660
  }
1599
1661
  const { getSessionMessages } =
@@ -1784,7 +1846,7 @@ function createSessionRunner(
1784
1846
  agent: shared.agent,
1785
1847
  paneId,
1786
1848
  serverUrl,
1787
- port,
1849
+ port: serverUrl ? Number(serverUrl.split(":").pop()) : 0,
1788
1850
  startedAt: new Date().toISOString(),
1789
1851
  },
1790
1852
  null,
@@ -1886,7 +1948,8 @@ export async function runOrchestrator(
1886
1948
  definition: WorkflowDefinition,
1887
1949
  inputs: Record<string, string> = {},
1888
1950
  ): Promise<void> {
1889
- const { workflowRunId, tmuxSessionName, agent, cwd } = validateOrchestratorEnv();
1951
+ const { workflowRunId, tmuxSessionName, agent, cwd } =
1952
+ validateOrchestratorEnv();
1890
1953
  // A bare prompt string is still useful for the panel header and the
1891
1954
  // session-dir metadata.json — both just want something displayable.
1892
1955
  // Free-form workflows store their single positional prompt under the