@gaia-ai/conductor 0.6.0 → 0.6.1

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +42 -7
  3. package/dist/src/cli/config-schema.js +64 -9
  4. package/dist/src/cli/init.js +16 -10
  5. package/dist/src/cli/migrate-addon-names.d.ts +72 -0
  6. package/dist/src/cli/migrate-addon-names.js +318 -0
  7. package/dist/src/cli/upgrade.d.ts +15 -0
  8. package/dist/src/cli/upgrade.js +87 -8
  9. package/dist/src/commands/conductor.d.ts +27 -3
  10. package/dist/src/commands/conductor.js +39 -52
  11. package/dist/src/config.d.ts +15 -1
  12. package/dist/src/config.js +280 -3
  13. package/dist/src/contract.d.ts +8 -0
  14. package/dist/src/contract.js +16 -0
  15. package/dist/src/core/conductor.d.ts +16 -1
  16. package/dist/src/core/conductor.js +23 -1
  17. package/dist/src/index.d.ts +5 -4
  18. package/dist/src/index.js +17 -3
  19. package/dist/src/plugins/agent.d.ts +61 -0
  20. package/dist/src/plugins/agent.js +11 -0
  21. package/dist/src/plugins/executor.d.ts +104 -0
  22. package/dist/src/plugins/executor.js +1 -0
  23. package/dist/src/plugins/plugins.d.ts +60 -0
  24. package/dist/src/plugins/plugins.js +42 -0
  25. package/dist/src/plugins/preset.d.ts +48 -0
  26. package/dist/src/plugins/preset.js +23 -0
  27. package/dist/src/plugins/remote.d.ts +203 -0
  28. package/dist/src/plugins/remote.js +1 -0
  29. package/dist/src/plugins/workspace.d.ts +35 -0
  30. package/dist/src/plugins/workspace.js +1 -0
  31. package/dist/src/preset.d.ts +2 -2
  32. package/dist/src/types.d.ts +65 -0
  33. package/dist/src/types.js +1 -0
  34. package/package.json +4 -3
  35. package/dist/src/cli/conductor-registry.d.ts +0 -7
  36. package/dist/src/cli/conductor-registry.js +0 -6
  37. package/dist/src/cli/deployment.d.ts +0 -34
  38. package/dist/src/cli/deployment.js +0 -63
@@ -2,6 +2,7 @@ import { readdirSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { discoverAddons, emptyContributions, resolveConductorSlots, resolveModuleEslintStyle, } from '@gaia-ai/core';
5
+ import { narrowConductorContributions } from './plugins/preset.js';
5
6
  // GAIA-201: the `.gaia/` walk-up + connection resolution moved to `@gaia-ai/core`
6
7
  // (`resolveConfigPath` / `resolveGaiaConfigPath` / `findGaiaDir`). Re-export the
7
8
  // engine resolver here so existing conductor callers/tests keep importing it
@@ -40,7 +41,7 @@ export { findGaiaDir, resolveConfigPath, } from '@gaia-ai/core';
40
41
  * `qualification` for unclassified tickets).
41
42
  */
42
43
  export const DEFAULT_AGENT_PROMPT = `You are working ticket {identifier}, current state: {state}. Read the ticket and all comments ` +
43
- `(gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
44
+ `(gaia dropsh --format md read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
44
45
  `then read ./WORKFLOW.md and execute only its ordered "State: {state}" section. ` +
45
46
  `Do not start or prepare a later state.`;
46
47
  function requirePlugin(value, kind) {
@@ -169,6 +170,15 @@ export async function loadConductorConfig(configFile) {
169
170
  if (!isRecord(raw)) {
170
171
  throw new Error('conductor config default export must be an object');
171
172
  }
173
+ // GAIA-218 (AC-6): the engine config must not carry connection/auth material.
174
+ // A legacy config still declaring `site`/`plugins` gets exactly ONE actionable
175
+ // warning naming the file + the offending key(s); the values are never merged
176
+ // (`ConductorEngineConfig` omits both structurally).
177
+ const leftover = ['site', 'plugins'].filter((k) => k in raw);
178
+ if (leftover.length > 0) {
179
+ console.warn(`warning: ${configPath} declares ${leftover.join('/')} — ignored; ` +
180
+ 'auth/connection belong in the sibling gaia.config.js (GAIA-218)');
181
+ }
172
182
  const config = raw;
173
183
  const project = requireNonEmptyString(config.project, 'project');
174
184
  // `states` is optional. An empty (or absent) list means the conductor serves
@@ -192,13 +202,18 @@ export async function loadConductorConfig(configFile) {
192
202
  // GAIA-215: discover the conductor `addons: []` surface (last-wins singletons,
193
203
  // agent candidate list). A pre-existing per-slot descriptor still loads and
194
204
  // WINS over a discovered contributor of the same kind (back-compat additive).
195
- const discovered = Array.isArray(config.addons)
205
+ // GAIA-224: `discoverAddons` is surface-AGNOSTIC — it returns opaque
206
+ // accumulators. This is the single, reviewed narrowing seam where the engine
207
+ // reinterprets them as its own concrete conductor contributions; the runtime
208
+ // `.kind` guard inside `resolveConductorSlots` still fails loudly on a
209
+ // mis-declared contribution.
210
+ const discovered = narrowConductorContributions(Array.isArray(config.addons)
196
211
  ? await discoverAddons(config.addons, 'conductor', [
197
212
  pathToFileURL(configPath).href,
198
213
  pathToFileURL(`${process.cwd()}/`).href,
199
214
  import.meta.url,
200
215
  ])
201
- : emptyContributions();
216
+ : emptyContributions());
202
217
  const wired = resolveConductorSlots(discovered);
203
218
  const remote = config.remote !== undefined
204
219
  ? await resolveSlot(config.remote, 'remote', configPath)
@@ -301,3 +316,265 @@ export function listConductorConfigFiles(gaiaDir) {
301
316
  export function stripStatesFromConfigSource(source) {
302
317
  return source.replace(/\n?[ \t]*(?<![A-Za-z0-9_$])states[ \t]*:[ \t]*\[[^\]]*\][ \t]*,?/g, '');
303
318
  }
319
+ // ---------------------------------------------------------------------------
320
+ // GAIA-218 (AC-7): strip legacy connection/auth from an ENGINE config source.
321
+ //
322
+ // Unlike `stripStatesFromConfigSource`'s flat `\[[^\]]*\]` regex (nested-unsafe),
323
+ // this is a balanced-delimiter, comment-aware source transform. The engine
324
+ // config's `plugins: [ … ]` array holds nested `{}` (oauth2 profiles AND
325
+ // `@gaia-ai/addon-essentials`), a sibling `agent: [ … ]` array, and the word
326
+ // "plugins" also appears in comments — so the removal keys on the REAL `site:` /
327
+ // `plugins:` *properties* of the default-export object, scanning each value from
328
+ // its opening delimiter to the matching close while skipping string/comment
329
+ // bytes. It also removes the now-orphaned connection preamble consts
330
+ // (`baseUrl`/`clientId`/`clientSecret`) and a connection-only `loadMachine`
331
+ // (with its `const machine = await loadMachine()`) when they become unreferenced.
332
+ // ---------------------------------------------------------------------------
333
+ const CLOSER = { '{': '}', '[': ']', '(': ')' };
334
+ /** Skip a `//`-line comment; return the index of the terminating newline (or EOF). */
335
+ function skipLineComment(src, i) {
336
+ const nl = src.indexOf('\n', i);
337
+ return nl === -1 ? src.length : nl;
338
+ }
339
+ /** Skip a `/* *\/` block comment; return the index just past it. */
340
+ function skipBlockComment(src, i) {
341
+ const end = src.indexOf('*/', i + 2);
342
+ return end === -1 ? src.length : end + 2;
343
+ }
344
+ /** Skip a string/template literal (honouring `\` escapes and `${ … }`
345
+ * interpolations in backtick strings); return the index just past the close. */
346
+ function skipString(src, i) {
347
+ const quote = src[i];
348
+ i++;
349
+ while (i < src.length) {
350
+ const c = src[i];
351
+ if (c === '\\') {
352
+ i += 2;
353
+ continue;
354
+ }
355
+ if (quote === '`' && c === '$' && src[i + 1] === '{') {
356
+ i = matchDelimiter(src, i + 1) + 1;
357
+ continue;
358
+ }
359
+ if (c === quote)
360
+ return i + 1;
361
+ i++;
362
+ }
363
+ return i;
364
+ }
365
+ /** Given the index of an opening `{`/`[`/`(`, return the index of its matching
366
+ * close, skipping nested delimiters, strings and comments. -1 if unbalanced. */
367
+ function matchDelimiter(src, openIdx) {
368
+ const stack = [CLOSER[src[openIdx]]];
369
+ let i = openIdx + 1;
370
+ while (i < src.length && stack.length > 0) {
371
+ const c = src[i];
372
+ if (c === '/' && src[i + 1] === '/') {
373
+ i = skipLineComment(src, i);
374
+ continue;
375
+ }
376
+ if (c === '/' && src[i + 1] === '*') {
377
+ i = skipBlockComment(src, i);
378
+ continue;
379
+ }
380
+ if (c === "'" || c === '"' || c === '`') {
381
+ i = skipString(src, i);
382
+ continue;
383
+ }
384
+ if (c === '{' || c === '[' || c === '(') {
385
+ stack.push(CLOSER[c]);
386
+ i++;
387
+ continue;
388
+ }
389
+ if (c === '}' || c === ']' || c === ')') {
390
+ if (c === stack[stack.length - 1])
391
+ stack.pop();
392
+ i++;
393
+ continue;
394
+ }
395
+ i++;
396
+ }
397
+ return stack.length === 0 ? i - 1 : -1;
398
+ }
399
+ /** Skip whitespace + comments starting at `i` (bounded by `limit`). */
400
+ function skipTrivia(src, i, limit) {
401
+ while (i < limit) {
402
+ const c = src[i];
403
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
404
+ i++;
405
+ continue;
406
+ }
407
+ if (c === '/' && src[i + 1] === '/') {
408
+ i = skipLineComment(src, i);
409
+ continue;
410
+ }
411
+ if (c === '/' && src[i + 1] === '*') {
412
+ i = skipBlockComment(src, i);
413
+ continue;
414
+ }
415
+ break;
416
+ }
417
+ return i;
418
+ }
419
+ /** Read a property key (bare identifier or quoted string) at `i`. */
420
+ function readKey(src, i) {
421
+ const c = src[i];
422
+ if (c === "'" || c === '"') {
423
+ const end = skipString(src, i);
424
+ return { name: src.slice(i + 1, end - 1), end };
425
+ }
426
+ const m = /^[A-Za-z0-9_$]+/.exec(src.slice(i));
427
+ if (m)
428
+ return { name: m[0], end: i + m[0].length };
429
+ return { end: i };
430
+ }
431
+ /** From `from`, scan to just past the next top-level `,` (or to `limit` when the
432
+ * value runs to the object close), skipping nested delimiters/strings/comments. */
433
+ function scanToTopLevelComma(src, from, limit) {
434
+ let i = from;
435
+ while (i < limit) {
436
+ const c = src[i];
437
+ if (c === '/' && src[i + 1] === '/') {
438
+ i = skipLineComment(src, i);
439
+ continue;
440
+ }
441
+ if (c === '/' && src[i + 1] === '*') {
442
+ i = skipBlockComment(src, i);
443
+ continue;
444
+ }
445
+ if (c === "'" || c === '"' || c === '`') {
446
+ i = skipString(src, i);
447
+ continue;
448
+ }
449
+ if (c === '{' || c === '[' || c === '(') {
450
+ i = matchDelimiter(src, i) + 1;
451
+ continue;
452
+ }
453
+ if (c === ',')
454
+ return i + 1;
455
+ i++;
456
+ }
457
+ return limit;
458
+ }
459
+ /** Locate the `export default { … }` object; return the body span (exclusive of
460
+ * the braces) or undefined when the default export is not an object literal. */
461
+ function findDefaultExportObject(src) {
462
+ const m = /export\s+default\s*/.exec(src);
463
+ if (!m)
464
+ return undefined;
465
+ const braceIdx = m.index + m[0].length;
466
+ if (src[braceIdx] !== '{')
467
+ return undefined;
468
+ const end = matchDelimiter(src, braceIdx);
469
+ if (end === -1)
470
+ return undefined;
471
+ return { bodyStart: braceIdx + 1, bodyEnd: end };
472
+ }
473
+ /** Enumerate the top-level properties of the object body [bodyStart, bodyEnd). */
474
+ function scanTopLevelProperties(src, bodyStart, bodyEnd) {
475
+ const entries = [];
476
+ let i = bodyStart;
477
+ while (i < bodyEnd) {
478
+ const entryStart = i;
479
+ const keyPos = skipTrivia(src, i, bodyEnd);
480
+ if (keyPos >= bodyEnd)
481
+ break;
482
+ const key = readKey(src, keyPos);
483
+ let j = skipTrivia(src, key.end, bodyEnd);
484
+ if (src[j] !== ':') {
485
+ // Not a `key: value` property (spread/computed/shorthand) — keep it.
486
+ const term = scanToTopLevelComma(src, keyPos, bodyEnd);
487
+ entries.push({ key: undefined, start: entryStart, end: term });
488
+ i = term;
489
+ continue;
490
+ }
491
+ j++;
492
+ const term = scanToTopLevelComma(src, j, bodyEnd);
493
+ entries.push({ key: key.name, start: entryStart, end: term });
494
+ i = term;
495
+ }
496
+ return entries;
497
+ }
498
+ /** Count word-boundary occurrences of an identifier in the source. */
499
+ function countIdent(src, name) {
500
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${name}(?![A-Za-z0-9_$])`, 'g');
501
+ return (src.match(re) ?? []).length;
502
+ }
503
+ /** Remove a single-line `const <name> = …;` declaration (no-op if absent). */
504
+ function removeConstDecl(src, name) {
505
+ const re = new RegExp(`^[ \\t]*const ${name}\\b[^\\n]*\\n`, 'm');
506
+ return src.replace(re, '');
507
+ }
508
+ /** Remove an `[async ]function <name>(…) { … }` declaration (balanced body). */
509
+ function removeFunctionDecl(src, name) {
510
+ const re = new RegExp(`(?:async[ \\t]+)?function[ \\t]+${name}[ \\t]*\\(`);
511
+ const m = re.exec(src);
512
+ if (!m)
513
+ return src;
514
+ const braceIdx = src.indexOf('{', m.index);
515
+ if (braceIdx === -1)
516
+ return src;
517
+ const end = matchDelimiter(src, braceIdx);
518
+ if (end === -1)
519
+ return src;
520
+ let e = end + 1;
521
+ if (src[e] === '\n')
522
+ e++;
523
+ return src.slice(0, m.index) + src.slice(e);
524
+ }
525
+ /**
526
+ * Is `machine` referenced anywhere OTHER than its own `const machine = …;` decl
527
+ * and inside the `loadMachine` function body? (The body's `…/machine.config.js`
528
+ * path string must not count as a live reference.) Used to decide whether a
529
+ * connection-only `loadMachine` has become orphaned.
530
+ */
531
+ function machineReferencedElsewhere(src) {
532
+ let probe = removeFunctionDecl(src, 'loadMachine');
533
+ probe = removeConstDecl(probe, 'machine');
534
+ return countIdent(probe, 'machine') > 0;
535
+ }
536
+ /** Drop connection preamble that is orphaned once `site`/`plugins` are gone. */
537
+ function removeOrphanedPreamble(src) {
538
+ let text = src;
539
+ for (const name of ['baseUrl', 'clientId', 'clientSecret']) {
540
+ if (countIdent(text, name) <= 1)
541
+ text = removeConstDecl(text, name);
542
+ }
543
+ // A connection-only `loadMachine`: remove `const machine = await loadMachine()`
544
+ // + the function itself only when `machine` is otherwise unreferenced (so an
545
+ // engine config that still composes `machine_id` from `machine.user_id` keeps
546
+ // its loadMachine — the "connection-only" qualifier).
547
+ if (/\bconst machine\b/.test(text) &&
548
+ /\bfunction loadMachine\b/.test(text) &&
549
+ !machineReferencedElsewhere(text)) {
550
+ text = removeConstDecl(text, 'machine');
551
+ text = removeFunctionDecl(text, 'loadMachine');
552
+ }
553
+ return text;
554
+ }
555
+ /**
556
+ * Strip the `site:` and `plugins:` properties from an engine config's
557
+ * `export default { … }` (with their leading comment/whitespace), plus the
558
+ * now-orphaned connection preamble consts. Returns the rewritten source and the
559
+ * list of object keys removed (`[]` — source returned unchanged — when there is
560
+ * nothing connection-related to remove). remote/executor/agent/workspace/hooks/
561
+ * project/machine_id/loadLocal are left intact.
562
+ */
563
+ export function stripLegacyConnectionFromConfigSource(source) {
564
+ const obj = findDefaultExportObject(source);
565
+ if (!obj)
566
+ return { text: source, removed: [] };
567
+ const entries = scanTopLevelProperties(source, obj.bodyStart, obj.bodyEnd);
568
+ const targets = new Set(['site', 'plugins']);
569
+ const toRemove = entries.filter((e) => e.key !== undefined && targets.has(e.key));
570
+ if (toRemove.length === 0)
571
+ return { text: source, removed: [] };
572
+ let text = source;
573
+ for (const e of [...toRemove].sort((a, b) => b.start - a.start)) {
574
+ text = text.slice(0, e.start) + text.slice(e.end);
575
+ }
576
+ text = removeOrphanedPreamble(text);
577
+ // Report in source order for a stable `site/plugins` message.
578
+ const removed = ['site', 'plugins'].filter((k) => toRemove.some((e) => e.key === k));
579
+ return { text, removed };
580
+ }
@@ -0,0 +1,8 @@
1
+ export { type AgentFootprint, emptyAgentFootprint, type GaiaAgent, } from './plugins/agent.js';
2
+ export type { ExecutorCapabilities, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput, } from './plugins/executor.js';
3
+ export type { AgentCandidate, AgentPlugin, ExecutorDeps, ExecutorPlugin, RemotePlugin, ResolvedAgent, WorkspacePlugin, } from './plugins/plugins.js';
4
+ export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
5
+ export { type ConductorAddonEntry, type ConductorContributions, narrowConductorContributions, type Preset, } from './plugins/preset.js';
6
+ export type { ActiveRun, ClaimedRun, ClaimOptions, ConductorRegistration, ConductorStatus, FinalizableRun, GaiaRemote, RunMetrics, RunWriteAttributes, Ticket, UncleanTicket, } from './plugins/remote.js';
7
+ export type { EnsuredWorkspace, GaiaWorkspace, } from './plugins/workspace.js';
8
+ export type { ConductorEngineConfig, ConductorFileConfig, ConductorSettings, } from './types.js';
@@ -0,0 +1,16 @@
1
+ // GAIA-224 (Finding 6): the CONDUCTOR-SURFACE CONTRACT, exposed as the
2
+ // runtime-light subpath `@gaia-ai/conductor/contract`.
3
+ //
4
+ // This is the module a conductor-surface addon (`addons/remote-drupal`,
5
+ // `addons/workspace-git`, `addons/herdr`, `addons/claude`, …) imports. It carries
6
+ // ONLY the surface interfaces, the config contract, the preset view, and the two
7
+ // tiny runtime helpers (`emptyAgentFootprint`, the `select*` slot selectors) — it
8
+ // imports NO engine, NO commander, NO dropsh runtime. Importing it therefore
9
+ // costs an addon nothing at boot, unlike the package main (`@gaia-ai/conductor`),
10
+ // which pulls the whole run engine + the `conductor` command plugin.
11
+ //
12
+ // The package main re-exports everything here too, so `import type { GaiaRemote }
13
+ // from '@gaia-ai/conductor'` keeps working for type-only consumers.
14
+ export { emptyAgentFootprint, } from './plugins/agent.js';
15
+ export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
16
+ export { narrowConductorContributions, } from './plugins/preset.js';
@@ -1,4 +1,9 @@
1
- import { type ConductorFileConfig, type ConductorLogger, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace, type ResolvedAgent } from '@gaia-ai/core';
1
+ import { type ConductorLogger } from '@gaia-ai/core';
2
+ import type { GaiaExecutor } from '../plugins/executor.js';
3
+ import { type ResolvedAgent } from '../plugins/plugins.js';
4
+ import type { GaiaRemote } from '../plugins/remote.js';
5
+ import type { GaiaWorkspace } from '../plugins/workspace.js';
6
+ import type { ConductorFileConfig } from '../types.js';
2
7
  /**
3
8
  * Resolve the environment a run executes in (GAIA-99): parse the ticket's
4
9
  * effective env_vars, drop any reserved key (loud warn — key NAME only, never
@@ -66,5 +71,15 @@ export declare class Conductor {
66
71
  reap(): Promise<void>;
67
72
  private dispatch;
68
73
  serve(signal?: AbortSignal): Promise<void>;
74
+ /**
75
+ * True once this conductor's own checkout has been removed from disk (its
76
+ * worktree was reaped while the process kept running). Such a conductor is a
77
+ * pure liability: it still heartbeats and still claims runs, but every
78
+ * dispatch fails — no git command and no relative path can resolve from a
79
+ * deleted directory — so each claim burns one attempt and three of them park
80
+ * the ticket behind the circuit breaker. Observed as 30 orphans claiming and
81
+ * failing tickets they could never dispatch.
82
+ */
83
+ private checkoutGone;
69
84
  private pollLoop;
70
85
  }
@@ -1,4 +1,6 @@
1
- import { conductorId, selectAgent, } from '@gaia-ai/core';
1
+ import { existsSync } from 'node:fs';
2
+ import { conductorId } from '@gaia-ai/core';
3
+ import { selectAgent } from '../plugins/plugins.js';
2
4
  function sleep(ms, signal) {
3
5
  return new Promise((resolve) => {
4
6
  if (signal?.aborted) {
@@ -433,8 +435,28 @@ export class Conductor {
433
435
  async serve(signal) {
434
436
  await this.pollLoop(signal);
435
437
  }
438
+ /**
439
+ * True once this conductor's own checkout has been removed from disk (its
440
+ * worktree was reaped while the process kept running). Such a conductor is a
441
+ * pure liability: it still heartbeats and still claims runs, but every
442
+ * dispatch fails — no git command and no relative path can resolve from a
443
+ * deleted directory — so each claim burns one attempt and three of them park
444
+ * the ticket behind the circuit breaker. Observed as 30 orphans claiming and
445
+ * failing tickets they could never dispatch.
446
+ */
447
+ checkoutGone() {
448
+ return !existsSync(this.checkoutRoot);
449
+ }
436
450
  async pollLoop(signal) {
437
451
  while (!signal?.aborted) {
452
+ if (this.checkoutGone()) {
453
+ // Stop claiming and let the loop end — the process exits and the cron
454
+ // reaper flips status to offline on lease expiry, same as any crash.
455
+ // Deliberately NOT an offline write from here: the loop never owns that
456
+ // transition (see the note at the end of this method).
457
+ this.logger.error({ conductorId: this.id, workspace: this.checkoutRoot }, 'checkout is gone — stopping conductor instead of claiming runs it cannot dispatch');
458
+ return;
459
+ }
438
460
  try {
439
461
  await this.tick();
440
462
  }
@@ -1,9 +1,10 @@
1
1
  export type * from '@gaia-ai/core';
2
+ export type { ConductorRegistryEntry } from '@gaia-ai/core';
2
3
  export { conductorId } from '@gaia-ai/core';
3
- export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
4
- export type { ConductorRegistryEntry } from './cli/conductor-registry.js';
5
4
  export { renderGaiaConfig } from './cli/init.js';
6
- export { runConnectionUpgrade, runUpgrade, type UpgradeReport, } from './cli/upgrade.js';
5
+ export { type AddonRenameResult, type ConfigSurface, migrateAddonNames, migrateAddonNamesInFile, RENAMED_ADDONS, runAddonRenameMigration, } from './cli/migrate-addon-names.js';
6
+ export { hasProjectConnection, type ProjectConnectionChoice, runConnectionUpgrade, runUpgrade, type UpgradeReport, } from './cli/upgrade.js';
7
7
  export { default as conductorCommandPlugin, type GaiaCliDeps, runConductorCli, } from './commands/conductor.js';
8
- export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, } from './config.js';
8
+ export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, stripLegacyConnectionFromConfigSource, } from './config.js';
9
+ export * from './contract.js';
9
10
  export { Conductor } from './core/conductor.js';
package/dist/src/index.js CHANGED
@@ -1,14 +1,28 @@
1
1
  export { conductorId } from '@gaia-ai/core';
2
- export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
3
2
  // GAIA-201: the connection-config template, reused by `gaia upgrade` to seed a
4
3
  // `gaia.config.js`.
5
4
  export { renderGaiaConfig } from './cli/init.js';
5
+ // GAIA-224 (Finding 8b): the addon package-name rename pass `gaia upgrade` runs
6
+ // as its last step (`@gaia-ai/plugin-*` + the deleted `@gaia-ai/core/builtins`
7
+ // and `@gaia-ai/core/plugins` → `@gaia-ai/addon-*`).
8
+ export { migrateAddonNames, migrateAddonNamesInFile, RENAMED_ADDONS, runAddonRenameMigration, } from './cli/migrate-addon-names.js';
6
9
  // GAIA-216: the seed/migration routine, hoisted from the host so `conductor init`
7
10
  // (intra-package) and `gaia upgrade` (host→engine) share one implementation.
8
- export { runConnectionUpgrade, runUpgrade, } from './cli/upgrade.js';
11
+ export { hasProjectConnection, runConnectionUpgrade, runUpgrade, } from './cli/upgrade.js';
9
12
  // GAIA-201: the conductor is now a COMMAND PLUGIN mounted by the `@gaia-ai/gaia`
10
13
  // host, not the CLI entrypoint. `main`/`runGaiaCli` are gone; the default export
11
14
  // is the `GaiaCommandPlugin`, exposed here as `./commands/conductor` too.
12
15
  export { default as conductorCommandPlugin, runConductorCli, } from './commands/conductor.js';
13
- export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, } from './config.js';
16
+ // GAIA-218: the balanced strip is exported for the host/tests + reuse.
17
+ export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, stripLegacyConnectionFromConfigSource, } from './config.js';
18
+ // GAIA-224 (Finding 6): this package now OWNS the conductor-surface contract —
19
+ // the interfaces, the config types, the preset view and the slot selectors — and
20
+ // re-exports the whole of it here. The runtime-light subpath
21
+ // `@gaia-ai/conductor/contract` is the one an ADDON should import (this main
22
+ // entry pulls the engine + the command plugin). The built-in IMPLEMENTATIONS it
23
+ // used to re-export from the deleted `@gaia-ai/core/plugins` barrel now live in
24
+ // their own addons (`@gaia-ai/addon-remote-drupal`,
25
+ // `@gaia-ai/addon-workspace-git`, `@gaia-ai/addon-fake`) and are deliberately NOT
26
+ // re-exported: the engine must not edge an addon (acyclic layer rule).
27
+ export * from './contract.js';
14
28
  export { Conductor } from './core/conductor.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Per-agent effort footprint parsed from that agent's run transcript
3
+ * (GAIA-132). The metrics are transcript-derived and therefore
4
+ * **agent-specific** (each agent's transcript has its own format), so parsing
5
+ * lives behind the agent abstraction (see {@link GaiaAgent.parseFootprint}) —
6
+ * the conductor stays agent-agnostic and writes the footprint verbatim.
7
+ * `duration_s` is likewise derived from the transcript (its first→last entry
8
+ * timestamps), NOT recomputed by the conductor from a re-read `started_at`
9
+ * (GAIA-151): the log is the single source of the run's wall-clock length, so
10
+ * there is no timestamp round-trip through JSON:API to mis-parse.
11
+ */
12
+ export interface AgentFootprint {
13
+ /** Total tokens across every usage bucket of every assistant turn. */
14
+ tokens: number;
15
+ /**
16
+ * Wall-clock run length in seconds, derived from the transcript's first→last
17
+ * entry timestamps (GAIA-151). 0 when the transcript has fewer than two
18
+ * timestamped entries (empty/absent log).
19
+ */
20
+ duration_s: number;
21
+ /** Number of assistant turns in the transcript. */
22
+ agent_turns: number;
23
+ /** Number of tool-use calls across all assistant turns. */
24
+ tool_calls: number;
25
+ /** Number of user-submitted prompts. */
26
+ user_prompts: number;
27
+ /** Total words across those user prompts. */
28
+ user_prompt_words: number;
29
+ /** Model of the last assistant turn, when present (informational). */
30
+ model?: string;
31
+ }
32
+ /** An all-zero footprint — the honest result for an empty/absent transcript. */
33
+ export declare function emptyAgentFootprint(): AgentFootprint;
34
+ /**
35
+ * A GAIA agent: the program the conductor runs to work a ticket (e.g. claude).
36
+ * It knows how it is launched (CLI + model + flags) and where its per-run
37
+ * transcript/log lives — so the conductor stays agent-agnostic and the CLI can
38
+ * attach the run log on release without agent-specific knowledge.
39
+ */
40
+ export interface GaiaAgent {
41
+ /** Stable id, e.g. 'claude'. */
42
+ readonly id: string;
43
+ /**
44
+ * Build the full agent CLI invocation for a GAIA prompt. With an empty
45
+ * prompt, returns the bare launch command (no prompt argument).
46
+ */
47
+ launchCommand(prompt: string): string;
48
+ /**
49
+ * Locate and read this agent's run transcript for a run that executed in
50
+ * `worktreePath`. MUST return '' (never throw) when nothing is found.
51
+ */
52
+ getRunLog(worktreePath: string): Promise<string>;
53
+ /**
54
+ * Parse this agent's run transcript (as returned by {@link getRunLog}) into
55
+ * a footprint of effort metrics (GAIA-132). The transcript format is
56
+ * agent-specific, so each agent owns its own parser. MUST be tolerant of
57
+ * blank/partial/absent input (never throw) — an empty log yields
58
+ * {@link emptyAgentFootprint}.
59
+ */
60
+ parseFootprint(log: string): AgentFootprint;
61
+ }
@@ -0,0 +1,11 @@
1
+ /** An all-zero footprint — the honest result for an empty/absent transcript. */
2
+ export function emptyAgentFootprint() {
3
+ return {
4
+ tokens: 0,
5
+ duration_s: 0,
6
+ agent_turns: 0,
7
+ tool_calls: 0,
8
+ user_prompts: 0,
9
+ user_prompt_words: 0,
10
+ };
11
+ }
@@ -0,0 +1,104 @@
1
+ export interface SpawnedSession {
2
+ sessionRef: string;
3
+ }
4
+ export interface ExecutorCapabilities {
5
+ persistent: boolean;
6
+ }
7
+ export interface SpawnRunInput {
8
+ ticket: {
9
+ uuid: string;
10
+ identifier: string;
11
+ title: string;
12
+ branchName: string;
13
+ state: string;
14
+ url?: string;
15
+ };
16
+ run: {
17
+ uuid: string;
18
+ id: number;
19
+ handler: string;
20
+ };
21
+ workspacePath: string;
22
+ instructions: {
23
+ path: string;
24
+ sha256: string;
25
+ text: string;
26
+ } | null;
27
+ command?: string;
28
+ env?: Record<string, string>;
29
+ }
30
+ /** The four lifecycle-hook slots, keyed exactly like `config.hooks`. */
31
+ export type HookName = 'after_create' | 'before_run' | 'after_run' | 'after_done';
32
+ /** Context for a hook invocation — carries the ticket for observable logging. */
33
+ export interface HookContext {
34
+ /** The ticket identifier/uuid the hook is running for (log context). */
35
+ ticket: string;
36
+ }
37
+ export interface GaiaExecutor {
38
+ id: string;
39
+ capabilities(): ExecutorCapabilities;
40
+ /**
41
+ * Run the lifecycle hook `name` (command from `config.hooks[name]`) in `cwd`,
42
+ * best-effort. This is the SINGLE catch point for all lifecycle hooks:
43
+ *
44
+ * - MUST NEVER throw. No configured command → silent no-op. A failing command
45
+ * → `logger.error({hook,worktree,ticket,err}, 'lifecycle hook failed')` then
46
+ * return. So a hook failure never aborts dispatch or wedges a run in
47
+ * `claimed`.
48
+ * - The underlying shell command still fails honestly (a non-zero exit
49
+ * rejects); only the executor catches + logs + continues.
50
+ *
51
+ * `env`, when given, is the run's resolved environment (per-ticket env_vars
52
+ * merged with the core GAIA_* vars, GAIA-99), merged over the hook process's
53
+ * inherited env. Values may be sensitive — implementations must never log
54
+ * them (log key names only).
55
+ */
56
+ runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
57
+ startRun(input: SpawnRunInput): Promise<SpawnedSession>;
58
+ /**
59
+ * Signal the run's agent to stop. Called by the conductor's run-finalise pass
60
+ * BEFORE it captures the agent transcript (GAIA-132), so a stop must not
61
+ * destroy the log. For herdr this is a NO-OP: at finalise time the agent is
62
+ * idle and its jsonl transcript is already fully written to disk, and the
63
+ * subsequent {@link cleanupRun} tab-close kills the PTY (the agent dies with
64
+ * it). The seam exists for executors whose agent outlives its UI surface.
65
+ * Best-effort; gated by the conductor on `capabilities().persistent`.
66
+ */
67
+ stopRun(branch: string): Promise<void>;
68
+ /**
69
+ * Tear down ONE run's hosted UI surface — for herdr, close only the tab whose
70
+ * label carries the `#<runId>` token (`startRun` labels every tab
71
+ * `<identifier> · <state> #<run.id>`), via `herdr tab close <tab_id>` (killing
72
+ * that PTY). Run-scoped by design (GAIA-183): a sibling run's tab in the same
73
+ * branch workspace — a still-open prior-state run or a concurrent re-claim —
74
+ * and any non-run tab are left untouched. Called as the LAST finalisation step,
75
+ * strictly after the transcript is captured, and again at dispatch to clear a
76
+ * reused workspace's leftover tab for the run being (re-)started. No matching
77
+ * tab → a quiet no-op. Does NOT touch the branch worktree (that is the
78
+ * reap/{@link removeWorktree} lifecycle). Best-effort: a failing tab-close is
79
+ * swallowed and never aborts finalisation or dispatch. A no-op for
80
+ * non-persistent executors.
81
+ */
82
+ cleanupRun(branch: string, runId: number): Promise<void>;
83
+ /**
84
+ * Tear down the branch's entire worktree (git worktree + hosted workspace),
85
+ * reclaiming its disk. Called by the conductor's ticket-cleanup pass once a
86
+ * ticket is done. A no-op for non-persistent executors (no hosted workspace);
87
+ * the conductor gates the call on `capabilities().persistent`.
88
+ *
89
+ * `worktreePath` is the stable, identifier-derived worktree path (from the
90
+ * ticket's latest run). Prefer it over `branch` to resolve the worktree: the
91
+ * checked-out branch is mutable (the coding agent may rename/switch it), the
92
+ * path is not.
93
+ *
94
+ * Returns whether the worktree was actually present on THIS host and torn
95
+ * down (or reclaimed on disk) — i.e. whether the teardown happened locally.
96
+ * `false` means nothing matched here: the worktree is either already gone or
97
+ * lives on another conductor's host. The reaper uses this to avoid marking a
98
+ * ticket `cleaned_up` for a worktree it did not actually tear down (a
99
+ * cross-host false-teardown), leaving it for the host that physically holds
100
+ * it. A genuine failure still throws (surfaced as a teardown miss); a `false`
101
+ * return is a clean "not here", not an error.
102
+ */
103
+ removeWorktree(branch: string, worktreePath?: string): Promise<boolean>;
104
+ }
@@ -0,0 +1 @@
1
+ export {};