@bastani/atomic 0.5.14 → 0.5.15-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +24 -0
- package/.opencode/opencode.json +10 -0
- package/README.md +10 -58
- package/assets/settings.schema.json +29 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/services/config/atomic-config.d.ts +44 -0
- package/dist/services/config/atomic-config.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +18 -13
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/dist/services/config/index.d.ts +7 -0
- package/dist/services/config/index.d.ts.map +1 -0
- package/dist/services/config/settings-schema.d.ts +2 -0
- package/dist/services/config/settings-schema.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +8 -1
- package/dist/services/system/copy.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/cli.ts +1 -30
- package/src/commands/cli/chat/index.ts +21 -6
- package/src/commands/cli/init/index.ts +78 -323
- package/src/commands/cli/init/onboarding.ts +4 -10
- package/src/commands/cli/init/scm.ts +3 -34
- package/src/lib/common-ignore.ts +46 -0
- package/src/lib/merge.ts +28 -1
- package/src/sdk/runtime/executor.ts +85 -52
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +9 -4
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +12 -7
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +12 -7
- package/src/services/config/atomic-config.ts +95 -1
- package/src/services/config/atomic-global-config.ts +8 -21
- package/src/services/config/definitions.ts +41 -44
- package/src/services/config/settings.ts +2 -1
- package/src/services/system/agents.ts +2 -1
- package/src/services/system/copy.ts +18 -7
- package/src/services/system/skills.ts +3 -1
|
@@ -33,7 +33,11 @@ import type {
|
|
|
33
33
|
ProviderClient,
|
|
34
34
|
ProviderSession,
|
|
35
35
|
} from "../types.ts";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
isValidAgent,
|
|
38
|
+
type ProviderOverrides,
|
|
39
|
+
} from "../../services/config/definitions.ts";
|
|
40
|
+
import { getProviderOverrides } from "../../services/config/atomic-config.ts";
|
|
37
41
|
import { ensureDir } from "../../services/system/copy.ts";
|
|
38
42
|
import type { SessionEvent } from "@github/copilot-sdk";
|
|
39
43
|
import type { SessionPromptResponse } from "@opencode-ai/sdk/v2";
|
|
@@ -62,13 +66,7 @@ const AGENT_CLI: Record<
|
|
|
62
66
|
> = {
|
|
63
67
|
copilot: {
|
|
64
68
|
cmd: "copilot",
|
|
65
|
-
chatFlags: [
|
|
66
|
-
"--add-dir",
|
|
67
|
-
".",
|
|
68
|
-
"--yolo",
|
|
69
|
-
"--experimental",
|
|
70
|
-
"--no-auto-update",
|
|
71
|
-
],
|
|
69
|
+
chatFlags: ["--add-dir", ".", "--yolo", "--experimental"],
|
|
72
70
|
envVars: {
|
|
73
71
|
COPILOT_ALLOW_ALL: "true",
|
|
74
72
|
},
|
|
@@ -85,7 +83,6 @@ const AGENT_CLI: Record<
|
|
|
85
83
|
// which the idle detection in claude.ts watches for to know when the
|
|
86
84
|
// agent has finished processing a prompt.
|
|
87
85
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
88
|
-
CLAUDE_CODE_NO_FLICKER: "1",
|
|
89
86
|
},
|
|
90
87
|
},
|
|
91
88
|
};
|
|
@@ -110,7 +107,11 @@ export { errorMessage } from "../errors.ts";
|
|
|
110
107
|
function isValidSavedMessage(msg: unknown): msg is SavedMessage {
|
|
111
108
|
if (!msg || typeof msg !== "object") return false;
|
|
112
109
|
const m = msg as Record<string, unknown>;
|
|
113
|
-
return
|
|
110
|
+
return (
|
|
111
|
+
m.provider === "copilot" ||
|
|
112
|
+
m.provider === "opencode" ||
|
|
113
|
+
m.provider === "claude"
|
|
114
|
+
);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export interface WorkflowRunOptions {
|
|
@@ -184,15 +185,28 @@ async function getRandomPort(): Promise<number> {
|
|
|
184
185
|
function buildPaneCommand(
|
|
185
186
|
agent: AgentType,
|
|
186
187
|
port: number,
|
|
188
|
+
overrides: ProviderOverrides = {},
|
|
187
189
|
): { command: string; envVars: Record<string, string> } {
|
|
188
|
-
const {
|
|
190
|
+
const {
|
|
191
|
+
cmd,
|
|
192
|
+
chatFlags: defaultFlags,
|
|
193
|
+
envVars: defaultEnvVars,
|
|
194
|
+
} = AGENT_CLI[agent];
|
|
195
|
+
const chatFlags = overrides.chatFlags ?? defaultFlags;
|
|
196
|
+
const envVars = overrides.envVars
|
|
197
|
+
? { ...defaultEnvVars, ...overrides.envVars }
|
|
198
|
+
: defaultEnvVars;
|
|
189
199
|
|
|
190
200
|
switch (agent) {
|
|
191
201
|
case "copilot":
|
|
192
202
|
return {
|
|
193
|
-
command: [
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
command: [
|
|
204
|
+
cmd,
|
|
205
|
+
"--ui-server",
|
|
206
|
+
"--port",
|
|
207
|
+
String(port),
|
|
208
|
+
...chatFlags,
|
|
209
|
+
].join(" "),
|
|
196
210
|
envVars,
|
|
197
211
|
};
|
|
198
212
|
case "opencode":
|
|
@@ -286,7 +300,9 @@ export function escPwsh(s: string): string {
|
|
|
286
300
|
* structured inputs are optional, so a corrupt payload must never
|
|
287
301
|
* prevent free-form workflows from running.
|
|
288
302
|
*/
|
|
289
|
-
export function parseInputsEnv(
|
|
303
|
+
export function parseInputsEnv(
|
|
304
|
+
raw: string | undefined,
|
|
305
|
+
): Record<string, string> {
|
|
290
306
|
if (!raw) return {};
|
|
291
307
|
try {
|
|
292
308
|
const decoded = Buffer.from(raw, "base64").toString("utf-8");
|
|
@@ -487,8 +503,7 @@ function createTranscriptReader(
|
|
|
487
503
|
const refName = resolveRef(ref);
|
|
488
504
|
const prev = completedRegistry.get(refName);
|
|
489
505
|
if (!prev) {
|
|
490
|
-
const available =
|
|
491
|
-
[...completedRegistry.keys()].join(", ") || "(none)";
|
|
506
|
+
const available = [...completedRegistry.keys()].join(", ") || "(none)";
|
|
492
507
|
throw new Error(
|
|
493
508
|
`No transcript for "${refName}". Available: ${available}`,
|
|
494
509
|
);
|
|
@@ -510,11 +525,8 @@ function createMessagesReader(
|
|
|
510
525
|
const refName = resolveRef(ref);
|
|
511
526
|
const prev = completedRegistry.get(refName);
|
|
512
527
|
if (!prev) {
|
|
513
|
-
const available =
|
|
514
|
-
|
|
515
|
-
throw new Error(
|
|
516
|
-
`No messages for "${refName}". Available: ${available}`,
|
|
517
|
-
);
|
|
528
|
+
const available = [...completedRegistry.keys()].join(", ") || "(none)";
|
|
529
|
+
throw new Error(`No messages for "${refName}". Available: ${available}`);
|
|
518
530
|
}
|
|
519
531
|
const filePath = join(prev.sessionDir, "messages.json");
|
|
520
532
|
const raw = await Bun.file(filePath).text();
|
|
@@ -542,6 +554,8 @@ interface SharedRunnerState {
|
|
|
542
554
|
* specifically via `ctx.inputs.prompt` for the free-form case.
|
|
543
555
|
*/
|
|
544
556
|
inputs: Record<string, string>;
|
|
557
|
+
/** User-configured provider overrides (global + local merged). */
|
|
558
|
+
providerOverrides: ProviderOverrides;
|
|
545
559
|
panel: OrchestratorPanel;
|
|
546
560
|
/** Sessions that have been spawned (for name uniqueness + cleanup). */
|
|
547
561
|
activeRegistry: Map<string, ActiveSession>;
|
|
@@ -615,7 +629,10 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
615
629
|
}
|
|
616
630
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
617
631
|
const ocClientOpts = clientOpts as StageClientOptions<"opencode">;
|
|
618
|
-
const client = createOpencodeClient({
|
|
632
|
+
const client = createOpencodeClient({
|
|
633
|
+
...ocClientOpts,
|
|
634
|
+
baseUrl: serverUrl,
|
|
635
|
+
});
|
|
619
636
|
const sessionResult = await client.session.create(ocSessionOpts);
|
|
620
637
|
await client.tui.selectSession({ sessionID: sessionResult.data!.id });
|
|
621
638
|
return { client, session: sessionResult.data! } as Result;
|
|
@@ -632,7 +649,11 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
632
649
|
const claudeSessionOpts = sessionOpts as StageSessionOptions<"claude">;
|
|
633
650
|
const client = new ClaudeClientWrapper(paneId, claudeClientOpts);
|
|
634
651
|
await client.start();
|
|
635
|
-
const session = new ClaudeSessionWrapper(
|
|
652
|
+
const session = new ClaudeSessionWrapper(
|
|
653
|
+
paneId,
|
|
654
|
+
sessionId,
|
|
655
|
+
claudeSessionOpts,
|
|
656
|
+
);
|
|
636
657
|
return { client, session } as Result;
|
|
637
658
|
}
|
|
638
659
|
default:
|
|
@@ -661,12 +682,16 @@ async function cleanupProvider<A extends AgentType>(
|
|
|
661
682
|
try {
|
|
662
683
|
await session.disconnect();
|
|
663
684
|
} catch (e) {
|
|
664
|
-
console.warn(
|
|
685
|
+
console.warn(
|
|
686
|
+
`[cleanup] copilot session disconnect failed: ${errorMessage(e)}`,
|
|
687
|
+
);
|
|
665
688
|
}
|
|
666
689
|
try {
|
|
667
690
|
await client.stop();
|
|
668
691
|
} catch (e) {
|
|
669
|
-
console.warn(
|
|
692
|
+
console.warn(
|
|
693
|
+
`[cleanup] copilot client stop failed: ${errorMessage(e)}`,
|
|
694
|
+
);
|
|
670
695
|
}
|
|
671
696
|
break;
|
|
672
697
|
}
|
|
@@ -756,12 +781,12 @@ function createSessionRunner(
|
|
|
756
781
|
let panelSessionAdded = false;
|
|
757
782
|
|
|
758
783
|
try {
|
|
759
|
-
|
|
760
784
|
// ── 6. Allocate port ──
|
|
761
785
|
const port = await getRandomPort();
|
|
762
786
|
const { command: paneCmd, envVars: paneEnvVars } = buildPaneCommand(
|
|
763
787
|
shared.agent,
|
|
764
788
|
port,
|
|
789
|
+
shared.providerOverrides,
|
|
765
790
|
);
|
|
766
791
|
|
|
767
792
|
// ── 7. Create tmux window or headless execution ──
|
|
@@ -818,16 +843,13 @@ function createSessionRunner(
|
|
|
818
843
|
arg: SessionEvent[] | SessionPromptResponse | string,
|
|
819
844
|
): Promise<SavedMessage[]> {
|
|
820
845
|
if (typeof arg === "string") {
|
|
821
|
-
const { getSessionMessages, listSessions } =
|
|
822
|
-
"@anthropic-ai/claude-agent-sdk"
|
|
823
|
-
);
|
|
846
|
+
const { getSessionMessages, listSessions } =
|
|
847
|
+
await import("@anthropic-ai/claude-agent-sdk");
|
|
824
848
|
const dir = process.cwd();
|
|
825
849
|
const sessions = await listSessions({ dir });
|
|
826
850
|
|
|
827
851
|
const newSessions = knownClaudeSessionIds
|
|
828
|
-
? sessions.filter(
|
|
829
|
-
(s) => !knownClaudeSessionIds!.has(s.sessionId),
|
|
830
|
-
)
|
|
852
|
+
? sessions.filter((s) => !knownClaudeSessionIds!.has(s.sessionId))
|
|
831
853
|
: sessions.filter(
|
|
832
854
|
(s) => s.lastModified >= claudeSessionStartedAfter,
|
|
833
855
|
);
|
|
@@ -890,16 +912,19 @@ function createSessionRunner(
|
|
|
890
912
|
const getMessagesFn = createMessagesReader(shared.completedRegistry);
|
|
891
913
|
|
|
892
914
|
// ── 12. Auto-create provider client and session ──
|
|
893
|
-
const {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
915
|
+
const {
|
|
916
|
+
client: providerClient,
|
|
917
|
+
session: providerSession,
|
|
918
|
+
cleanup: providerCleanup,
|
|
919
|
+
} = await initProviderClientAndSession(
|
|
920
|
+
shared.agent,
|
|
921
|
+
serverUrl,
|
|
922
|
+
paneId,
|
|
923
|
+
sessionId,
|
|
924
|
+
clientOpts,
|
|
925
|
+
sessionOpts,
|
|
926
|
+
isHeadless,
|
|
927
|
+
);
|
|
903
928
|
|
|
904
929
|
// ── 12a. Copilot: wrap send() to await session.idle ──
|
|
905
930
|
// Copilot's send() is fire-and-forget — it returns immediately after
|
|
@@ -924,7 +949,10 @@ function createSessionRunner(
|
|
|
924
949
|
const idle = new Promise<void>((resolve, reject) => {
|
|
925
950
|
let unsubIdle: (() => void) | undefined;
|
|
926
951
|
let unsubError: (() => void) | undefined;
|
|
927
|
-
const cleanup = () => {
|
|
952
|
+
const cleanup = () => {
|
|
953
|
+
unsubIdle?.();
|
|
954
|
+
unsubError?.();
|
|
955
|
+
};
|
|
928
956
|
unsubIdle = copilotSession.on("session.idle", () => {
|
|
929
957
|
cleanup();
|
|
930
958
|
resolve();
|
|
@@ -984,16 +1012,18 @@ function createSessionRunner(
|
|
|
984
1012
|
callbackResult = await run(ctx);
|
|
985
1013
|
if (pendingSaves.length > 0) await Promise.all(pendingSaves);
|
|
986
1014
|
} catch (error) {
|
|
987
|
-
const message =
|
|
988
|
-
|
|
989
|
-
await Bun.write(join(sessionDir, "error.txt"), message).catch(
|
|
990
|
-
() => {},
|
|
991
|
-
);
|
|
1015
|
+
const message = errorMessage(error);
|
|
1016
|
+
await Bun.write(join(sessionDir, "error.txt"), message).catch(() => {});
|
|
992
1017
|
if (!isHeadless) shared.panel.sessionError(name, message);
|
|
993
1018
|
throw error;
|
|
994
1019
|
} finally {
|
|
995
1020
|
// ── 14a. Auto-cleanup provider resources ──
|
|
996
|
-
await cleanupProvider(
|
|
1021
|
+
await cleanupProvider(
|
|
1022
|
+
shared.agent,
|
|
1023
|
+
providerClient,
|
|
1024
|
+
providerSession,
|
|
1025
|
+
paneId,
|
|
1026
|
+
);
|
|
997
1027
|
if (providerCleanup) {
|
|
998
1028
|
try {
|
|
999
1029
|
providerCleanup();
|
|
@@ -1067,7 +1097,9 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1067
1097
|
const tmuxSessionName = process.env.ATOMIC_WF_TMUX!;
|
|
1068
1098
|
const rawAgent = process.env.ATOMIC_WF_AGENT!;
|
|
1069
1099
|
if (!isValidAgent(rawAgent)) {
|
|
1070
|
-
throw new Error(
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`Invalid ATOMIC_WF_AGENT: "${rawAgent}". Expected one of: copilot, opencode, claude`,
|
|
1102
|
+
);
|
|
1071
1103
|
}
|
|
1072
1104
|
const agent: AgentType = rawAgent;
|
|
1073
1105
|
// ATOMIC_WF_INPUTS carries the full input payload. Free-form
|
|
@@ -1084,6 +1116,7 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1084
1116
|
|
|
1085
1117
|
process.chdir(cwd);
|
|
1086
1118
|
|
|
1119
|
+
const providerOverrides = await getProviderOverrides(agent, cwd);
|
|
1087
1120
|
const sessionsBaseDir = join(getSessionsBaseDir(), workflowRunId);
|
|
1088
1121
|
await ensureDir(sessionsBaseDir);
|
|
1089
1122
|
|
|
@@ -1114,6 +1147,7 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1114
1147
|
sessionsBaseDir,
|
|
1115
1148
|
agent,
|
|
1116
1149
|
inputs,
|
|
1150
|
+
providerOverrides,
|
|
1117
1151
|
panel,
|
|
1118
1152
|
activeRegistry: new Map(),
|
|
1119
1153
|
completedRegistry: new Map(),
|
|
@@ -1204,4 +1238,3 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1204
1238
|
process.off("SIGINT", signalHandler);
|
|
1205
1239
|
}
|
|
1206
1240
|
}
|
|
1207
|
-
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
* │
|
|
18
18
|
* ▼
|
|
19
19
|
* ┌──────────────────────────────────────────────────┐
|
|
20
|
-
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all)
|
|
20
|
+
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all, headless)
|
|
21
21
|
* └──────────────────────────────────────────────────┘
|
|
22
22
|
* │
|
|
23
23
|
* ▼
|
|
24
24
|
* aggregator
|
|
25
25
|
*
|
|
26
|
+
* Explorers run headless (in-process, no tmux window) — they are transparent
|
|
27
|
+
* to the graph, so the visible topology is: [scout, history] → aggregator.
|
|
28
|
+
*
|
|
26
29
|
* Stage 1a — codebase-scout
|
|
27
30
|
* Pure-TypeScript: lists files (git ls-files), counts LOC (batched wc -l),
|
|
28
31
|
* renders a depth-bounded ASCII tree, and bin-packs directories into N
|
|
@@ -211,9 +214,10 @@ export default defineWorkflow({
|
|
|
211
214
|
const scoutOverview = (await ctx.transcript(scout)).content;
|
|
212
215
|
const historyOverview = (await ctx.transcript(history)).content;
|
|
213
216
|
|
|
214
|
-
// ── Stage 2: parallel explorers
|
|
215
|
-
// Each explorer
|
|
216
|
-
//
|
|
217
|
+
// ── Stage 2: parallel headless explorers ─────────────────────────────────
|
|
218
|
+
// Each explorer runs headless (in-process, no tmux pane) via Promise.all.
|
|
219
|
+
// They are invisible in the workflow graph but tracked by the background
|
|
220
|
+
// task counter in the statusline. Each one receives:
|
|
217
221
|
// - the original research question (top + bottom of prompt)
|
|
218
222
|
// - the scout's architectural overview
|
|
219
223
|
// - its OWN partition (never the full file list)
|
|
@@ -232,6 +236,7 @@ export default defineWorkflow({
|
|
|
232
236
|
return ctx.stage(
|
|
233
237
|
{
|
|
234
238
|
name: `explorer-${i}`,
|
|
239
|
+
headless: true,
|
|
235
240
|
description: `Explore ${partition
|
|
236
241
|
.map((u) => u.path)
|
|
237
242
|
.join(
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
* │
|
|
19
19
|
* ▼
|
|
20
20
|
* ┌──────────────────────────────────────────────────┐
|
|
21
|
-
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all)
|
|
21
|
+
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all, headless)
|
|
22
22
|
* └──────────────────────────────────────────────────┘
|
|
23
23
|
* │
|
|
24
24
|
* ▼
|
|
25
25
|
* aggregator
|
|
26
26
|
*
|
|
27
|
+
* Explorers run headless (in-process, no tmux window) — they are transparent
|
|
28
|
+
* to the graph, so the visible topology is: [scout, history] → aggregator.
|
|
29
|
+
*
|
|
27
30
|
* Copilot-specific concerns baked in:
|
|
28
31
|
*
|
|
29
32
|
*
|
|
@@ -177,12 +180,13 @@ export default defineWorkflow({
|
|
|
177
180
|
const scoutOverview = (await ctx.transcript(scout)).content;
|
|
178
181
|
const historyOverview = (await ctx.transcript(history)).content;
|
|
179
182
|
|
|
180
|
-
// ── Stage 2: parallel explorers
|
|
181
|
-
// Each explorer
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
183
|
+
// ── Stage 2: parallel headless explorers ─────────────────────────────────
|
|
184
|
+
// Each explorer runs headless (in-process, no tmux pane) via Promise.all.
|
|
185
|
+
// They are invisible in the workflow graph but tracked by the background
|
|
186
|
+
// task counter in the statusline. Because each session is fresh (F5),
|
|
187
|
+
// every piece of context it needs — question, architectural orientation,
|
|
188
|
+
// historical context, partition assignment, scratch path — is injected
|
|
189
|
+
// into the first prompt via buildExplorerPromptGeneric.
|
|
186
190
|
const explorerHandles = await Promise.all(
|
|
187
191
|
partitions.map((partition, idx) => {
|
|
188
192
|
const i = idx + 1;
|
|
@@ -190,6 +194,7 @@ export default defineWorkflow({
|
|
|
190
194
|
return ctx.stage(
|
|
191
195
|
{
|
|
192
196
|
name: `explorer-${i}`,
|
|
197
|
+
headless: true,
|
|
193
198
|
description: `Explore ${partition
|
|
194
199
|
.map((u) => u.path)
|
|
195
200
|
.join(", ")} (${partition.reduce((s, u) => s + u.fileCount, 0)} files)`,
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
* │
|
|
19
19
|
* ▼
|
|
20
20
|
* ┌──────────────────────────────────────────────────┐
|
|
21
|
-
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all)
|
|
21
|
+
* │ explorer-1 explorer-2 ... explorer-N │ (Promise.all, headless)
|
|
22
22
|
* └──────────────────────────────────────────────────┘
|
|
23
23
|
* │
|
|
24
24
|
* ▼
|
|
25
25
|
* aggregator
|
|
26
26
|
*
|
|
27
|
+
* Explorers run headless (in-process, no tmux window) — they are transparent
|
|
28
|
+
* to the graph, so the visible topology is: [scout, history] → aggregator.
|
|
29
|
+
*
|
|
27
30
|
* OpenCode-specific concerns baked in:
|
|
28
31
|
*
|
|
29
32
|
* • F5 — every `ctx.stage()` call is a FRESH session with no memory of
|
|
@@ -196,12 +199,13 @@ export default defineWorkflow({
|
|
|
196
199
|
const scoutOverview = (await ctx.transcript(scout)).content;
|
|
197
200
|
const historyOverview = (await ctx.transcript(history)).content;
|
|
198
201
|
|
|
199
|
-
// ── Stage 2: parallel explorers
|
|
200
|
-
// Each explorer
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
202
|
+
// ── Stage 2: parallel headless explorers ─────────────────────────────────
|
|
203
|
+
// Each explorer runs headless (in-process, no tmux pane) via Promise.all.
|
|
204
|
+
// They are invisible in the workflow graph but tracked by the background
|
|
205
|
+
// task counter in the statusline. Because each session is fresh (F5),
|
|
206
|
+
// every piece of context it needs — question, architectural orientation,
|
|
207
|
+
// historical context, partition assignment, scratch path — is injected
|
|
208
|
+
// into the first prompt via buildExplorerPromptGeneric.
|
|
205
209
|
const explorerHandles = await Promise.all(
|
|
206
210
|
partitions.map((partition, idx) => {
|
|
207
211
|
const i = idx + 1;
|
|
@@ -209,6 +213,7 @@ export default defineWorkflow({
|
|
|
209
213
|
return ctx.stage(
|
|
210
214
|
{
|
|
211
215
|
name: `explorer-${i}`,
|
|
216
|
+
headless: true,
|
|
212
217
|
description: `Explore ${partition
|
|
213
218
|
.map((u) => u.path)
|
|
214
219
|
.join(", ")} (${partition.reduce((s, u) => s + u.fileCount, 0)} files)`,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { type SourceControlType } from "./index.ts";
|
|
12
|
+
import { type SourceControlType, type AgentKey, type ProviderOverrides } from "./index.ts";
|
|
13
13
|
import { SETTINGS_SCHEMA_URL } from "./settings-schema.ts";
|
|
14
14
|
import { ensureDir } from "../system/copy.ts";
|
|
15
15
|
|
|
@@ -26,6 +26,8 @@ export interface AtomicConfig {
|
|
|
26
26
|
scm?: SourceControlType;
|
|
27
27
|
/** Timestamp of last init */
|
|
28
28
|
lastUpdated?: string;
|
|
29
|
+
/** Per-provider overrides for chatFlags and envVars */
|
|
30
|
+
providers?: Partial<Record<AgentKey, ProviderOverrides>>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
type JsonRecord = Record<string, unknown>;
|
|
@@ -47,6 +49,41 @@ async function readJsonFile(path: string): Promise<JsonRecord | null> {
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function pickProviderOverrides(raw: unknown): ProviderOverrides | null {
|
|
53
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
54
|
+
const obj = raw as Record<string, unknown>;
|
|
55
|
+
const result: ProviderOverrides = {};
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(obj.chatFlags) && obj.chatFlags.every((f): f is string => typeof f === "string")) {
|
|
58
|
+
result.chatFlags = obj.chatFlags;
|
|
59
|
+
}
|
|
60
|
+
if (obj.envVars && typeof obj.envVars === "object" && !Array.isArray(obj.envVars)) {
|
|
61
|
+
const envVars: Record<string, string> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(obj.envVars)) {
|
|
63
|
+
if (typeof v === "string") envVars[k] = v;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(envVars).length > 0) result.envVars = envVars;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const VALID_AGENT_KEYS = new Set<string>(["claude", "opencode", "copilot"]);
|
|
72
|
+
|
|
73
|
+
function pickProviders(raw: unknown): Partial<Record<AgentKey, ProviderOverrides>> | null {
|
|
74
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
75
|
+
const obj = raw as Record<string, unknown>;
|
|
76
|
+
const result: Partial<Record<AgentKey, ProviderOverrides>> = {};
|
|
77
|
+
|
|
78
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
79
|
+
if (!VALID_AGENT_KEYS.has(key)) continue;
|
|
80
|
+
const overrides = pickProviderOverrides(value);
|
|
81
|
+
if (overrides) result[key as AgentKey] = overrides;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
|
|
51
88
|
if (!record) return null;
|
|
52
89
|
|
|
@@ -59,9 +96,42 @@ function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
|
|
|
59
96
|
if (typeof scm === "string") config.scm = scm as SourceControlType;
|
|
60
97
|
if (typeof lastUpdated === "string") config.lastUpdated = lastUpdated;
|
|
61
98
|
|
|
99
|
+
const providers = pickProviders(record.providers);
|
|
100
|
+
if (providers) config.providers = providers;
|
|
101
|
+
|
|
62
102
|
return Object.keys(config).length > 0 ? config : null;
|
|
63
103
|
}
|
|
64
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Merge two ProviderOverrides, with `over` taking precedence.
|
|
107
|
+
* - chatFlags: later config replaces earlier
|
|
108
|
+
* - envVars: merged, later values win on conflict
|
|
109
|
+
*/
|
|
110
|
+
function mergeProviderOverrides(
|
|
111
|
+
base: ProviderOverrides | undefined,
|
|
112
|
+
over: ProviderOverrides | undefined,
|
|
113
|
+
): ProviderOverrides | undefined {
|
|
114
|
+
if (!base && !over) return undefined;
|
|
115
|
+
if (!base) return over;
|
|
116
|
+
if (!over) return base;
|
|
117
|
+
|
|
118
|
+
const result: ProviderOverrides = {};
|
|
119
|
+
|
|
120
|
+
// chatFlags: later replaces earlier entirely
|
|
121
|
+
if (over.chatFlags !== undefined) {
|
|
122
|
+
result.chatFlags = over.chatFlags;
|
|
123
|
+
} else if (base.chatFlags !== undefined) {
|
|
124
|
+
result.chatFlags = base.chatFlags;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// envVars: merged, later wins on conflict
|
|
128
|
+
if (base.envVars || over.envVars) {
|
|
129
|
+
result.envVars = { ...base.envVars, ...over.envVars };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | null {
|
|
66
136
|
const merged: AtomicConfig = {};
|
|
67
137
|
for (const config of configs) {
|
|
@@ -69,6 +139,14 @@ function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | nu
|
|
|
69
139
|
if (config.version !== undefined) merged.version = config.version;
|
|
70
140
|
if (config.scm !== undefined) merged.scm = config.scm;
|
|
71
141
|
if (config.lastUpdated !== undefined) merged.lastUpdated = config.lastUpdated;
|
|
142
|
+
|
|
143
|
+
if (config.providers) {
|
|
144
|
+
if (!merged.providers) merged.providers = {};
|
|
145
|
+
for (const [key, overrides] of Object.entries(config.providers)) {
|
|
146
|
+
const agentKey = key as AgentKey;
|
|
147
|
+
merged.providers[agentKey] = mergeProviderOverrides(merged.providers[agentKey], overrides);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
72
150
|
}
|
|
73
151
|
return Object.keys(merged).length > 0 ? merged : null;
|
|
74
152
|
}
|
|
@@ -121,3 +199,19 @@ export async function getSelectedScm(projectDir: string): Promise<SourceControlT
|
|
|
121
199
|
const config = await readAtomicConfig(projectDir);
|
|
122
200
|
return config?.scm ?? null;
|
|
123
201
|
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve provider overrides from global + local settings (local wins).
|
|
205
|
+
*
|
|
206
|
+
* Returns `{ chatFlags, envVars }` that are meant to be layered on top
|
|
207
|
+
* of the provider's hardcoded defaults:
|
|
208
|
+
* - `chatFlags`: when set, replaces the provider's default chat_flags entirely
|
|
209
|
+
* - `envVars`: merged on top of the provider's default env_vars (user values win)
|
|
210
|
+
*/
|
|
211
|
+
export async function getProviderOverrides(
|
|
212
|
+
agentKey: AgentKey,
|
|
213
|
+
projectDir: string,
|
|
214
|
+
): Promise<ProviderOverrides> {
|
|
215
|
+
const config = await readAtomicConfig(projectDir);
|
|
216
|
+
return config?.providers?.[agentKey] ?? {};
|
|
217
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat, readdir, rm, rmdir } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
5
|
import { AGENT_CONFIG, getAgentKeys, type AgentKey } from "./index.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { syncJsonFile } from "../../lib/merge.ts";
|
|
7
|
+
import { createCommonIgnoreFilter } from "../../lib/common-ignore.ts";
|
|
8
|
+
import { copyDir, ensureDir, pathExists, shouldExclude } from "../system/copy.ts";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const ATOMIC_HOME_DIR = join(homedir(), ".atomic");
|
|
@@ -117,15 +118,7 @@ async function collectManagedTreeEntries(
|
|
|
117
118
|
? join(relativeDir, entry.name)
|
|
118
119
|
: entry.name;
|
|
119
120
|
|
|
120
|
-
if (
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const normalizedRelativePath = relativePath.replace(/\\/g, "/");
|
|
125
|
-
if (exclude.some((excluded) =>
|
|
126
|
-
normalizedRelativePath === excluded.replace(/\\/g, "/") ||
|
|
127
|
-
normalizedRelativePath.startsWith(`${excluded.replace(/\\/g, "/")}/`)
|
|
128
|
-
)) {
|
|
121
|
+
if (shouldExclude(relativePath, entry.name, [...exclude])) {
|
|
129
122
|
continue;
|
|
130
123
|
}
|
|
131
124
|
|
|
@@ -174,14 +167,7 @@ async function syncManagedGlobalFile(
|
|
|
174
167
|
sourcePath: string,
|
|
175
168
|
destinationPath: string,
|
|
176
169
|
): Promise<void> {
|
|
177
|
-
await
|
|
178
|
-
|
|
179
|
-
if (await pathExists(destinationPath)) {
|
|
180
|
-
await mergeJsonFile(sourcePath, destinationPath);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await copyFile(sourcePath, destinationPath);
|
|
170
|
+
await syncJsonFile(sourcePath, destinationPath);
|
|
185
171
|
}
|
|
186
172
|
|
|
187
173
|
/**
|
|
@@ -264,10 +250,11 @@ export async function syncAtomicGlobalAgentConfigs(
|
|
|
264
250
|
const destinationFolder = getAtomicManagedAgentDir(agentKey, baseDir);
|
|
265
251
|
await ensureDir(destinationFolder);
|
|
266
252
|
|
|
253
|
+
const ignoreFilter = createCommonIgnoreFilter();
|
|
267
254
|
for (const subdirectory of GLOBAL_SYNC_SUBDIRECTORIES) {
|
|
268
255
|
const sourceSubdir = join(sourceFolder, subdirectory);
|
|
269
256
|
if (await pathExists(sourceSubdir)) {
|
|
270
|
-
await copyDir(sourceSubdir, join(destinationFolder, subdirectory));
|
|
257
|
+
await copyDir(sourceSubdir, join(destinationFolder, subdirectory), { ignoreFilter });
|
|
271
258
|
}
|
|
272
259
|
}
|
|
273
260
|
|