@gaia-ai/core 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/src/cli/commands.d.ts +27 -0
  2. package/dist/src/cli/commands.js +1 -0
  3. package/dist/src/cli/gaia-dir.d.ts +51 -0
  4. package/dist/src/cli/gaia-dir.js +152 -0
  5. package/dist/src/cli/load-gaia-config.d.ts +25 -0
  6. package/dist/src/cli/load-gaia-config.js +110 -0
  7. package/dist/src/cli/machine-context.d.ts +24 -0
  8. package/dist/src/cli/machine-context.js +45 -0
  9. package/dist/src/cli/paths.d.ts +11 -0
  10. package/dist/src/cli/paths.js +31 -0
  11. package/dist/src/cli/resolve-module.d.ts +6 -0
  12. package/dist/src/cli/resolve-module.js +24 -0
  13. package/dist/src/conductor-registry/index.d.ts +23 -0
  14. package/dist/src/conductor-registry/index.js +59 -0
  15. package/dist/src/index.d.ts +11 -0
  16. package/dist/src/index.js +14 -0
  17. package/dist/src/plugins/auth/basic.d.ts +1 -0
  18. package/dist/src/plugins/auth/basic.js +3 -1
  19. package/dist/src/plugins/builtins-preset.d.ts +5 -0
  20. package/dist/src/plugins/builtins-preset.js +32 -0
  21. package/dist/src/plugins/discover-addons.d.ts +22 -0
  22. package/dist/src/plugins/discover-addons.js +228 -0
  23. package/dist/src/plugins/preset.d.ts +76 -0
  24. package/dist/src/plugins/preset.js +42 -0
  25. package/dist/src/plugins/remote/drupal.d.ts +5 -0
  26. package/dist/src/plugins/remote/drupal.js +24 -0
  27. package/dist/src/plugins/remote/fake.d.ts +4 -0
  28. package/dist/src/plugins/remote/fake.js +4 -0
  29. package/dist/src/plugins/remote/remote.d.ts +10 -0
  30. package/dist/src/types.d.ts +14 -1
  31. package/dist/src/workflow/step-contract.d.ts +119 -0
  32. package/dist/src/workflow/step-contract.js +430 -0
  33. package/package.json +10 -4
@@ -6,6 +6,7 @@
6
6
  * hand-written into each dropsh.config.js.
7
7
  */
8
8
  export declare function basicAuthProvider(tokenBase64: string): {
9
+ kind: 'auth';
9
10
  id: string;
10
11
  authProvider: unknown;
11
12
  };
@@ -31,5 +31,7 @@ export function basicAuthProvider(tokenBase64) {
31
31
  };
32
32
  },
33
33
  };
34
- return { id: 'gaia-basic-auth', authProvider };
34
+ // GAIA-215: `.kind='auth'` makes the plugin tag total (every GAIA plugin
35
+ // self-identifies); dropsh ignores the extra field and reads `authProvider`.
36
+ return { kind: 'auth', id: 'gaia-basic-auth', authProvider };
35
37
  }
@@ -0,0 +1,5 @@
1
+ import type { GaiaPreset } from './preset.js';
2
+ /** Conductor surface: the Drupal control-plane remote (the sole shipped remote). */
3
+ export declare const remotes: GaiaPreset['remotes'];
4
+ /** Connection surface: inline HTTP Basic auth, only when a `basic` token is set. */
5
+ export declare const connectionPlugins: GaiaPreset['connectionPlugins'];
@@ -0,0 +1,32 @@
1
+ // GAIA-215: the core built-ins preset, exposed as `@gaia-ai/core/builtins`. It
2
+ // self-declares GAIA's shipped default contributions the way any addon does —
3
+ // retiring the `@gaia-ai/core/plugins` barrel `export:` warts (`drupalRemote`,
4
+ // `basicAuthProvider`). List it in `conductor.config.js` addons to auto-wire the
5
+ // Drupal control-plane remote; list it in `gaia.config.js` addons with a
6
+ // `{ basic: '<base64>' }` option for inline HTTP Basic auth. The barrel stays
7
+ // for back-compat (the descriptor `export:` path); new configs name this preset.
8
+ //
9
+ // It deliberately does NOT contribute a workspace: every executor addon ships
10
+ // its own paired workspace (herdr → herdrWorkspace, fake → fakeWorkspace), so a
11
+ // builtin `gitWorkspace` would only fight the executor's workspace on the
12
+ // canonical config (a guaranteed last-wins warning). A git-only setup can still
13
+ // name `gitWorkspace` explicitly via the `@gaia-ai/core/plugins` barrel.
14
+ //
15
+ // It is deliberately light + dependency-free at the top level (factory calls
16
+ // only), so the host can import it at boot without pulling the remote runtime.
17
+ import { basicAuthProvider } from './auth/basic.js';
18
+ import { drupalRemote } from './remote/drupal.js';
19
+ /** Conductor surface: the Drupal control-plane remote (the sole shipped remote). */
20
+ export const remotes = (acc) => [...acc, drupalRemote()];
21
+ /** Connection surface: inline HTTP Basic auth, only when a `basic` token is set. */
22
+ export const connectionPlugins = (acc, opts) => {
23
+ const o = (opts ?? {});
24
+ if (typeof o.basic !== 'string' || o.basic.trim() === '')
25
+ return acc;
26
+ // basicAuthProvider returns the dropsh auth-plugin shape ({ id, authProvider })
27
+ // minus `requiredModules` (dropsh tolerates its absence for auth-only plugins).
28
+ return [
29
+ ...acc,
30
+ basicAuthProvider(o.basic),
31
+ ];
32
+ };
@@ -0,0 +1,22 @@
1
+ import type { ConductorLogger } from '../core/logger.js';
2
+ import type { AgentCandidate, ExecutorPlugin, RemotePlugin, WorkspacePlugin } from './plugins.js';
3
+ import { type AddonEntry, type DiscoveredContributions, type GaiaSurface } from './preset.js';
4
+ /**
5
+ * Discover + accumulate the `entries` for one `surface`. `bases` is the
6
+ * ESLint-style resolution order (config dir → cwd → install). Depth-first over
7
+ * meta-addon `addons` (children before parent), visited-set keyed by resolved
8
+ * path + serialized options (dedup + cycle-safe). Returns the accumulated
9
+ * per-extension-point arrays; singleton resolution is `resolveConductorSlots`.
10
+ */
11
+ export declare function discoverAddons(entries: AddonEntry[] | undefined, surface: GaiaSurface, bases: string[], opts?: {
12
+ logger?: ConductorLogger;
13
+ }): Promise<DiscoveredContributions>;
14
+ /** The resolved conductor slots (last-wins singletons + agent candidate list). */
15
+ export interface ResolvedConductorSlots {
16
+ remote?: RemotePlugin;
17
+ executor?: ExecutorPlugin;
18
+ workspace?: WorkspacePlugin;
19
+ agents: AgentCandidate[];
20
+ }
21
+ /** Resolve discovered conductor contributions into the four engine slots. */
22
+ export declare function resolveConductorSlots(contributions: DiscoveredContributions, logger?: ConductorLogger): ResolvedConductorSlots;
@@ -0,0 +1,228 @@
1
+ // GAIA-215: addon discovery. One shared `discoverAddons(entries, surface, bases)`
2
+ // called by the conductor loader, the connection loader, and the host — each
3
+ // runs ONLY its surface's accumulators. Resolution reuses the ESLint-style
4
+ // resolver (`resolveModuleEslintStyle`) so an addon named in a config resolves
5
+ // the same way a slot plugin did before (config dir → cwd → install).
6
+ //
7
+ // Ordering / override semantics (Storybook never wrote these down; spec'd here):
8
+ // 1. addons apply in `addons[]` ARRAY ORDER;
9
+ // 2. a preset's `addons` children apply DEPTH-FIRST BEFORE the declaring preset
10
+ // (a parent overrides its children — essentials-style composition);
11
+ // 3. the user config file is the final preset (its own `addons[]` are the
12
+ // top-level entries, processed last → it always wins);
13
+ // 4. an EXACT duplicate addon (same resolved path AND same options) is
14
+ // processed once (visited-set keyed by path + serialized options) and
15
+ // warned — this also prevents meta-addon cycles and fixes the old one-level
16
+ // `composePlugins` flatten. The same package with DIFFERENT options is a
17
+ // legitimate multi-instance (e.g. two oauth2 profiles) and both load.
18
+ // Per entry: resolve → import `<pkg>/preset` (fallback: the package's default
19
+ // export adapted as a single contribution, logged) → run the surface's
20
+ // accumulators with the entry's `with` → for `agents`, attach the entry's
21
+ // `priority`. The runtime `.kind` guard is retained at singleton resolution.
22
+ import { pathToFileURL } from 'node:url';
23
+ import { resolveModuleEslintStyle } from '../cli/resolve-module.js';
24
+ import { emptyContributions, PRESET_FUNCTION_KEYS, SURFACE_KEYS, } from './preset.js';
25
+ function warnFrom(logger) {
26
+ if (logger)
27
+ return (msg) => logger.warn({}, msg);
28
+ return (msg) => {
29
+ console.warn(msg);
30
+ };
31
+ }
32
+ /** Normalize an `AddonEntry` to `{ spec, opts, priority }`. */
33
+ function normalizeEntry(entry) {
34
+ if (typeof entry === 'string')
35
+ return { spec: entry, opts: undefined };
36
+ const spec = entry.use ?? entry.name;
37
+ if (typeof spec !== 'string' || spec.trim() === '') {
38
+ throw new Error(`addons[] entry must name a package via a string or { use } / { name }; got ${JSON.stringify(entry)}`);
39
+ }
40
+ return {
41
+ spec,
42
+ opts: entry.with ?? entry.options,
43
+ ...(entry.priority ? { priority: entry.priority } : {}),
44
+ };
45
+ }
46
+ /** Does an imported module look like a preset (any accumulator fn or `addons`)? */
47
+ function isPresetShaped(mod) {
48
+ if (Array.isArray(mod.addons))
49
+ return true;
50
+ return PRESET_FUNCTION_KEYS.some((k) => typeof mod[k] === 'function');
51
+ }
52
+ /** Pick a module's single factory: the default export, else a sole function. */
53
+ function pickFactory(mod) {
54
+ if (typeof mod.default === 'function') {
55
+ return mod.default;
56
+ }
57
+ const fns = Object.keys(mod).filter((k) => typeof mod[k] === 'function');
58
+ if (fns.length === 1)
59
+ return mod[fns[0]];
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Adapt a not-yet-migrated package (no `./preset`, module not preset-shaped) by
64
+ * constructing its default/sole factory and routing the built plugin into the
65
+ * surface accumulator matching its `.kind`. Incremental-migration path; logged.
66
+ * A factory returning an array (dropsh `composePlugins` aggregator) is flattened.
67
+ */
68
+ function adaptDefaultExport(mod, spec, warn) {
69
+ const factory = pickFactory(mod);
70
+ if (!factory) {
71
+ throw new Error(`addon '${spec}' has no ./preset, is not preset-shaped, and has no default/sole factory to adapt`);
72
+ }
73
+ warn(`addon '${spec}' has no ./preset export — adapting its default export (legacy; migrate it to a ./preset, GAIA-215 AC-3)`);
74
+ return {
75
+ connectionPlugins: (acc, opts) => {
76
+ const built = factory(opts);
77
+ const list = Array.isArray(built) ? built : [built];
78
+ // Route by kind: only plugins WITHOUT a GAIA engine kind are dropsh
79
+ // connection plugins; engine kinds are routed by the branches below.
80
+ const conn = list.filter((p) => !isEngineKind(p.kind));
81
+ return [
82
+ ...acc,
83
+ ...conn,
84
+ ];
85
+ },
86
+ remotes: (acc, opts) => routeByKind(factory, opts, 'remote', acc),
87
+ executors: (acc, opts) => routeByKind(factory, opts, 'executor', acc),
88
+ workspaces: (acc, opts) => routeByKind(factory, opts, 'workspace', acc),
89
+ agents: (acc, opts) => {
90
+ const built = factory(opts);
91
+ const list = Array.isArray(built) ? built : [built];
92
+ const agents = list.filter((p) => p.kind === 'agent');
93
+ return [...acc, ...agents.map((agent) => ({ agent }))];
94
+ },
95
+ };
96
+ }
97
+ function isEngineKind(kind) {
98
+ return (kind === 'remote' ||
99
+ kind === 'executor' ||
100
+ kind === 'workspace' ||
101
+ kind === 'agent');
102
+ }
103
+ function routeByKind(factory, opts, kind, acc) {
104
+ const built = factory(opts);
105
+ const list = Array.isArray(built) ? built : [built];
106
+ const matching = list.filter((p) => p.kind === kind);
107
+ return [...acc, ...matching];
108
+ }
109
+ /** Resolve + import the preset for one addon spec. Returns the preset + a stable
110
+ * visited key (the resolved module path). */
111
+ async function loadPreset(spec, bases, warn) {
112
+ // 1. the conventional `<pkg>/preset` subpath.
113
+ const presetPath = resolveModuleEslintStyle(`${spec}/preset`, bases);
114
+ if (presetPath !== undefined) {
115
+ const mod = (await import(pathToFileURL(presetPath).href));
116
+ if (!isPresetShaped(mod)) {
117
+ throw new Error(`addon '${spec}' exposes a ./preset that declares no contributions`);
118
+ }
119
+ return { preset: mod, key: presetPath };
120
+ }
121
+ // 2. the package's main export — either preset-shaped, or a legacy default.
122
+ const mainPath = resolveModuleEslintStyle(spec, bases);
123
+ if (mainPath === undefined) {
124
+ throw new Error(`addons[] cannot resolve addon '${spec}'`);
125
+ }
126
+ const mod = (await import(pathToFileURL(mainPath).href));
127
+ if (isPresetShaped(mod))
128
+ return { preset: mod, key: mainPath };
129
+ return { preset: adaptDefaultExport(mod, spec, warn), key: mainPath };
130
+ }
131
+ /** Run one preset's surface accumulators into `acc`, attaching `priority` to any
132
+ * newly appended agent candidates. */
133
+ async function applyPreset(preset, opts, priority, surface, acc) {
134
+ for (const key of SURFACE_KEYS[surface]) {
135
+ const fn = preset[key];
136
+ if (typeof fn !== 'function')
137
+ continue;
138
+ const before = key === 'agents' ? acc.agents.length : 0;
139
+ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous per-key arrays
140
+ acc[key] = await fn(acc[key], opts);
141
+ if (key === 'agents' && priority) {
142
+ for (let i = before; i < acc.agents.length; i++) {
143
+ const c = acc.agents[i];
144
+ if (!c.priority)
145
+ acc.agents[i] = { ...c, priority };
146
+ }
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Discover + accumulate the `entries` for one `surface`. `bases` is the
152
+ * ESLint-style resolution order (config dir → cwd → install). Depth-first over
153
+ * meta-addon `addons` (children before parent), visited-set keyed by resolved
154
+ * path + serialized options (dedup + cycle-safe). Returns the accumulated
155
+ * per-extension-point arrays; singleton resolution is `resolveConductorSlots`.
156
+ */
157
+ export async function discoverAddons(entries, surface, bases, opts = {}) {
158
+ const acc = emptyContributions();
159
+ if (!Array.isArray(entries) || entries.length === 0)
160
+ return acc;
161
+ const warn = warnFrom(opts.logger);
162
+ const visited = new Set();
163
+ const walk = async (list) => {
164
+ for (const entry of list) {
165
+ const { spec, opts: withOpts, priority } = normalizeEntry(entry);
166
+ const { preset, key } = await loadPreset(spec, bases, warn);
167
+ // Dedup on (resolved path + serialized options): an EXACT repeat is a
168
+ // mistake (skip + warn), but the SAME package with DIFFERENT `with` is a
169
+ // legitimate multi-instance (e.g. two @dropsh/plugin-oauth2 profiles —
170
+ // session + pm), so both must load. Cycle-safe: a meta-addon that re-lists
171
+ // itself with the same options hits the visited key and stops.
172
+ const dedupKey = `${key}${optionsKey(withOpts)}`;
173
+ if (visited.has(dedupKey)) {
174
+ warn(`addon '${spec}' already registered — skipping the exact duplicate`);
175
+ continue;
176
+ }
177
+ visited.add(dedupKey);
178
+ // Depth-first: meta-addon children apply BEFORE the declaring preset.
179
+ if (Array.isArray(preset.addons) && preset.addons.length > 0) {
180
+ await walk(preset.addons);
181
+ }
182
+ await applyPreset(preset, withOpts, priority, surface, acc);
183
+ }
184
+ };
185
+ await walk(entries);
186
+ return acc;
187
+ }
188
+ /** Stable-ish serialization of an addon entry's `with` options for dedup keying.
189
+ * Functions collapse to a marker; unserializable values fall back to String(). */
190
+ function optionsKey(opts) {
191
+ if (opts === undefined)
192
+ return '';
193
+ try {
194
+ return (JSON.stringify(opts, (_k, v) => (typeof v === 'function' ? '[fn]' : v)) ??
195
+ '');
196
+ }
197
+ catch {
198
+ return String(opts);
199
+ }
200
+ }
201
+ /** Take the LAST contributor for a singleton slot; warn when >1 competed; run
202
+ * the runtime `.kind` guard so a mis-declared contribution still fails loudly. */
203
+ function resolveSingleton(list, kind, logger) {
204
+ if (list.length === 0)
205
+ return undefined;
206
+ const warn = warnFrom(logger);
207
+ if (list.length > 1) {
208
+ const ids = list.map((p) => p.id ?? '<?>').join(', ');
209
+ warn(`${kind}: ${list.length} addons contributed a ${kind} ([${ids}]); using the last (override wins)`);
210
+ }
211
+ const picked = list[list.length - 1];
212
+ if (picked.kind !== kind) {
213
+ throw new Error(`addon contributed a '${picked.kind}' into the ${kind} slot`);
214
+ }
215
+ return picked;
216
+ }
217
+ /** Resolve discovered conductor contributions into the four engine slots. */
218
+ export function resolveConductorSlots(contributions, logger) {
219
+ const remote = resolveSingleton(contributions.remotes, 'remote', logger);
220
+ const executor = resolveSingleton(contributions.executors, 'executor', logger);
221
+ const workspace = resolveSingleton(contributions.workspaces, 'workspace', logger);
222
+ return {
223
+ ...(remote ? { remote } : {}),
224
+ ...(executor ? { executor } : {}),
225
+ ...(workspace ? { workspace } : {}),
226
+ agents: contributions.agents,
227
+ };
228
+ }
@@ -0,0 +1,76 @@
1
+ import type { DropSHPlugin } from 'dropsh/plugin';
2
+ import type { AgentCandidate, ExecutorPlugin, RemotePlugin, WorkspacePlugin } from './plugins.js';
3
+ import type { Ticket } from './remote/remote.js';
4
+ /**
5
+ * A lazy command pointer (moved here from the host, GAIA-201, + `describe`). The
6
+ * command surface accumulates DESCRIPTORS — never constructed command plugins —
7
+ * so the host runs every addon's `commands` at boot without importing their
8
+ * runtime (preserves the GAIA-201 lazy-mount + "gaia --help imports none").
9
+ */
10
+ export interface CommandDescriptor {
11
+ /** The subcommand name mounted as `gaia <name> …`. */
12
+ name: string;
13
+ /** Module specifier that default- (or `export`-) exports the `GaiaCommandPlugin`. */
14
+ plugin: string;
15
+ /** Shown in `gaia --help` even when the plugin is NOT loaded (describe-only stub). */
16
+ describe: string;
17
+ /** Named export to pick instead of the module default. */
18
+ export?: string;
19
+ }
20
+ /** The three surfaces, each named for its config home (surface = file = plugins). */
21
+ export type GaiaSurface = 'command' | 'conductor' | 'connection';
22
+ /**
23
+ * One entry in an `addons[]` array — a bare package name, or a descriptor naming
24
+ * the package (`use`, or SB's `name`) plus its `with` options and, for agents, a
25
+ * static `priority` over the ticket. Never carries a `kind`/`export`: the surface
26
+ * is decided by which preset function the addon exports; the kind by the
27
+ * contribution's own `.kind`.
28
+ */
29
+ export type AddonEntry = string | {
30
+ /** The addon package (GAIA term). */
31
+ use?: string;
32
+ /** Storybook's term for the same field; accepted as an alias. */
33
+ name?: string;
34
+ /** Options handed to the addon's accumulators as `opts`. */
35
+ with?: unknown;
36
+ /** Alias for `with`. */
37
+ options?: unknown;
38
+ /** Agent-surface only: the static per-ticket priority (see `selectAgent`). */
39
+ priority?: (ticket: Ticket) => number;
40
+ };
41
+ /**
42
+ * The accumulated contributions across an `addons[]` list, one array per
43
+ * extension point. `discoverAddons` populates only the arrays belonging to the
44
+ * requested surface (the others stay empty) — so a connection load never runs
45
+ * the conductor accumulators.
46
+ */
47
+ export interface DiscoveredContributions {
48
+ commands: CommandDescriptor[];
49
+ remotes: RemotePlugin[];
50
+ executors: ExecutorPlugin[];
51
+ workspaces: WorkspacePlugin[];
52
+ agents: AgentCandidate[];
53
+ connectionPlugins: DropSHPlugin[];
54
+ }
55
+ /** The extension points each surface runs (the preset function names to call). */
56
+ export declare const SURFACE_KEYS: Record<GaiaSurface, Array<keyof DiscoveredContributions>>;
57
+ /**
58
+ * A preset: named per-surface accumulator functions + a meta-addon `addons`
59
+ * array. Every accumulator is `(acc, opts) => acc` (sync or async); returning a
60
+ * new array lets an addon append, replace, or even FILTER a prior contribution
61
+ * (override semantics — the user config participates as the final preset).
62
+ */
63
+ export interface GaiaPreset<O = unknown> {
64
+ /** Meta-addon composition: children apply depth-first BEFORE this preset. */
65
+ addons?: AddonEntry[];
66
+ commands?: (acc: CommandDescriptor[], opts: O) => CommandDescriptor[] | Promise<CommandDescriptor[]>;
67
+ remotes?: (acc: RemotePlugin[], opts: O) => RemotePlugin[] | Promise<RemotePlugin[]>;
68
+ executors?: (acc: ExecutorPlugin[], opts: O) => ExecutorPlugin[] | Promise<ExecutorPlugin[]>;
69
+ workspaces?: (acc: WorkspacePlugin[], opts: O) => WorkspacePlugin[] | Promise<WorkspacePlugin[]>;
70
+ agents?: (acc: AgentCandidate[], opts: O) => AgentCandidate[] | Promise<AgentCandidate[]>;
71
+ connectionPlugins?: (acc: DropSHPlugin[], opts: O) => DropSHPlugin[] | Promise<DropSHPlugin[]>;
72
+ }
73
+ /** The preset function keys, for shape-detection during discovery. */
74
+ export declare const PRESET_FUNCTION_KEYS: Array<keyof DiscoveredContributions>;
75
+ /** An empty contributions accumulator. */
76
+ export declare function emptyContributions(): DiscoveredContributions;
@@ -0,0 +1,42 @@
1
+ // GAIA-215: the preset contract — the Storybook-style self-declaration every
2
+ // GAIA addon exposes through its `./preset` export subpath. A preset declares an
3
+ // addon's contributions through named, per-surface ACCUMULATOR functions
4
+ // `(acc, opts) => acc` (Storybook's `managerEntries`/`previewAnnotations` model),
5
+ // plus an `addons` array for meta-addon composition. Each extension point is
6
+ // list-valued; the core discovers presets and threads these accumulators, so a
7
+ // conforming addon needs no per-slot `export:`/kind wiring in config.
8
+ //
9
+ // The three GAIA surfaces (each named for its config home):
10
+ // - command → the `@gaia-ai/gaia` host registry (CommandDescriptor[]).
11
+ // - conductor → the run engine (remote / executor / workspace / agent).
12
+ // - connection → dropsh (auth + renderer plugins).
13
+ //
14
+ // Settings (machine_id / project / states / hooks / scheduler) are NEVER
15
+ // preset-contributed — identity must not be composable — so this interface has
16
+ // no such members and an unknown preset export is rejected loudly at discovery.
17
+ /** The extension points each surface runs (the preset function names to call). */
18
+ export const SURFACE_KEYS = {
19
+ command: ['commands'],
20
+ conductor: ['remotes', 'executors', 'workspaces', 'agents'],
21
+ connection: ['connectionPlugins'],
22
+ };
23
+ /** The preset function keys, for shape-detection during discovery. */
24
+ export const PRESET_FUNCTION_KEYS = [
25
+ 'commands',
26
+ 'remotes',
27
+ 'executors',
28
+ 'workspaces',
29
+ 'agents',
30
+ 'connectionPlugins',
31
+ ];
32
+ /** An empty contributions accumulator. */
33
+ export function emptyContributions() {
34
+ return {
35
+ commands: [],
36
+ remotes: [],
37
+ executors: [],
38
+ workspaces: [],
39
+ agents: [],
40
+ connectionPlugins: [],
41
+ };
42
+ }
@@ -30,6 +30,11 @@ export declare class DrupalGaiaRemote implements GaiaRemote {
30
30
  * ticket's run carries the path even after the run closed.
31
31
  */
32
32
  private latestRunWorktree;
33
+ resolveTicketByIdentifier(project: string, identifier: string): Promise<{
34
+ uuid: string;
35
+ title: string;
36
+ } | null>;
37
+ private readonly projectUuidCache;
33
38
  private projectUuid;
34
39
  }
35
40
  export declare function drupalRemote(): RemotePlugin;
@@ -333,7 +333,30 @@ export class DrupalGaiaRemote {
333
333
  }
334
334
  return '';
335
335
  }
336
+ async resolveTicketByIdentifier(project, identifier) {
337
+ // Scope by the project uuid: identifiers (GAIA-nnn) are unique per project,
338
+ // not globally, so a bare identifier filter could match another project's
339
+ // ticket in a multi-project instance.
340
+ const projectId = await this.projectUuid(project);
341
+ const t = await this.api
342
+ .collection('gaia_ticket')
343
+ .where('identifier', '=', identifier)
344
+ .where('project_id.id', '=', projectId)
345
+ .first();
346
+ if (!t) {
347
+ return null;
348
+ }
349
+ return { uuid: t.id, title: t.attr('title') ?? '' };
350
+ }
351
+ projectUuidCache = new Map();
336
352
  async projectUuid(name) {
353
+ // A project's name→uuid never changes, so cache it: gatherBatch resolves
354
+ // many identifiers per `gaia deployment tickets` call, each of which would
355
+ // otherwise re-fetch the same project.
356
+ const cached = this.projectUuidCache.get(name);
357
+ if (cached) {
358
+ return cached;
359
+ }
337
360
  const p = await this.api
338
361
  .collection('gaia_project')
339
362
  .where('name', '=', name)
@@ -341,6 +364,7 @@ export class DrupalGaiaRemote {
341
364
  if (!p) {
342
365
  throw new Error(`gaia project "${name}" not found`);
343
366
  }
367
+ this.projectUuidCache.set(name, p.id);
344
368
  return p.id;
345
369
  }
346
370
  }
@@ -102,6 +102,10 @@ export declare class FakeGaiaRemote implements GaiaRemote {
102
102
  fetchUncleanedTickets(conductorId: string): Promise<UncleanTicket[]>;
103
103
  closeTicket(uuid: string): Promise<void>;
104
104
  markTicketCleanedUp(uuid: string): Promise<void>;
105
+ resolveTicketByIdentifier(_project: string, identifier: string): Promise<{
106
+ uuid: string;
107
+ title: string;
108
+ } | null>;
105
109
  /** Worktree path of the ticket's most recently seeded run, or '' when none. */
106
110
  private latestRunWorktree;
107
111
  private internalActiveRuns;
@@ -216,6 +216,10 @@ export class FakeGaiaRemote {
216
216
  t.cleanedUp = true;
217
217
  }
218
218
  }
219
+ async resolveTicketByIdentifier(_project, identifier) {
220
+ const hit = Object.entries(this.tickets).find(([, t]) => t.identifier === identifier);
221
+ return hit ? { uuid: hit[0], title: hit[1].title ?? '' } : null;
222
+ }
219
223
  /** Worktree path of the ticket's most recently seeded run, or '' when none. */
220
224
  latestRunWorktree(ticketUuid) {
221
225
  let worktree = '';
@@ -1,6 +1,7 @@
1
1
  export interface ConductorRegistration {
2
2
  id: string;
3
3
  project: string;
4
+ /** Empty = serve all claimable states in the project (GAIA-207). */
4
5
  states: string[];
5
6
  workspace: string;
6
7
  label: string;
@@ -190,4 +191,13 @@ export interface GaiaRemote {
190
191
  * already-cleaned ticket is a no-op (it is no longer on the list).
191
192
  */
192
193
  markTicketCleanedUp(uuid: string): Promise<void>;
194
+ /**
195
+ * Resolve a ticket identifier (e.g. "GAIA-134") to its uuid+title within a
196
+ * project, or null when no such ticket exists. Used by the
197
+ * `gaia deployment tickets` helper to turn commit-message identifiers into tickets.
198
+ */
199
+ resolveTicketByIdentifier(project: string, identifier: string): Promise<{
200
+ uuid: string;
201
+ title: string;
202
+ } | null>;
193
203
  }
@@ -13,7 +13,11 @@ export interface ConductorSettings {
13
13
  machine_id?: string;
14
14
  /** Project name (gaia_project.name); resolved at registration. */
15
15
  project: string;
16
- /** Workflow states this conductor serves. */
16
+ /**
17
+ * Workflow states this conductor serves. An empty list means it serves every
18
+ * claimable state in its project; a non-empty list narrows it to those
19
+ * states (GAIA-207).
20
+ */
17
21
  states: string[];
18
22
  /**
19
23
  * Agent prompt template - the GAIA run contract, NOT project workflow. Bounds
@@ -50,3 +54,12 @@ export interface ConductorFileConfig extends ConductorSettings {
50
54
  /** dropsh-layer plugins (auth etc.); separate from the GAIA slots. */
51
55
  plugins?: DropSHPlugin[];
52
56
  }
57
+ /**
58
+ * GAIA-201 split config model: the ENGINE half of a conductor config —
59
+ * everything a `conductor.config.js` carries EXCEPT the connection (`site`) and
60
+ * the auth `plugins`, which now live in a separate `gaia.config.js`
61
+ * (`GaiaConnectionConfig`). `loadConductorConfig` returns this; the conductor
62
+ * command composes it with the connection (`composeConductorConfig`) into the
63
+ * full `ConductorFileConfig` the engine consumes.
64
+ */
65
+ export type ConductorEngineConfig = Omit<ConductorFileConfig, 'site' | 'plugins'>;