@gaia-ai/conductor 0.4.6 → 0.5.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/dist/src/cli/gaia.d.ts +4 -2
- package/dist/src/cli/gaia.js +53 -29
- package/dist/src/cli/init.js +9 -3
- package/dist/src/cli/version-check.d.ts +72 -0
- package/dist/src/cli/version-check.js +182 -0
- package/dist/src/config.d.ts +10 -1
- package/dist/src/config.js +27 -2
- package/dist/src/core/conductor.d.ts +14 -13
- package/dist/src/core/conductor.js +56 -23
- package/package.json +6 -5
package/dist/src/cli/gaia.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { type ConductorFileConfig, type ConductorLogger, type
|
|
1
|
+
import { type ConductorFileConfig, type ConductorLogger, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace, type ResolvedAgent } from '@gaia-ai/core';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
/** Test seam: inject any subset of dependencies. */
|
|
4
4
|
export interface GaiaCliDeps {
|
|
5
5
|
remote?: GaiaRemote;
|
|
6
6
|
executor?: GaiaExecutor;
|
|
7
7
|
workspace?: GaiaWorkspace;
|
|
8
|
-
|
|
8
|
+
agents?: ResolvedAgent[];
|
|
9
9
|
config?: ConductorFileConfig;
|
|
10
|
+
/** Injectable registry fetch for the start-time version check (tests). */
|
|
11
|
+
fetch?: typeof fetch;
|
|
10
12
|
}
|
|
11
13
|
export declare function parseHerdrJson(output: string, command: string): unknown;
|
|
12
14
|
/**
|
package/dist/src/cli/gaia.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { dirname
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import { CommandRunner, createLogger, exec, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
6
|
-
import {
|
|
5
|
+
import { selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
7
6
|
import { Command } from 'commander';
|
|
8
7
|
import { authStatus, buildProgram as buildDropshProgram } from 'dropsh';
|
|
9
8
|
import { loadConductorConfig, resolveConfigPath } from '../config.js';
|
|
10
9
|
import { Conductor } from '../core/conductor.js';
|
|
11
10
|
import { machineContextPath, readMachineContext, scaffold, } from './init.js';
|
|
12
11
|
import * as registry from './local-registry.js';
|
|
12
|
+
import { fetchUpdateNotice, printVersionLine, resolveCliVersion, runUpdate, } from './version-check.js';
|
|
13
13
|
/**
|
|
14
14
|
* Default config path. `--config` / `$GAIA_CONDUCTOR_CONFIG` win; otherwise walk
|
|
15
15
|
* up from cwd to the nearest `.gaia/conductor.config.js` (git/eslint style), so a
|
|
@@ -185,8 +185,8 @@ async function cmdPoll(deps, log = {}) {
|
|
|
185
185
|
const remote = await resolveRemote(deps, config);
|
|
186
186
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
187
187
|
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
188
|
-
const
|
|
189
|
-
const conductor = new Conductor(config, remote, executor, workspace,
|
|
188
|
+
const agents = deps.agents ?? (await selectAgents(config));
|
|
189
|
+
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
190
190
|
await conductor.start();
|
|
191
191
|
await conductor.tick();
|
|
192
192
|
}
|
|
@@ -199,8 +199,8 @@ async function cmdReap(deps, log = {}) {
|
|
|
199
199
|
const remote = await resolveRemote(deps, config);
|
|
200
200
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
201
201
|
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
202
|
-
const
|
|
203
|
-
const conductor = new Conductor(config, remote, executor, workspace,
|
|
202
|
+
const agents = deps.agents ?? (await selectAgents(config));
|
|
203
|
+
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
204
204
|
// The reaper reconciles finished-but-uncleaned tickets (the cleaned_up flag)
|
|
205
205
|
// against their worktrees; it needs no registration/heartbeat (it is not a
|
|
206
206
|
// poll), just the executor + remote, so it runs standalone after a
|
|
@@ -216,8 +216,8 @@ async function cmdStartForeground(deps, log = {}) {
|
|
|
216
216
|
const remote = await resolveRemote(deps, config);
|
|
217
217
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
218
218
|
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
219
|
-
const
|
|
220
|
-
const conductor = new Conductor(config, remote, executor, workspace,
|
|
219
|
+
const agents = deps.agents ?? (await selectAgents(config));
|
|
220
|
+
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
221
221
|
await conductor.start();
|
|
222
222
|
const controller = new AbortController();
|
|
223
223
|
const onSignal = () => controller.abort();
|
|
@@ -232,6 +232,24 @@ async function cmdStartForeground(deps, log = {}) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
async function cmdStart(deps, log = {}) {
|
|
235
|
+
// Print the installed version (AC-2) immediately, then kick the newer-version
|
|
236
|
+
// registry check off CONCURRENTLY with config resolution so a slow/offline
|
|
237
|
+
// registry never stalls start (finding #1 — the check no longer runs serially
|
|
238
|
+
// before resolveConfig). Fail-silent + bounded (AC-3/AC-4/AC-5); the notice is
|
|
239
|
+
// printed once it resolves, after the start work below. Not in
|
|
240
|
+
// cmdStartForeground — that path is herdr-hosted, no TTY.
|
|
241
|
+
const current = printVersionLine();
|
|
242
|
+
const noticePromise = fetchUpdateNotice(current, deps.fetch ?? globalThis.fetch).catch(() => null);
|
|
243
|
+
try {
|
|
244
|
+
await cmdStartBody(deps, log);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
const notice = await noticePromise;
|
|
248
|
+
if (notice !== null)
|
|
249
|
+
console.log(notice);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function cmdStartBody(deps, log) {
|
|
235
253
|
const config = await resolveConfig(deps);
|
|
236
254
|
const checkoutRoot = checkoutRootOf(config);
|
|
237
255
|
const logger = loggerFor(checkoutRoot, log);
|
|
@@ -373,25 +391,6 @@ async function promptSecret() {
|
|
|
373
391
|
}
|
|
374
392
|
}
|
|
375
393
|
// --- program ----------------------------------------------------------------
|
|
376
|
-
// Report the CLI's own package version. Walks up from this module to the
|
|
377
|
-
// nearest @gaia-ai/conductor package.json so it resolves both from the
|
|
378
|
-
// compiled dist/src/cli/gaia.js (3 dirs up) and the src/cli/gaia.ts vitest
|
|
379
|
-
// runs (2 dirs up). Same release tag across packages ⇒ == @gaia-ai/gaia.
|
|
380
|
-
function resolveCliVersion() {
|
|
381
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
382
|
-
for (let i = 0; i < 6; i++) {
|
|
383
|
-
try {
|
|
384
|
-
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
385
|
-
if (pkg.name === '@gaia-ai/conductor')
|
|
386
|
-
return pkg.version ?? '0.0.0';
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
// no package.json here — keep walking up
|
|
390
|
-
}
|
|
391
|
-
dir = dirname(dir);
|
|
392
|
-
}
|
|
393
|
-
return '0.0.0';
|
|
394
|
-
}
|
|
395
394
|
export function buildProgram(deps) {
|
|
396
395
|
const program = new Command();
|
|
397
396
|
program
|
|
@@ -417,6 +416,31 @@ export function buildProgram(deps) {
|
|
|
417
416
|
.action(() => {
|
|
418
417
|
console.log(cliVersion);
|
|
419
418
|
});
|
|
419
|
+
program
|
|
420
|
+
.command('update')
|
|
421
|
+
.description('upgrade the globally installed gaia CLI to the latest npm release')
|
|
422
|
+
.action(async () => {
|
|
423
|
+
const { ok, before, after } = await runUpdate();
|
|
424
|
+
if (!ok) {
|
|
425
|
+
console.error('gaia update failed — see npm output above');
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Report the real on-disk change (finding #2): never a fabricated
|
|
430
|
+
// "latest", never a false "upgraded" when npm changed nothing.
|
|
431
|
+
if (after === null) {
|
|
432
|
+
console.log('gaia update completed (installed version undetermined)');
|
|
433
|
+
}
|
|
434
|
+
else if (before === null) {
|
|
435
|
+
console.log(`gaia installed: ${after}`);
|
|
436
|
+
}
|
|
437
|
+
else if (before === after) {
|
|
438
|
+
console.log(`gaia is already up to date (${after})`);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
console.log(`gaia updated: ${before} → ${after}`);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
420
444
|
const conductor = program
|
|
421
445
|
.command('conductor')
|
|
422
446
|
.description('node-agent lifecycle + local registry')
|
package/dist/src/cli/init.js
CHANGED
|
@@ -28,8 +28,10 @@ export function renderCommittedConfig(inputs) {
|
|
|
28
28
|
// resolves each name ESLint-style (config dir → cwd → conductor install), so
|
|
29
29
|
// config load never depends on a \`node_modules/@gaia-ai\` symlink beside this
|
|
30
30
|
// file. Each plugin package default-exports its factory, so the resolver's
|
|
31
|
-
// auto-pick needs no \`export:\` here —
|
|
32
|
-
// barrel (many exports
|
|
31
|
+
// auto-pick needs no \`export:\` here — EXCEPT the \`@gaia-ai/gaia/plugins\` host
|
|
32
|
+
// barrel (many exports → \`export: 'drupalRemote'\`) and the merged herdr
|
|
33
|
+
// workspace slot (\`@gaia-ai/plugin-herdr\` default-exports the executor, so the
|
|
34
|
+
// workspace names \`export: 'herdrWorkspace'\` — GAIA-139).
|
|
33
35
|
|
|
34
36
|
// The user-global machine context: identity + connection (incl. secret), shared
|
|
35
37
|
// by every project on this machine. Never committed.
|
|
@@ -79,8 +81,12 @@ export default {
|
|
|
79
81
|
plugin: '@gaia-ai/plugin-claude',
|
|
80
82
|
with: { model: local.model ?? 'claude-opus-4-8' },
|
|
81
83
|
},
|
|
84
|
+
// GAIA-139: the herdr workspace ships in @gaia-ai/plugin-herdr now (the
|
|
85
|
+
// separate plugin-herdr-workspace was merged in). That package default-exports
|
|
86
|
+
// the EXECUTOR, so the workspace slot must name export: 'herdrWorkspace'.
|
|
82
87
|
workspace: {
|
|
83
|
-
plugin: '@gaia-ai/plugin-herdr
|
|
88
|
+
plugin: '@gaia-ai/plugin-herdr',
|
|
89
|
+
export: 'herdrWorkspace',
|
|
84
90
|
},
|
|
85
91
|
// oauth2 is a real dep of the host (npm installs it alongside @gaia-ai/gaia).
|
|
86
92
|
// NOTE: plugins[] is consumed by DROPSH, which reloads this config with its OWN
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** The global install target on npm (the meta package). */
|
|
2
|
+
export declare const GAIA_PACKAGE = "@gaia-ai/gaia";
|
|
3
|
+
/** The npm registry endpoint for the latest published version. */
|
|
4
|
+
export declare const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@gaia-ai/gaia/latest";
|
|
5
|
+
/**
|
|
6
|
+
* The sentinel returned when the installed version cannot be resolved (the
|
|
7
|
+
* package.json walk failed). Never a real published release, so callers treat
|
|
8
|
+
* it as "unknown" and suppress the update notice rather than nagging.
|
|
9
|
+
*/
|
|
10
|
+
export declare const UNKNOWN_VERSION = "0.0.0";
|
|
11
|
+
/**
|
|
12
|
+
* The currently installed CLI version. Walks up from this module to the nearest
|
|
13
|
+
* @gaia-ai/conductor package.json so it resolves both from the compiled
|
|
14
|
+
* dist/src/cli/version-check.js and from a vitest src run. Same release tag
|
|
15
|
+
* across packages ⇒ == @gaia-ai/gaia. Memoized — the version cannot change
|
|
16
|
+
* within a process, so the walk runs at most once (finding #6).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveCliVersion(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the latest published version of @gaia-ai/gaia from the npm registry.
|
|
21
|
+
* Bounded by an AbortController timeout; NEVER throws — any failure (network,
|
|
22
|
+
* non-2xx, timeout, malformed JSON, missing field) resolves to null so the
|
|
23
|
+
* caller can treat the check as best-effort (AC-4). fetchImpl is injectable so
|
|
24
|
+
* tests run without network.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fetchLatestVersion(fetchImpl?: typeof fetch, timeoutMs?: number): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* A human-readable "update available" notice when `latest` is strictly newer
|
|
29
|
+
* than `current`, else null (already current or ahead — AC-5). Pure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function updateNotice(current: string, latest: string): string | null;
|
|
32
|
+
/** Spawns a command, inheriting stdio, resolving to its exit code. */
|
|
33
|
+
export type SpawnUpdate = (cmd: string, args: string[]) => Promise<number>;
|
|
34
|
+
/** Spawns a command, capturing stdout, resolving to its exit code + stdout. */
|
|
35
|
+
export type SpawnCapture = (cmd: string, args: string[]) => Promise<{
|
|
36
|
+
code: number;
|
|
37
|
+
stdout: string;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Upgrade the globally installed gaia CLI to the latest published version via
|
|
41
|
+
* `npm install -g @gaia-ai/gaia@latest`. Reports the actual global version
|
|
42
|
+
* before and after the install (queried from npm, not from a separate registry
|
|
43
|
+
* fetch — finding #2), so the caller reports the real change: `before` is null
|
|
44
|
+
* when gaia was not installed globally, and no "upgraded" claim is made unless
|
|
45
|
+
* npm actually changed the on-disk version. On install failure `after` mirrors
|
|
46
|
+
* `before` and the post-install query is skipped. spawnImpl/captureImpl are
|
|
47
|
+
* injectable for tests.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runUpdate(spawnImpl?: SpawnUpdate, captureImpl?: SpawnCapture): Promise<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
before: string | null;
|
|
52
|
+
after: string | null;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Print the installed gaia version line (AC-2), synchronously, and return it.
|
|
56
|
+
* Never blocks on the registry — the caller kicks off the newer-version check
|
|
57
|
+
* separately (see fetchUpdateNotice) so start is not stalled waiting on npm.
|
|
58
|
+
*/
|
|
59
|
+
export declare function printVersionLine(out?: (s: string) => void): string;
|
|
60
|
+
/**
|
|
61
|
+
* Best-effort "update available" notice for the given installed version
|
|
62
|
+
* (AC-3/AC-5). Fetches the latest published version (fail-silent → null on any
|
|
63
|
+
* error, AC-4) and compares. Returns the notice string or null. fetchImpl is
|
|
64
|
+
* injectable for tests.
|
|
65
|
+
*/
|
|
66
|
+
export declare function fetchUpdateNotice(current: string, fetchImpl?: typeof fetch): Promise<string | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Print the installed gaia version (AC-2) and, best-effort, a newer-version
|
|
69
|
+
* notice (AC-3/AC-5). The registry check is fail-silent (AC-4): an offline
|
|
70
|
+
* registry prints only the version line. out/fetchImpl are injectable for tests.
|
|
71
|
+
*/
|
|
72
|
+
export declare function printStartVersionBanner(out?: (s: string) => void, fetchImpl?: typeof fetch): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
/** The global install target on npm (the meta package). */
|
|
7
|
+
export const GAIA_PACKAGE = '@gaia-ai/gaia';
|
|
8
|
+
/** The npm registry endpoint for the latest published version. */
|
|
9
|
+
export const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@gaia-ai/gaia/latest';
|
|
10
|
+
/**
|
|
11
|
+
* The sentinel returned when the installed version cannot be resolved (the
|
|
12
|
+
* package.json walk failed). Never a real published release, so callers treat
|
|
13
|
+
* it as "unknown" and suppress the update notice rather than nagging.
|
|
14
|
+
*/
|
|
15
|
+
export const UNKNOWN_VERSION = '0.0.0';
|
|
16
|
+
let cachedCliVersion;
|
|
17
|
+
/**
|
|
18
|
+
* The currently installed CLI version. Walks up from this module to the nearest
|
|
19
|
+
* @gaia-ai/conductor package.json so it resolves both from the compiled
|
|
20
|
+
* dist/src/cli/version-check.js and from a vitest src run. Same release tag
|
|
21
|
+
* across packages ⇒ == @gaia-ai/gaia. Memoized — the version cannot change
|
|
22
|
+
* within a process, so the walk runs at most once (finding #6).
|
|
23
|
+
*/
|
|
24
|
+
export function resolveCliVersion() {
|
|
25
|
+
if (cachedCliVersion !== undefined)
|
|
26
|
+
return cachedCliVersion;
|
|
27
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
for (let i = 0; i < 6; i++) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
31
|
+
if (pkg.name === '@gaia-ai/conductor') {
|
|
32
|
+
cachedCliVersion = pkg.version ?? UNKNOWN_VERSION;
|
|
33
|
+
return cachedCliVersion;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// no package.json here — keep walking up
|
|
38
|
+
}
|
|
39
|
+
dir = dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
cachedCliVersion = UNKNOWN_VERSION;
|
|
42
|
+
return cachedCliVersion;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Fetch the latest published version of @gaia-ai/gaia from the npm registry.
|
|
46
|
+
* Bounded by an AbortController timeout; NEVER throws — any failure (network,
|
|
47
|
+
* non-2xx, timeout, malformed JSON, missing field) resolves to null so the
|
|
48
|
+
* caller can treat the check as best-effort (AC-4). fetchImpl is injectable so
|
|
49
|
+
* tests run without network.
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchLatestVersion(fetchImpl = globalThis.fetch, timeoutMs = 1500) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetchImpl(REGISTRY_LATEST_URL, {
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
headers: { accept: 'application/json' },
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
return null;
|
|
61
|
+
const body = (await res.json());
|
|
62
|
+
return typeof body.version === 'string' ? body.version : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A human-readable "update available" notice when `latest` is strictly newer
|
|
73
|
+
* than `current`, else null (already current or ahead — AC-5). Pure.
|
|
74
|
+
*/
|
|
75
|
+
export function updateNotice(current, latest) {
|
|
76
|
+
// Unknown installed version ⇒ suppress the notice (finding #5): otherwise a
|
|
77
|
+
// failed package.json walk yields a perpetual bogus "0.0.0 → x.y.z" nag.
|
|
78
|
+
if (current === UNKNOWN_VERSION)
|
|
79
|
+
return null;
|
|
80
|
+
if (!semver.valid(current) || !semver.valid(latest))
|
|
81
|
+
return null;
|
|
82
|
+
if (!semver.gt(latest, current))
|
|
83
|
+
return null;
|
|
84
|
+
return (`A new gaia version is available: ${current} → ${latest}\n` +
|
|
85
|
+
'Run `gaia update` to upgrade.');
|
|
86
|
+
}
|
|
87
|
+
const defaultSpawn = (cmd, args) => new Promise((resolve) => {
|
|
88
|
+
const child = spawn(cmd, args, { stdio: 'inherit', shell: false });
|
|
89
|
+
child.on('error', () => resolve(1));
|
|
90
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
91
|
+
});
|
|
92
|
+
const defaultCapture = (cmd, args) => new Promise((resolve) => {
|
|
93
|
+
let stdout = '';
|
|
94
|
+
const child = spawn(cmd, args, {
|
|
95
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
96
|
+
shell: false,
|
|
97
|
+
});
|
|
98
|
+
child.stdout?.on('data', (chunk) => {
|
|
99
|
+
stdout += String(chunk);
|
|
100
|
+
});
|
|
101
|
+
child.on('error', () => resolve({ code: 1, stdout }));
|
|
102
|
+
child.on('close', (code) => resolve({ code: code ?? 1, stdout }));
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* The version of the globally installed @gaia-ai/gaia, read from
|
|
106
|
+
* `npm ls -g @gaia-ai/gaia --json`, or null when it is not installed globally /
|
|
107
|
+
* the output cannot be parsed. This reflects what npm actually has on disk —
|
|
108
|
+
* independent of the (possibly dev-shim) CLI running this process.
|
|
109
|
+
*/
|
|
110
|
+
async function queryGlobalVersion(captureImpl) {
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await captureImpl('npm', [
|
|
113
|
+
'ls',
|
|
114
|
+
'-g',
|
|
115
|
+
GAIA_PACKAGE,
|
|
116
|
+
'--json',
|
|
117
|
+
'--depth',
|
|
118
|
+
'0',
|
|
119
|
+
]);
|
|
120
|
+
const parsed = JSON.parse(stdout);
|
|
121
|
+
const v = parsed.dependencies?.[GAIA_PACKAGE]?.version;
|
|
122
|
+
return typeof v === 'string' ? v : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Upgrade the globally installed gaia CLI to the latest published version via
|
|
130
|
+
* `npm install -g @gaia-ai/gaia@latest`. Reports the actual global version
|
|
131
|
+
* before and after the install (queried from npm, not from a separate registry
|
|
132
|
+
* fetch — finding #2), so the caller reports the real change: `before` is null
|
|
133
|
+
* when gaia was not installed globally, and no "upgraded" claim is made unless
|
|
134
|
+
* npm actually changed the on-disk version. On install failure `after` mirrors
|
|
135
|
+
* `before` and the post-install query is skipped. spawnImpl/captureImpl are
|
|
136
|
+
* injectable for tests.
|
|
137
|
+
*/
|
|
138
|
+
export async function runUpdate(spawnImpl = defaultSpawn, captureImpl = defaultCapture) {
|
|
139
|
+
const before = await queryGlobalVersion(captureImpl);
|
|
140
|
+
const code = await spawnImpl('npm', [
|
|
141
|
+
'install',
|
|
142
|
+
'-g',
|
|
143
|
+
`${GAIA_PACKAGE}@latest`,
|
|
144
|
+
]);
|
|
145
|
+
if (code !== 0)
|
|
146
|
+
return { ok: false, before, after: before };
|
|
147
|
+
const after = await queryGlobalVersion(captureImpl);
|
|
148
|
+
return { ok: true, before, after };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Print the installed gaia version line (AC-2), synchronously, and return it.
|
|
152
|
+
* Never blocks on the registry — the caller kicks off the newer-version check
|
|
153
|
+
* separately (see fetchUpdateNotice) so start is not stalled waiting on npm.
|
|
154
|
+
*/
|
|
155
|
+
export function printVersionLine(out = console.log) {
|
|
156
|
+
const current = resolveCliVersion();
|
|
157
|
+
out(`gaia v${current}`);
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Best-effort "update available" notice for the given installed version
|
|
162
|
+
* (AC-3/AC-5). Fetches the latest published version (fail-silent → null on any
|
|
163
|
+
* error, AC-4) and compares. Returns the notice string or null. fetchImpl is
|
|
164
|
+
* injectable for tests.
|
|
165
|
+
*/
|
|
166
|
+
export async function fetchUpdateNotice(current, fetchImpl = globalThis.fetch) {
|
|
167
|
+
const latest = await fetchLatestVersion(fetchImpl);
|
|
168
|
+
if (latest === null)
|
|
169
|
+
return null;
|
|
170
|
+
return updateNotice(current, latest);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Print the installed gaia version (AC-2) and, best-effort, a newer-version
|
|
174
|
+
* notice (AC-3/AC-5). The registry check is fail-silent (AC-4): an offline
|
|
175
|
+
* registry prints only the version line. out/fetchImpl are injectable for tests.
|
|
176
|
+
*/
|
|
177
|
+
export async function printStartVersionBanner(out = console.log, fetchImpl = globalThis.fetch) {
|
|
178
|
+
const current = printVersionLine(out);
|
|
179
|
+
const notice = await fetchUpdateNotice(current, fetchImpl);
|
|
180
|
+
if (notice !== null)
|
|
181
|
+
out(notice);
|
|
182
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConductorFileConfig } from '@gaia-ai/core';
|
|
1
|
+
import type { AgentCandidate, ConductorFileConfig } from '@gaia-ai/core';
|
|
2
2
|
/**
|
|
3
3
|
* Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
|
|
4
4
|
* the agent must stop instead of running the whole flow in one session. The run
|
|
@@ -34,6 +34,15 @@ import type { ConductorFileConfig } from '@gaia-ai/core';
|
|
|
34
34
|
* `triage` for unclassified tickets).
|
|
35
35
|
*/
|
|
36
36
|
export declare const DEFAULT_AGENT_PROMPT: string;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the `agent` slot into candidate(s), **preserving the authored shape**:
|
|
39
|
+
* a single descriptor/plugin/wrapper → a single `AgentCandidate`, an array →
|
|
40
|
+
* `AgentCandidate[]`. Each entry is either a bare descriptor/plugin or a
|
|
41
|
+
* `{ agent, priority? }` wrapper; the inner agent value is resolved through the
|
|
42
|
+
* same `agent` kind-guard as any slot, and a `priority` function is preserved
|
|
43
|
+
* verbatim (AC-1, AC-2). Normalization to a list happens at selection time.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveAgents(raw: unknown, configPath: string): Promise<AgentCandidate | AgentCandidate[]>;
|
|
37
46
|
/**
|
|
38
47
|
* Resolve the conductor config path from `cwd`.
|
|
39
48
|
*
|
package/dist/src/config.js
CHANGED
|
@@ -42,7 +42,7 @@ export const DEFAULT_AGENT_PROMPT = `You are a GAIA agent working ticket {identi
|
|
|
42
42
|
`(gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
|
|
43
43
|
`renders the intake splash, runs the {state} engine, and applies ` +
|
|
44
44
|
`WORKFLOW.md's {state} policy. Do ONLY the {state} work — never start or ` +
|
|
45
|
-
`prepare a later state
|
|
45
|
+
`prepare a later state.`;
|
|
46
46
|
function requirePlugin(value, kind) {
|
|
47
47
|
if (!isRecord(value) || value.kind !== kind) {
|
|
48
48
|
throw new Error(`conductor config requires a ${kind} plugin in the "${kind}" slot`);
|
|
@@ -119,6 +119,31 @@ async function resolveSlot(value, kind, configPath) {
|
|
|
119
119
|
: value;
|
|
120
120
|
return requirePlugin(resolved, kind);
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the `agent` slot into candidate(s), **preserving the authored shape**:
|
|
124
|
+
* a single descriptor/plugin/wrapper → a single `AgentCandidate`, an array →
|
|
125
|
+
* `AgentCandidate[]`. Each entry is either a bare descriptor/plugin or a
|
|
126
|
+
* `{ agent, priority? }` wrapper; the inner agent value is resolved through the
|
|
127
|
+
* same `agent` kind-guard as any slot, and a `priority` function is preserved
|
|
128
|
+
* verbatim (AC-1, AC-2). Normalization to a list happens at selection time.
|
|
129
|
+
*/
|
|
130
|
+
export async function resolveAgents(raw, configPath) {
|
|
131
|
+
const resolveOne = async (entry) => {
|
|
132
|
+
const hasWrapper = isRecord(entry) &&
|
|
133
|
+
'agent' in entry &&
|
|
134
|
+
!('kind' in entry) &&
|
|
135
|
+
!('plugin' in entry);
|
|
136
|
+
const agentValue = hasWrapper ? entry.agent : entry;
|
|
137
|
+
const priority = hasWrapper
|
|
138
|
+
? entry.priority
|
|
139
|
+
: undefined;
|
|
140
|
+
const agent = await resolveSlot(agentValue, 'agent', configPath);
|
|
141
|
+
return { agent, ...(priority ? { priority } : {}) };
|
|
142
|
+
};
|
|
143
|
+
return Array.isArray(raw)
|
|
144
|
+
? Promise.all(raw.map(resolveOne))
|
|
145
|
+
: resolveOne(raw);
|
|
146
|
+
}
|
|
122
147
|
/**
|
|
123
148
|
* Resolve the `plugins[]` array: descriptor entries are constructed, already-
|
|
124
149
|
* constructed entries pass through. No kind-guard (plugins are not slotted).
|
|
@@ -259,7 +284,7 @@ export async function loadConductorConfig(configFile) {
|
|
|
259
284
|
: {}),
|
|
260
285
|
remote: await resolveSlot(config.remote, 'remote', configPath),
|
|
261
286
|
executor: await resolveSlot(config.executor, 'executor', configPath),
|
|
262
|
-
agent: await
|
|
287
|
+
agent: await resolveAgents(config.agent, configPath),
|
|
263
288
|
workspace: await resolveSlot(config.workspace, 'workspace', configPath),
|
|
264
289
|
label,
|
|
265
290
|
machine_id: machineId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConductorFileConfig, type ConductorLogger, type
|
|
1
|
+
import { type ConductorFileConfig, type ConductorLogger, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace, type ResolvedAgent } from '@gaia-ai/core';
|
|
2
2
|
/**
|
|
3
3
|
* Resolve the environment a run executes in (GAIA-99): parse the ticket's
|
|
4
4
|
* effective env_vars, drop any reserved key (loud warn — key NAME only, never
|
|
@@ -14,11 +14,11 @@ export declare class Conductor {
|
|
|
14
14
|
private readonly remote;
|
|
15
15
|
private readonly executor;
|
|
16
16
|
private readonly workspace;
|
|
17
|
-
private readonly
|
|
17
|
+
private readonly agents;
|
|
18
18
|
private readonly logger;
|
|
19
19
|
private readonly checkoutRoot;
|
|
20
20
|
private uuid;
|
|
21
|
-
constructor(config: ConductorFileConfig, remote: GaiaRemote, executor: GaiaExecutor, workspace: GaiaWorkspace,
|
|
21
|
+
constructor(config: ConductorFileConfig, remote: GaiaRemote, executor: GaiaExecutor, workspace: GaiaWorkspace, agents: ResolvedAgent[], logger: ConductorLogger, checkoutRoot?: string);
|
|
22
22
|
get id(): string;
|
|
23
23
|
/** This conductor's registration payload, built from config. */
|
|
24
24
|
private registration;
|
|
@@ -47,16 +47,17 @@ export declare class Conductor {
|
|
|
47
47
|
*
|
|
48
48
|
* Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
|
|
49
49
|
* is closed at once as a lifecycle step, independent of whether its worktree
|
|
50
|
-
* teardown then succeeds.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* no-op — it is off the list
|
|
59
|
-
* try/catch so one failure never
|
|
50
|
+
* teardown then succeeds. `cleaned_up` is flagged ONLY on a VERIFIED teardown
|
|
51
|
+
* (GAIA-141 RC-2): `removeWorktree` returns `true` when the worktree is gone on
|
|
52
|
+
* this host (removed by us, or confirmed already absent), `false` when it could
|
|
53
|
+
* not be resolved and may still be on disk (e.g. its `worktree_path` was never
|
|
54
|
+
* persisted). A `false` return — like a THROW — logs loudly and leaves
|
|
55
|
+
* `cleaned_up=0`, so the next reconciliation retries; flagging cleaned on an
|
|
56
|
+
* unverified teardown was the root cause of the ~28 never-touched leftovers,
|
|
57
|
+
* because the ticket then dropped off the (cleaned_up=0) work list forever.
|
|
58
|
+
* Re-running on an already-cleaned ticket is a no-op — it is off the list
|
|
59
|
+
* (idempotent). Each ticket is isolated in a try/catch so one failure never
|
|
60
|
+
* aborts the rest.
|
|
60
61
|
*
|
|
61
62
|
* The `after_done` hook runs through the executor (GAIA-84), which owns the
|
|
62
63
|
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { conductorId, } from '@gaia-ai/core';
|
|
1
|
+
import { conductorId, selectAgent, } from '@gaia-ai/core';
|
|
2
2
|
function sleep(ms, signal) {
|
|
3
3
|
return new Promise((resolve) => {
|
|
4
4
|
if (signal?.aborted) {
|
|
@@ -86,16 +86,16 @@ export class Conductor {
|
|
|
86
86
|
remote;
|
|
87
87
|
executor;
|
|
88
88
|
workspace;
|
|
89
|
-
|
|
89
|
+
agents;
|
|
90
90
|
logger;
|
|
91
91
|
checkoutRoot;
|
|
92
92
|
uuid = null;
|
|
93
|
-
constructor(config, remote, executor, workspace,
|
|
93
|
+
constructor(config, remote, executor, workspace, agents, logger, checkoutRoot = process.cwd()) {
|
|
94
94
|
this.config = config;
|
|
95
95
|
this.remote = remote;
|
|
96
96
|
this.executor = executor;
|
|
97
97
|
this.workspace = workspace;
|
|
98
|
-
this.
|
|
98
|
+
this.agents = agents;
|
|
99
99
|
this.logger = logger;
|
|
100
100
|
this.checkoutRoot = checkoutRoot;
|
|
101
101
|
}
|
|
@@ -191,14 +191,21 @@ export class Conductor {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
// Route to the agent that actually ran (GAIA-144): the persisted
|
|
195
|
+
// run.agent id, falling back to the first candidate for a run written
|
|
196
|
+
// before the field existed (empty id).
|
|
197
|
+
const resolved = this.agents.find((a) => a.id === r.agent) ?? this.agents[0];
|
|
198
|
+
if (!resolved) {
|
|
199
|
+
throw new Error('no agent candidates configured for finalize');
|
|
200
|
+
}
|
|
194
201
|
const log = r.worktreePath
|
|
195
|
-
? await
|
|
202
|
+
? await resolved.agent.getRunLog(r.worktreePath)
|
|
196
203
|
: '';
|
|
197
204
|
// Footprint (GAIA-132): the agent parses its own transcript for the
|
|
198
205
|
// effort metrics (transcript format is agent-specific); the conductor
|
|
199
206
|
// adds duration_s from started_at → now. The conductor owns the run, so
|
|
200
207
|
// this write always lands (the agent session could not).
|
|
201
|
-
const parsed =
|
|
208
|
+
const parsed = resolved.agent.parseFootprint(log);
|
|
202
209
|
const now = Math.floor(Date.now() / 1000);
|
|
203
210
|
const metrics = {
|
|
204
211
|
tokens: parsed.tokens,
|
|
@@ -231,16 +238,17 @@ export class Conductor {
|
|
|
231
238
|
*
|
|
232
239
|
* Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
|
|
233
240
|
* is closed at once as a lifecycle step, independent of whether its worktree
|
|
234
|
-
* teardown then succeeds.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* no-op — it is off the list
|
|
243
|
-
* try/catch so one failure never
|
|
241
|
+
* teardown then succeeds. `cleaned_up` is flagged ONLY on a VERIFIED teardown
|
|
242
|
+
* (GAIA-141 RC-2): `removeWorktree` returns `true` when the worktree is gone on
|
|
243
|
+
* this host (removed by us, or confirmed already absent), `false` when it could
|
|
244
|
+
* not be resolved and may still be on disk (e.g. its `worktree_path` was never
|
|
245
|
+
* persisted). A `false` return — like a THROW — logs loudly and leaves
|
|
246
|
+
* `cleaned_up=0`, so the next reconciliation retries; flagging cleaned on an
|
|
247
|
+
* unverified teardown was the root cause of the ~28 never-touched leftovers,
|
|
248
|
+
* because the ticket then dropped off the (cleaned_up=0) work list forever.
|
|
249
|
+
* Re-running on an already-cleaned ticket is a no-op — it is off the list
|
|
250
|
+
* (idempotent). Each ticket is isolated in a try/catch so one failure never
|
|
251
|
+
* aborts the rest.
|
|
244
252
|
*
|
|
245
253
|
* The `after_done` hook runs through the executor (GAIA-84), which owns the
|
|
246
254
|
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
@@ -271,12 +279,14 @@ export class Conductor {
|
|
|
271
279
|
});
|
|
272
280
|
}
|
|
273
281
|
if (this.executor.capabilities().persistent) {
|
|
282
|
+
let removed;
|
|
274
283
|
try {
|
|
275
|
-
//
|
|
276
|
-
// worktree
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
284
|
+
// removeWorktree returns whether the teardown was VERIFIED on this
|
|
285
|
+
// host: `true` = the worktree is gone (removed by us, or confirmed
|
|
286
|
+
// already absent); `false` = it could NOT be resolved and may still
|
|
287
|
+
// be on disk (GAIA-141 RC-2 — e.g. worktree_path was never persisted
|
|
288
|
+
// and herdr lost track of the branch). A THROW is a hard failure.
|
|
289
|
+
removed = await this.executor.removeWorktree(t.branchName, t.worktreePath || undefined);
|
|
280
290
|
}
|
|
281
291
|
catch (err) {
|
|
282
292
|
this.logger.warn({
|
|
@@ -287,6 +297,19 @@ export class Conductor {
|
|
|
287
297
|
}, 'worktree teardown failed');
|
|
288
298
|
continue; // leave cleaned_up=0 → the next reconciliation retries.
|
|
289
299
|
}
|
|
300
|
+
if (!removed) {
|
|
301
|
+
// Unverified teardown (GAIA-141 RC-2): do NOT flag cleaned_up, or the
|
|
302
|
+
// ticket drops off fetchUncleanedTickets (which filters cleaned_up=0)
|
|
303
|
+
// and the orphan is never retried. Leave it on the work list so a
|
|
304
|
+
// later reap — with a persisted path, or once the executor resolves
|
|
305
|
+
// it via the parent-repo git worktree list — can finish the job.
|
|
306
|
+
this.logger.warn({
|
|
307
|
+
ticket: t.ticketUuid,
|
|
308
|
+
branch: t.branchName,
|
|
309
|
+
worktreePath: t.worktreePath,
|
|
310
|
+
}, 'worktree teardown unverified — retrying next reap');
|
|
311
|
+
continue; // leave cleaned_up=0 → the next reconciliation retries.
|
|
312
|
+
}
|
|
290
313
|
}
|
|
291
314
|
// Teardown verified locally (or nothing hosted to tear down): flag it
|
|
292
315
|
// cleaned so it drops off the work list.
|
|
@@ -300,6 +323,11 @@ export class Conductor {
|
|
|
300
323
|
}
|
|
301
324
|
async dispatch(run) {
|
|
302
325
|
const t = await this.remote.getTicket(run.ticketUuid);
|
|
326
|
+
// Config-side agent selection (GAIA-144): pick the highest-priority
|
|
327
|
+
// candidate for this ticket (labels/environments already sideloaded by
|
|
328
|
+
// getTicket, so priority(ticket) never fetches). The chosen id is persisted
|
|
329
|
+
// on the run below so the stateless finalize step routes footprint parsing.
|
|
330
|
+
const chosen = selectAgent(this.agents, t);
|
|
303
331
|
const baseRef = t.baseBranch ? `origin/${t.baseBranch}` : undefined;
|
|
304
332
|
// Resolve the run env once (per-ticket env_vars + core GAIA_* vars) and
|
|
305
333
|
// inject it into BOTH the executor-owned lifecycle hooks AND the agent run
|
|
@@ -352,7 +380,7 @@ export class Conductor {
|
|
|
352
380
|
run: { uuid: run.runUuid, id: run.runId, handler: run.handler },
|
|
353
381
|
workspacePath: ws.path,
|
|
354
382
|
instructions: ws.instructions,
|
|
355
|
-
command:
|
|
383
|
+
command: chosen.agent.launchCommand(prompt),
|
|
356
384
|
env,
|
|
357
385
|
});
|
|
358
386
|
this.logger.info({
|
|
@@ -365,7 +393,12 @@ export class Conductor {
|
|
|
365
393
|
}, 'dispatched run');
|
|
366
394
|
// The conductor — not the agent — knows the per-run git worktree path, so
|
|
367
395
|
// it persists `worktree_path` to gaia_run here alongside the running state.
|
|
368
|
-
|
|
396
|
+
// It also persists the chosen agent id (GAIA-144) for finalize footprint
|
|
397
|
+
// routing.
|
|
398
|
+
await this.remote.markRunning(run.runUuid, {
|
|
399
|
+
worktree_path: ws.path,
|
|
400
|
+
agent: chosen.id,
|
|
401
|
+
});
|
|
369
402
|
}
|
|
370
403
|
async serve(signal) {
|
|
371
404
|
await this.pollLoop(signal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
"directory": "conductor/packages/conductor"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gaia-ai/core": "^0.
|
|
29
|
-
"@dropsh/plugin-oauth2": "^0.
|
|
30
|
-
"@dropsh/plugin-jsonapi-schema": "^0.5.
|
|
28
|
+
"@gaia-ai/core": "^0.5.0",
|
|
29
|
+
"@dropsh/plugin-oauth2": "^0.5.7",
|
|
30
|
+
"@dropsh/plugin-jsonapi-schema": "^0.5.7",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
|
-
"dropsh": "^0.5.
|
|
32
|
+
"dropsh": "^0.5.7",
|
|
33
|
+
"semver": "^7.6.0"
|
|
33
34
|
}
|
|
34
35
|
}
|