@bastani/atomic 0.6.5-0 → 0.6.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 (125) 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 +4 -2
  95. package/dist/sdk/runtime/executor.d.ts +8 -0
  96. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  97. package/dist/sdk/runtime/port-discovery.d.ts +71 -0
  98. package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
  99. package/dist/sdk/runtime/tmux.d.ts +10 -0
  100. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  101. package/dist/sdk/types.d.ts +1 -0
  102. package/dist/sdk/types.d.ts.map +1 -1
  103. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  104. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  105. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  106. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  107. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
  108. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
  109. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/sdk/runtime/executor.test.ts +254 -1
  112. package/src/sdk/runtime/executor.ts +135 -89
  113. package/src/sdk/runtime/port-discovery.test.ts +573 -0
  114. package/src/sdk/runtime/port-discovery.ts +496 -0
  115. package/src/sdk/runtime/tmux.ts +16 -0
  116. package/src/sdk/types.ts +1 -0
  117. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
  118. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
  119. package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
  120. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
  121. package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
  122. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
  123. package/.agents/skills/shape/SKILL.md +0 -96
  124. /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
  125. /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,
@@ -66,8 +68,8 @@ import { buildSnapshot, writeSnapshot } from "./status-writer.ts";
66
68
  import { errorMessage } from "../errors.ts";
67
69
  import { createPainter } from "../../theme/colors.ts";
68
70
 
69
- /** Maximum time (ms) to wait for an agent's server to become reachable. */
70
- const SERVER_WAIT_TIMEOUT_MS = 60_000;
71
+ /** Maximum time (ms) for the SDK probe to succeed after port is discovered. */
72
+ export const SERVER_PROBE_TIMEOUT_MS = 60_000;
71
73
 
72
74
  /** Agent CLI configuration for spawning in tmux panes. */
73
75
  const AGENT_CLI: Record<
@@ -170,33 +172,6 @@ function getSessionsBaseDir(): string {
170
172
  return join(homedir(), ".atomic", "sessions");
171
173
  }
172
174
 
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
175
  /**
201
176
  * Resolve a non-JS Copilot CLI binary on PATH.
202
177
  *
@@ -280,9 +255,29 @@ export function applyContainerEnvDefaults(): void {
280
255
  if (bin) process.env.COPILOT_CLI_PATH = bin;
281
256
  }
282
257
 
283
- function buildPaneCommand(
258
+ /**
259
+ * Resolve a CLI binary name to its absolute path using the parent atomic
260
+ * process's PATH. tmux's child shell can have a stripped or differently
261
+ * ordered PATH from the user's interactive shell — most visibly when atomic
262
+ * is launched from a globally-installed bin wrapper rather than `bun run dev`.
263
+ * Resolving here, where we still have the full interactive PATH, mirrors
264
+ * how `attached-footer.ts` injects `process.execPath` + an absolute cli.ts
265
+ * path so the footer always spawns regardless of the child shell's PATH.
266
+ *
267
+ * Falls back to the bare name when the binary isn't found on PATH so behavior
268
+ * stays unchanged for callers running entirely inside a normal interactive shell.
269
+ */
270
+ function resolveCliBinary(cmd: string): string {
271
+ return Bun.which(cmd, { PATH: process.env.PATH ?? "" }) ?? cmd;
272
+ }
273
+
274
+ /** Wrap a path in bash double quotes only when it contains shell-significant characters. */
275
+ function quotePathIfNeeded(path: string): string {
276
+ return /[\s'"$`!\\]/.test(path) ? `"${escBash(path)}"` : path;
277
+ }
278
+
279
+ export function buildPaneCommand(
284
280
  agent: AgentType,
285
- port: number,
286
281
  overrides: ProviderOverrides = {},
287
282
  extraChatFlags: string[] = [],
288
283
  ): { command: string; envVars: Record<string, string> } {
@@ -296,14 +291,16 @@ function buildPaneCommand(
296
291
  ? { ...defaultEnvVars, ...overrides.envVars }
297
292
  : defaultEnvVars;
298
293
 
294
+ const resolvedCmd = quotePathIfNeeded(resolveCliBinary(cmd));
295
+
299
296
  switch (agent) {
300
297
  case "copilot":
301
298
  return {
302
299
  command: [
303
- cmd,
300
+ resolvedCmd,
304
301
  "--ui-server",
305
302
  "--port",
306
- String(port),
303
+ "0",
307
304
  ...chatFlags,
308
305
  ...extraChatFlags,
309
306
  ].join(" "),
@@ -311,43 +308,67 @@ function buildPaneCommand(
311
308
  };
312
309
  case "opencode":
313
310
  return {
314
- command: [cmd, "--port", String(port), ...chatFlags].join(" "),
311
+ command: [resolvedCmd, "--port", "0", ...chatFlags].join(" "),
315
312
  envVars,
316
313
  };
317
- case "claude":
318
- // Claude is started via createClaudeSession() in the workflow's run()
314
+ case "claude": {
315
+ // Claude is started via createClaudeSession() in the workflow's run().
316
+ // Resolve $SHELL (or the platform default) to an absolute path for the
317
+ // same reason the agent CLIs are resolved above.
318
+ const fallback = process.platform === "win32" ? "pwsh" : "sh";
319
+ const shellCandidate = process.env.SHELL || fallback;
320
+ const resolvedShell =
321
+ shellCandidate.includes("/") || shellCandidate.includes("\\")
322
+ ? shellCandidate
323
+ : resolveCliBinary(shellCandidate);
319
324
  return {
320
- command:
321
- process.env.SHELL || (process.platform === "win32" ? "pwsh" : "sh"),
325
+ command: quotePathIfNeeded(resolvedShell),
322
326
  envVars,
323
327
  };
328
+ }
324
329
  default:
325
330
  return assertNever(agent);
326
331
  }
327
332
  }
328
333
 
329
- async function waitForServer(
334
+ export async function waitForServer(
330
335
  agent: AgentType,
331
- port: number,
332
336
  paneId: string,
333
337
  ): Promise<string> {
334
338
  if (agent === "claude") return "";
335
339
 
336
- const serverUrl = `localhost:${port}`;
337
- const deadline = Date.now() + SERVER_WAIT_TIMEOUT_MS;
340
+ const portDeadline = Date.now() + PORT_DISCOVERY_TIMEOUT_MS;
338
341
 
339
- // Wait for the TUI to render first
340
- while (Date.now() < deadline) {
342
+ // 1. Wait for the agent process to start and the TUI to render.
343
+ while (Date.now() < portDeadline) {
341
344
  const content = tmux.capturePane(paneId);
342
345
  const lines = content.split("\n").filter((l) => l.trim().length > 0);
343
346
  if (lines.length >= 3) break;
344
347
  await Bun.sleep(1_000);
345
348
  }
346
349
 
347
- // Then verify the SDK can actually connect and list sessions
350
+ // 2. Discover the listening port via the agent's PID.
351
+ const panePid = tmux.getPanePid(paneId);
352
+ if (!panePid) {
353
+ throw new Error(`failed to resolve agent PID for pane ${paneId}`);
354
+ }
355
+ const remainingMs = Math.max(0, portDeadline - Date.now());
356
+ const port = await getListeningPortForPid(panePid, {
357
+ timeoutMs: remainingMs,
358
+ });
359
+ if (port === null) {
360
+ throw new Error(
361
+ `agent (${agent}) did not bind a TCP port within ${PORT_DISCOVERY_TIMEOUT_MS}ms ` +
362
+ `(pane ${paneId}, pid ${panePid})`,
363
+ );
364
+ }
365
+ const serverUrl = `localhost:${port}`;
366
+
367
+ // 3. Verify the SDK can actually connect.
348
368
  if (agent === "copilot") {
369
+ const probeDeadline = Date.now() + SERVER_PROBE_TIMEOUT_MS;
349
370
  const { CopilotClient } = await import("@github/copilot-sdk");
350
- while (Date.now() < deadline) {
371
+ while (Date.now() < probeDeadline) {
351
372
  try {
352
373
  const probe = new CopilotClient({ cliUrl: serverUrl });
353
374
  await probe.start();
@@ -358,10 +379,13 @@ async function waitForServer(
358
379
  await Bun.sleep(1_000);
359
380
  }
360
381
  }
382
+ throw new Error(
383
+ `copilot SDK probe did not respond at ${serverUrl} within ${SERVER_PROBE_TIMEOUT_MS}ms`,
384
+ );
361
385
  }
362
386
 
363
- // For OpenCode, give it extra time after TUI renders
364
- await Bun.sleep(3_000);
387
+ // OpenCode: short settle delay, then return.
388
+ await Bun.sleep(1_000);
365
389
  return serverUrl;
366
390
  }
367
391
 
@@ -482,6 +506,12 @@ export async function executeWorkflow(
482
506
  const orchestratorEntry = join(import.meta.dir, "orchestrator-entry.ts");
483
507
  const workflowSource = definition.source;
484
508
 
509
+ // Resolve the bun binary to an absolute path here — `process.execPath` is
510
+ // the exact bun interpreter currently running atomic, so we don't depend on
511
+ // bare `bun` being on the tmux child shell's PATH (the same reason
512
+ // `attached-footer.ts` uses it).
513
+ const bunBinary = process.execPath;
514
+
485
515
  const launcherScript = isWin
486
516
  ? [
487
517
  `Set-Location "${escPwsh(projectRoot)}"`,
@@ -489,7 +519,7 @@ export async function executeWorkflow(
489
519
  `$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
490
520
  `$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
491
521
  `$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
492
- `bun run "${escPwsh(orchestratorEntry)}" "${escPwsh(workflowSource)}" "${escPwsh(agent)}" "${escPwsh(inputsB64)}" 2>"${escPwsh(logPath)}"`,
522
+ `& "${escPwsh(bunBinary)}" run "${escPwsh(orchestratorEntry)}" "${escPwsh(workflowSource)}" "${escPwsh(agent)}" "${escPwsh(inputsB64)}" 2>"${escPwsh(logPath)}"`,
493
523
  ].join("\n")
494
524
  : [
495
525
  "#!/bin/bash",
@@ -498,7 +528,7 @@ export async function executeWorkflow(
498
528
  `export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
499
529
  `export ATOMIC_WF_AGENT="${escBash(agent)}"`,
500
530
  `export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
501
- `bun run "${escBash(orchestratorEntry)}" "${escBash(workflowSource)}" "${escBash(agent)}" "${escBash(inputsB64)}" 2>"${escBash(logPath)}"`,
531
+ `"${escBash(bunBinary)}" run "${escBash(orchestratorEntry)}" "${escBash(workflowSource)}" "${escBash(agent)}" "${escBash(inputsB64)}" 2>"${escBash(logPath)}"`,
502
532
  ].join("\n");
503
533
 
504
534
  await writeFile(launcherPath, launcherScript, { mode: 0o755 });
@@ -541,12 +571,28 @@ function printDetachedBanner(tmuxSessionName: string): void {
541
571
  const paint = createPainter();
542
572
  process.stdout.write(
543
573
  "\n" +
544
- " " + paint("success", "✓") + " " + paint("text", "workflow started in background", { bold: true }) + "\n" +
545
- " " + paint("dim", "session: ") + paint("accent", tmuxSessionName) + "\n" +
574
+ " " +
575
+ paint("success", "") +
576
+ " " +
577
+ paint("text", "workflow started in background", { bold: true }) +
578
+ "\n" +
579
+ " " +
580
+ paint("dim", "session: ") +
581
+ paint("accent", tmuxSessionName) +
582
+ "\n" +
583
+ "\n" +
584
+ " " +
585
+ paint("dim", "attach: ") +
586
+ paint("accent", `atomic workflow session connect ${tmuxSessionName}`) +
587
+ "\n" +
588
+ " " +
589
+ paint("dim", "list: ") +
590
+ paint("accent", "atomic workflow session list") +
591
+ "\n" +
592
+ " " +
593
+ paint("dim", "kill: ") +
594
+ paint("accent", `atomic workflow session kill ${tmuxSessionName}`) +
546
595
  "\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
596
  "\n",
551
597
  );
552
598
  }
@@ -579,7 +625,9 @@ function resolveProviderSessionId(
579
625
  return typeof obj["id"] === "string" ? (obj["id"] as string) : "";
580
626
  }
581
627
  // claude and copilot both expose `sessionId` as a string.
582
- return typeof obj["sessionId"] === "string" ? (obj["sessionId"] as string) : "";
628
+ return typeof obj["sessionId"] === "string"
629
+ ? (obj["sessionId"] as string)
630
+ : "";
583
631
  }
584
632
 
585
633
  /** Type guard for objects with a string `content` property (Copilot assistant.message data). */
@@ -792,7 +840,9 @@ function renderCopilotTranscript(
792
840
  * invocations. `reasoning` and `subtask` parts are internal and omitted.
793
841
  */
794
842
  function renderOpencodeTranscript(response: {
795
- parts?: ReadonlyArray<{ type?: unknown; text?: unknown } & Record<string, unknown>>;
843
+ parts?: ReadonlyArray<
844
+ { type?: unknown; text?: unknown } & Record<string, unknown>
845
+ >;
796
846
  }): string {
797
847
  if (!response.parts) return "";
798
848
  const parts: string[] = [];
@@ -811,8 +861,8 @@ function renderOpencodeTranscript(response: {
811
861
  const state = part["state"];
812
862
  const args =
813
863
  state && typeof state === "object"
814
- ? (state as Record<string, unknown>)["input"] ??
815
- (state as Record<string, unknown>)["args"]
864
+ ? ((state as Record<string, unknown>)["input"] ??
865
+ (state as Record<string, unknown>)["args"])
816
866
  : undefined;
817
867
  parts.push(
818
868
  `**→ \`${name}\`**\n\n\`\`\`json\n${renderToolInput(args)}\n\`\`\``,
@@ -842,9 +892,7 @@ export function renderMessagesToText(messages: SavedMessage[]): string {
842
892
 
843
893
  for (const m of messages) {
844
894
  if (m.provider === "claude") {
845
- claudeBatch.push(
846
- m.data as unknown as { type: string; message: unknown },
847
- );
895
+ claudeBatch.push(m.data as unknown as { type: string; message: unknown });
848
896
  continue;
849
897
  }
850
898
  flushClaude();
@@ -880,7 +928,10 @@ function resolveRef(ref: SessionRef): string {
880
928
  * CopilotSession and lightweight test mocks.
881
929
  */
882
930
  export interface CopilotSendSessionSurface {
883
- on(eventType: string, handler: (event: { data?: unknown }) => void): () => void;
931
+ on(
932
+ eventType: string,
933
+ handler: (event: { data?: unknown }) => void,
934
+ ): () => void;
884
935
  }
885
936
 
886
937
  /**
@@ -1078,11 +1129,7 @@ export function watchCopilotSessionForElicitation(
1078
1129
  });
1079
1130
  const unsubCompleted = session.on("elicitation.completed", (event) => {
1080
1131
  const data = event.data as { requestId?: string } | undefined;
1081
- if (
1082
- data?.requestId &&
1083
- active.delete(data.requestId) &&
1084
- active.size === 0
1085
- ) {
1132
+ if (data?.requestId && active.delete(data.requestId) && active.size === 0) {
1086
1133
  onHIL(false);
1087
1134
  }
1088
1135
  });
@@ -1275,12 +1322,10 @@ async function initProviderClientAndSession<A extends AgentType>(
1275
1322
  switch (agent) {
1276
1323
  case "copilot": {
1277
1324
  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
- );
1325
+ const { copilotSubprocessEnv, mergeCopilotSystemMessage } =
1326
+ await import("../providers/copilot.ts");
1327
+ const { resolveAdditionalInstructionsContent } =
1328
+ await import("../../services/config/additional-instructions.ts");
1284
1329
  const copilotClientOpts = clientOpts as StageClientOptions<"copilot">;
1285
1330
  const copilotSessionOpts = sessionOpts as StageSessionOptions<"copilot">;
1286
1331
  // Headless: let the SDK spawn its own CLI process (no cliUrl).
@@ -1311,9 +1356,8 @@ async function initProviderClientAndSession<A extends AgentType>(
1311
1356
  // In headless stages, add `ask_user` to the session's excludedTools so
1312
1357
  // the agent cannot call the interactive question tool — there is no
1313
1358
  // human attached to answer and the SDK would otherwise sit blocked.
1314
- const additionalInstructions = await resolveAdditionalInstructionsContent(
1315
- projectRoot,
1316
- );
1359
+ const additionalInstructions =
1360
+ await resolveAdditionalInstructionsContent(projectRoot);
1317
1361
  const sessionConfig = {
1318
1362
  onPermissionRequest: approveAll,
1319
1363
  ...copilotSessionOpts,
@@ -1356,7 +1400,10 @@ async function initProviderClientAndSession<A extends AgentType>(
1356
1400
  // the session permission ruleset).
1357
1401
  return await withHeadlessOpencodeEnv(async () => {
1358
1402
  const oc = await createOpencode({ port: 0 });
1359
- const sessionResult = await oc.client.session.create(ocSessionOpts);
1403
+ const sessionResult = await oc.client.session.create({
1404
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1405
+ ...ocSessionOpts,
1406
+ });
1360
1407
  return {
1361
1408
  client: oc.client,
1362
1409
  session: sessionResult.data!,
@@ -1530,11 +1577,9 @@ function createSessionRunner(
1530
1577
  let panelSessionAdded = false;
1531
1578
 
1532
1579
  try {
1533
- // ── 6. Allocate port ──
1534
- const port = await getRandomPort();
1580
+ // ── 6. Build pane command (OS allocates port via --port 0) ──
1535
1581
  const { command: paneCmd, envVars: paneEnvVars } = buildPaneCommand(
1536
1582
  shared.agent,
1537
- port,
1538
1583
  shared.providerOverrides,
1539
1584
  shared.extraChatFlags,
1540
1585
  );
@@ -1564,7 +1609,7 @@ function createSessionRunner(
1564
1609
 
1565
1610
  spawnAttachedFooter(name, paneId);
1566
1611
 
1567
- serverUrl = await waitForServer(shared.agent, port, paneId);
1612
+ serverUrl = await waitForServer(shared.agent, paneId);
1568
1613
 
1569
1614
  shared.panel.addSession(name, graphParents);
1570
1615
  panelSessionAdded = true;
@@ -1592,8 +1637,8 @@ function createSessionRunner(
1592
1637
  if (!arg) {
1593
1638
  throw new Error(
1594
1639
  "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).",
1640
+ "only after a successful s.session.query() (headless wrappers " +
1641
+ "only know their session_id once a query completes).",
1597
1642
  );
1598
1643
  }
1599
1644
  const { getSessionMessages } =
@@ -1784,7 +1829,7 @@ function createSessionRunner(
1784
1829
  agent: shared.agent,
1785
1830
  paneId,
1786
1831
  serverUrl,
1787
- port,
1832
+ port: serverUrl ? Number(serverUrl.split(":").pop()) : 0,
1788
1833
  startedAt: new Date().toISOString(),
1789
1834
  },
1790
1835
  null,
@@ -1886,7 +1931,8 @@ export async function runOrchestrator(
1886
1931
  definition: WorkflowDefinition,
1887
1932
  inputs: Record<string, string> = {},
1888
1933
  ): Promise<void> {
1889
- const { workflowRunId, tmuxSessionName, agent, cwd } = validateOrchestratorEnv();
1934
+ const { workflowRunId, tmuxSessionName, agent, cwd } =
1935
+ validateOrchestratorEnv();
1890
1936
  // A bare prompt string is still useful for the panel header and the
1891
1937
  // session-dir metadata.json — both just want something displayable.
1892
1938
  // Free-form workflows store their single positional prompt under the