@gaia-ai/conductor 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/conductor-registry.d.ts +7 -0
- package/dist/src/cli/conductor-registry.js +6 -0
- package/dist/src/cli/config-schema.d.ts +54 -0
- package/dist/src/cli/config-schema.js +91 -0
- package/dist/src/cli/deployment.d.ts +34 -0
- package/dist/src/cli/deployment.js +63 -0
- package/dist/src/cli/init.d.ts +31 -33
- package/dist/src/cli/init.js +96 -80
- package/dist/src/cli/upgrade.d.ts +38 -0
- package/dist/src/cli/upgrade.js +143 -0
- package/dist/src/cli/version-check.js +5 -1
- package/dist/src/commands/conductor.d.ts +33 -0
- package/dist/src/{cli/gaia.js → commands/conductor.js} +115 -210
- package/dist/src/config.d.ts +50 -23
- package/dist/src/config.js +127 -153
- package/dist/src/core/conductor.js +5 -8
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +11 -2
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/package.json +7 -5
- package/dist/src/cli/gaia.d.ts +0 -23
- package/dist/src/cli/local-registry.d.ts +0 -14
- package/dist/src/cli/local-registry.js +0 -56
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import { CommandRunner, createLogger, exec, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
4
|
+
import { CommandRunner, createLogger, exec, loadGaiaConfig, machineContextPath, readMachineContext, resolveConfigPath, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
5
5
|
import { selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import { authStatus
|
|
8
|
-
import
|
|
7
|
+
import { authStatus } from 'dropsh';
|
|
8
|
+
import * as conductorRegistry from '../cli/conductor-registry.js';
|
|
9
|
+
import { registerDeployment } from '../cli/deployment.js';
|
|
10
|
+
import { scaffold, scaffoldGaiaConfig } from '../cli/init.js';
|
|
11
|
+
import { runUpgrade } from '../cli/upgrade.js';
|
|
12
|
+
import { fetchUpdateNotice, printVersionLine } from '../cli/version-check.js';
|
|
13
|
+
import { composeConductorConfig, loadConductorConfig } from '../config.js';
|
|
9
14
|
import { Conductor } from '../core/conductor.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
/** Default host bases for connection/plugin resolution when none is injected. */
|
|
16
|
+
function defaultHost() {
|
|
17
|
+
return { resolveBases: [import.meta.url, `${process.cwd()}/`] };
|
|
18
|
+
}
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `gaia`
|
|
17
|
-
*
|
|
20
|
+
* Resolve + compose the full conductor config: the engine half
|
|
21
|
+
* (`conductor.config.js` via `loadConductorConfig`) plus the connection half
|
|
22
|
+
* (`gaia.config.js` via `loadGaiaConfig`, walk-up → home → shipped fallback).
|
|
23
|
+
* `--config` / `$GAIA_CONDUCTOR_CONFIG` selects the engine file; the connection
|
|
24
|
+
* is resolved from cwd.
|
|
18
25
|
*/
|
|
19
|
-
function defaultConfigPath(override) {
|
|
20
|
-
return resolveConfigPath(override);
|
|
21
|
-
}
|
|
22
26
|
async function resolveConfig(deps, configPathOverride) {
|
|
23
27
|
if (deps.config) {
|
|
24
28
|
return deps.config;
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
const host = deps.host ?? defaultHost();
|
|
31
|
+
const engine = await loadConductorConfig(resolveConfigPath(configPathOverride));
|
|
32
|
+
const connection = await loadGaiaConfig(host, {});
|
|
33
|
+
return composeConductorConfig(engine, connection);
|
|
27
34
|
}
|
|
28
35
|
async function resolveRemote(deps, config) {
|
|
29
36
|
return deps.remote ?? (await selectRemote(config));
|
|
@@ -33,9 +40,8 @@ function checkoutRootOf(config) {
|
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* The conductor's stable identity (gaia_conductor.machine_id). machine_id is
|
|
36
|
-
* required — the loader
|
|
37
|
-
*
|
|
38
|
-
* somehow lacks it fails loudly rather than silently minting a divergent id.
|
|
43
|
+
* required — the loader throws without it — so lifecycle commands read it here,
|
|
44
|
+
* never re-derive.
|
|
39
45
|
*/
|
|
40
46
|
function conductorIdOf(config) {
|
|
41
47
|
const id = config.machine_id;
|
|
@@ -102,8 +108,6 @@ function freshnessThresholdS(config) {
|
|
|
102
108
|
: DEFAULT_FRESH_S;
|
|
103
109
|
}
|
|
104
110
|
function classify(hub, freshS) {
|
|
105
|
-
// No host probe here (host calls are untested) → can't tell host-missing
|
|
106
|
-
// from wedged. registry+no-hub = registry-only; stale-hub = wedged.
|
|
107
111
|
if (!hub) {
|
|
108
112
|
return 'registry-only';
|
|
109
113
|
}
|
|
@@ -115,7 +119,7 @@ function classify(hub, freshS) {
|
|
|
115
119
|
return fresh ? 'running' : 'wedged';
|
|
116
120
|
}
|
|
117
121
|
async function buildLsRows(remote, config, onlyId) {
|
|
118
|
-
const entries = await
|
|
122
|
+
const entries = await conductorRegistry.list();
|
|
119
123
|
let hub = [];
|
|
120
124
|
try {
|
|
121
125
|
hub = await remote.listConductors('me');
|
|
@@ -161,9 +165,7 @@ function printRows(rows) {
|
|
|
161
165
|
// --- command handlers -------------------------------------------------------
|
|
162
166
|
/**
|
|
163
167
|
* Start-time auth gate. Returns true if authenticated (session or session-less
|
|
164
|
-
* provider); otherwise logs a single clear line and returns false.
|
|
165
|
-
* before remote resolution (resolveRemote), which calls resolveAuth and throws
|
|
166
|
-
* when unauthenticated.
|
|
168
|
+
* provider); otherwise logs a single clear line and returns false.
|
|
167
169
|
*/
|
|
168
170
|
export async function ensureAuthenticated(config, logger) {
|
|
169
171
|
const st = await authStatus({
|
|
@@ -201,10 +203,6 @@ async function cmdReap(deps, log = {}) {
|
|
|
201
203
|
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
202
204
|
const agents = deps.agents ?? (await selectAgents(config));
|
|
203
205
|
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
204
|
-
// The reaper reconciles finished-but-uncleaned tickets (the cleaned_up flag)
|
|
205
|
-
// against their worktrees; it needs no registration/heartbeat (it is not a
|
|
206
|
-
// poll), just the executor + remote, so it runs standalone after a
|
|
207
|
-
// crash/restart.
|
|
208
206
|
await conductor.reap();
|
|
209
207
|
}
|
|
210
208
|
async function cmdStartForeground(deps, log = {}) {
|
|
@@ -232,12 +230,6 @@ async function cmdStartForeground(deps, log = {}) {
|
|
|
232
230
|
}
|
|
233
231
|
}
|
|
234
232
|
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
233
|
const current = printVersionLine();
|
|
242
234
|
const noticePromise = fetchUpdateNotice(current, deps.fetch ?? globalThis.fetch).catch(() => null);
|
|
243
235
|
try {
|
|
@@ -257,7 +249,7 @@ async function cmdStartBody(deps, log) {
|
|
|
257
249
|
return;
|
|
258
250
|
const remote = await resolveRemote(deps, config);
|
|
259
251
|
const id = conductorIdOf(config);
|
|
260
|
-
const existing = await
|
|
252
|
+
const existing = await conductorRegistry.get(id);
|
|
261
253
|
if (existing) {
|
|
262
254
|
const hubStatus = await remote.getConductorStatus(id);
|
|
263
255
|
if (hubStatus !== null && hubStatus !== 'offline') {
|
|
@@ -266,7 +258,7 @@ async function cmdStartBody(deps, log) {
|
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
260
|
const handle = `gaia-conductor:${id}`;
|
|
269
|
-
await
|
|
261
|
+
await conductorRegistry.register({
|
|
270
262
|
id,
|
|
271
263
|
path: checkoutRoot,
|
|
272
264
|
project: config.project,
|
|
@@ -274,9 +266,6 @@ async function cmdStartBody(deps, log) {
|
|
|
274
266
|
host: 'herdr',
|
|
275
267
|
handle,
|
|
276
268
|
});
|
|
277
|
-
// The detached foreground process is a fresh CLI invocation — forward the
|
|
278
|
-
// log flags so the herdr-hosted loop logs at the requested level. Sink stays
|
|
279
|
-
// forced to file (herdr-hosted = no TTY) unless the caller overrode it.
|
|
280
269
|
const fgFlags = [
|
|
281
270
|
log.level ? `--log-level ${log.level}` : '',
|
|
282
271
|
log.sink ? `--log-sink ${log.sink}` : '',
|
|
@@ -298,7 +287,7 @@ async function cmdStop(deps, now) {
|
|
|
298
287
|
const remote = await resolveRemote(deps, config);
|
|
299
288
|
const id = conductorIdOf(config);
|
|
300
289
|
if (now) {
|
|
301
|
-
const entry = await
|
|
290
|
+
const entry = await conductorRegistry.get(id);
|
|
302
291
|
if (entry && entry.host === 'herdr') {
|
|
303
292
|
await killViaHerdr(entry.handle);
|
|
304
293
|
console.log(`hard-stopped conductor ${id} (${entry.handle})`);
|
|
@@ -306,16 +295,9 @@ async function cmdStop(deps, now) {
|
|
|
306
295
|
else {
|
|
307
296
|
console.log(`no herdr-hosted conductor to hard-stop for ${id}`);
|
|
308
297
|
}
|
|
309
|
-
// A hard kill just stops the heartbeat; it never deletes the entity. The
|
|
310
|
-
// Drupal cron reaper flips the now-stale registration to offline (single
|
|
311
|
-
// authority for the offline transition — see gaia_core cron).
|
|
312
298
|
return;
|
|
313
299
|
}
|
|
314
|
-
|
|
315
|
-
// just like a hard kill, but writes offline itself instead of waiting for the
|
|
316
|
-
// cron reaper. Stop the process first (else its next heartbeat would flip it
|
|
317
|
-
// back online), then mark it offline. In-flight runs are not awaited.
|
|
318
|
-
const entry = await registry.get(id);
|
|
300
|
+
const entry = await conductorRegistry.get(id);
|
|
319
301
|
if (entry && entry.host === 'herdr') {
|
|
320
302
|
await killViaHerdr(entry.handle);
|
|
321
303
|
}
|
|
@@ -338,7 +320,7 @@ async function cmdStatus(deps) {
|
|
|
338
320
|
async function cmdRm(deps) {
|
|
339
321
|
const config = await resolveConfig(deps);
|
|
340
322
|
const id = conductorIdOf(config);
|
|
341
|
-
await
|
|
323
|
+
await conductorRegistry.remove(id);
|
|
342
324
|
console.log(`removed conductor ${id} from registry`);
|
|
343
325
|
}
|
|
344
326
|
/** ls may run without a config file; best-effort load. */
|
|
@@ -347,7 +329,7 @@ async function tryConfig(deps) {
|
|
|
347
329
|
return deps.config;
|
|
348
330
|
}
|
|
349
331
|
try {
|
|
350
|
-
return await
|
|
332
|
+
return await resolveConfig(deps);
|
|
351
333
|
}
|
|
352
334
|
catch {
|
|
353
335
|
return undefined;
|
|
@@ -371,7 +353,6 @@ async function promptSecret() {
|
|
|
371
353
|
output: process.stdout,
|
|
372
354
|
terminal: true,
|
|
373
355
|
});
|
|
374
|
-
// Mute character echo while the secret is typed.
|
|
375
356
|
rl._writeToOutput = (s) => {
|
|
376
357
|
if (!rl.muted || s.includes('\n'))
|
|
377
358
|
process.stdout.write(s);
|
|
@@ -390,60 +371,10 @@ async function promptSecret() {
|
|
|
390
371
|
rl.close();
|
|
391
372
|
}
|
|
392
373
|
}
|
|
393
|
-
// ---
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
.name('gaia')
|
|
398
|
-
.description('GAIA conductor + client CLI')
|
|
399
|
-
.option('--conductor <name>', 'select a conductor by stem (conductor→conductor.config.js, else <name>.conductor.config.js; default: conductor.config.js, else the sole config; env $GAIA_CONDUCTOR)');
|
|
400
|
-
// GAIA-126/137: a repo may hold several .gaia/ conductor configs — the default
|
|
401
|
-
// conductor.config.js plus <stem>.conductor.config.js variants.
|
|
402
|
-
// resolveConfigPath already honours $GAIA_CONDUCTOR; thread the global
|
|
403
|
-
// --conductor flag into it (flag wins over env) so every command selects the
|
|
404
|
-
// named config without per-command wiring. A per-invocation flag beats any
|
|
405
|
-
// ambient env for this run.
|
|
406
|
-
program.hook('preAction', () => {
|
|
407
|
-
const name = program.opts().conductor;
|
|
408
|
-
if (typeof name === 'string' && name !== '') {
|
|
409
|
-
process.env.GAIA_CONDUCTOR = name;
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
const cliVersion = resolveCliVersion();
|
|
413
|
-
program.version(cliVersion); // -V, --version
|
|
414
|
-
program
|
|
415
|
-
.command('version')
|
|
416
|
-
.description('output the gaia CLI version')
|
|
417
|
-
.action(() => {
|
|
418
|
-
console.log(cliVersion);
|
|
419
|
-
});
|
|
420
|
-
program
|
|
421
|
-
.command('update')
|
|
422
|
-
.description('upgrade the globally installed gaia CLI to the latest npm release')
|
|
423
|
-
.action(async () => {
|
|
424
|
-
const { ok, before, after } = await runUpdate();
|
|
425
|
-
if (!ok) {
|
|
426
|
-
console.error('gaia update failed — see npm output above');
|
|
427
|
-
process.exitCode = 1;
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
// Report the real on-disk change (finding #2): never a fabricated
|
|
431
|
-
// "latest", never a false "upgraded" when npm changed nothing.
|
|
432
|
-
if (after === null) {
|
|
433
|
-
console.log('gaia update completed (installed version undetermined)');
|
|
434
|
-
}
|
|
435
|
-
else if (before === null) {
|
|
436
|
-
console.log(`gaia installed: ${after}`);
|
|
437
|
-
}
|
|
438
|
-
else if (before === after) {
|
|
439
|
-
console.log(`gaia is already up to date (${after})`);
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
console.log(`gaia updated: ${before} → ${after}`);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
const conductor = program
|
|
446
|
-
.command('conductor')
|
|
374
|
+
// --- the conductor command tree --------------------------------------------
|
|
375
|
+
/** Build the `conductor` subcommand tree (lifecycle + registry + init). */
|
|
376
|
+
export function createConductorCommand(deps) {
|
|
377
|
+
const conductor = new Command('conductor')
|
|
447
378
|
.description('node-agent lifecycle + local registry')
|
|
448
379
|
.option('--log-level <level>', 'log verbosity: debug | info | warn | error (overrides GAIA_LOG_LEVEL)')
|
|
449
380
|
.option('--log-sink <sink>', 'log sink: stdout | file (overrides GAIA_CONDUCTOR_LOG)')
|
|
@@ -509,36 +440,32 @@ Examples:
|
|
|
509
440
|
.action(async () => {
|
|
510
441
|
await cmdRm(deps);
|
|
511
442
|
});
|
|
443
|
+
registerInit(conductor);
|
|
444
|
+
return conductor;
|
|
445
|
+
}
|
|
446
|
+
/** `gaia conductor init` — scaffold the home machine context + the project's
|
|
447
|
+
* split config (connection `gaia.config.js` + engine `conductor.config.js`). */
|
|
448
|
+
function registerInit(conductor) {
|
|
512
449
|
conductor
|
|
513
450
|
.command('init')
|
|
514
|
-
.description('scaffold the
|
|
451
|
+
.description('scaffold the project split config (.gaia/gaia.config.js connection + .gaia/conductor.config.js engine) plus the user-global ~/.gaia/machine.config.js context (identity + connection incl. secret)')
|
|
515
452
|
.option('--base-url <url>', 'control-plane base URL (site.base_url) — required only when onboarding this machine')
|
|
516
453
|
.option('--project <name>', 'GAIA project name — required only to scaffold the committed repo config; omit for machine-only onboarding')
|
|
517
454
|
.option('--secret-env <VAR>', 'env var name to read the oauth client secret from (else TTY prompt)')
|
|
518
455
|
.option('--client-id <id>', 'oauth consumer id', 'gaia-agent')
|
|
519
456
|
.option('--machine-id <id>', 'machine host token for the context (defaults to hostname())')
|
|
520
457
|
.option('--user-id <kuerzel>', 'developer Kürzel for the user-global context')
|
|
521
|
-
.option('--machine-path <path>', 'user-global machine context path (defaults to ~/.
|
|
522
|
-
.option('--config <path>', 'target committed config path', './.gaia/conductor.config.js')
|
|
458
|
+
.option('--machine-path <path>', 'user-global machine context path (defaults to ~/.gaia/machine.config.js)')
|
|
459
|
+
.option('--config <path>', 'target committed engine config path', './.gaia/conductor.config.js')
|
|
523
460
|
.option('--force', 'overwrite an existing committed config', false)
|
|
524
461
|
.option('--reonboard', 'force machine-context onboarding even if a context file exists', false)
|
|
525
462
|
.action(async (opts) => {
|
|
526
|
-
// Two independent axes decide what init writes:
|
|
527
|
-
// - machine axis: an existing context means project-only; --reonboard
|
|
528
|
-
// (or an absent context) forces machine-context (re)scaffolding.
|
|
529
|
-
// - repo axis: --project scaffolds the committed repo config; omitting
|
|
530
|
-
// it means machine-only (no repo). The 4 quadrants:
|
|
531
|
-
// context absent + project → both files
|
|
532
|
-
// context absent + no proj → machine-only onboarding
|
|
533
|
-
// context present + project → project-only
|
|
534
|
-
// context present + no proj → no-op (machine already onboarded)
|
|
535
463
|
const machinePath = opts.machinePath ?? machineContextPath();
|
|
536
464
|
const existing = await readMachineContext(machinePath);
|
|
537
465
|
const contextPresent = existsSync(machinePath);
|
|
538
466
|
const onboarding = !contextPresent || opts.reonboard;
|
|
539
467
|
const project = opts.project ?? '';
|
|
540
468
|
const hasProject = project.trim() !== '';
|
|
541
|
-
// Context present and nothing repo-scoped to do → no-op with guidance.
|
|
542
469
|
if (!onboarding && !hasProject) {
|
|
543
470
|
console.log(`machine already onboarded (${machinePath}) — pass --project to set up a repo`);
|
|
544
471
|
return;
|
|
@@ -547,7 +474,6 @@ Examples:
|
|
|
547
474
|
let secret = '';
|
|
548
475
|
const baseUrl = opts.baseUrl ?? '';
|
|
549
476
|
if (onboarding) {
|
|
550
|
-
// Full onboarding: base-url + user-id + secret are required here.
|
|
551
477
|
if (baseUrl.trim() === '') {
|
|
552
478
|
throw new Error('--base-url is required when onboarding this machine (no machine context yet, or --reonboard)');
|
|
553
479
|
}
|
|
@@ -558,9 +484,6 @@ Examples:
|
|
|
558
484
|
throw new Error('--user-id is required (or run in a TTY to be prompted)');
|
|
559
485
|
}
|
|
560
486
|
}
|
|
561
|
-
// The secret lives in the machine context; resolve it only when that
|
|
562
|
-
// context does not already carry one (existing values are never
|
|
563
|
-
// overwritten). Source: --secret-env value, else a TTY prompt.
|
|
564
487
|
if (typeof existing.client_secret !== 'string' ||
|
|
565
488
|
existing.client_secret.trim() === '') {
|
|
566
489
|
secret =
|
|
@@ -593,13 +516,21 @@ Examples:
|
|
|
593
516
|
skipMachine: !onboarding,
|
|
594
517
|
skipCommitted: !hasProject,
|
|
595
518
|
});
|
|
596
|
-
//
|
|
519
|
+
// Scaffold the sibling connection config (project-local gaia.config.js)
|
|
520
|
+
// next to the engine config when setting up a repo.
|
|
521
|
+
let wroteConnection = false;
|
|
522
|
+
let connectionPath = '';
|
|
523
|
+
if (hasProject) {
|
|
524
|
+
const conn = scaffoldGaiaConfig(opts.config, opts.force);
|
|
525
|
+
wroteConnection = conn.wrote;
|
|
526
|
+
connectionPath = conn.path;
|
|
527
|
+
}
|
|
597
528
|
if (!onboarding) {
|
|
598
529
|
console.log(`machine context found (${machinePath}) → setting up project only`);
|
|
599
530
|
if (typeof existing.base_url !== 'string' ||
|
|
600
531
|
existing.base_url.trim() === '') {
|
|
601
532
|
console.log(`warning: ${machinePath} exists but looks incomplete (no base_url) — ` +
|
|
602
|
-
|
|
533
|
+
'run with --reonboard to fill the machine context');
|
|
603
534
|
}
|
|
604
535
|
}
|
|
605
536
|
else if (!hasProject) {
|
|
@@ -607,13 +538,30 @@ Examples:
|
|
|
607
538
|
}
|
|
608
539
|
else {
|
|
609
540
|
console.log(opts.reonboard
|
|
610
|
-
?
|
|
611
|
-
:
|
|
541
|
+
? 're-onboarding this machine (--reonboard)'
|
|
542
|
+
: 'no machine context → onboarding this machine');
|
|
612
543
|
}
|
|
613
544
|
if (hasProject) {
|
|
614
545
|
console.log(res.wroteCommitted
|
|
615
|
-
? `wrote ${res.committedPath}`
|
|
546
|
+
? `wrote ${res.committedPath} (engine)`
|
|
616
547
|
: `kept ${res.committedPath} (exists — pass --force to replace)`);
|
|
548
|
+
console.log(wroteConnection
|
|
549
|
+
? `wrote ${connectionPath} (connection)`
|
|
550
|
+
: `kept ${connectionPath} (exists — pass --force to replace)`);
|
|
551
|
+
// GAIA-216: make the split-config seed reachable from `init` itself —
|
|
552
|
+
// migrate a legacy machine context and seed the home connection config
|
|
553
|
+
// so an install can never be left with an engine config and no
|
|
554
|
+
// connection config. Idempotent: a fresh init reports these as kept.
|
|
555
|
+
// `home` is derived from the machine-context path (canonical
|
|
556
|
+
// `<home>/.gaia/machine.config.js`) so the home-side steps target the
|
|
557
|
+
// same home the context lives in — real home in production, isolated
|
|
558
|
+
// when `--machine-path` overrides it.
|
|
559
|
+
const reach = runUpgrade({
|
|
560
|
+
cwd: dirname(dirname(opts.config)),
|
|
561
|
+
home: dirname(dirname(machinePath)),
|
|
562
|
+
});
|
|
563
|
+
for (const line of reach.actions)
|
|
564
|
+
console.log(line);
|
|
617
565
|
}
|
|
618
566
|
const m = res.machine;
|
|
619
567
|
if (onboarding) {
|
|
@@ -623,93 +571,50 @@ Examples:
|
|
|
623
571
|
? `updated ${m.path} (filled: ${m.filledKeys.join(', ')})`
|
|
624
572
|
: `kept ${m.path} (already complete)`);
|
|
625
573
|
}
|
|
626
|
-
console.log(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
574
|
+
console.log('\nNext steps:\n' +
|
|
575
|
+
' gaia dropsh auth login --provider session\n' +
|
|
576
|
+
' gaia dropsh auth login --provider pm\n' +
|
|
577
|
+
' gaia dropsh auth status # both profiles present');
|
|
630
578
|
});
|
|
631
|
-
return program;
|
|
632
579
|
}
|
|
580
|
+
/** The `conductor` command plugin the host mounts (GAIA-201). It also registers
|
|
581
|
+
* the `gaia deployment` batch helper (conductor-scoped: needs remote + project). */
|
|
582
|
+
const conductorCommandPlugin = {
|
|
583
|
+
kind: 'command',
|
|
584
|
+
name: 'conductor',
|
|
585
|
+
describe: 'node-agent lifecycle + local registry',
|
|
586
|
+
register(program, host) {
|
|
587
|
+
const deps = { host };
|
|
588
|
+
program.addCommand(createConductorCommand(deps));
|
|
589
|
+
registerDeployment(program, async () => {
|
|
590
|
+
const config = await resolveConfig(deps);
|
|
591
|
+
return resolveRemote(deps, config);
|
|
592
|
+
}, async () => {
|
|
593
|
+
const config = await resolveConfig(deps);
|
|
594
|
+
return config.project;
|
|
595
|
+
}, () => process.cwd());
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
export default conductorCommandPlugin;
|
|
633
599
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* When a config is loadable, mounts the dropsh program built with the
|
|
637
|
-
* conductor's plugins (auth providers, etc.). Best-effort: if no config is
|
|
638
|
-
* loadable, the passthrough is simply not mounted.
|
|
639
|
-
*
|
|
640
|
-
* dropsh's own commands (e.g. `auth login`) reload their config from
|
|
641
|
-
* `--config` / `$DROPSH_CONFIG` / the `dropsh.config.js` default — they do not
|
|
642
|
-
* see the conductor config we already loaded. Default `$DROPSH_CONFIG` to the
|
|
643
|
-
* conductor's own config path so `gaia dropsh auth login` uses the same site +
|
|
644
|
-
* plugins without an explicit `--config` flag. An existing `$DROPSH_CONFIG`
|
|
645
|
-
* (or a `--config` flag) still wins.
|
|
600
|
+
* Build a standalone program with the conductor command mounted — the test entry
|
|
601
|
+
* (mirrors what the host does for `gaia conductor …`). `deps` inject fakes.
|
|
646
602
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
plugins: config?.plugins ?? [],
|
|
660
|
-
});
|
|
661
|
-
if (!config) {
|
|
662
|
-
// Registration is unconditional; a config problem must surface WHEN a dropsh
|
|
663
|
-
// command runs, not make the command vanish. The hook fires only on a real
|
|
664
|
-
// subcommand dispatch — never for `--help` — so discoverability holds.
|
|
665
|
-
dropsh.hook('preSubcommand', async (thisCommand) => {
|
|
666
|
-
const override = thisCommand.opts().config ??
|
|
667
|
-
process.env.DROPSH_CONFIG;
|
|
668
|
-
const resolved = defaultConfigPath(override);
|
|
669
|
-
let ok = false;
|
|
670
|
-
try {
|
|
671
|
-
await loadConductorConfig(resolved);
|
|
672
|
-
ok = true;
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
ok = false;
|
|
676
|
-
}
|
|
677
|
-
if (!ok) {
|
|
678
|
-
throw new Error(dropshConfigHint(resolved));
|
|
679
|
-
}
|
|
680
|
-
if (!process.env.DROPSH_CONFIG) {
|
|
681
|
-
process.env.DROPSH_CONFIG = resolved;
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
program.addCommand(dropsh);
|
|
686
|
-
}
|
|
687
|
-
export async function runGaiaCli(argv, deps = {}) {
|
|
688
|
-
const program = buildProgram(deps);
|
|
689
|
-
await attachDropsh(program, deps);
|
|
690
|
-
try {
|
|
691
|
-
await program.parseAsync(argv, { from: 'user' });
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
// The mounted dropsh program calls exitOverride(), so commander surfaces
|
|
695
|
-
// help/errors as a thrown CommanderError instead of exiting the process.
|
|
696
|
-
// `--help`/`--version` are clean exits; commander-generated errors have
|
|
697
|
-
// already written their message to stderr, so only report our own thrown
|
|
698
|
-
// errors (e.g. the dropsh missing-config hint) here.
|
|
699
|
-
const e = err;
|
|
700
|
-
if (e.code === 'commander.helpDisplayed' ||
|
|
701
|
-
e.code === 'commander.version') {
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (typeof e.code === 'string' && e.code.startsWith('commander.')) {
|
|
705
|
-
process.exitCode = process.exitCode ?? 1;
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
process.stderr.write(`${e.message ?? String(err)}\n`);
|
|
709
|
-
process.exitCode = process.exitCode ?? 1;
|
|
710
|
-
}
|
|
603
|
+
export function buildConductorProgram(deps = {}) {
|
|
604
|
+
const program = new Command();
|
|
605
|
+
program.name('gaia').description('GAIA conductor + client CLI');
|
|
606
|
+
program.addCommand(createConductorCommand(deps));
|
|
607
|
+
registerDeployment(program, async () => {
|
|
608
|
+
const config = await resolveConfig(deps);
|
|
609
|
+
return resolveRemote(deps, config);
|
|
610
|
+
}, async () => {
|
|
611
|
+
const config = await resolveConfig(deps);
|
|
612
|
+
return config.project;
|
|
613
|
+
}, () => process.cwd());
|
|
614
|
+
return program;
|
|
711
615
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
616
|
+
/** Parse argv against the conductor program (test entry). */
|
|
617
|
+
export async function runConductorCli(argv, deps = {}) {
|
|
618
|
+
const program = buildConductorProgram(deps);
|
|
619
|
+
await program.parseAsync(argv, { from: 'user' });
|
|
715
620
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentCandidate, type ConductorEngineConfig, type ConductorFileConfig, type GaiaConnectionConfig } from '@gaia-ai/core';
|
|
2
|
+
export { findGaiaDir, resolveConfigPath, } from '@gaia-ai/core';
|
|
2
3
|
/**
|
|
3
|
-
* Default agent prompt - the
|
|
4
|
+
* Default agent prompt - the project instruction contract. One run works EXACTLY one state;
|
|
4
5
|
* the agent must stop instead of running the whole flow in one session. The run
|
|
5
6
|
* is closed automatically when the ticket state changes on the next claim - the
|
|
6
7
|
* agent does not release it. Run mechanics live here (not in the repo's
|
|
7
|
-
* WORKFLOW.md). The prompt
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* intake line unprinted unless an external skill-forcing hook happened to fire. A
|
|
11
|
-
* conductor config may override via the `prompt` field.
|
|
8
|
+
* WORKFLOW.md). The prompt points directly at the matching project-owned state
|
|
9
|
+
* section and does not prescribe an agent skill. A conductor config may override
|
|
10
|
+
* it via the `prompt` field.
|
|
12
11
|
*
|
|
13
12
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
14
13
|
* ticket content into a single typed pane line overran the PTY canonical line
|
|
@@ -31,7 +30,7 @@ import type { AgentCandidate, ConductorFileConfig } from '@gaia-ai/core';
|
|
|
31
30
|
* wrapper, GAIA-118, was the thing that re-inflated the line past the cap and
|
|
32
31
|
* truncated it mid-quote).
|
|
33
32
|
* Placeholders: `{identifier}`, `{state}`, `{runUuid}` ({state} falls back to
|
|
34
|
-
* `
|
|
33
|
+
* `qualification` for unclassified tickets).
|
|
35
34
|
*/
|
|
36
35
|
export declare const DEFAULT_AGENT_PROMPT: string;
|
|
37
36
|
/**
|
|
@@ -44,19 +43,47 @@ export declare const DEFAULT_AGENT_PROMPT: string;
|
|
|
44
43
|
*/
|
|
45
44
|
export declare function resolveAgents(raw: unknown, configPath: string): Promise<AgentCandidate | AgentCandidate[]>;
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
46
|
+
* Load the ENGINE half of a conductor config (GAIA-201 split model). Reads
|
|
47
|
+
* remote/executor/agent/workspace + project/states/machine_id/label/prompt/
|
|
48
|
+
* scheduler/hooks — and deliberately NOT `site` or the auth `plugins`, which now
|
|
49
|
+
* live in a `gaia.config.js` connection config. The conductor command composes
|
|
50
|
+
* the two via `composeConductorConfig`. A legacy `conductor.config.js` still
|
|
51
|
+
* carrying `site`/`plugins` loads fine — those two keys are simply ignored here
|
|
52
|
+
* (the connection loader reads them, back-compat).
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadConductorConfig(configFile: string): Promise<ConductorEngineConfig>;
|
|
55
|
+
/**
|
|
56
|
+
* Compose the full `ConductorFileConfig` the engine consumes from the engine
|
|
57
|
+
* half (`loadConductorConfig`) and the connection half (`loadGaiaConfig`). The
|
|
58
|
+
* connection supplies `site` + the auth `plugins`; `ensureAuthenticated` /
|
|
59
|
+
* `selectRemote` read them exactly as before — the fields just arrive from
|
|
60
|
+
* `gaia.config.js` now.
|
|
61
|
+
*/
|
|
62
|
+
export declare function composeConductorConfig(engine: ConductorEngineConfig, connection: Pick<GaiaConnectionConfig, 'site' | 'plugins'>): ConductorFileConfig;
|
|
63
|
+
/**
|
|
64
|
+
* The conductor **stems** in a `.gaia/` dir — the file naming predicate of
|
|
65
|
+
* GAIA-137: exactly `conductor.config.js` (stem `conductor`) or
|
|
66
|
+
* `<variant>.conductor.config.js` (stem `<variant>`). The resolution itself
|
|
67
|
+
* lives in `@gaia-ai/core` (re-exported above); this is the enumeration.
|
|
68
|
+
*/
|
|
69
|
+
export declare function configStems(gaiaDir: string): string[];
|
|
70
|
+
/** Map a conductor stem back to its file name. */
|
|
71
|
+
export declare function fileForStem(stem: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* List the conductor-config **file names** in a `.gaia/` dir, using the same
|
|
74
|
+
* naming predicate the loader uses (`configStems`). This is the only supported
|
|
75
|
+
* enumeration for the `@gaia/upgrade-project` states-strip: it visits the
|
|
76
|
+
* default `conductor.config.js` + every `<variant>.conductor.config.js`, and
|
|
77
|
+
* never an unrelated JS config (`vite.config.js`, the near-miss
|
|
78
|
+
* `myconductor.config.js`).
|
|
79
|
+
*/
|
|
80
|
+
export declare function listConductorConfigFiles(gaiaDir: string): string[];
|
|
81
|
+
/**
|
|
82
|
+
* Remove the obsolete `states:` array property from a conductor-config JS
|
|
83
|
+
* source (GAIA-207 made `states` optional — empty ⇒ serve all claimable
|
|
84
|
+
* states — so `@gaia/upgrade-project` drops the now-dead key). Pure string
|
|
85
|
+
* transform: it strips the `states: [ ... ]` property (line form or inline in
|
|
86
|
+
* an object literal) with its trailing comma, and rewrites nothing else. A
|
|
87
|
+
* `substates:`/other key containing "states" is not matched.
|
|
60
88
|
*/
|
|
61
|
-
export declare function
|
|
62
|
-
export declare function loadConductorConfig(configFile: string): Promise<ConductorFileConfig>;
|
|
89
|
+
export declare function stripStatesFromConfigSource(source: string): string;
|