@zgeoff/atc 2.36.0 → 2.37.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/README.md +2 -1
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +6 -1
- package/src/agents/build-agent-adapters.ts +32 -0
- package/src/agents/claude-adapter.ts +145 -65
- package/src/agents/codex-adapter.ts +17 -11
- package/src/agents/gateway-adapter.ts +17 -1
- package/src/agents/grok-adapter.ts +15 -9
- package/src/build-migrated-config.ts +184 -0
- package/src/cli.ts +49 -17
- package/src/client/collect-agent-picks.ts +3 -10
- package/src/client/index.ts +1 -1
- package/src/client/resolve-agent-metadata.ts +4 -10
- package/src/client/spawn-picker.ts +1 -2
- package/src/daemon/build-auth-binding.ts +2 -1
- package/src/daemon/daemon-connection.ts +1 -1
- package/src/daemon/daemon.ts +12 -3
- package/src/daemon/sessions.ts +8 -2
- package/src/mcp/build-spawn-descriptions.ts +2 -2
- package/src/run-config-migrate.ts +85 -0
- package/src/shared/check-gateway-auth.ts +173 -0
- package/src/shared/collect-agents.ts +326 -0
- package/src/shared/collect-claude-auth.ts +8 -6
- package/src/shared/collect-gateways.ts +2 -168
- package/src/shared/collect-legacy-agents.ts +77 -0
- package/src/shared/config.ts +70 -60
- package/src/shared/format-mixed-agent-keys.ts +10 -0
- package/src/shared/is-subscription-override-variable.ts +14 -0
- package/src/shared/legacy-agent-keys.ts +15 -0
- package/src/shared/outranking-variables.ts +17 -0
- package/src/shared/pick-default-agent.ts +12 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { AgentEntry } from './shared/collect-agents';
|
|
2
|
+
import { parseConfig } from './shared/config';
|
|
3
|
+
import { formatJSONKind } from './shared/format-json-kind';
|
|
4
|
+
import { formatMixedAgentKeys } from './shared/format-mixed-agent-keys';
|
|
5
|
+
import { LEGACY_AGENT_KEYS } from './shared/legacy-agent-keys';
|
|
6
|
+
import { isRecord } from './shared/report';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What `atc config migrate` makes of a config.json's decoded value: the file
|
|
10
|
+
* rewritten around `agents` with a note per gateway it could not carry over,
|
|
11
|
+
* a file that needs no change, or a file it cannot migrate.
|
|
12
|
+
*/
|
|
13
|
+
type MigratedConfig =
|
|
14
|
+
| { readonly kind: 'migrated'; readonly text: string; readonly notes: readonly string[] }
|
|
15
|
+
| { readonly kind: 'current' }
|
|
16
|
+
| { readonly kind: 'unusable'; readonly detail: string };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Rewrites a config.json that uses the old agent keys so it holds the same
|
|
20
|
+
* agents under `agents`. The old keys are removed and `agents` takes the
|
|
21
|
+
* place of the first of them, or the end of the file when it sets none. Each
|
|
22
|
+
* entry holds only the fields that differ from a default. Values are copied
|
|
23
|
+
* and never reported, so a note names a key and a reason only.
|
|
24
|
+
*/
|
|
25
|
+
export function buildMigratedConfig(raw: unknown): MigratedConfig {
|
|
26
|
+
if (!isRecord(raw) || Array.isArray(raw)) {
|
|
27
|
+
return {
|
|
28
|
+
kind: 'unusable',
|
|
29
|
+
detail: `the root is ${formatJSONKind(raw)}, not an object`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const present = Object.keys(raw).filter((key) => LEGACY_AGENT_KEYS.includes(key));
|
|
34
|
+
|
|
35
|
+
if (Object.hasOwn(raw, 'agents')) {
|
|
36
|
+
return present.length === 0
|
|
37
|
+
? { kind: 'current' }
|
|
38
|
+
: { kind: 'unusable', detail: formatMixedAgentKeys(present) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = parseConfig(raw);
|
|
42
|
+
const agents: Record<string, unknown> = {};
|
|
43
|
+
|
|
44
|
+
for (const entry of config.agents) {
|
|
45
|
+
agents[entry.id] = renderAgentEntry(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const migrated: Record<string, unknown> = {};
|
|
49
|
+
let placed = false;
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
52
|
+
if (!LEGACY_AGENT_KEYS.includes(key)) {
|
|
53
|
+
migrated[key] = value;
|
|
54
|
+
} else if (!placed) {
|
|
55
|
+
migrated['agents'] = agents;
|
|
56
|
+
placed = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!placed) {
|
|
61
|
+
migrated['agents'] = agents;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
kind: 'migrated',
|
|
66
|
+
text: `${JSON.stringify(migrated, null, 2)}\n`,
|
|
67
|
+
notes: collectDroppedGateways(raw['gateways'], config.agents, config.agentErrors),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The fields of an entry that differ from the defaults the `agents` parser
|
|
72
|
+
// applies, so a migrated entry reads as short as one written by hand.
|
|
73
|
+
function renderAgentEntry(entry: AgentEntry): Record<string, unknown> {
|
|
74
|
+
const rendered: Record<string, unknown> = {};
|
|
75
|
+
|
|
76
|
+
if (entry.baseURL !== undefined) {
|
|
77
|
+
rendered['kind'] = 'claude';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.label !== entry.id && entry.baseURL !== undefined) {
|
|
81
|
+
rendered['label'] = entry.label;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
entry.baseURL !== undefined &&
|
|
86
|
+
entry.mark !== String.fromCodePoint(entry.id.codePointAt(0) ?? 0)
|
|
87
|
+
) {
|
|
88
|
+
rendered['mark'] = entry.mark;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (entry.bin !== entry.kind) {
|
|
92
|
+
rendered['bin'] = entry.bin;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (entry.args.length > 0) {
|
|
96
|
+
rendered['args'] = entry.args;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (entry.baseURL !== undefined) {
|
|
100
|
+
rendered['baseURL'] = entry.baseURL;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (entry.apiKeyHelper !== undefined) {
|
|
104
|
+
rendered['apiKeyHelper'] = entry.apiKeyHelper;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Object.keys(entry.env).length > 0) {
|
|
108
|
+
rendered['env'] = entry.env;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (entry.settings !== undefined) {
|
|
112
|
+
rendered['settings'] = entry.settings;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (entry.auth !== undefined) {
|
|
116
|
+
rendered['auth'] =
|
|
117
|
+
Object.keys(entry.auth.placeholderEnv).length > 0
|
|
118
|
+
? { profiles: entry.auth.profiles, placeholderEnv: entry.auth.placeholderEnv }
|
|
119
|
+
: { profiles: entry.auth.profiles };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return rendered;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// One note per gateway the old parse left out, with the reason.
|
|
126
|
+
function collectDroppedGateways(
|
|
127
|
+
raw: unknown,
|
|
128
|
+
agents: readonly AgentEntry[],
|
|
129
|
+
errors: readonly string[],
|
|
130
|
+
): string[] {
|
|
131
|
+
if (!isRecord(raw) || Array.isArray(raw)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The three built-in agents come first, so the gateways follow them.
|
|
136
|
+
const kept = new Set(agents.slice(BUILT_IN_COUNT).map((entry) => entry.id));
|
|
137
|
+
|
|
138
|
+
const notes: string[] = [];
|
|
139
|
+
|
|
140
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
141
|
+
if (!isDropped(id, kept)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
notes.push(
|
|
146
|
+
`atc config migrate: gateways.${id} is left out: ${findDropReason(id, entry, errors)}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return notes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const BUILT_IN_COUNT = 3;
|
|
154
|
+
|
|
155
|
+
function isDropped(id: string, kept: ReadonlySet<string>): boolean {
|
|
156
|
+
return id === '__proto__' || !kept.has(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findDropReason(id: string, entry: unknown, errors: readonly string[]): string {
|
|
160
|
+
if (id === '') {
|
|
161
|
+
return 'its id is empty';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (id === '__proto__') {
|
|
165
|
+
return 'its id cannot be used';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (id === 'claude' || id === 'grok' || id === 'codex') {
|
|
169
|
+
return `its id is the built-in agent ${id}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const baseURL = isRecord(entry) ? entry['baseURL'] : undefined;
|
|
173
|
+
|
|
174
|
+
if (typeof baseURL !== 'string' || baseURL === '') {
|
|
175
|
+
return 'it has no baseURL';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const prefix = `gateways.${id}: `;
|
|
179
|
+
const problems = errors.filter((error) => error.startsWith(prefix));
|
|
180
|
+
|
|
181
|
+
return problems.length > 0
|
|
182
|
+
? problems.map((problem) => problem.slice(prefix.length)).join('; ')
|
|
183
|
+
: 'its entry could not be read';
|
|
184
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -178,6 +178,44 @@ const main = defineCommand({
|
|
|
178
178
|
await grants.runGrants(ctx.args.revoke ?? null);
|
|
179
179
|
},
|
|
180
180
|
}),
|
|
181
|
+
config: () =>
|
|
182
|
+
defineCommand({
|
|
183
|
+
meta: { name: 'config', description: "Work on atc's config.json" },
|
|
184
|
+
subCommands: {
|
|
185
|
+
migrate: () =>
|
|
186
|
+
defineCommand({
|
|
187
|
+
meta: {
|
|
188
|
+
name: 'migrate',
|
|
189
|
+
description:
|
|
190
|
+
'Move the old agent keys of config.json into agents: print the result, or rewrite the file with --write',
|
|
191
|
+
},
|
|
192
|
+
args: {
|
|
193
|
+
write: {
|
|
194
|
+
type: 'boolean',
|
|
195
|
+
default: false,
|
|
196
|
+
description: 'Back the file up beside itself, then rewrite it in place',
|
|
197
|
+
},
|
|
198
|
+
file: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
description: 'The config file to migrate (default ~/.config/atc/config.json)',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
async run(ctx) {
|
|
204
|
+
const migrate = await import('./run-config-migrate');
|
|
205
|
+
const config = await import('./shared/config');
|
|
206
|
+
|
|
207
|
+
const code = migrate.runConfigMigrate(
|
|
208
|
+
ctx.args.file ?? config.configFile,
|
|
209
|
+
ctx.args.write,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (code !== 0) {
|
|
213
|
+
process.exit(code);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
181
219
|
daemon: () =>
|
|
182
220
|
defineCommand({
|
|
183
221
|
meta: {
|
|
@@ -358,10 +396,7 @@ async function runDaemon(listenArg: string | null, tokenFile: string | null): Pr
|
|
|
358
396
|
|
|
359
397
|
const daemon = await import('./daemon/daemon');
|
|
360
398
|
const config = await import('./shared/config');
|
|
361
|
-
const
|
|
362
|
-
const grok = await import('./agents/grok-adapter');
|
|
363
|
-
const codex = await import('./agents/codex-adapter');
|
|
364
|
-
const gateway = await import('./agents/gateway-adapter');
|
|
399
|
+
const adapterBuilder = await import('./agents/build-agent-adapters');
|
|
365
400
|
const headless = await import('./agents/start-claude-headless-run');
|
|
366
401
|
const targets = await import('./daemon/build-execution-targets');
|
|
367
402
|
const sourceOrder = await import('./sources/build-sources');
|
|
@@ -389,12 +424,17 @@ async function runDaemon(listenArg: string | null, tokenFile: string | null): Pr
|
|
|
389
424
|
...cfg.principalErrors,
|
|
390
425
|
...cfg.workspaceErrors,
|
|
391
426
|
...cfg.authProfileErrors,
|
|
392
|
-
...cfg.
|
|
393
|
-
...cfg.gatewayErrors,
|
|
427
|
+
...cfg.agentErrors,
|
|
394
428
|
]) {
|
|
395
429
|
console.error(`atc daemon: config: ${problem}`);
|
|
396
430
|
}
|
|
397
431
|
|
|
432
|
+
if (cfg.legacyAgentKeys.length > 0) {
|
|
433
|
+
console.error(
|
|
434
|
+
`atc daemon: config: config.json uses the old agent keys (${cfg.legacyAgentKeys.join(', ')}); run 'atc config migrate' to move them into agents`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
398
438
|
const sources = sourceOrder.buildSources(
|
|
399
439
|
builtinSources.collectBuiltinSources({
|
|
400
440
|
roots: cfg.dirs.roots,
|
|
@@ -423,15 +463,7 @@ async function runDaemon(listenArg: string | null, tokenFile: string | null): Pr
|
|
|
423
463
|
// How long a started session may go without a tap before a message
|
|
424
464
|
// to it is refused. Tests pin it to 0 to reach the refusal at once.
|
|
425
465
|
const graceOverride = Number(process.env['ATC_TAP_GRACE_MS']);
|
|
426
|
-
|
|
427
|
-
const claudeAdapter = new claude.ClaudeAdapter(cfg, headless.startClaudeHeadlessRun);
|
|
428
|
-
const grokAdapter = new grok.GrokAdapter(cfg);
|
|
429
|
-
const codexAdapter = new codex.CodexAdapter(cfg);
|
|
430
|
-
|
|
431
|
-
const gatewayAdapters = cfg.gateways.map(
|
|
432
|
-
(entry) => new gateway.GatewayAdapter(entry, cfg, headless.startClaudeHeadlessRun),
|
|
433
|
-
);
|
|
434
|
-
|
|
466
|
+
const adapters = adapterBuilder.buildAgentAdapters(cfg, headless.startClaudeHeadlessRun);
|
|
435
467
|
let handle: Awaited<ReturnType<typeof daemon.startDaemon>>;
|
|
436
468
|
|
|
437
469
|
try {
|
|
@@ -440,8 +472,8 @@ async function runDaemon(listenArg: string | null, tokenFile: string | null): Pr
|
|
|
440
472
|
reporterSocketPath: config.socketPath,
|
|
441
473
|
eventsSocketPath: config.eventsSocketPath,
|
|
442
474
|
build: getBuild(),
|
|
443
|
-
|
|
444
|
-
|
|
475
|
+
adapters,
|
|
476
|
+
defaultAgent: cfg.defaultAgent,
|
|
445
477
|
dbPath: config.dbFile,
|
|
446
478
|
legacyFleetPath: config.legacyFleetFile,
|
|
447
479
|
pidPath: config.daemonPidFile,
|
|
@@ -18,14 +18,7 @@ export function collectAgentPicks(config: Config): AgentPick[] {
|
|
|
18
18
|
// rather than left to the snapshot Bun.which defaults to.
|
|
19
19
|
const opts = { PATH: process.env['PATH'] ?? '' };
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ agent:
|
|
24
|
-
{ agent: 'codex', label: 'Codex', bin: config.codexBin },
|
|
25
|
-
...config.gateways.map((g) => ({ agent: g.id, label: g.label, bin: g.bin })),
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
return candidates
|
|
29
|
-
.filter((c) => Bun.which(c.bin, opts) !== null)
|
|
30
|
-
.map((c) => ({ agent: c.agent, label: c.label }));
|
|
21
|
+
return config.agents
|
|
22
|
+
.filter((entry) => Bun.which(entry.bin, opts) !== null)
|
|
23
|
+
.map((entry) => ({ agent: entry.id, label: entry.label }));
|
|
31
24
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ const doneAt = new Map<string, number>();
|
|
|
41
41
|
|
|
42
42
|
let focusedID: string | null = null;
|
|
43
43
|
let fleetCount = 0;
|
|
44
|
-
let lastUsedAgent: AgentID =
|
|
44
|
+
let lastUsedAgent: AgentID = loadConfig().defaultAgent;
|
|
45
45
|
|
|
46
46
|
// The readable harness label and tier-alias map per agent: the config alone
|
|
47
47
|
// until the daemon's `agents.list` answer arrives and re-resolves them
|
|
@@ -12,12 +12,6 @@ export interface AgentMetadata {
|
|
|
12
12
|
readonly models: Readonly<Record<AgentID, Readonly<Record<string, string>>>>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const BUILT_IN_LABELS: Readonly<Record<AgentID, string>> = {
|
|
16
|
-
claude: 'Claude',
|
|
17
|
-
grok: 'Grok',
|
|
18
|
-
codex: 'Codex',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
15
|
/**
|
|
22
16
|
* The labels and model aliases the overlay draws with, resolved from the
|
|
23
17
|
* config a client reads at start and the daemon's `agents.list` answer once
|
|
@@ -26,12 +20,12 @@ const BUILT_IN_LABELS: Readonly<Record<AgentID, string>> = {
|
|
|
26
20
|
* the window before the answer lands.
|
|
27
21
|
*/
|
|
28
22
|
export function resolveAgentMetadata(config: Config, listed: unknown): AgentMetadata {
|
|
29
|
-
const labels: Record<AgentID, string> = {
|
|
23
|
+
const labels: Record<AgentID, string> = {};
|
|
30
24
|
const models: Record<AgentID, Readonly<Record<string, string>>> = {};
|
|
31
25
|
|
|
32
|
-
for (const
|
|
33
|
-
if (isSafeKey(
|
|
34
|
-
labels[
|
|
26
|
+
for (const entry of config.agents) {
|
|
27
|
+
if (isSafeKey(entry.id)) {
|
|
28
|
+
labels[entry.id] = entry.label;
|
|
35
29
|
}
|
|
36
30
|
}
|
|
37
31
|
|
|
@@ -434,8 +434,7 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
434
434
|
let hint = '↑↓ move · ⏎ select · esc cancel';
|
|
435
435
|
|
|
436
436
|
if (this.picks.length === 0) {
|
|
437
|
-
hint =
|
|
438
|
-
'no agent CLI found — set claudeBin, grokBin, or codexBin in config.json · esc cancel';
|
|
437
|
+
hint = 'no agent CLI found — set agents in config.json · esc cancel';
|
|
439
438
|
} else if (this.sourcesPending) {
|
|
440
439
|
hint = 'reading targets and sources… · esc cancel';
|
|
441
440
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import type { AgentID } from '../shared/agent-id';
|
|
3
|
+
import type { GatewayAuth } from '../shared/check-gateway-auth';
|
|
3
4
|
import type { AuthProfile } from '../shared/collect-auth-profiles';
|
|
4
|
-
import type {
|
|
5
|
+
import type { GatewayConfig } from '../shared/collect-gateways';
|
|
5
6
|
import { resolveAuthProfiles } from '../shared/resolve-auth-profiles';
|
|
6
7
|
import type { AuthProfileProblem, ResolvedAuthSecret } from '../shared/resolve-auth-profiles';
|
|
7
8
|
import { sortJSONKeys } from '../shared/sort-json-keys';
|
|
@@ -941,7 +941,7 @@ export class DaemonConnection {
|
|
|
941
941
|
// The target goes first: a config or target problem is the cause a
|
|
942
942
|
// spawn reports, ahead of any agent check that problem can skew.
|
|
943
943
|
const target = ctx.resolveSpawnTarget(data.target);
|
|
944
|
-
const agent: AgentID = data.agent ??
|
|
944
|
+
const agent: AgentID = data.agent ?? ctx.collectAgents().spawnDefaults.agent;
|
|
945
945
|
const adapter = ctx.findAdapter(agent);
|
|
946
946
|
const entry = ctx.collectAgents().agents.find((candidate) => candidate.id === agent);
|
|
947
947
|
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { MAX_CHUNK, PROTOCOL_V } from '../protocol/protocol';
|
|
|
11
11
|
import type { EventMsg } from '../protocol/protocol';
|
|
12
12
|
import type { SpawnWorkspaceSource } from '../protocol/request-param-schemas';
|
|
13
13
|
import type { SessionState } from '../protocol/session-state';
|
|
14
|
+
import type { AgentID } from '../shared/agent-id';
|
|
14
15
|
import type { HooksConfig } from '../shared/collect-hooks';
|
|
15
16
|
import type { TargetConfigError } from '../shared/collect-targets';
|
|
16
17
|
import type { InvalidGitTransports } from '../shared/collect-workspaces-config';
|
|
@@ -101,13 +102,17 @@ export interface DaemonOptions {
|
|
|
101
102
|
|
|
102
103
|
// Build string sent in the handshake and in mismatch errors, e.g. "atc/0.1.0".
|
|
103
104
|
readonly build: string;
|
|
104
|
-
readonly adapter
|
|
105
|
+
readonly adapter?: AgentAdapter | null;
|
|
105
106
|
|
|
106
107
|
// Adapters registered over and above the default one, each keyed by the id
|
|
107
108
|
// it declares. Lookup never falls back across ids: a grok session with no
|
|
108
109
|
// grok adapter is unsupported, not a Claude spawn.
|
|
109
110
|
readonly adapters?: readonly AgentAdapter[];
|
|
110
111
|
|
|
112
|
+
// The agent a spawn without one runs, reported in `agents.list`'s spawn
|
|
113
|
+
// defaults; `claude` when unset.
|
|
114
|
+
readonly defaultAgent?: AgentID;
|
|
115
|
+
|
|
111
116
|
// Where sessions run, in config order; one `local` target on the local
|
|
112
117
|
// pseudo-terminal provider when unset. A spawn without a target runs on
|
|
113
118
|
// the default one: `local` when unset and the targets hold it, and none
|
|
@@ -888,7 +893,11 @@ export async function startDaemon(opts: DaemonOptions): Promise<DaemonHandle> {
|
|
|
888
893
|
if (
|
|
889
894
|
before !== undefined &&
|
|
890
895
|
runtime !== undefined &&
|
|
891
|
-
!isOwnHookEvent(
|
|
896
|
+
!isOwnHookEvent(
|
|
897
|
+
e.agent,
|
|
898
|
+
mgr.findAdapter(before.agent)?.hookAgent ?? before.agent,
|
|
899
|
+
runtime.hasAgentHookLines,
|
|
900
|
+
)
|
|
892
901
|
) {
|
|
893
902
|
return;
|
|
894
903
|
}
|
|
@@ -1412,7 +1421,7 @@ export async function startDaemon(opts: DaemonOptions): Promise<DaemonHandle> {
|
|
|
1412
1421
|
targets.some((target) => target.provider?.brokerAuth !== undefined),
|
|
1413
1422
|
),
|
|
1414
1423
|
targets: buildTargetList(targets, defaultTarget),
|
|
1415
|
-
spawnDefaults: { agent: 'claude', target: defaultTarget },
|
|
1424
|
+
spawnDefaults: { agent: opts.defaultAgent ?? 'claude', target: defaultTarget },
|
|
1416
1425
|
configRevision,
|
|
1417
1426
|
targetErrors,
|
|
1418
1427
|
sources: sources.map((source) => ({
|
package/src/daemon/sessions.ts
CHANGED
|
@@ -383,7 +383,7 @@ export class SessionManager {
|
|
|
383
383
|
private nextBridgeEpoch = 1;
|
|
384
384
|
|
|
385
385
|
constructor(
|
|
386
|
-
fallback: AgentAdapter,
|
|
386
|
+
fallback: AgentAdapter | null | undefined,
|
|
387
387
|
store: FleetStore,
|
|
388
388
|
statusPath: string | undefined = statusFile,
|
|
389
389
|
adapters: readonly AgentAdapter[] = [],
|
|
@@ -400,7 +400,13 @@ export class SessionManager {
|
|
|
400
400
|
) {
|
|
401
401
|
// Each adapter names the id it answers to, so a registry key can never
|
|
402
402
|
// disagree with the adapter behind it. A later one wins the id.
|
|
403
|
-
this.adapters = Object.fromEntries(
|
|
403
|
+
this.adapters = Object.fromEntries(
|
|
404
|
+
[...(fallback === null || fallback === undefined ? [] : [fallback]), ...adapters].map((a) => [
|
|
405
|
+
a.id,
|
|
406
|
+
a,
|
|
407
|
+
]),
|
|
408
|
+
);
|
|
409
|
+
|
|
404
410
|
this.hasScreenDetector = Object.values(this.adapters).some((a) => a.screenDetector !== null);
|
|
405
411
|
this.store = store;
|
|
406
412
|
this.statusPath = statusPath ?? statusFile;
|
|
@@ -29,8 +29,8 @@ export function buildSpawnDescriptions(
|
|
|
29
29
|
: ` When this tool list was built, the host registered: ${formatRoster(agents)}.`;
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
|
-
tool: `Spawn a new session in a directory. Optional agent is a registered agent id; omitted agent is
|
|
33
|
-
agent: `Registered agent id to spawn; defaults to claude.${roster} atc_agents_list returns the current list.`,
|
|
32
|
+
tool: `Spawn a new session in a directory. Optional agent is a registered agent id; omitted agent is the host's default agent (claude when it is registered, else the first registered agent), never the TUI last-used value.${roster} atc_agents_list returns the current agents, whether each is installed, and the model and effort each takes. An unregistered agent, a registered agent that is not installed, and a model or effort the agent does not take are refused before anything spawns. Called from inside an atc session, the new session is a sub-session of the caller unless detached is true. Returns the new session descriptor. Give it a prompt to start it working immediately.`,
|
|
33
|
+
agent: `Registered agent id to spawn; defaults to claude when it is registered, else the first registered agent.${roster} atc_agents_list returns the current list.`,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { buildMigratedConfig } from './build-migrated-config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs `atc config migrate`: prints the config.json rewritten around
|
|
6
|
+
* `agents`, or with `write` copies the file to a timestamped backup beside it
|
|
7
|
+
* and rewrites it in place. Returns the exit code. Notes and problems go to
|
|
8
|
+
* stderr and name keys and paths, never values.
|
|
9
|
+
*/
|
|
10
|
+
export function runConfigMigrate(file: string, write: boolean): number {
|
|
11
|
+
let text: string;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
text = readFileSync(file, 'utf8');
|
|
15
|
+
} catch {
|
|
16
|
+
console.error(`atc config migrate: ${file} cannot be read`);
|
|
17
|
+
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let raw: unknown;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.parse(text);
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`atc config migrate: ${file} is not valid JSON`);
|
|
27
|
+
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = buildMigratedConfig(raw);
|
|
32
|
+
|
|
33
|
+
if (result.kind === 'unusable') {
|
|
34
|
+
console.error(`atc config migrate: ${file}: ${result.detail}`);
|
|
35
|
+
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.kind === 'current') {
|
|
40
|
+
console.log('config.json already uses agents; nothing to migrate');
|
|
41
|
+
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const note of result.notes) {
|
|
46
|
+
console.error(note);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!write) {
|
|
50
|
+
process.stdout.write(result.text);
|
|
51
|
+
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const backup = `${file}.bak-${formatBackupStamp(Date.now())}`;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
copyFileSync(file, backup);
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(`atc config migrate: ${backup} cannot be written; ${file} is unchanged`);
|
|
61
|
+
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
writeFileSync(file, result.text);
|
|
67
|
+
} catch {
|
|
68
|
+
console.error(`atc config migrate: ${file} cannot be written; the backup is at ${backup}`);
|
|
69
|
+
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`backup: ${backup}`);
|
|
74
|
+
console.log(`wrote: ${file}`);
|
|
75
|
+
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A UTC time as `20261006T152200Z`.
|
|
80
|
+
function formatBackupStamp(time: number): string {
|
|
81
|
+
return new Date(time)
|
|
82
|
+
.toISOString()
|
|
83
|
+
.replaceAll(/[-:]/gu, '')
|
|
84
|
+
.replace(/\.\d+Z$/u, 'Z');
|
|
85
|
+
}
|