@gaia-ai/conductor 0.5.4 → 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.
@@ -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, buildProgram as buildDropshProgram } from 'dropsh';
8
- import { loadConductorConfig, resolveConfigPath } from '../config.js';
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
- import { machineContextPath, readMachineContext, scaffold, } from './init.js';
11
- import * as registry from './local-registry.js';
12
- import { fetchUpdateNotice, printVersionLine, resolveCliVersion, runUpdate, } from './version-check.js';
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
- * Default config path. `--config` / `$GAIA_CONDUCTOR_CONFIG` win; otherwise walk
15
- * up from cwd to the nearest `.gaia/conductor.config.js` (git/eslint style), so a
16
- * `gaia` command works from any subdirectory of a project/worktree. Throws an
17
- * actionable error when nothing is found up the tree (see resolveConfigPath).
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
- return loadConductorConfig(defaultConfigPath(configPathOverride));
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 (loadConductorConfig) throws without it — so lifecycle
37
- * commands read it here, never re-derive. A config not built by the loader that
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 registry.list();
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. Must run
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 registry.get(id);
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 registry.register({
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 registry.get(id);
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
- // Graceful stop: there is no drain phase — the conductor goes offline at once,
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 registry.remove(id);
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 loadConductorConfig(defaultConfigPath());
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
- // --- program ----------------------------------------------------------------
394
- export function buildProgram(deps) {
395
- const program = new Command();
396
- program
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 committed .gaia/conductor.config.js for this repo plus the user-global conductor.config.machine.js context (identity + connection incl. secret)')
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 ~/.config/conductor/conductor.config.machine.js)')
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
- // Mode banner.
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
- `run with --reonboard to fill the machine context`);
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
- ? `re-onboarding this machine (--reonboard)`
611
- : `no machine context → onboarding this machine`);
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(`\nNext steps:\n` +
627
- ` gaia dropsh auth login --provider session\n` +
628
- ` gaia dropsh auth login --provider pm\n` +
629
- ` gaia dropsh auth status # both profiles present`);
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
- * Attach the inherited dropsh CLI as a `gaia dropsh ...` subcommand.
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
- /** Actionable hint shown when a `gaia dropsh` command runs with no loadable config. */
648
- function dropshConfigHint(resolvedPath) {
649
- return (`gaia dropsh: no conductor config could be loaded (looked at ${resolvedPath}).\n` +
650
- `Point at one with --config <path>, set $DROPSH_CONFIG, or run from a directory ` +
651
- `containing .gaia/conductor.config.js.`);
652
- }
653
- async function attachDropsh(program, deps) {
654
- const config = deps.config ?? (await tryConfig(deps));
655
- if (config && !process.env.DROPSH_CONFIG) {
656
- process.env.DROPSH_CONFIG = config.config_path;
657
- }
658
- const dropsh = buildDropshProgram({
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
- export async function main(argv) {
713
- // Drop node + script path; commander parses the rest as user args.
714
- await runGaiaCli(argv.slice(2));
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
  }
@@ -1,14 +1,13 @@
1
- import type { AgentCandidate, ConductorFileConfig } from '@gaia-ai/core';
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 GAIA run contract. One run works EXACTLY one state;
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 routes through the gaia skill (`ticket:run`) rather
8
- * than pointing at WORKFLOW.md directly (GAIA-125): the intake line + state
9
- * engine are a skill mechanic, so a bare "follow WORKFLOW.md" pointer left the
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
- * `triage` for unclassified tickets).
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
- * Resolve the conductor config path from `cwd`.
48
- *
49
- * 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
50
- * 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding a conductor config
51
- * so any subdirectory of a project/worktree resolves the same dir.
52
- * - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves the stem's
53
- * file (`conductor` `conductor.config.js`, else
54
- * `<name>.conductor.config.js`; error listing the stems if absent).
55
- * - No selector: `conductor.config.js` present → it is the default; else
56
- * exactly one config → use it (back-compat); else → error naming the
57
- * stems + the selector.
58
- * 3. No `.gaia/` config anywhere up the tree an actionable `gaia init` error
59
- * (never leak a raw "Cannot find module" from a later import()).
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 resolveConfigPath(override?: string, cwd?: string, conductorName?: string): string;
62
- export declare function loadConductorConfig(configFile: string): Promise<ConductorFileConfig>;
89
+ export declare function stripStatesFromConfigSource(source: string): string;