@gaia-ai/conductor 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +89 -0
  3. package/dist/src/cli/config-schema.js +146 -0
  4. package/dist/src/cli/init.d.ts +31 -33
  5. package/dist/src/cli/init.js +106 -84
  6. package/dist/src/cli/migrate-addon-names.d.ts +72 -0
  7. package/dist/src/cli/migrate-addon-names.js +318 -0
  8. package/dist/src/cli/upgrade.d.ts +53 -0
  9. package/dist/src/cli/upgrade.js +222 -0
  10. package/dist/src/cli/version-check.js +5 -1
  11. package/dist/src/commands/conductor.d.ts +57 -0
  12. package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
  13. package/dist/src/config.d.ts +65 -24
  14. package/dist/src/config.js +405 -154
  15. package/dist/src/contract.d.ts +8 -0
  16. package/dist/src/contract.js +16 -0
  17. package/dist/src/core/conductor.d.ts +16 -1
  18. package/dist/src/core/conductor.js +28 -9
  19. package/dist/src/index.d.ts +7 -5
  20. package/dist/src/index.js +26 -3
  21. package/dist/src/plugins/agent.d.ts +61 -0
  22. package/dist/src/plugins/agent.js +11 -0
  23. package/dist/src/plugins/executor.d.ts +104 -0
  24. package/dist/src/plugins/executor.js +1 -0
  25. package/dist/src/plugins/plugins.d.ts +60 -0
  26. package/dist/src/plugins/plugins.js +42 -0
  27. package/dist/src/plugins/preset.d.ts +48 -0
  28. package/dist/src/plugins/preset.js +23 -0
  29. package/dist/src/plugins/remote.d.ts +203 -0
  30. package/dist/src/plugins/remote.js +1 -0
  31. package/dist/src/plugins/workspace.d.ts +35 -0
  32. package/dist/src/plugins/workspace.js +1 -0
  33. package/dist/src/preset.d.ts +2 -0
  34. package/dist/src/preset.js +8 -0
  35. package/dist/src/types.d.ts +65 -0
  36. package/dist/src/types.js +1 -0
  37. package/package.json +8 -5
  38. package/dist/src/cli/gaia.d.ts +0 -23
  39. package/dist/src/cli/local-registry.d.ts +0 -14
  40. package/dist/src/cli/local-registry.js +0 -56
@@ -0,0 +1,203 @@
1
+ export interface ConductorRegistration {
2
+ id: string;
3
+ project: string;
4
+ /** Empty = serve all claimable states in the project (GAIA-207). */
5
+ states: string[];
6
+ workspace: string;
7
+ label: string;
8
+ max_parallel: number;
9
+ }
10
+ export interface ClaimOptions {
11
+ leaseSeconds: number;
12
+ conductorId: string;
13
+ }
14
+ export interface ClaimedRun {
15
+ runUuid: string;
16
+ runId: number;
17
+ ticketUuid: string;
18
+ stateAtStart: string;
19
+ handler: string;
20
+ }
21
+ export interface Ticket {
22
+ uuid: string;
23
+ identifier: string;
24
+ title: string;
25
+ state: string;
26
+ branchName: string;
27
+ /** Branch the ticket's work is based on (computed server-side): parent branch, project default, or 'main'. */
28
+ baseBranch?: string;
29
+ /**
30
+ * Effective environment variables (GAIA-99), resolved server-side along the
31
+ * parent_id chain (child wins) as canonical `.env`-style `KEY=value` lines.
32
+ * The conductor parses this, strips reserved keys, overlays the core GAIA_*
33
+ * vars, and injects the result into the agent run env AND the workspace hooks.
34
+ * Omitted when the ticket (and its ancestors) set none. Values may be secret.
35
+ */
36
+ effectiveEnvVars?: string;
37
+ issueUrl?: string;
38
+ /** Ticket label term names (gaia_labels vocab), sideloaded for agent selection. Empty when none. */
39
+ labels: string[];
40
+ /** Ticket environments (gaia_environment refs), each as name + tier, sideloaded for agent selection. Empty when none. */
41
+ environments: {
42
+ name: string;
43
+ tier: string;
44
+ }[];
45
+ }
46
+ export interface ActiveRun {
47
+ runUuid: string;
48
+ ticketUuid: string;
49
+ ticketIdentifier: string;
50
+ branchName: string;
51
+ state: string;
52
+ stateAtStart: string;
53
+ /** Absolute path of the per-run git worktree, or '' when unset. */
54
+ worktreePath: string;
55
+ }
56
+ export interface ConductorStatus {
57
+ id: string;
58
+ project: string;
59
+ label: string;
60
+ status: string;
61
+ lastSeen: number;
62
+ load: number;
63
+ }
64
+ export interface FinalizableRun {
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;
68
+ /** Absolute path of the per-run git worktree, or '' when unset. */
69
+ worktreePath: string;
70
+ /** Id of the agent that ran (GAIA-144); routes footprint parsing at finalize. '' / undefined when unset. */
71
+ agent?: string;
72
+ }
73
+ /**
74
+ * Per-run effort footprint the conductor writes onto `gaia_run` at close
75
+ * (GAIA-132). Six integer metrics, all parsed from the agent transcript —
76
+ * including `duration_s`, the run's wall-clock length derived from the
77
+ * transcript's first→last timestamps (GAIA-151), not a re-read `started_at`.
78
+ */
79
+ export interface RunMetrics {
80
+ tokens: number;
81
+ duration_s: number;
82
+ agent_turns: number;
83
+ tool_calls: number;
84
+ user_prompts: number;
85
+ user_prompt_words: number;
86
+ }
87
+ /**
88
+ * A finished ticket whose worktree has not yet been torn down — the
89
+ * conductor's teardown work list, driven by the durable `cleaned_up` flag
90
+ * (GAIA-89). "Finished" = reached `done` OR already `closed`; `cleaned_up=0`
91
+ * means the herdr worktree still needs reclaiming. The reconciliation is NOT
92
+ * scoped to a conductor's machine_id, so it also surfaces tickets whose
93
+ * conductor was down at `done` or whose `conductor_id` was reassigned.
94
+ */
95
+ export interface UncleanTicket {
96
+ ticketUuid: string;
97
+ /** The ticket's git branch — the key herdr resolves the worktree by. */
98
+ branchName: string;
99
+ /**
100
+ * Absolute path of the ticket's worktree (from its latest run's
101
+ * `worktree_path`), or '' when unresolved. The cwd the teardown runs in.
102
+ */
103
+ worktreePath: string;
104
+ /** Workflow state (e.g. `coding`, `done`). */
105
+ state: string;
106
+ /** Whether the ticket's lifecycle has already been closed out. */
107
+ closed: boolean;
108
+ }
109
+ /** Extra conductor-writable gaia_run attributes (besides state/lease). */
110
+ export interface RunWriteAttributes {
111
+ /** Absolute path of the per-run git worktree (set by the conductor). */
112
+ worktree_path?: string;
113
+ /** Id of the agent the conductor chose for this run (GAIA-144), for footprint routing. */
114
+ agent?: string;
115
+ }
116
+ export interface GaiaRemote {
117
+ /** Upserts by machine_id; sets status=online. Returns the Drupal entity uuid. */
118
+ registerConductor(reg: ConductorRegistration): Promise<string>;
119
+ /**
120
+ * Records a heartbeat server-side: upserts by machine_id (recreating a
121
+ * vanished registration), refreshes last_seen/lease/load, and brings the
122
+ * conductor online. Keyed on machine_id, so it never 404s — the self-heal
123
+ * lives on the server, not the client. Returns the resulting status
124
+ * (`online` | `offline`).
125
+ */
126
+ heartbeat(reg: ConductorRegistration, currentLoad: number, leaseSeconds?: number): Promise<string>;
127
+ getConductorStatus(conductorId: string): Promise<string | null>;
128
+ setConductorStatus(conductorId: string, status: 'offline' | 'online'): Promise<void>;
129
+ listConductors(owner?: 'me'): Promise<ConductorStatus[]>;
130
+ activeRunCount(conductorId: string): Promise<number>;
131
+ fetchActiveRuns(conductorId: string): Promise<ActiveRun[]>;
132
+ claimNext(claim: ClaimOptions): Promise<ClaimedRun | null>;
133
+ getTicket(ticketUuid: string): Promise<Ticket>;
134
+ /** Returns the run's worktree_path, or '' when unset. */
135
+ getRunWorktree(runUuid: string): Promise<string>;
136
+ /**
137
+ * Returns the identifier of the run's ticket, or '' when unresolved. Lets the
138
+ * release path derive the tab matching ref (the ticket identifier) from a run
139
+ * uuid alone.
140
+ */
141
+ getRunTicketIdentifier(runUuid: string): Promise<string>;
142
+ /**
143
+ * Returns the branch_name of the run's ticket, or '' when unresolved. Used
144
+ * by dispatch for the per-(branch,state) herdr tab model.
145
+ */
146
+ getRunTicketBranchName(runUuid: string): Promise<string>;
147
+ markRunning(runUuid: string, attrs?: RunWriteAttributes): Promise<void>;
148
+ /**
149
+ * Terminalise a run to the `failed` state on a dispatch/setup error (GAIA-149).
150
+ * PATCHes state=failed, records the failure detail in `error_log`, and closes
151
+ * the run (closed + closed_date). `failed` is a terminal sibling of `expired`
152
+ * (dispatch error vs lease lapse), so the run stops counting toward capacity
153
+ * and its ticket is no longer blocked by the single-active-run gate — instead
154
+ * of the old "leave it `claimed` to expire after ~300s" silent stall. The
155
+ * failure is visible immediately and its cause is recorded.
156
+ */
157
+ markFailed(runUuid: string, errorLog: string): Promise<void>;
158
+ /**
159
+ * Runs this conductor owns that are state=done but not yet closed — the
160
+ * conductor's one-shot finalisation work list.
161
+ */
162
+ fetchFinalizableRuns(conductorId: string): Promise<FinalizableRun[]>;
163
+ /**
164
+ * Finalise a run: set closed + closed_date, (when non-empty) log, and (when
165
+ * given) the per-run footprint metrics (GAIA-132). No state change — the run
166
+ * is already done.
167
+ */
168
+ finalizeRun(uuid: string, log: string, metrics?: RunMetrics): Promise<void>;
169
+ /**
170
+ * Finished tickets (state=done OR closed) whose worktree is not yet torn
171
+ * down (cleaned_up=0) — the conductor's teardown work list (GAIA-89). Driven
172
+ * by the durable `cleaned_up` flag AND scoped to the reaping conductor
173
+ * (GAIA-121): only tickets assigned to `conductorId` are loaded, so every
174
+ * ticket on the list is unambiguously this conductor's — a missing worktree
175
+ * means "already torn down on this host", not "belongs to another host". The
176
+ * same query backs the live tick and the standalone `gaia conductor reap`.
177
+ * Empty once every finished ticket this conductor owns is cleaned.
178
+ */
179
+ fetchUncleanedTickets(conductorId: string): Promise<UncleanTicket[]>;
180
+ /**
181
+ * Close a ticket: set closed=true + closed_date. Ticket-lifecycle only, and
182
+ * decoupled from teardown — a done ticket is closed even if its worktree
183
+ * teardown later fails. No state change (the ticket is already done).
184
+ */
185
+ closeTicket(uuid: string): Promise<void>;
186
+ /**
187
+ * Mark a ticket's worktree torn down: set cleaned_up=true so it drops off the
188
+ * {@link fetchUncleanedTickets} work list. Written ONLY after a verified
189
+ * teardown, so a teardown miss leaves cleaned_up=0 and the next reconciliation
190
+ * retries — one miss never orphans the workspace, and a re-run on an
191
+ * already-cleaned ticket is a no-op (it is no longer on the list).
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>;
203
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ export interface EnsuredWorkspace {
2
+ path: string;
3
+ instructions: {
4
+ path: string;
5
+ sha256: string;
6
+ text: string;
7
+ } | null;
8
+ /**
9
+ * Whether this call CREATED the worktree (true) or reused an existing one
10
+ * (false). The core runs the `after_create` lifecycle hook only on a fresh
11
+ * worktree, so it needs to know which happened. Lifecycle hooks themselves
12
+ * are no longer a workspace concern (GAIA-84): the executor owns and runs
13
+ * them best-effort.
14
+ */
15
+ created: boolean;
16
+ }
17
+ export interface GaiaWorkspace {
18
+ /**
19
+ * Ensure a per-ticket worktree exists. `branch`, when given, is the branch
20
+ * name the dispatcher computed for the ticket. The git plugin treats it as a
21
+ * readable title to slug into its branch template; the herdr plugin uses it
22
+ * verbatim as the worktree branch. The worktree directory key/path stays
23
+ * identifier- or branch-derived so reuse is stable across runs.
24
+ *
25
+ * `baseRef`, when given, overrides the base the new branch is created from
26
+ * (e.g. `origin/<base_branch>` so a sub-ticket stacks on its parent); if it
27
+ * does not resolve on the remote the implementation falls back to its default
28
+ * base — never a hard error.
29
+ *
30
+ * Reports `created` so the caller can run the `after_create` hook only on a
31
+ * fresh worktree. Lifecycle-hook invocation is NOT a workspace responsibility
32
+ * anymore — the executor owns all hooks (GAIA-84).
33
+ */
34
+ ensure(identifier: string, branch?: string, baseRef?: string): Promise<EnsuredWorkspace>;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Preset } from './plugins/preset.js';
2
+ export declare const commands: Preset['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
+ ];
@@ -0,0 +1,65 @@
1
+ import type { DropSHPlugin } from 'dropsh/plugin';
2
+ import type { AgentCandidate, ExecutorPlugin, RemotePlugin, WorkspacePlugin } from './plugins/plugins.js';
3
+ /** Normalized single conductor. */
4
+ export interface ConductorSettings {
5
+ /** Default: `${project} @ ${checkoutRoot}`. */
6
+ label: string;
7
+ /**
8
+ * Stable node identity (gaia_conductor.machine_id) this process registers as.
9
+ * Defaults to a hash of hostname + checkout path. Set it to pin a conductor to
10
+ * a known identity — e.g. so a ticket can be pre-assigned to it (conductor
11
+ * assignment is the run-start trigger), which the e2e fixtures rely on.
12
+ */
13
+ machine_id?: string;
14
+ /** Project name (gaia_project.name); resolved at registration. */
15
+ project: string;
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
+ */
21
+ states: string[];
22
+ /**
23
+ * Agent prompt template - the GAIA run contract, NOT project workflow. Bounds
24
+ * the agent to exactly one state and tells it to release + stop, keeping run
25
+ * mechanics out of the repo's WORKFLOW.md. Placeholders: `{identifier}`,
26
+ * `{state}`, `{runUuid}`. Defaults to `DEFAULT_AGENT_PROMPT`.
27
+ */
28
+ prompt: string;
29
+ /** Default: 1. */
30
+ max_parallel: number;
31
+ poll_interval_ms: number;
32
+ lease_seconds: number;
33
+ hooks?: {
34
+ after_create?: string;
35
+ before_run?: string;
36
+ after_run?: string;
37
+ after_done?: string;
38
+ };
39
+ /** Control-plane site settings used for dropsh and agent env. */
40
+ site: {
41
+ base_url: string;
42
+ jsonapi_prefix: string;
43
+ };
44
+ /** Absolute path of the loaded config. */
45
+ config_path: string;
46
+ }
47
+ /** Whole conductor.config.js (dropsh-superset): site + named plugin slots. */
48
+ export interface ConductorFileConfig extends ConductorSettings {
49
+ remote: RemotePlugin;
50
+ executor: ExecutorPlugin;
51
+ /** The agent slot: a single candidate or an array of candidates (shape preserved as authored). Selection normalizes + picks one per dispatch. */
52
+ agent: AgentCandidate | AgentCandidate[];
53
+ workspace: WorkspacePlugin;
54
+ /** dropsh-layer plugins (auth etc.); separate from the GAIA slots. */
55
+ plugins?: DropSHPlugin[];
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'>;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/conductor",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
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,9 @@
9
9
  },
10
10
  "exports": {
11
11
  ".": "./dist/src/index.js",
12
+ "./contract": "./dist/src/contract.js",
13
+ "./preset": "./dist/src/preset.js",
14
+ "./commands/conductor": "./dist/src/commands/conductor.js",
12
15
  "./package.json": "./package.json"
13
16
  },
14
17
  "files": [
@@ -22,14 +25,14 @@
22
25
  "repository": {
23
26
  "type": "git",
24
27
  "url": "git+https://git.key-tec.de/keytec/gaia.git",
25
- "directory": "conductor/packages/conductor"
28
+ "directory": "gaia-cli/conductor"
26
29
  },
27
30
  "dependencies": {
28
- "@gaia-ai/core": "^0.5.5",
31
+ "@gaia-ai/core": "^0.6.1",
29
32
  "@dropsh/plugin-oauth2": "^0.5.7",
30
- "@dropsh/plugin-jsonapi-schema": "^0.5.7",
33
+ "@dropsh/plugin-jsonapi-schema": "^0.5.8",
31
34
  "commander": "^12.1.0",
32
- "dropsh": "^0.5.7",
35
+ "dropsh": "^0.5.8",
33
36
  "semver": "^7.6.0"
34
37
  }
35
38
  }
@@ -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
- }