@gaia-ai/core 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.
Files changed (34) 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/executor/executor.d.ts +13 -8
  24. package/dist/src/plugins/preset.d.ts +76 -0
  25. package/dist/src/plugins/preset.js +42 -0
  26. package/dist/src/plugins/remote/drupal.d.ts +5 -0
  27. package/dist/src/plugins/remote/drupal.js +27 -1
  28. package/dist/src/plugins/remote/fake.d.ts +4 -0
  29. package/dist/src/plugins/remote/fake.js +5 -0
  30. package/dist/src/plugins/remote/remote.d.ts +12 -0
  31. package/dist/src/types.d.ts +14 -1
  32. package/dist/src/workflow/step-contract.d.ts +119 -0
  33. package/dist/src/workflow/step-contract.js +430 -0
  34. 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
+ }
@@ -66,15 +66,20 @@ export interface GaiaExecutor {
66
66
  */
67
67
  stopRun(branch: string): Promise<void>;
68
68
  /**
69
- * Tear down the run's hosted UI surface — for herdr, close every tab of the
70
- * branch workspace via `herdr tab close <tab_id>` (killing the PTY). Called
71
- * as the LAST finalisation step, strictly after the transcript is captured,
72
- * and again at dispatch to clear a reused workspace's leftover tabs. Does NOT
73
- * touch the branch worktree (that is the reap/{@link removeWorktree}
74
- * lifecycle). Best-effort: one failing tab-close must not abort the others,
75
- * finalisation, or dispatch. A no-op for non-persistent executors.
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.
76
81
  */
77
- cleanupRun(branch: string): Promise<void>;
82
+ cleanupRun(branch: string, runId: number): Promise<void>;
78
83
  /**
79
84
  * Tear down the branch's entire worktree (git worktree + hosted workspace),
80
85
  * reclaiming its disk. Called by the conductor's ticket-cleanup pass once a
@@ -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;
@@ -237,7 +237,7 @@ export class DrupalGaiaRemote {
237
237
  .where('conductor_id.machine_id', '=', id)
238
238
  .where('state', '=', 'done')
239
239
  .where('closed', '=', '0')
240
- .fields(['worktree_path', 'agent'])
240
+ .fields(['worktree_path', 'agent', 'drupal_internal__id'])
241
241
  .page(100)
242
242
  .list();
243
243
  // No `started_at` read: the run's duration_s is derived from the agent
@@ -245,6 +245,8 @@ export class DrupalGaiaRemote {
245
245
  // the timestamp back through JSON:API.
246
246
  return rows.map((r) => ({
247
247
  runUuid: r.id,
248
+ // The `#<runId>` tab-label token: scopes the run's tab-close (GAIA-183).
249
+ runId: Number(r.attr('drupal_internal__id')),
248
250
  worktreePath: r.attr('worktree_path') ?? '',
249
251
  agent: r.attr('agent') ?? '',
250
252
  }));
@@ -331,7 +333,30 @@ export class DrupalGaiaRemote {
331
333
  }
332
334
  return '';
333
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();
334
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
+ }
335
360
  const p = await this.api
336
361
  .collection('gaia_project')
337
362
  .where('name', '=', name)
@@ -339,6 +364,7 @@ export class DrupalGaiaRemote {
339
364
  if (!p) {
340
365
  throw new Error(`gaia project "${name}" not found`);
341
366
  }
367
+ this.projectUuidCache.set(name, p.id);
342
368
  return p.id;
343
369
  }
344
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;
@@ -168,6 +168,7 @@ export class FakeGaiaRemote {
168
168
  .filter((r) => r.state === 'done' && !r.closed)
169
169
  .map((r) => ({
170
170
  runUuid: r.runUuid,
171
+ runId: r.runId,
171
172
  worktreePath: r.worktreePath ?? '',
172
173
  agent: r.agent ?? '',
173
174
  }));
@@ -215,6 +216,10 @@ export class FakeGaiaRemote {
215
216
  t.cleanedUp = true;
216
217
  }
217
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
+ }
218
223
  /** Worktree path of the ticket's most recently seeded run, or '' when none. */
219
224
  latestRunWorktree(ticketUuid) {
220
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;
@@ -62,6 +63,8 @@ export interface ConductorStatus {
62
63
  }
63
64
  export interface FinalizableRun {
64
65
  runUuid: string;
66
+ /** Numeric run id (the `#<runId>` tab-label token); scopes the run's tab-close at finalise (GAIA-183). */
67
+ runId: number;
65
68
  /** Absolute path of the per-run git worktree, or '' when unset. */
66
69
  worktreePath: string;
67
70
  /** Id of the agent that ran (GAIA-144); routes footprint parsing at finalize. '' / undefined when unset. */
@@ -188,4 +191,13 @@ export interface GaiaRemote {
188
191
  * already-cleaned ticket is a no-op (it is no longer on the list).
189
192
  */
190
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>;
191
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'>;