@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,17 +1,20 @@
1
- import { existsSync, readdirSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import { dirname, join, resolve } from 'node:path';
1
+ import { readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
4
3
  import { pathToFileURL } from 'node:url';
4
+ import { discoverAddons, emptyContributions, resolveConductorSlots, resolveModuleEslintStyle, } from '@gaia-ai/core';
5
+ // GAIA-201: the `.gaia/` walk-up + connection resolution moved to `@gaia-ai/core`
6
+ // (`resolveConfigPath` / `resolveGaiaConfigPath` / `findGaiaDir`). Re-export the
7
+ // engine resolver here so existing conductor callers/tests keep importing it
8
+ // from `../config.js`.
9
+ export { findGaiaDir, resolveConfigPath, } from '@gaia-ai/core';
5
10
  /**
6
- * Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
11
+ * Default agent prompt - the project instruction contract. One run works EXACTLY one state;
7
12
  * the agent must stop instead of running the whole flow in one session. The run
8
13
  * is closed automatically when the ticket state changes on the next claim - the
9
14
  * agent does not release it. Run mechanics live here (not in the repo's
10
- * WORKFLOW.md). The prompt routes through the gaia skill (`ticket:run`) rather
11
- * than pointing at WORKFLOW.md directly (GAIA-125): the intake line + state
12
- * engine are a skill mechanic, so a bare "follow WORKFLOW.md" pointer left the
13
- * intake line unprinted unless an external skill-forcing hook happened to fire. A
14
- * conductor config may override via the `prompt` field.
15
+ * WORKFLOW.md). The prompt points directly at the matching project-owned state
16
+ * section and does not prescribe an agent skill. A conductor config may override
17
+ * it via the `prompt` field.
15
18
  *
16
19
  * The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
17
20
  * ticket content into a single typed pane line overran the PTY canonical line
@@ -34,15 +37,12 @@ import { pathToFileURL } from 'node:url';
34
37
  * wrapper, GAIA-118, was the thing that re-inflated the line past the cap and
35
38
  * truncated it mid-quote).
36
39
  * Placeholders: `{identifier}`, `{state}`, `{runUuid}` ({state} falls back to
37
- * `triage` for unclassified tickets).
40
+ * `qualification` for unclassified tickets).
38
41
  */
39
- export const DEFAULT_AGENT_PROMPT = `You are a GAIA agent working ticket {identifier}, current state: {state}. ` +
40
- `Invoke the gaia skill and run \`ticket:run {identifier} {state}\` — it first ` +
41
- `reads the ticket + all comments ` +
42
+ export const DEFAULT_AGENT_PROMPT = `You are working ticket {identifier}, current state: {state}. Read the ticket and all comments ` +
42
43
  `(gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
43
- `prints an intake line, runs the {state} engine, and applies ` +
44
- `WORKFLOW.md's {state} policy. Do ONLY the {state} work never start or ` +
45
- `prepare a later state.`;
44
+ `then read ./WORKFLOW.md and execute only its ordered "State: {state}" section. ` +
45
+ `Do not start or prepare a later state.`;
46
46
  function requirePlugin(value, kind) {
47
47
  if (!isRecord(value) || value.kind !== kind) {
48
48
  throw new Error(`conductor config requires a ${kind} plugin in the "${kind}" slot`);
@@ -73,16 +73,7 @@ async function loadNamedPlugin(entry, configPath) {
73
73
  pathToFileURL(`${process.cwd()}/`).href,
74
74
  import.meta.url,
75
75
  ];
76
- let resolved;
77
- for (const base of bases) {
78
- try {
79
- resolved = createRequire(base).resolve(entry.plugin);
80
- break;
81
- }
82
- catch {
83
- // try the next base
84
- }
85
- }
76
+ const resolved = resolveModuleEslintStyle(entry.plugin, bases);
86
77
  if (resolved === undefined) {
87
78
  throw new Error(`conductor config cannot resolve plugin '${entry.plugin}'`);
88
79
  }
@@ -144,23 +135,6 @@ export async function resolveAgents(raw, configPath) {
144
135
  ? Promise.all(raw.map(resolveOne))
145
136
  : resolveOne(raw);
146
137
  }
147
- /**
148
- * Resolve the `plugins[]` array: descriptor entries are constructed, already-
149
- * constructed entries pass through. No kind-guard (plugins are not slotted).
150
- *
151
- * A factory may return one plugin OR an array of plugins — an aggregator built
152
- * with dropsh's `composePlugins(a(), b())` returns the flat child list. dropsh's
153
- * own `loadConfig` flattens such a nested entry one level; we mirror that here so
154
- * `buildProgram({ plugins })` — which reads each entry's hooks/renderers per
155
- * top-level element and does NOT recurse — sees every child.
156
- */
157
- async function resolvePlugins(raw, configPath) {
158
- if (!Array.isArray(raw)) {
159
- return [];
160
- }
161
- const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadNamedPlugin(entry, configPath) : entry));
162
- return resolved.flat();
163
- }
164
138
  function isRecord(value) {
165
139
  return typeof value === 'object' && value !== null && !Array.isArray(value);
166
140
  }
@@ -179,98 +153,15 @@ function optionalPositiveInteger(value, fallback, key) {
179
153
  }
180
154
  return value;
181
155
  }
182
- /** A repo's config files live in this dir, one conductor config per conductor. */
183
- const GAIA_DIR = '.gaia';
184
- /** The default conductor's file name; its stem is `conductor`. */
185
- const DEFAULT_CONFIG = 'conductor.config.js';
186
- /** Variant files are `<stem>.conductor.config.js`. */
187
- const VARIANT_SUFFIX = '.conductor.config.js';
188
156
  /**
189
- * List the conductor-config stems in a `.gaia/` dir. A file is a conductor
190
- * config iff it is exactly `conductor.config.js` (stem `conductor`, the
191
- * default) or ends with `.conductor.config.js` (stem = the leading part).
192
- * Every other file (`vite.config.js`, the near-miss `myconductor.config.js`)
193
- * is ignored.
157
+ * Load the ENGINE half of a conductor config (GAIA-201 split model). Reads
158
+ * remote/executor/agent/workspace + project/states/machine_id/label/prompt/
159
+ * scheduler/hooks and deliberately NOT `site` or the auth `plugins`, which now
160
+ * live in a `gaia.config.js` connection config. The conductor command composes
161
+ * the two via `composeConductorConfig`. A legacy `conductor.config.js` still
162
+ * carrying `site`/`plugins` loads fine — those two keys are simply ignored here
163
+ * (the connection loader reads them, back-compat).
194
164
  */
195
- function configStems(gaiaDir) {
196
- return readdirSync(gaiaDir)
197
- .map((f) => f === DEFAULT_CONFIG
198
- ? 'conductor'
199
- : f.endsWith(VARIANT_SUFFIX)
200
- ? f.slice(0, -VARIANT_SUFFIX.length)
201
- : undefined)
202
- .filter((s) => s !== undefined)
203
- .sort();
204
- }
205
- /** Map a conductor stem back to its file name. */
206
- function fileForStem(stem) {
207
- return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
208
- }
209
- /**
210
- * Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
211
- * `.gaia/` dir holds at least one conductor config; return that `.gaia/` dir, or
212
- * `undefined` if none is found up to the filesystem root.
213
- */
214
- function findGaiaDir(cwd) {
215
- let dir = resolve(cwd);
216
- for (;;) {
217
- const gaiaDir = join(dir, GAIA_DIR);
218
- if (existsSync(gaiaDir) && configStems(gaiaDir).length > 0) {
219
- return gaiaDir;
220
- }
221
- const parent = dirname(dir);
222
- if (parent === dir) {
223
- return undefined;
224
- }
225
- dir = parent;
226
- }
227
- }
228
- /**
229
- * Resolve the conductor config path from `cwd`.
230
- *
231
- * 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
232
- * 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding a conductor config
233
- * — so any subdirectory of a project/worktree resolves the same dir.
234
- * - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves the stem's
235
- * file (`conductor` → `conductor.config.js`, else
236
- * `<name>.conductor.config.js`; error listing the stems if absent).
237
- * - No selector: `conductor.config.js` present → it is the default; else
238
- * exactly one config → use it (back-compat); else → error naming the
239
- * stems + the selector.
240
- * 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
241
- * (never leak a raw "Cannot find module" from a later import()).
242
- */
243
- export function resolveConfigPath(override, cwd = process.cwd(), conductorName) {
244
- const explicit = override ?? process.env.GAIA_CONDUCTOR_CONFIG;
245
- if (explicit) {
246
- return explicit;
247
- }
248
- const gaiaDir = findGaiaDir(cwd);
249
- if (gaiaDir === undefined) {
250
- throw new Error(`no .gaia/conductor.config.js found from ${cwd} upward; run \`gaia init\``);
251
- }
252
- const stems = configStems(gaiaDir);
253
- const name = conductorName ?? process.env.GAIA_CONDUCTOR;
254
- if (name !== undefined && name !== '') {
255
- const candidate = join(gaiaDir, fileForStem(name));
256
- if (!existsSync(candidate)) {
257
- throw new Error(`no conductor '${name}' in ${gaiaDir}; available: ${stems.join(', ')}`);
258
- }
259
- return candidate;
260
- }
261
- // No selector: the file named conductor.config.js is the default (AC-1, AC-2).
262
- // Two conductor.config.js files cannot coexist in one dir, so ">1 default"
263
- // (AC-5) is structurally impossible — no ambiguity branch is needed.
264
- if (stems.includes('conductor')) {
265
- return join(gaiaDir, DEFAULT_CONFIG);
266
- }
267
- // Back-compat: a lone config resolves even without the default name.
268
- if (stems.length === 1) {
269
- return join(gaiaDir, fileForStem(stems[0]));
270
- }
271
- throw new Error(`${stems.length} conductors in ${gaiaDir} (${stems.join(', ')}); ` +
272
- 'select one with --conductor <name> or $GAIA_CONDUCTOR');
273
- }
274
165
  export async function loadConductorConfig(configFile) {
275
166
  const configPath = resolve(configFile);
276
167
  const module = (await import(pathToFileURL(configPath).href));
@@ -279,13 +170,13 @@ export async function loadConductorConfig(configFile) {
279
170
  throw new Error('conductor config default export must be an object');
280
171
  }
281
172
  const config = raw;
282
- const site = isRecord(config.site) ? config.site : {};
283
- const baseUrl = requireNonEmptyString(site.base_url, 'site.base_url');
284
173
  const project = requireNonEmptyString(config.project, 'project');
285
- if (!Array.isArray(config.states) || config.states.length === 0) {
286
- throw new Error('conductor config requires non-empty states');
287
- }
288
- const states = config.states.map((state) => requireNonEmptyString(state, 'states'));
174
+ // `states` is optional. An empty (or absent) list means the conductor serves
175
+ // every claimable state in its project; a non-empty list narrows it to those
176
+ // states (GAIA-207). When present it must be an array of non-empty strings.
177
+ const states = Array.isArray(config.states)
178
+ ? config.states.map((state) => requireNonEmptyString(state, 'states'))
179
+ : [];
289
180
  // machine_id is required — the config MUST set it; there is no derived
290
181
  // fallback. The id is defined in a single place (config.machine_id), which
291
182
  // the CLI lifecycle commands and registration all read, never re-derive. The
@@ -298,21 +189,46 @@ export async function loadConductorConfig(configFile) {
298
189
  const label = typeof config.label === 'string' && config.label.trim() !== ''
299
190
  ? config.label
300
191
  : machineId;
192
+ // GAIA-215: discover the conductor `addons: []` surface (last-wins singletons,
193
+ // agent candidate list). A pre-existing per-slot descriptor still loads and
194
+ // WINS over a discovered contributor of the same kind (back-compat additive).
195
+ const discovered = Array.isArray(config.addons)
196
+ ? await discoverAddons(config.addons, 'conductor', [
197
+ pathToFileURL(configPath).href,
198
+ pathToFileURL(`${process.cwd()}/`).href,
199
+ import.meta.url,
200
+ ])
201
+ : emptyContributions();
202
+ const wired = resolveConductorSlots(discovered);
203
+ const remote = config.remote !== undefined
204
+ ? await resolveSlot(config.remote, 'remote', configPath)
205
+ : wired.remote;
206
+ if (!remote) {
207
+ throw new Error('conductor config resolves no remote — name a remote addon in addons[] or set the remote slot');
208
+ }
209
+ const executor = config.executor !== undefined
210
+ ? await resolveSlot(config.executor, 'executor', configPath)
211
+ : wired.executor;
212
+ if (!executor) {
213
+ throw new Error('conductor config resolves no executor — name an executor addon in addons[] or set the executor slot');
214
+ }
215
+ const workspace = config.workspace !== undefined
216
+ ? await resolveSlot(config.workspace, 'workspace', configPath)
217
+ : wired.workspace;
218
+ if (!workspace) {
219
+ throw new Error('conductor config resolves no workspace — name a workspace addon in addons[] or set the workspace slot');
220
+ }
221
+ const agent = config.agent !== undefined
222
+ ? await resolveAgents(config.agent, configPath)
223
+ : wired.agents;
224
+ if (Array.isArray(agent) && agent.length === 0) {
225
+ throw new Error('conductor config resolves no agent — name an agent addon in addons[] or set the agent slot');
226
+ }
301
227
  return {
302
- site: {
303
- base_url: baseUrl,
304
- jsonapi_prefix: typeof site.jsonapi_prefix === 'string' &&
305
- site.jsonapi_prefix.trim() !== ''
306
- ? site.jsonapi_prefix
307
- : '/jsonapi',
308
- },
309
- ...(Array.isArray(config.plugins)
310
- ? { plugins: await resolvePlugins(config.plugins, configPath) }
311
- : {}),
312
- remote: await resolveSlot(config.remote, 'remote', configPath),
313
- executor: await resolveSlot(config.executor, 'executor', configPath),
314
- agent: await resolveAgents(config.agent, configPath),
315
- workspace: await resolveSlot(config.workspace, 'workspace', configPath),
228
+ remote,
229
+ executor,
230
+ agent,
231
+ workspace,
316
232
  label,
317
233
  machine_id: machineId,
318
234
  project,
@@ -327,3 +243,61 @@ export async function loadConductorConfig(configFile) {
327
243
  config_path: configPath,
328
244
  };
329
245
  }
246
+ /**
247
+ * Compose the full `ConductorFileConfig` the engine consumes from the engine
248
+ * half (`loadConductorConfig`) and the connection half (`loadGaiaConfig`). The
249
+ * connection supplies `site` + the auth `plugins`; `ensureAuthenticated` /
250
+ * `selectRemote` read them exactly as before — the fields just arrive from
251
+ * `gaia.config.js` now.
252
+ */
253
+ export function composeConductorConfig(engine, connection) {
254
+ return {
255
+ ...engine,
256
+ site: connection.site,
257
+ ...(connection.plugins.length > 0 ? { plugins: connection.plugins } : {}),
258
+ };
259
+ }
260
+ const DEFAULT_CONFIG = 'conductor.config.js';
261
+ const VARIANT_SUFFIX = '.conductor.config.js';
262
+ /**
263
+ * The conductor **stems** in a `.gaia/` dir — the file naming predicate of
264
+ * GAIA-137: exactly `conductor.config.js` (stem `conductor`) or
265
+ * `<variant>.conductor.config.js` (stem `<variant>`). The resolution itself
266
+ * lives in `@gaia-ai/core` (re-exported above); this is the enumeration.
267
+ */
268
+ export function configStems(gaiaDir) {
269
+ return readdirSync(gaiaDir)
270
+ .map((f) => f === DEFAULT_CONFIG
271
+ ? 'conductor'
272
+ : f.endsWith(VARIANT_SUFFIX)
273
+ ? f.slice(0, -VARIANT_SUFFIX.length)
274
+ : undefined)
275
+ .filter((s) => s !== undefined)
276
+ .sort();
277
+ }
278
+ /** Map a conductor stem back to its file name. */
279
+ export function fileForStem(stem) {
280
+ return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
281
+ }
282
+ /**
283
+ * List the conductor-config **file names** in a `.gaia/` dir, using the same
284
+ * naming predicate the loader uses (`configStems`). This is the only supported
285
+ * enumeration for the `@gaia/upgrade-project` states-strip: it visits the
286
+ * default `conductor.config.js` + every `<variant>.conductor.config.js`, and
287
+ * never an unrelated JS config (`vite.config.js`, the near-miss
288
+ * `myconductor.config.js`).
289
+ */
290
+ export function listConductorConfigFiles(gaiaDir) {
291
+ return configStems(gaiaDir).map(fileForStem);
292
+ }
293
+ /**
294
+ * Remove the obsolete `states:` array property from a conductor-config JS
295
+ * source (GAIA-207 made `states` optional — empty ⇒ serve all claimable
296
+ * states — so `@gaia/upgrade-project` drops the now-dead key). Pure string
297
+ * transform: it strips the `states: [ ... ]` property (line form or inline in
298
+ * an object literal) with its trailing comma, and rewrites nothing else. A
299
+ * `substates:`/other key containing "states" is not matched.
300
+ */
301
+ export function stripStatesFromConfigSource(source) {
302
+ return source.replace(/\n?[ \t]*(?<![A-Za-z0-9_$])states[ \t]*:[ \t]*\[[^\]]*\][ \t]*,?/g, '');
303
+ }
@@ -236,7 +236,9 @@ export class Conductor {
236
236
  const branch = await this.remote.getRunTicketBranchName(r.runUuid);
237
237
  if (branch) {
238
238
  try {
239
- await this.executor.cleanupRun(branch);
239
+ // Run-scoped (GAIA-183): close only this done run's own tab
240
+ // (label `#<runId>`), never a sibling/concurrent run's tab.
241
+ await this.executor.cleanupRun(branch, r.runId);
240
242
  }
241
243
  catch (err) {
242
244
  this.logger.warn({ run: r.runUuid, err: String(err) }, 'run tab cleanup failed');
@@ -368,14 +370,11 @@ export class Conductor {
368
370
  };
369
371
  const env = resolveRunEnv(t.effectiveEnvVars, core, this.logger);
370
372
  const ws = await this.workspace.ensure(t.identifier, t.branchName, baseRef);
371
- // A claimable state has no deterministic policy without a WORKFLOW.md
372
- // (SKILL.md dispatch reads its `## State: <state>` sections). The human
373
- // steered this to WARN-and-proceed (not a hard fail): the agent runs on
374
- // engine defaults, and the gap is observable in the conductor log rather
375
- // than wedging the run. `ws.instructions` IS the loaded WORKFLOW.md (null
376
- // when absent), so no extra stat.
373
+ // A claimable state has no project policy without a WORKFLOW.md. Warn and
374
+ // proceed so the missing project instructions are observable without
375
+ // wedging the run. `ws.instructions` is the loaded file (null when absent).
377
376
  if (!ws.instructions) {
378
- this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing — dispatching on engine defaults');
377
+ this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing — no project instructions dispatched');
379
378
  }
380
379
  // Lifecycle hooks are executor-owned + best-effort (GAIA-84): runHook never
381
380
  // throws, so neither call can abort dispatch or wedge a run in `claimed`.
@@ -386,12 +385,16 @@ export class Conductor {
386
385
  await this.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
387
386
  }
388
387
  await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
389
- // Clear any leftover tabs of a reused workspace before starting the run.
390
- await this.executor.cleanupRun(t.branchName);
388
+ // Clear a leftover tab for THIS run id in a reused workspace before
389
+ // starting it (GAIA-183): run-scoped so a still-open sibling run's tab is
390
+ // never killed. A fresh dispatch has no matching tab yet → a quiet no-op;
391
+ // a re-dispatch of the same run drops its stale duplicate before startRun
392
+ // re-creates it. Sibling prior-state run tabs are left to the worktree reap.
393
+ await this.executor.cleanupRun(t.branchName, run.runId);
391
394
  const prompt = ws.instructions
392
395
  ? renderPrompt(this.config.prompt, {
393
396
  identifier: t.identifier,
394
- state: t.state || 'triage',
397
+ state: t.state || 'qualification',
395
398
  runUuid: run.runUuid,
396
399
  })
397
400
  : '';
@@ -1,8 +1,9 @@
1
1
  export type * from '@gaia-ai/core';
2
2
  export { conductorId } from '@gaia-ai/core';
3
3
  export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
4
- export type { GaiaCliDeps } from './cli/gaia.js';
5
- export { main, runGaiaCli } from './cli/gaia.js';
6
- export type { ConductorRegistryEntry } from './cli/local-registry.js';
7
- export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
4
+ export type { ConductorRegistryEntry } from './cli/conductor-registry.js';
5
+ export { renderGaiaConfig } from './cli/init.js';
6
+ export { runConnectionUpgrade, runUpgrade, type UpgradeReport, } from './cli/upgrade.js';
7
+ export { default as conductorCommandPlugin, type GaiaCliDeps, runConductorCli, } from './commands/conductor.js';
8
+ export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, } from './config.js';
8
9
  export { Conductor } from './core/conductor.js';
package/dist/src/index.js CHANGED
@@ -1,5 +1,14 @@
1
1
  export { conductorId } from '@gaia-ai/core';
2
2
  export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
3
- export { main, runGaiaCli } from './cli/gaia.js';
4
- export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
3
+ // GAIA-201: the connection-config template, reused by `gaia upgrade` to seed a
4
+ // `gaia.config.js`.
5
+ export { renderGaiaConfig } from './cli/init.js';
6
+ // GAIA-216: the seed/migration routine, hoisted from the host so `conductor init`
7
+ // (intra-package) and `gaia upgrade` (host→engine) share one implementation.
8
+ export { runConnectionUpgrade, runUpgrade, } from './cli/upgrade.js';
9
+ // GAIA-201: the conductor is now a COMMAND PLUGIN mounted by the `@gaia-ai/gaia`
10
+ // host, not the CLI entrypoint. `main`/`runGaiaCli` are gone; the default export
11
+ // is the `GaiaCommandPlugin`, exposed here as `./commands/conductor` too.
12
+ export { default as conductorCommandPlugin, runConductorCli, } from './commands/conductor.js';
13
+ export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, } from './config.js';
5
14
  export { Conductor } from './core/conductor.js';
@@ -0,0 +1,2 @@
1
+ import type { GaiaPreset } from '@gaia-ai/core';
2
+ export declare const commands: GaiaPreset['commands'];
@@ -0,0 +1,8 @@
1
+ export const commands = (acc) => [
2
+ ...acc,
3
+ {
4
+ name: 'conductor',
5
+ plugin: '@gaia-ai/conductor/commands/conductor',
6
+ describe: 'node-agent lifecycle + local registry',
7
+ },
8
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/conductor",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,6 +9,8 @@
9
9
  },
10
10
  "exports": {
11
11
  ".": "./dist/src/index.js",
12
+ "./preset": "./dist/src/preset.js",
13
+ "./commands/conductor": "./dist/src/commands/conductor.js",
12
14
  "./package.json": "./package.json"
13
15
  },
14
16
  "files": [
@@ -22,14 +24,14 @@
22
24
  "repository": {
23
25
  "type": "git",
24
26
  "url": "git+https://git.key-tec.de/keytec/gaia.git",
25
- "directory": "conductor/packages/conductor"
27
+ "directory": "conductor/engine"
26
28
  },
27
29
  "dependencies": {
28
- "@gaia-ai/core": "^0.5.4",
30
+ "@gaia-ai/core": "^0.6.0",
29
31
  "@dropsh/plugin-oauth2": "^0.5.7",
30
- "@dropsh/plugin-jsonapi-schema": "^0.5.7",
32
+ "@dropsh/plugin-jsonapi-schema": "^0.5.8",
31
33
  "commander": "^12.1.0",
32
- "dropsh": "^0.5.7",
34
+ "dropsh": "^0.5.8",
33
35
  "semver": "^7.6.0"
34
36
  }
35
37
  }
@@ -1,23 +0,0 @@
1
- import { type ConductorFileConfig, type ConductorLogger, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace, type ResolvedAgent } from '@gaia-ai/core';
2
- import { Command } from 'commander';
3
- /** Test seam: inject any subset of dependencies. */
4
- export interface GaiaCliDeps {
5
- remote?: GaiaRemote;
6
- executor?: GaiaExecutor;
7
- workspace?: GaiaWorkspace;
8
- agents?: ResolvedAgent[];
9
- config?: ConductorFileConfig;
10
- /** Injectable registry fetch for the start-time version check (tests). */
11
- fetch?: typeof fetch;
12
- }
13
- export declare function parseHerdrJson(output: string, command: string): unknown;
14
- /**
15
- * Start-time auth gate. Returns true if authenticated (session or session-less
16
- * provider); otherwise logs a single clear line and returns false. Must run
17
- * before remote resolution (resolveRemote), which calls resolveAuth and throws
18
- * when unauthenticated.
19
- */
20
- export declare function ensureAuthenticated(config: ConductorFileConfig, logger: ConductorLogger): Promise<boolean>;
21
- export declare function buildProgram(deps: GaiaCliDeps): Command;
22
- export declare function runGaiaCli(argv: string[], deps?: GaiaCliDeps): Promise<void>;
23
- export declare function main(argv: string[]): Promise<void>;
@@ -1,14 +0,0 @@
1
- export interface ConductorRegistryEntry {
2
- id: string;
3
- path: string;
4
- project: string;
5
- label: string;
6
- host: 'herdr' | 'process' | 'systemd';
7
- handle: string;
8
- }
9
- /** `${GAIA_HOME ?? ~}/.gaia/conductors.json`. */
10
- export declare function registryPath(): string;
11
- export declare function register(entry: ConductorRegistryEntry): Promise<void>;
12
- export declare function remove(id: string): Promise<void>;
13
- export declare function list(): Promise<ConductorRegistryEntry[]>;
14
- export declare function get(id: string): Promise<ConductorRegistryEntry | null>;
@@ -1,56 +0,0 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { dirname, join } from 'node:path';
4
- /** `${GAIA_HOME ?? ~}/.gaia/conductors.json`. */
5
- export function registryPath() {
6
- const home = process.env.GAIA_HOME ?? homedir();
7
- return join(home, '.gaia', 'conductors.json');
8
- }
9
- async function read() {
10
- let raw;
11
- try {
12
- raw = await readFile(registryPath(), 'utf8');
13
- }
14
- catch {
15
- return {};
16
- }
17
- if (raw.trim() === '') {
18
- return {};
19
- }
20
- try {
21
- const parsed = JSON.parse(raw);
22
- if (typeof parsed === 'object' &&
23
- parsed !== null &&
24
- !Array.isArray(parsed)) {
25
- return parsed;
26
- }
27
- return {};
28
- }
29
- catch {
30
- return {};
31
- }
32
- }
33
- async function write(data) {
34
- const path = registryPath();
35
- await mkdir(dirname(path), { recursive: true });
36
- await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
37
- }
38
- export async function register(entry) {
39
- const data = await read();
40
- data[entry.id] = entry;
41
- await write(data);
42
- }
43
- export async function remove(id) {
44
- const data = await read();
45
- if (data[id]) {
46
- delete data[id];
47
- await write(data);
48
- }
49
- }
50
- export async function list() {
51
- return Object.values(await read());
52
- }
53
- export async function get(id) {
54
- const data = await read();
55
- return data[id] ?? null;
56
- }