@gaia-ai/gaia 0.2.0 → 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.
Files changed (52) hide show
  1. package/bin/gaia +1 -1
  2. package/dist/src/plugins-barrel.d.ts +1 -0
  3. package/dist/src/plugins-barrel.js +1 -0
  4. package/package.json +10 -16
  5. package/dist/src/cli/gaia.d.ts +0 -24
  6. package/dist/src/cli/gaia.js +0 -572
  7. package/dist/src/cli/init.d.ts +0 -74
  8. package/dist/src/cli/init.js +0 -220
  9. package/dist/src/cli/local-registry.d.ts +0 -14
  10. package/dist/src/cli/local-registry.js +0 -56
  11. package/dist/src/config.d.ts +0 -13
  12. package/dist/src/config.js +0 -186
  13. package/dist/src/core/conductor-id.d.ts +0 -1
  14. package/dist/src/core/conductor-id.js +0 -8
  15. package/dist/src/core/conductor.d.ts +0 -47
  16. package/dist/src/core/conductor.js +0 -287
  17. package/dist/src/core/exec.d.ts +0 -33
  18. package/dist/src/core/exec.js +0 -65
  19. package/dist/src/core/logger.d.ts +0 -31
  20. package/dist/src/core/logger.js +0 -36
  21. package/dist/src/core/slug.d.ts +0 -9
  22. package/dist/src/core/slug.js +0 -18
  23. package/dist/src/index.d.ts +0 -16
  24. package/dist/src/index.js +0 -9
  25. package/dist/src/plugin-api.d.ts +0 -7
  26. package/dist/src/plugin-api.js +0 -3
  27. package/dist/src/plugins/agent/agent.d.ts +0 -20
  28. package/dist/src/plugins/agent/agent.js +0 -1
  29. package/dist/src/plugins/auth/basic.d.ts +0 -11
  30. package/dist/src/plugins/auth/basic.js +0 -35
  31. package/dist/src/plugins/executor/executor.d.ts +0 -61
  32. package/dist/src/plugins/executor/executor.js +0 -1
  33. package/dist/src/plugins/plugins.d.ts +0 -38
  34. package/dist/src/plugins/plugins.js +0 -16
  35. package/dist/src/plugins/registry-exports.d.ts +0 -6
  36. package/dist/src/plugins/registry-exports.js +0 -6
  37. package/dist/src/plugins/remote/drupal.d.ts +0 -47
  38. package/dist/src/plugins/remote/drupal.js +0 -337
  39. package/dist/src/plugins/remote/fake.d.ts +0 -81
  40. package/dist/src/plugins/remote/fake.js +0 -203
  41. package/dist/src/plugins/remote/remote.d.ts +0 -143
  42. package/dist/src/plugins/remote/remote.js +0 -1
  43. package/dist/src/plugins/workspace/fake.d.ts +0 -9
  44. package/dist/src/plugins/workspace/fake.js +0 -19
  45. package/dist/src/plugins/workspace/git.d.ts +0 -53
  46. package/dist/src/plugins/workspace/git.js +0 -113
  47. package/dist/src/plugins/workspace/instructions.d.ts +0 -6
  48. package/dist/src/plugins/workspace/instructions.js +0 -16
  49. package/dist/src/plugins/workspace/workspace.d.ts +0 -33
  50. package/dist/src/plugins/workspace/workspace.js +0 -1
  51. package/dist/src/types.d.ts +0 -51
  52. package/dist/src/types.js +0 -1
@@ -1,74 +0,0 @@
1
- export interface InitInputs {
2
- baseUrl: string;
3
- project: string;
4
- clientId: string;
5
- /** The resolved OAuth client secret value (stored in the machine context). */
6
- secret: string;
7
- machineId?: string;
8
- userId?: string;
9
- }
10
- export interface ScaffoldOptions {
11
- configPath: string;
12
- force: boolean;
13
- machinePath?: string;
14
- }
15
- export interface ScaffoldResult {
16
- committedPath: string;
17
- wroteCommitted: boolean;
18
- machine: MachineContextResult;
19
- }
20
- /**
21
- * The user-global machine context: a plain importable module carrying the
22
- * developer's machine identity and connection (incl. the OAuth client secret).
23
- * Committed configs import it to compose machine_id and read
24
- * base_url / client_id / client_secret. Gitignored, user-only (chmod 0600).
25
- */
26
- export interface MachineContext {
27
- machine_id: string;
28
- user_id: string;
29
- base_url: string;
30
- client_id: string;
31
- client_secret: string;
32
- }
33
- export interface MachineContextOptions {
34
- path: string;
35
- userId: string;
36
- baseUrl: string;
37
- clientId: string;
38
- secret: string;
39
- machineId?: string;
40
- }
41
- export interface MachineContextResult {
42
- path: string;
43
- created: boolean;
44
- filledKeys: string[];
45
- }
46
- /**
47
- * The committed, structural conductor config. `project` is the only per-repo
48
- * value and is baked in here; connection + identity (incl. the secret) come from
49
- * the user-global machine context (~/.config/conductor/conductor.config.machine.js),
50
- * and machine_id is composed as `${user_id}-${machine_id}-${project}`. An
51
- * optional, gitignored conductor.config.local.js beside this file may override
52
- * any field — it is loaded if present but never created by `gaia conductor init`.
53
- */
54
- export declare function renderCommittedConfig(inputs: Pick<InitInputs, 'project'>): string;
55
- /** The user-global machine context module: identity + connection (incl. secret). */
56
- export declare function renderMachineContext(ctx: MachineContext): string;
57
- /** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
58
- export declare function machineContextPath(): string;
59
- /** Import an existing context module's default export, or {} if absent/broken. */
60
- export declare function readMachineContext(path: string): Promise<Partial<MachineContext>>;
61
- /**
62
- * Create-if-missing / fill-only-missing the user-global machine context.
63
- * Existing values always win; only absent/blank keys are filled. machine_id
64
- * defaults to hostname(). A no-op (no rewrite) when the file is already complete.
65
- * The file is written user-only (chmod 0600) since it holds the client secret.
66
- */
67
- export declare function scaffoldMachineContext(opts: MachineContextOptions): Promise<MachineContextResult>;
68
- /**
69
- * Scaffold the two conductor config files: the committed conductor.config.js
70
- * (created only if missing — never overwritten unless `force`) and the
71
- * user-global machine context (create-if-missing / fill-only-missing). The
72
- * optional per-project conductor.config.local.js is NOT generated.
73
- */
74
- export declare function scaffold(inputs: InitInputs, opts: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -1,220 +0,0 @@
1
- import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { hostname } from 'node:os';
3
- import { dirname, join } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- /** JS single-quoted string literal for a trusted, simple value. */
6
- function q(value) {
7
- return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
8
- }
9
- /**
10
- * The committed, structural conductor config. `project` is the only per-repo
11
- * value and is baked in here; connection + identity (incl. the secret) come from
12
- * the user-global machine context (~/.config/conductor/conductor.config.machine.js),
13
- * and machine_id is composed as `${user_id}-${machine_id}-${project}`. An
14
- * optional, gitignored conductor.config.local.js beside this file may override
15
- * any field — it is loaded if present but never created by `gaia conductor init`.
16
- */
17
- export function renderCommittedConfig(inputs) {
18
- return `// Canonical GAIA conductor config — committed. Connection + identity come from
19
- // your user-global machine context (~/.config/conductor/conductor.config.machine.js:
20
- // { machine_id, user_id, base_url, client_id, client_secret }); machine_id is
21
- // composed here as \`\${user_id}-\${machine_id}-\${project}\`. \`project\` is the only
22
- // per-repo value and is baked in below. The client_secret is read from the
23
- // machine context (gitignored, user-only) — never committed here.
24
- //
25
- // IMPORT-FREE (GAIA-78): the plugin slots + plugins[] are \`{ plugin, with }\`
26
- // descriptors naming the REAL published package (\`@gaia-ai/plugin-*\`,
27
- // \`@dropsh/plugin-*\`), not \`import\`ed constructors. loadConductorConfig
28
- // resolves each name ESLint-style (config dir → cwd → conductor install), so
29
- // config load never depends on a \`node_modules/@gaia-ai\` symlink beside this
30
- // file. Each plugin package default-exports its factory, so the resolver's
31
- // auto-pick needs no \`export:\` here — only the \`@gaia-ai/gaia/plugins\` host
32
- // barrel (many exports) still names one via \`export: 'drupalRemote'\`.
33
-
34
- // The user-global machine context: identity + connection (incl. secret), shared
35
- // by every project on this machine. Never committed.
36
- async function loadMachine() {
37
- try {
38
- return (await import(\`\${process.env.HOME}/.config/conductor/conductor.config.machine.js\`)).default ?? {};
39
- } catch {}
40
- return {};
41
- }
42
-
43
- // OPTIONAL per-project override — create conductor.config.local.js beside this
44
- // file to override any field (machine_id, base_url, model, …). It is loaded only
45
- // if present and is NOT created by \`gaia conductor init\`.
46
- async function loadLocal() {
47
- try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
48
- return {};
49
- }
50
-
51
- const machine = await loadMachine();
52
- const local = await loadLocal();
53
- const project = local.project ?? ${q(inputs.project)};
54
- const baseUrl = local.base_url ?? machine.base_url;
55
- const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
56
- const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
57
- const composedMachineId =
58
- machine.user_id && machine.machine_id
59
- ? \`\${machine.user_id}-\${machine.machine_id}-\${project}\`
60
- : undefined;
61
-
62
- export default {
63
- site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
64
- project,
65
- machine_id: local.machine_id ?? composedMachineId,
66
- states: ['spec', 'diagnose', 'coding', 'review'],
67
- max_parallel: 5,
68
- remote: { plugin: '@gaia-ai/gaia/plugins', export: 'drupalRemote' },
69
- // No hard-wired diff pane for review: the review diff surface is hunk
70
- // (GAIA-55) — agent-driven + opt-in in the human's interactive pane, not an
71
- // executor-forced git-diff pane. Clicking a changed file in that hunk pane
72
- // opens it editable in a spiceedit overlay (see conductor/README.md).
73
- executor: { plugin: '@gaia-ai/plugin-herdr' },
74
- agent: {
75
- plugin: '@gaia-ai/plugin-claude',
76
- with: { model: local.model ?? 'claude-opus-4-8' },
77
- },
78
- workspace: {
79
- plugin: '@gaia-ai/plugin-herdr-workspace',
80
- with: {
81
- hooks: { after_create: 'ddev init-worktree', after_done: 'ddev delete -Oy' },
82
- },
83
- },
84
- // oauth2 is a real dep of the host (npm installs it alongside @gaia-ai/gaia).
85
- // NOTE: plugins[] is consumed by DROPSH, which reloads this config with its OWN
86
- // resolver (\`export ?? 'default'\`, no sole-function auto-pick) on every
87
- // \`gaia dropsh …\` command. @dropsh/plugin-oauth2 has no default export, so
88
- // these entries MUST name \`export: 'oauth2Plugin'\` — unlike the four conductor
89
- // slots above, which the conductor resolves and auto-picks.
90
- plugins: [
91
- {
92
- plugin: '@dropsh/plugin-oauth2',
93
- export: 'oauth2Plugin',
94
- with: {
95
- id: 'session',
96
- default: true,
97
- type: 'oauth2_client_credentials',
98
- client_id: clientId,
99
- client_secret: clientSecret,
100
- token_url: \`\${baseUrl}/oauth/token\`,
101
- scope: 'gaia:session',
102
- },
103
- },
104
- {
105
- plugin: '@dropsh/plugin-oauth2',
106
- export: 'oauth2Plugin',
107
- with: {
108
- id: 'pm',
109
- type: 'oauth2_client_credentials',
110
- client_id: clientId,
111
- client_secret: clientSecret,
112
- token_url: \`\${baseUrl}/oauth/token\`,
113
- scope: 'gaia:project_manager',
114
- },
115
- },
116
- ],
117
- };
118
- `;
119
- }
120
- /** The user-global machine context module: identity + connection (incl. secret). */
121
- export function renderMachineContext(ctx) {
122
- return `// User-global conductor context — gitignored, user-only (chmod 0600), never
123
- // committed. A plain importable module holding your machine identity +
124
- // connection, incl. the OAuth client secret. Committed conductor.config.js files
125
- // import this to compose machine_id (\`\${user_id}-\${machine_id}-\${project}\`) and
126
- // read base_url / client_id / client_secret. Created and gap-filled by
127
- // \`gaia conductor init\`; existing values are never overwritten.
128
- export default {
129
- machine_id: ${q(ctx.machine_id)},
130
- user_id: ${q(ctx.user_id)},
131
- base_url: ${q(ctx.base_url)},
132
- client_id: ${q(ctx.client_id)},
133
- client_secret: ${q(ctx.client_secret)},
134
- };
135
- `;
136
- }
137
- /** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
138
- export function machineContextPath() {
139
- return join(process.env.HOME ?? '', '.config', 'conductor', 'conductor.config.machine.js');
140
- }
141
- /** Import an existing context module's default export, or {} if absent/broken. */
142
- export async function readMachineContext(path) {
143
- if (!existsSync(path))
144
- return {};
145
- try {
146
- // Cache-bust so a re-render within one process re-reads fresh.
147
- const mod = await import(`${pathToFileURL(path).href}?t=${Date.now()}`);
148
- const raw = mod.default;
149
- return raw && typeof raw === 'object'
150
- ? raw
151
- : {};
152
- }
153
- catch {
154
- return {};
155
- }
156
- }
157
- /**
158
- * Create-if-missing / fill-only-missing the user-global machine context.
159
- * Existing values always win; only absent/blank keys are filled. machine_id
160
- * defaults to hostname(). A no-op (no rewrite) when the file is already complete.
161
- * The file is written user-only (chmod 0600) since it holds the client secret.
162
- */
163
- export async function scaffoldMachineContext(opts) {
164
- const existing = await readMachineContext(opts.path);
165
- const derived = {
166
- machine_id: opts.machineId ?? hostname(),
167
- user_id: opts.userId,
168
- base_url: opts.baseUrl,
169
- client_id: opts.clientId,
170
- client_secret: opts.secret,
171
- };
172
- const filledKeys = [];
173
- const merged = { ...derived, ...existing };
174
- for (const key of [
175
- 'machine_id',
176
- 'user_id',
177
- 'base_url',
178
- 'client_id',
179
- 'client_secret',
180
- ]) {
181
- const cur = existing[key];
182
- if (typeof cur !== 'string' || cur.trim() === '') {
183
- merged[key] = derived[key];
184
- filledKeys.push(key);
185
- }
186
- }
187
- const created = !existsSync(opts.path);
188
- if (filledKeys.length > 0 || created) {
189
- mkdirSync(dirname(opts.path), { recursive: true });
190
- writeFileSync(opts.path, renderMachineContext(merged), 'utf8');
191
- }
192
- // Always tighten perms — the file holds a secret.
193
- if (existsSync(opts.path))
194
- chmodSync(opts.path, 0o600);
195
- return { path: opts.path, created, filledKeys };
196
- }
197
- /**
198
- * Scaffold the two conductor config files: the committed conductor.config.js
199
- * (created only if missing — never overwritten unless `force`) and the
200
- * user-global machine context (create-if-missing / fill-only-missing). The
201
- * optional per-project conductor.config.local.js is NOT generated.
202
- */
203
- export async function scaffold(inputs, opts) {
204
- const committedPath = opts.configPath;
205
- mkdirSync(dirname(committedPath), { recursive: true });
206
- let wroteCommitted = false;
207
- if (!existsSync(committedPath) || opts.force) {
208
- writeFileSync(committedPath, renderCommittedConfig(inputs), 'utf8');
209
- wroteCommitted = true;
210
- }
211
- const machine = await scaffoldMachineContext({
212
- path: opts.machinePath ?? machineContextPath(),
213
- userId: inputs.userId ?? '',
214
- baseUrl: inputs.baseUrl,
215
- clientId: inputs.clientId,
216
- secret: inputs.secret,
217
- ...(inputs.machineId !== undefined ? { machineId: inputs.machineId } : {}),
218
- });
219
- return { committedPath, wroteCommitted, machine };
220
- }
@@ -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
- }
@@ -1,13 +0,0 @@
1
- import type { ConductorFileConfig } from './types.js';
2
- /**
3
- * Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
4
- * the agent must stop instead of running the whole flow in one session. The run
5
- * is closed automatically when the ticket state changes on the next claim - the
6
- * agent does not release it. Run mechanics live here (not in the repo's
7
- * WORKFLOW.md). A conductor config may override via the `prompt` field.
8
- * Placeholders: `{identifier}`, `{state}`, `{runUuid}`, `{comments}` ({state}
9
- * falls back to `triage` for unclassified tickets; {comments} is the ticket's
10
- * comments, oldest first, or a "no comments yet" note).
11
- */
12
- export declare const DEFAULT_AGENT_PROMPT: string;
13
- export declare function loadConductorConfig(configFile: string): Promise<ConductorFileConfig>;
@@ -1,186 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { dirname, resolve } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- import { conductorId } from './core/conductor-id.js';
5
- /**
6
- * Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
7
- * the agent must stop instead of running the whole flow in one session. The run
8
- * is closed automatically when the ticket state changes on the next claim - the
9
- * agent does not release it. Run mechanics live here (not in the repo's
10
- * WORKFLOW.md). A conductor config may override via the `prompt` field.
11
- * Placeholders: `{identifier}`, `{state}`, `{runUuid}`, `{comments}` ({state}
12
- * falls back to `triage` for unclassified tickets; {comments} is the ticket's
13
- * comments, oldest first, or a "no comments yet" note).
14
- */
15
- export const DEFAULT_AGENT_PROMPT = `You are a GAIA agent working ticket {identifier}, current state: {state}. ` +
16
- `Follow the "{state}" section of ./WORKFLOW.md in this repository. ` +
17
- `This run covers EXACTLY the {state} state - do only its work. Do not start, ` +
18
- `prepare, or perform any later state's work, even if WORKFLOW.md mentions ` +
19
- `transitioning onward. When the {state} work is done and you have no open ` +
20
- `questions, STOP - do not pick up or work any further state or ticket. If ` +
21
- `blocked or you need a human, stop and surface the blocker. After you have ` +
22
- `written the ticket's next state, run /exit to end this session.\n\n` +
23
- `The ticket's comments are included below (oldest first). Read them before ` +
24
- `acting: the latest \`summary\` is the review feedback you MUST address, and ` +
25
- `any \`spec\`/\`debug_diagnose\` is the agreed plan/diagnosis. Treat their ` +
26
- `points as in-scope acceptance criteria.\n\n--- Ticket comments ---\n{comments}`;
27
- function requirePlugin(value, kind) {
28
- if (!isRecord(value) || value.kind !== kind) {
29
- throw new Error(`conductor config requires a ${kind} plugin in the "${kind}" slot`);
30
- }
31
- return value;
32
- }
33
- /**
34
- * A named-plugin descriptor `{ plugin, export?, with?/options? }` — the
35
- * import-free form. `config.ts` resolves it into a constructed plugin, so a
36
- * committed conductor.config.js need not `import` its plugin packages (which
37
- * would resolve relative to the config file, not the CLI — the source of the
38
- * "unknown command dropsh" breakage after the @gaia → @gaia-ai scope rename).
39
- * Mirror of dropsh 0.4.1 `loadNamedPlugin` / `resolveSlot`.
40
- */
41
- function isPluginDescriptor(entry) {
42
- return isRecord(entry) && typeof entry.plugin === 'string';
43
- }
44
- /**
45
- * Resolve + construct a descriptor. Resolution base order, ESLint-style:
46
- * config dir → cwd → conductor install (`import.meta.url`) — the first
47
- * `createRequire(base).resolve(name)` that succeeds wins. Then `import` the
48
- * module, pick `export` (default 'default'), and call the factory with
49
- * `with` (falling back to `options`).
50
- */
51
- async function loadNamedPlugin(entry, configPath) {
52
- const bases = [
53
- pathToFileURL(configPath).href,
54
- pathToFileURL(`${process.cwd()}/`).href,
55
- import.meta.url,
56
- ];
57
- let resolved;
58
- for (const base of bases) {
59
- try {
60
- resolved = createRequire(base).resolve(entry.plugin);
61
- break;
62
- }
63
- catch {
64
- // try the next base
65
- }
66
- }
67
- if (resolved === undefined) {
68
- throw new Error(`conductor config cannot resolve plugin '${entry.plugin}'`);
69
- }
70
- const mod = (await import(pathToFileURL(resolved).href));
71
- let factory;
72
- if (typeof entry.export === 'string') {
73
- factory = mod[entry.export];
74
- if (typeof factory !== 'function') {
75
- throw new Error(`plugin '${entry.plugin}' has no callable export '${entry.export}'`);
76
- }
77
- }
78
- else if (typeof mod.default === 'function') {
79
- factory = mod.default;
80
- }
81
- else {
82
- const fns = Object.keys(mod).filter((k) => typeof mod[k] === 'function');
83
- if (fns.length === 1) {
84
- factory = mod[fns[0]];
85
- }
86
- else {
87
- throw new Error(`plugin '${entry.plugin}' has no default export and ${fns.length} function exports (${fns.join(', ')}); specify "export"`);
88
- }
89
- }
90
- return factory(entry.with ?? entry.options);
91
- }
92
- /**
93
- * Resolve a slot value: a descriptor is constructed via `loadNamedPlugin`, an
94
- * already-constructed plugin passes straight through (backward-compat). Either
95
- * way the kind-guard runs, so a wrong-target descriptor still errors.
96
- */
97
- async function resolveSlot(value, kind, configPath) {
98
- const resolved = isPluginDescriptor(value)
99
- ? await loadNamedPlugin(value, configPath)
100
- : value;
101
- return requirePlugin(resolved, kind);
102
- }
103
- /**
104
- * Resolve the `plugins[]` array: descriptor entries are constructed, already-
105
- * constructed entries pass through. No kind-guard (plugins are not slotted).
106
- */
107
- async function resolvePlugins(raw, configPath) {
108
- if (!Array.isArray(raw)) {
109
- return [];
110
- }
111
- const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadNamedPlugin(entry, configPath) : entry));
112
- return resolved;
113
- }
114
- function isRecord(value) {
115
- return typeof value === 'object' && value !== null && !Array.isArray(value);
116
- }
117
- function requireNonEmptyString(value, key) {
118
- if (typeof value !== 'string' || value.trim() === '') {
119
- throw new Error(`conductor config requires non-empty ${key}`);
120
- }
121
- return value;
122
- }
123
- function optionalPositiveInteger(value, fallback, key) {
124
- if (value === undefined) {
125
- return fallback;
126
- }
127
- if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
128
- throw new Error(`conductor config requires positive integer ${key}`);
129
- }
130
- return value;
131
- }
132
- export async function loadConductorConfig(configFile) {
133
- const configPath = resolve(configFile);
134
- const module = (await import(pathToFileURL(configPath).href));
135
- const raw = module.default;
136
- if (!isRecord(raw)) {
137
- throw new Error('conductor config default export must be an object');
138
- }
139
- const config = raw;
140
- const site = isRecord(config.site) ? config.site : {};
141
- const baseUrl = requireNonEmptyString(site.base_url, 'site.base_url');
142
- const project = requireNonEmptyString(config.project, 'project');
143
- if (!Array.isArray(config.states) || config.states.length === 0) {
144
- throw new Error('conductor config requires non-empty states');
145
- }
146
- const states = config.states.map((state) => requireNonEmptyString(state, 'states'));
147
- const checkoutRoot = dirname(configPath);
148
- // Resolve the effective machine_id once (config override or hostname+path
149
- // hash) so the id is defined in a single place — the CLI lifecycle commands
150
- // and registration all read config.machine_id, never re-derive it. The
151
- // conductor label defaults to this id (a conductor is identified by it).
152
- const machineId = typeof config.machine_id === 'string' && config.machine_id.trim() !== ''
153
- ? config.machine_id
154
- : conductorId(checkoutRoot);
155
- const label = typeof config.label === 'string' && config.label.trim() !== ''
156
- ? config.label
157
- : machineId;
158
- return {
159
- site: {
160
- base_url: baseUrl,
161
- jsonapi_prefix: typeof site.jsonapi_prefix === 'string' &&
162
- site.jsonapi_prefix.trim() !== ''
163
- ? site.jsonapi_prefix
164
- : '/jsonapi',
165
- },
166
- ...(Array.isArray(config.plugins)
167
- ? { plugins: await resolvePlugins(config.plugins, configPath) }
168
- : {}),
169
- remote: await resolveSlot(config.remote, 'remote', configPath),
170
- executor: await resolveSlot(config.executor, 'executor', configPath),
171
- agent: await resolveSlot(config.agent, 'agent', configPath),
172
- workspace: await resolveSlot(config.workspace, 'workspace', configPath),
173
- label,
174
- machine_id: machineId,
175
- project,
176
- states,
177
- prompt: typeof config.prompt === 'string' && config.prompt.trim() !== ''
178
- ? config.prompt
179
- : DEFAULT_AGENT_PROMPT,
180
- max_parallel: optionalPositiveInteger(config.max_parallel, 1, 'max_parallel'),
181
- poll_interval_ms: optionalPositiveInteger(config.poll_interval_ms, 5000, 'poll_interval_ms'),
182
- lease_seconds: optionalPositiveInteger(config.lease_seconds, 300, 'lease_seconds'),
183
- ...(config.hooks !== undefined ? { hooks: config.hooks } : {}),
184
- config_path: configPath,
185
- };
186
- }
@@ -1 +0,0 @@
1
- export declare function conductorId(absPath: string): string;
@@ -1,8 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { hostname } from 'node:os';
3
- export function conductorId(absPath) {
4
- return createHash('sha256')
5
- .update(`${hostname()}:${absPath}`)
6
- .digest('hex')
7
- .slice(0, 16);
8
- }
@@ -1,47 +0,0 @@
1
- import type { GaiaAgent } from '../plugins/agent/agent.js';
2
- import type { GaiaExecutor } from '../plugins/executor/executor.js';
3
- import type { GaiaRemote } from '../plugins/remote/remote.js';
4
- import type { GaiaWorkspace } from '../plugins/workspace/workspace.js';
5
- import type { ConductorFileConfig } from '../types.js';
6
- import type { ConductorLogger } from './logger.js';
7
- export declare class Conductor {
8
- private readonly config;
9
- private readonly remote;
10
- private readonly executor;
11
- private readonly workspace;
12
- private readonly agent;
13
- private readonly logger;
14
- private readonly checkoutRoot;
15
- private uuid;
16
- constructor(config: ConductorFileConfig, remote: GaiaRemote, executor: GaiaExecutor, workspace: GaiaWorkspace, agent: GaiaAgent, logger: ConductorLogger, checkoutRoot?: string);
17
- get id(): string;
18
- /** This conductor's registration payload, built from config. */
19
- private registration;
20
- start(): Promise<void>;
21
- tick(): Promise<void>;
22
- /**
23
- * One-shot finalisation: for each of this conductor's runs that is
24
- * state=done but not yet closed, read the agent transcript from its worktree
25
- * and write it as the final log, then mark the run closed. Keyed off the
26
- * durable `state=done AND closed=false` query — no in-memory state, so a
27
- * restarted conductor still finalises any unclosed done run on its next tick.
28
- */
29
- private finalizeDoneRuns;
30
- /**
31
- * One-shot ticket cleanup, symmetric to {@link finalizeDoneRuns} on the
32
- * ticket axis: for each of this conductor's tickets that is state=done but not
33
- * yet closed, run the workspace's `after_done` hook in the ticket's worktree,
34
- * tear the worktree down (herdr), then mark the ticket closed. Keyed off the
35
- * durable `state=done AND closed=false` query — no in-memory state, so a
36
- * restarted conductor still cleans up.
37
- *
38
- * Error handling mirrors finalise: each ticket is isolated in a try/catch so
39
- * one failure never wedges the tick. The `after_done` hook itself is
40
- * best-effort — its failure is logged but does not block the teardown/close
41
- * (reclaiming the worktree matters more than a clean environment teardown).
42
- */
43
- private cleanupDoneTickets;
44
- private dispatch;
45
- serve(signal?: AbortSignal): Promise<void>;
46
- private pollLoop;
47
- }