@gaia-ai/conductor 0.4.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.
@@ -0,0 +1,71 @@
1
+ import { type ConductorFileConfig, type ConductorLogger, type GaiaAgent, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace } from '@gaia-ai/core';
2
+ /**
3
+ * Resolve the environment a run executes in (GAIA-99): parse the ticket's
4
+ * effective env_vars, drop any reserved key (loud warn — key NAME only, never
5
+ * the value), then overlay the conductor's core vars so they always win.
6
+ *
7
+ * Values are potentially secret (e.g. a DB DSN): this function logs key names
8
+ * only, never a value. The returned map is injected into BOTH the agent run env
9
+ * and every workspace hook.
10
+ */
11
+ export declare function resolveRunEnv(effectiveEnvVars: string | undefined, core: Record<string, string>, logger: ConductorLogger): Record<string, string>;
12
+ export declare class Conductor {
13
+ private readonly config;
14
+ private readonly remote;
15
+ private readonly executor;
16
+ private readonly workspace;
17
+ private readonly agent;
18
+ private readonly logger;
19
+ private readonly checkoutRoot;
20
+ private uuid;
21
+ constructor(config: ConductorFileConfig, remote: GaiaRemote, executor: GaiaExecutor, workspace: GaiaWorkspace, agent: GaiaAgent, logger: ConductorLogger, checkoutRoot?: string);
22
+ get id(): string;
23
+ /** This conductor's registration payload, built from config. */
24
+ private registration;
25
+ start(): Promise<void>;
26
+ tick(): Promise<void>;
27
+ /**
28
+ * One-shot finalisation: for each of this conductor's runs that is
29
+ * state=done but not yet closed, read the agent transcript from its worktree
30
+ * and write it as the final log, then mark the run closed. Keyed off the
31
+ * durable `state=done AND closed=false` query — no in-memory state, so a
32
+ * restarted conductor still finalises any unclosed done run on its next tick.
33
+ */
34
+ private finalizeDoneRuns;
35
+ /**
36
+ * Reconcile finished tickets against their herdr worktrees and tear down any
37
+ * whose workspace still lingers (GAIA-89). Driven by the durable `cleaned_up`
38
+ * flag via {@link GaiaRemote.fetchUncleanedTickets}: every ticket that is
39
+ * finished (state=done OR closed) but not yet cleaned. The SAME reconciliation
40
+ * runs on every tick AND standalone as `gaia conductor reap`, and the query is
41
+ * NOT machine-scoped — so it catches the orphans a live-tick-only path
42
+ * structurally cannot (RC2/RC3): the conductor was down when the ticket hit
43
+ * `done`, crashed mid-cleanup, or the ticket's `conductor_id` was reassigned.
44
+ * A foreign (non-gaia) worktree has no gaia ticket, so it is never on the work
45
+ * list and never touched — foreign-safety falls out of the DB-driven model
46
+ * with no host enumeration.
47
+ *
48
+ * Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
49
+ * is closed at once as a lifecycle step, independent of whether its worktree
50
+ * teardown then succeeds. Teardown is best-effort + verified — `cleaned_up` is
51
+ * set only when the worktree was actually torn down ON THIS HOST. Since the
52
+ * query is not machine-scoped, a conductor may see a finished ticket whose
53
+ * worktree lives on ANOTHER host; `removeWorktree` returns `false` there (a
54
+ * clean "not here") and the reaper DEFERS — it does not flag the ticket
55
+ * cleaned, leaving the teardown to the host that physically holds the
56
+ * worktree. That gate is what keeps the not-machine-scoped query from
57
+ * cross-host-false-teardowning a still-live workspace. A teardown that throws
58
+ * logs loudly and leaves `cleaned_up=0`, so the next reconciliation retries;
59
+ * one miss never orphans the workspace (RC1). Re-running on an already-cleaned
60
+ * ticket is a no-op — it is off the list (idempotent). Each ticket is isolated
61
+ * in a try/catch so one failure never aborts the rest.
62
+ *
63
+ * The `after_done` hook runs through the executor (GAIA-84), which owns the
64
+ * best-effort policy — it logs+swallows a hook failure and never throws, so
65
+ * the core needs no per-call-site try/catch around it.
66
+ */
67
+ reap(): Promise<void>;
68
+ private dispatch;
69
+ serve(signal?: AbortSignal): Promise<void>;
70
+ private pollLoop;
71
+ }
@@ -0,0 +1,410 @@
1
+ import { conductorId, } from '@gaia-ai/core';
2
+ function sleep(ms, signal) {
3
+ return new Promise((resolve) => {
4
+ if (signal?.aborted) {
5
+ resolve();
6
+ return;
7
+ }
8
+ const timer = setTimeout(() => {
9
+ signal?.removeEventListener('abort', onAbort);
10
+ resolve();
11
+ }, ms);
12
+ const onAbort = () => {
13
+ clearTimeout(timer);
14
+ resolve();
15
+ };
16
+ signal?.addEventListener('abort', onAbort, { once: true });
17
+ });
18
+ }
19
+ /**
20
+ * Formats a ticket's comments into the `{comments}` prompt block, oldest first.
21
+ * The conductor injects this so a review→coding bounce reaches the coder with the
22
+ * review summary in hand, independent of whether the agent remembers to fetch it.
23
+ *
24
+ * The body is the raw gaia_rich markup — the agent reads HTML fine, so we don't
25
+ * strip it (a tag/entity stripper is a maintenance footgun that buys nothing for
26
+ * an LLM reader).
27
+ */
28
+ function renderComments(comments) {
29
+ if (comments.length === 0) {
30
+ return '(no comments on this ticket yet)';
31
+ }
32
+ return comments
33
+ .map((c, i) => {
34
+ const when = c.created ? ` (${c.created})` : '';
35
+ return `[#${i + 1}] type=${c.type}${when}\n${c.body}`;
36
+ })
37
+ .join('\n\n---\n\n');
38
+ }
39
+ /**
40
+ * A run-env key is reserved when it names a conductor-provided core variable:
41
+ * any `GAIA_*`, `WORKSPACE_ROOT`, or `TICKET_URL`. User `env_vars` may not
42
+ * clobber these — the core vars always win (GAIA-99).
43
+ */
44
+ function isReservedEnvKey(key) {
45
+ return (key.startsWith('GAIA_') || key === 'WORKSPACE_ROOT' || key === 'TICKET_URL');
46
+ }
47
+ /**
48
+ * Parse the server-canonicalised effective env_vars (`KEY=value` lines, one per
49
+ * line) into a map. The server already stripped comments/blank/malformed lines,
50
+ * so this is deliberately minimal: split on the first `=`, keep a valid key.
51
+ * Defensive against stray blank lines only — never throws.
52
+ */
53
+ function parseEnvLines(text) {
54
+ const out = {};
55
+ for (const raw of text.split('\n')) {
56
+ const line = raw.trim();
57
+ if (line === '') {
58
+ continue;
59
+ }
60
+ const eq = line.indexOf('=');
61
+ if (eq <= 0) {
62
+ continue;
63
+ }
64
+ const key = line.slice(0, eq);
65
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
66
+ continue;
67
+ }
68
+ out[key] = line.slice(eq + 1);
69
+ }
70
+ return out;
71
+ }
72
+ /**
73
+ * Resolve the environment a run executes in (GAIA-99): parse the ticket's
74
+ * effective env_vars, drop any reserved key (loud warn — key NAME only, never
75
+ * the value), then overlay the conductor's core vars so they always win.
76
+ *
77
+ * Values are potentially secret (e.g. a DB DSN): this function logs key names
78
+ * only, never a value. The returned map is injected into BOTH the agent run env
79
+ * and every workspace hook.
80
+ */
81
+ export function resolveRunEnv(effectiveEnvVars, core, logger) {
82
+ const user = effectiveEnvVars ? parseEnvLines(effectiveEnvVars) : {};
83
+ const clobbered = [];
84
+ const safe = {};
85
+ for (const [key, value] of Object.entries(user)) {
86
+ if (isReservedEnvKey(key)) {
87
+ clobbered.push(key);
88
+ continue;
89
+ }
90
+ safe[key] = value;
91
+ }
92
+ if (clobbered.length > 0) {
93
+ logger.warn({ keys: clobbered }, 'ignored reserved keys in ticket env_vars (core vars win)');
94
+ }
95
+ // Core vars overlay last — they can never be overridden.
96
+ return { ...safe, ...core };
97
+ }
98
+ function renderPrompt(template, values) {
99
+ return template
100
+ .replaceAll('{identifier}', values.identifier)
101
+ .replaceAll('{state}', values.state)
102
+ .replaceAll('{runUuid}', values.runUuid)
103
+ .replaceAll('{comments}', renderComments(values.comments));
104
+ }
105
+ export class Conductor {
106
+ config;
107
+ remote;
108
+ executor;
109
+ workspace;
110
+ agent;
111
+ logger;
112
+ checkoutRoot;
113
+ uuid = null;
114
+ constructor(config, remote, executor, workspace, agent, logger, checkoutRoot = process.cwd()) {
115
+ this.config = config;
116
+ this.remote = remote;
117
+ this.executor = executor;
118
+ this.workspace = workspace;
119
+ this.agent = agent;
120
+ this.logger = logger;
121
+ this.checkoutRoot = checkoutRoot;
122
+ }
123
+ get id() {
124
+ return this.config.machine_id ?? conductorId(this.checkoutRoot);
125
+ }
126
+ /** This conductor's registration payload, built from config. */
127
+ registration() {
128
+ return {
129
+ id: this.id,
130
+ project: this.config.project,
131
+ states: this.config.states,
132
+ workspace: this.checkoutRoot,
133
+ label: this.config.label,
134
+ max_parallel: this.config.max_parallel,
135
+ };
136
+ }
137
+ async start() {
138
+ this.uuid = await this.remote.registerConductor(this.registration());
139
+ this.logger.info({ conductorId: this.id, uuid: this.uuid, project: this.config.project }, 'conductor started');
140
+ }
141
+ async tick() {
142
+ if (!this.uuid)
143
+ throw new Error('not started');
144
+ let count = await this.remote.activeRunCount(this.id);
145
+ // A heartbeat is the server-side self-heal: it upserts by machine_id (so it
146
+ // never 404s and recreates a vanished registration) and refreshes the lease
147
+ // — no client-side re-register, no separate status read.
148
+ await this.remote.heartbeat(this.registration(), count, this.config.lease_seconds);
149
+ this.logger.info({ conductorId: this.id, active: count }, 'conductor tick');
150
+ await this.finalizeDoneRuns();
151
+ await this.reap();
152
+ let claimed = 0;
153
+ while (count < this.config.max_parallel) {
154
+ const run = await this.remote.claimNext({
155
+ leaseSeconds: this.config.lease_seconds,
156
+ conductorId: this.id,
157
+ });
158
+ if (!run)
159
+ break;
160
+ this.logger.info({ conductorId: this.id, run: run.runUuid, ticket: run.ticketUuid }, 'claimed run');
161
+ try {
162
+ await this.dispatch(run);
163
+ }
164
+ catch (err) {
165
+ // A failed dispatch must NOT terminalise the run. The run lifecycle is
166
+ // server-owned; a client-side releaseRun('done') here fabricated a
167
+ // terminal state on failure, which reopened the ticket for claiming and
168
+ // re-dispatched it every tick — the GAIA-41 runaway. Leaving the run
169
+ // `claimed` lets the server's same-state claim conflict
170
+ // (TicketClaimService::retireSupersededRuns) block a re-claim of the
171
+ // same ticket, so we just log and move on. A run stuck `claimed` on a
172
+ // persistently-failing dispatch is freed by the lease-expiry reaper
173
+ // (separate follow-up), not here.
174
+ this.logger.error({ run: run.runUuid, err: String(err) }, 'dispatch failed');
175
+ }
176
+ count += 1;
177
+ claimed += 1;
178
+ }
179
+ if (claimed === 0) {
180
+ this.logger.info({
181
+ conductorId: this.id,
182
+ active: count,
183
+ capacity: this.config.max_parallel,
184
+ }, count >= this.config.max_parallel
185
+ ? 'at capacity, nothing claimed'
186
+ : 'idle, nothing to claim');
187
+ }
188
+ }
189
+ /**
190
+ * One-shot finalisation: for each of this conductor's runs that is
191
+ * state=done but not yet closed, read the agent transcript from its worktree
192
+ * and write it as the final log, then mark the run closed. Keyed off the
193
+ * durable `state=done AND closed=false` query — no in-memory state, so a
194
+ * restarted conductor still finalises any unclosed done run on its next tick.
195
+ */
196
+ async finalizeDoneRuns() {
197
+ const runs = await this.remote.fetchFinalizableRuns(this.id);
198
+ for (const r of runs) {
199
+ try {
200
+ // Ask the agent to exit gracefully (/exit) before capturing its log.
201
+ // Best-effort and gated on the persistent capability (only hosted
202
+ // executors have a live agent pane) — mirrors removeWorktree's gate.
203
+ // A stop failure must never block finalisation.
204
+ if (this.executor.capabilities().persistent) {
205
+ const branch = await this.remote.getRunTicketBranchName(r.runUuid);
206
+ if (branch) {
207
+ try {
208
+ await this.executor.stopAgent(branch);
209
+ }
210
+ catch (err) {
211
+ this.logger.warn({ run: r.runUuid, err: String(err) }, 'agent stop failed');
212
+ }
213
+ }
214
+ }
215
+ const log = r.worktreePath
216
+ ? await this.agent.getRunLog(r.worktreePath)
217
+ : '';
218
+ await this.remote.finalizeRun(r.runUuid, log);
219
+ this.logger.info({ run: r.runUuid }, 'run finalised');
220
+ }
221
+ catch (err) {
222
+ this.logger.warn({ run: r.runUuid, err: String(err) }, 'run finalise failed');
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * Reconcile finished tickets against their herdr worktrees and tear down any
228
+ * whose workspace still lingers (GAIA-89). Driven by the durable `cleaned_up`
229
+ * flag via {@link GaiaRemote.fetchUncleanedTickets}: every ticket that is
230
+ * finished (state=done OR closed) but not yet cleaned. The SAME reconciliation
231
+ * runs on every tick AND standalone as `gaia conductor reap`, and the query is
232
+ * NOT machine-scoped — so it catches the orphans a live-tick-only path
233
+ * structurally cannot (RC2/RC3): the conductor was down when the ticket hit
234
+ * `done`, crashed mid-cleanup, or the ticket's `conductor_id` was reassigned.
235
+ * A foreign (non-gaia) worktree has no gaia ticket, so it is never on the work
236
+ * list and never touched — foreign-safety falls out of the DB-driven model
237
+ * with no host enumeration.
238
+ *
239
+ * Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
240
+ * is closed at once as a lifecycle step, independent of whether its worktree
241
+ * teardown then succeeds. Teardown is best-effort + verified — `cleaned_up` is
242
+ * set only when the worktree was actually torn down ON THIS HOST. Since the
243
+ * query is not machine-scoped, a conductor may see a finished ticket whose
244
+ * worktree lives on ANOTHER host; `removeWorktree` returns `false` there (a
245
+ * clean "not here") and the reaper DEFERS — it does not flag the ticket
246
+ * cleaned, leaving the teardown to the host that physically holds the
247
+ * worktree. That gate is what keeps the not-machine-scoped query from
248
+ * cross-host-false-teardowning a still-live workspace. A teardown that throws
249
+ * logs loudly and leaves `cleaned_up=0`, so the next reconciliation retries;
250
+ * one miss never orphans the workspace (RC1). Re-running on an already-cleaned
251
+ * ticket is a no-op — it is off the list (idempotent). Each ticket is isolated
252
+ * in a try/catch so one failure never aborts the rest.
253
+ *
254
+ * The `after_done` hook runs through the executor (GAIA-84), which owns the
255
+ * best-effort policy — it logs+swallows a hook failure and never throws, so
256
+ * the core needs no per-call-site try/catch around it.
257
+ */
258
+ async reap() {
259
+ const tickets = await this.remote.fetchUncleanedTickets();
260
+ for (const t of tickets) {
261
+ try {
262
+ // Lifecycle: close a done+unclosed ticket at once, decoupled from the
263
+ // infra teardown below — a done ticket whose conductor was down was
264
+ // never closed, and withholding the close on a teardown hiccup would
265
+ // make a finished ticket look unfinished (the pre-GAIA-89 defect).
266
+ if (t.state === 'done' && !t.closed) {
267
+ await this.remote.closeTicket(t.ticketUuid);
268
+ }
269
+ // Infra teardown. The `after_done` hook runs in the worktree regardless
270
+ // of executor kind (a non-persistent gitWorkspace still has an on-disk
271
+ // worktree); only the workspace removal is gated on the persistent
272
+ // capability (mirrors the executor-agnostic release path). Pass the
273
+ // stable worktreePath so the executor resolves the worktree by path,
274
+ // not by the mutable checked-out branch (the coding agent may rename
275
+ // it). Teardown is idempotent: a worktree herdr no longer tracks is a
276
+ // no-op / on-disk reclaim, not a failure.
277
+ if (t.worktreePath) {
278
+ await this.executor.runHook('after_done', t.worktreePath, {
279
+ ticket: t.ticketUuid,
280
+ });
281
+ }
282
+ if (this.executor.capabilities().persistent) {
283
+ let removed;
284
+ try {
285
+ removed = await this.executor.removeWorktree(t.branchName, t.worktreePath || undefined);
286
+ }
287
+ catch (err) {
288
+ this.logger.warn({
289
+ ticket: t.ticketUuid,
290
+ branch: t.branchName,
291
+ worktreePath: t.worktreePath,
292
+ err: String(err),
293
+ }, 'worktree teardown failed');
294
+ continue; // leave cleaned_up=0 → the next reconciliation retries.
295
+ }
296
+ // A recorded worktree path that was NOT present on this host belongs
297
+ // to another conductor's host (the query is not machine-scoped, so we
298
+ // see every finished ticket). Do NOT flag it cleaned — that would be a
299
+ // cross-host false-teardown, orphaning a still-live workspace. Defer:
300
+ // the host that physically holds it reaps + flags it. A ticket with no
301
+ // recorded path (nothing hosted to locate) falls through to cleaned.
302
+ if (t.worktreePath && !removed) {
303
+ this.logger.info({
304
+ ticket: t.ticketUuid,
305
+ branch: t.branchName,
306
+ worktreePath: t.worktreePath,
307
+ }, 'worktree not on this host — deferring teardown to its host');
308
+ continue;
309
+ }
310
+ }
311
+ // Teardown verified locally (or nothing hosted to tear down): flag it
312
+ // cleaned so it drops off the work list.
313
+ await this.remote.markTicketCleanedUp(t.ticketUuid);
314
+ this.logger.info({ ticket: t.ticketUuid }, 'ticket cleaned up');
315
+ }
316
+ catch (err) {
317
+ this.logger.warn({ ticket: t.ticketUuid, err: String(err) }, 'ticket cleanup failed');
318
+ }
319
+ }
320
+ }
321
+ async dispatch(run) {
322
+ const t = await this.remote.getTicket(run.ticketUuid);
323
+ const baseRef = t.baseBranch ? `origin/${t.baseBranch}` : undefined;
324
+ // Resolve the run env once (per-ticket env_vars + core GAIA_* vars) and
325
+ // inject it into BOTH the executor-owned lifecycle hooks AND the agent run
326
+ // env (GAIA-99). Reserved core keys always win; values may be sensitive so
327
+ // only key names are ever logged.
328
+ const core = {
329
+ GAIA_URL: this.config.site.base_url,
330
+ GAIA_ID: run.ticketUuid,
331
+ GAIA_RUN_UUID: run.runUuid,
332
+ WORKSPACE_ROOT: this.checkoutRoot,
333
+ ...(t.issueUrl ? { TICKET_URL: t.issueUrl } : {}),
334
+ };
335
+ const env = resolveRunEnv(t.effectiveEnvVars, core, this.logger);
336
+ const ws = await this.workspace.ensure(t.identifier, t.branchName, baseRef);
337
+ // A claimable state has no deterministic policy without a WORKFLOW.md
338
+ // (SKILL.md dispatch reads its `## State: <state>` sections). The human
339
+ // steered this to WARN-and-proceed (not a hard fail): the agent runs on
340
+ // engine defaults, and the gap is observable in the conductor log rather
341
+ // than wedging the run. `ws.instructions` IS the loaded WORKFLOW.md (null
342
+ // when absent), so no extra stat.
343
+ if (!ws.instructions) {
344
+ this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing — dispatching on engine defaults');
345
+ }
346
+ // Lifecycle hooks are executor-owned + best-effort (GAIA-84): runHook never
347
+ // throws, so neither call can abort dispatch or wedge a run in `claimed`.
348
+ // after_create only on a freshly-created worktree; before_run always. Both
349
+ // get the resolved run env so a setup/pre hook sees the same vars (e.g. a DB
350
+ // DSN) the agent does (GAIA-99).
351
+ if (ws.created) {
352
+ await this.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
353
+ }
354
+ await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
355
+ await this.executor.retire(t.branchName);
356
+ const prompt = ws.instructions
357
+ ? renderPrompt(this.config.prompt, {
358
+ identifier: t.identifier,
359
+ state: t.state || 'triage',
360
+ runUuid: run.runUuid,
361
+ comments: t.comments,
362
+ })
363
+ : '';
364
+ await this.executor.startRun({
365
+ ticket: {
366
+ uuid: t.uuid,
367
+ identifier: t.identifier,
368
+ title: t.title,
369
+ branchName: t.branchName,
370
+ state: t.state,
371
+ ...(t.issueUrl ? { url: t.issueUrl } : {}),
372
+ },
373
+ run: { uuid: run.runUuid, id: run.runId, handler: run.handler },
374
+ workspacePath: ws.path,
375
+ instructions: ws.instructions,
376
+ command: this.agent.launchCommand(prompt),
377
+ env,
378
+ });
379
+ this.logger.info({
380
+ ticket: t.identifier,
381
+ run: run.runUuid,
382
+ state: t.state,
383
+ workspace: ws.path,
384
+ // Key names only — env values may be sensitive (GAIA-99).
385
+ envKeys: Object.keys(env),
386
+ }, 'dispatched run');
387
+ // The conductor — not the agent — knows the per-run git worktree path, so
388
+ // it persists `worktree_path` to gaia_run here alongside the running state.
389
+ await this.remote.markRunning(run.runUuid, { worktree_path: ws.path });
390
+ }
391
+ async serve(signal) {
392
+ await this.pollLoop(signal);
393
+ }
394
+ async pollLoop(signal) {
395
+ while (!signal?.aborted) {
396
+ try {
397
+ await this.tick();
398
+ }
399
+ catch (err) {
400
+ this.logger.error({ conductorId: this.id, err: String(err) }, 'tick failed');
401
+ }
402
+ await sleep(this.config.poll_interval_ms, signal);
403
+ }
404
+ // NB: the poll loop never writes status=offline itself. A graceful stop
405
+ // (`gaia conductor stop`) writes offline directly server-side; a crash or
406
+ // hard-kill leaves a stale lease that the Drupal cron reaper flips to
407
+ // offline (gaia_core cron → ConductorReaper). Liveness is thus always
408
+ // derived from status + last_seen/lease_expires_at, never from the loop.
409
+ }
410
+ }
@@ -0,0 +1,8 @@
1
+ export type * from '@gaia-ai/core';
2
+ 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 { 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';
8
+ export { Conductor } from './core/conductor.js';
@@ -0,0 +1,5 @@
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
+ export { main, runGaiaCli } from './cli/gaia.js';
4
+ export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
5
+ export { Conductor } from './core/conductor.js';
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@gaia-ai/conductor",
3
+ "version": "0.4.0",
4
+ "description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20.19"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/src/index.js",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist/src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://git.key-tec.de/keytec/gaia.git",
25
+ "directory": "conductor/packages/conductor"
26
+ },
27
+ "dependencies": {
28
+ "@gaia-ai/core": "^0.4.0",
29
+ "@dropsh/plugin-oauth2": "^0.4.1",
30
+ "@dropsh/plugin-jsonapi-schema": "^0.5.1",
31
+ "commander": "^12.1.0",
32
+ "dropsh": "^0.5.1"
33
+ }
34
+ }