@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.
- package/.agents/skills/ado-commit/SKILL.md +2 -0
- package/.agents/skills/ado-create-pr/SKILL.md +2 -0
- package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
- package/.agents/skills/ast-grep/SKILL.md +2 -0
- package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
- package/.agents/skills/bun/SKILL.md +156 -122
- package/.agents/skills/context-compression/SKILL.md +2 -0
- package/.agents/skills/context-degradation/SKILL.md +2 -0
- package/.agents/skills/context-fundamentals/SKILL.md +2 -0
- package/.agents/skills/context-optimization/SKILL.md +2 -0
- package/.agents/skills/create-spec/SKILL.md +2 -0
- package/.agents/skills/docx/SKILL.md +2 -0
- package/.agents/skills/evaluation/SKILL.md +2 -0
- package/.agents/skills/explain-code/SKILL.md +2 -0
- package/.agents/skills/filesystem-context/SKILL.md +2 -0
- package/.agents/skills/find-skills/SKILL.md +2 -0
- package/.agents/skills/gh-commit/SKILL.md +2 -0
- package/.agents/skills/gh-create-pr/SKILL.md +2 -0
- package/.agents/skills/hosted-agents/SKILL.md +2 -0
- package/.agents/skills/impeccable/SKILL.md +117 -304
- package/.agents/skills/impeccable/agents/openai.yaml +4 -0
- package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
- package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
- package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
- package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
- package/.agents/skills/impeccable/reference/brand.md +114 -0
- package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
- package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
- package/.agents/skills/impeccable/reference/craft.md +152 -29
- package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
- package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
- package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
- package/.agents/skills/impeccable/reference/document.md +427 -0
- package/.agents/skills/impeccable/reference/extract.md +1 -1
- package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
- package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
- package/.agents/skills/impeccable/reference/live.md +594 -0
- package/.agents/skills/impeccable/reference/motion-design.md +12 -2
- package/.agents/skills/impeccable/reference/onboard.md +234 -0
- package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
- package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
- package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
- package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
- package/.agents/skills/impeccable/reference/product.md +62 -0
- package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
- package/.agents/skills/impeccable/reference/shape.md +151 -0
- package/.agents/skills/impeccable/reference/teach.md +156 -0
- package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
- package/.agents/skills/impeccable/reference/typography.md +31 -14
- package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
- package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
- package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
- package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
- package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
- package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
- package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
- package/.agents/skills/impeccable/scripts/live.mjs +247 -0
- package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
- package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
- package/.agents/skills/init/SKILL.md +2 -0
- package/.agents/skills/liteparse/SKILL.md +1 -0
- package/.agents/skills/memory-systems/SKILL.md +2 -0
- package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
- package/.agents/skills/opentui/SKILL.md +1 -0
- package/.agents/skills/pdf/SKILL.md +2 -0
- package/.agents/skills/playwright-cli/SKILL.md +51 -5
- package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
- package/.agents/skills/playwright-cli/references/running-code.md +10 -0
- package/.agents/skills/playwright-cli/references/session-management.md +56 -0
- package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
- package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
- package/.agents/skills/pptx/SKILL.md +2 -0
- package/.agents/skills/project-development/SKILL.md +2 -0
- package/.agents/skills/prompt-engineer/SKILL.md +2 -0
- package/.agents/skills/research-codebase/SKILL.md +2 -0
- package/.agents/skills/ripgrep/SKILL.md +2 -0
- package/.agents/skills/skill-creator/LICENSE.txt +1 -1
- package/.agents/skills/skill-creator/SKILL.md +2 -0
- package/.agents/skills/sl-commit/SKILL.md +2 -0
- package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
- package/.agents/skills/tdd/SKILL.md +4 -0
- package/.agents/skills/tool-design/SKILL.md +2 -0
- package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
- package/.agents/skills/typescript-expert/SKILL.md +7 -1
- package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
- package/.agents/skills/workflow-creator/SKILL.md +75 -72
- package/.agents/skills/workflow-creator/references/session-config.md +48 -1
- package/.agents/skills/xlsx/SKILL.md +2 -0
- package/.opencode/opencode.json +6 -2
- package/README.md +39 -38
- package/dist/lib/atomic-temp.d.ts +8 -0
- package/dist/lib/atomic-temp.d.ts.map +1 -0
- package/dist/lib/terminal-env.d.ts +9 -0
- package/dist/lib/terminal-env.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +24 -14
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +8 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/port-discovery.d.ts +71 -0
- package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +10 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +1 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/commands/cli/chat/index.test.ts +194 -2
- package/src/commands/cli/chat/index.ts +83 -28
- package/src/lib/atomic-temp.test.ts +86 -0
- package/src/lib/atomic-temp.ts +62 -0
- package/src/lib/terminal-env.test.ts +343 -0
- package/src/lib/terminal-env.ts +100 -0
- package/src/scripts/clean-dist.test.ts +53 -0
- package/src/scripts/clean-dist.ts +37 -0
- package/src/sdk/providers/claude.ts +42 -20
- package/src/sdk/providers/copilot.test.ts +365 -0
- package/src/sdk/providers/copilot.ts +117 -15
- package/src/sdk/runtime/cc-debounce.ts +2 -2
- package/src/sdk/runtime/executor.test.ts +322 -1
- package/src/sdk/runtime/executor.ts +159 -96
- package/src/sdk/runtime/port-discovery.test.ts +573 -0
- package/src/sdk/runtime/port-discovery.ts +496 -0
- package/src/sdk/runtime/tmux.ts +22 -2
- package/src/sdk/types.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
- package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
- package/src/services/system/auth.test.ts +53 -0
- package/src/services/system/auth.ts +31 -28
- package/src/services/system/detect.ts +1 -1
- package/.agents/skills/shape/SKILL.md +0 -96
- /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
- /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)
|
|
70
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
quotePathIfNeeded(copilotBin),
|
|
304
310
|
"--ui-server",
|
|
305
311
|
"--port",
|
|
306
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
|
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
|
|
340
|
-
while (Date.now() <
|
|
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
|
-
//
|
|
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() <
|
|
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
|
-
//
|
|
364
|
-
await Bun.sleep(
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
" " +
|
|
545
|
-
|
|
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"
|
|
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<
|
|
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(
|
|
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 {
|
|
1279
|
-
"../providers/copilot.ts"
|
|
1280
|
-
|
|
1281
|
-
|
|
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 `
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
1596
|
-
|
|
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 } =
|
|
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
|