@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.
- package/README.md +1 -1
- package/dist/src/cli/config-schema.d.ts +89 -0
- package/dist/src/cli/config-schema.js +146 -0
- package/dist/src/cli/init.d.ts +31 -33
- package/dist/src/cli/init.js +106 -84
- package/dist/src/cli/migrate-addon-names.d.ts +72 -0
- package/dist/src/cli/migrate-addon-names.js +318 -0
- package/dist/src/cli/upgrade.d.ts +53 -0
- package/dist/src/cli/upgrade.js +222 -0
- package/dist/src/cli/version-check.js +5 -1
- package/dist/src/commands/conductor.d.ts +57 -0
- package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
- package/dist/src/config.d.ts +65 -24
- package/dist/src/config.js +405 -154
- package/dist/src/contract.d.ts +8 -0
- package/dist/src/contract.js +16 -0
- package/dist/src/core/conductor.d.ts +16 -1
- package/dist/src/core/conductor.js +28 -9
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.js +26 -3
- package/dist/src/plugins/agent.d.ts +61 -0
- package/dist/src/plugins/agent.js +11 -0
- package/dist/src/plugins/executor.d.ts +104 -0
- package/dist/src/plugins/executor.js +1 -0
- package/dist/src/plugins/plugins.d.ts +60 -0
- package/dist/src/plugins/plugins.js +42 -0
- package/dist/src/plugins/preset.d.ts +48 -0
- package/dist/src/plugins/preset.js +23 -0
- package/dist/src/plugins/remote.d.ts +203 -0
- package/dist/src/plugins/remote.js +1 -0
- package/dist/src/plugins/workspace.d.ts +35 -0
- package/dist/src/plugins/workspace.js +1 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/dist/src/types.d.ts +65 -0
- package/dist/src/types.js +1 -0
- package/package.json +8 -5
- package/dist/src/cli/gaia.d.ts +0 -23
- package/dist/src/cli/local-registry.d.ts +0 -14
- package/dist/src/cli/local-registry.js +0 -56
package/dist/src/config.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { discoverAddons, emptyContributions, resolveConductorSlots, resolveModuleEslintStyle, } from '@gaia-ai/core';
|
|
5
|
+
import { narrowConductorContributions } from './plugins/preset.js';
|
|
6
|
+
// GAIA-201: the `.gaia/` walk-up + connection resolution moved to `@gaia-ai/core`
|
|
7
|
+
// (`resolveConfigPath` / `resolveGaiaConfigPath` / `findGaiaDir`). Re-export the
|
|
8
|
+
// engine resolver here so existing conductor callers/tests keep importing it
|
|
9
|
+
// from `../config.js`.
|
|
10
|
+
export { findGaiaDir, resolveConfigPath, } from '@gaia-ai/core';
|
|
5
11
|
/**
|
|
6
|
-
* Default agent prompt - the
|
|
12
|
+
* Default agent prompt - the project instruction contract. One run works EXACTLY one state;
|
|
7
13
|
* the agent must stop instead of running the whole flow in one session. The run
|
|
8
14
|
* is closed automatically when the ticket state changes on the next claim - the
|
|
9
15
|
* agent does not release it. Run mechanics live here (not in the repo's
|
|
10
|
-
* WORKFLOW.md). The prompt
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* intake line unprinted unless an external skill-forcing hook happened to fire. A
|
|
14
|
-
* conductor config may override via the `prompt` field.
|
|
16
|
+
* WORKFLOW.md). The prompt points directly at the matching project-owned state
|
|
17
|
+
* section and does not prescribe an agent skill. A conductor config may override
|
|
18
|
+
* it via the `prompt` field.
|
|
15
19
|
*
|
|
16
20
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
17
21
|
* ticket content into a single typed pane line overran the PTY canonical line
|
|
@@ -34,15 +38,12 @@ import { pathToFileURL } from 'node:url';
|
|
|
34
38
|
* wrapper, GAIA-118, was the thing that re-inflated the line past the cap and
|
|
35
39
|
* truncated it mid-quote).
|
|
36
40
|
* Placeholders: `{identifier}`, `{state}`, `{runUuid}` ({state} falls back to
|
|
37
|
-
* `
|
|
41
|
+
* `qualification` for unclassified tickets).
|
|
38
42
|
*/
|
|
39
|
-
export const DEFAULT_AGENT_PROMPT = `You are
|
|
40
|
-
`
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
`prints an intake line, runs the {state} engine, and applies ` +
|
|
44
|
-
`WORKFLOW.md's {state} policy. Do ONLY the {state} work — never start or ` +
|
|
45
|
-
`prepare a later state.`;
|
|
43
|
+
export const DEFAULT_AGENT_PROMPT = `You are working ticket {identifier}, current state: {state}. Read the ticket and all comments ` +
|
|
44
|
+
`(gaia dropsh --format md read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
|
|
45
|
+
`then read ./WORKFLOW.md and execute only its ordered "State: {state}" section. ` +
|
|
46
|
+
`Do not start or prepare a later state.`;
|
|
46
47
|
function requirePlugin(value, kind) {
|
|
47
48
|
if (!isRecord(value) || value.kind !== kind) {
|
|
48
49
|
throw new Error(`conductor config requires a ${kind} plugin in the "${kind}" slot`);
|
|
@@ -73,16 +74,7 @@ async function loadNamedPlugin(entry, configPath) {
|
|
|
73
74
|
pathToFileURL(`${process.cwd()}/`).href,
|
|
74
75
|
import.meta.url,
|
|
75
76
|
];
|
|
76
|
-
|
|
77
|
-
for (const base of bases) {
|
|
78
|
-
try {
|
|
79
|
-
resolved = createRequire(base).resolve(entry.plugin);
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// try the next base
|
|
84
|
-
}
|
|
85
|
-
}
|
|
77
|
+
const resolved = resolveModuleEslintStyle(entry.plugin, bases);
|
|
86
78
|
if (resolved === undefined) {
|
|
87
79
|
throw new Error(`conductor config cannot resolve plugin '${entry.plugin}'`);
|
|
88
80
|
}
|
|
@@ -144,23 +136,6 @@ export async function resolveAgents(raw, configPath) {
|
|
|
144
136
|
? Promise.all(raw.map(resolveOne))
|
|
145
137
|
: resolveOne(raw);
|
|
146
138
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Resolve the `plugins[]` array: descriptor entries are constructed, already-
|
|
149
|
-
* constructed entries pass through. No kind-guard (plugins are not slotted).
|
|
150
|
-
*
|
|
151
|
-
* A factory may return one plugin OR an array of plugins — an aggregator built
|
|
152
|
-
* with dropsh's `composePlugins(a(), b())` returns the flat child list. dropsh's
|
|
153
|
-
* own `loadConfig` flattens such a nested entry one level; we mirror that here so
|
|
154
|
-
* `buildProgram({ plugins })` — which reads each entry's hooks/renderers per
|
|
155
|
-
* top-level element and does NOT recurse — sees every child.
|
|
156
|
-
*/
|
|
157
|
-
async function resolvePlugins(raw, configPath) {
|
|
158
|
-
if (!Array.isArray(raw)) {
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadNamedPlugin(entry, configPath) : entry));
|
|
162
|
-
return resolved.flat();
|
|
163
|
-
}
|
|
164
139
|
function isRecord(value) {
|
|
165
140
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
166
141
|
}
|
|
@@ -179,98 +154,15 @@ function optionalPositiveInteger(value, fallback, key) {
|
|
|
179
154
|
}
|
|
180
155
|
return value;
|
|
181
156
|
}
|
|
182
|
-
/** A repo's config files live in this dir, one conductor config per conductor. */
|
|
183
|
-
const GAIA_DIR = '.gaia';
|
|
184
|
-
/** The default conductor's file name; its stem is `conductor`. */
|
|
185
|
-
const DEFAULT_CONFIG = 'conductor.config.js';
|
|
186
|
-
/** Variant files are `<stem>.conductor.config.js`. */
|
|
187
|
-
const VARIANT_SUFFIX = '.conductor.config.js';
|
|
188
|
-
/**
|
|
189
|
-
* List the conductor-config stems in a `.gaia/` dir. A file is a conductor
|
|
190
|
-
* config iff it is exactly `conductor.config.js` (stem `conductor`, the
|
|
191
|
-
* default) or ends with `.conductor.config.js` (stem = the leading part).
|
|
192
|
-
* Every other file (`vite.config.js`, the near-miss `myconductor.config.js`)
|
|
193
|
-
* is ignored.
|
|
194
|
-
*/
|
|
195
|
-
function configStems(gaiaDir) {
|
|
196
|
-
return readdirSync(gaiaDir)
|
|
197
|
-
.map((f) => f === DEFAULT_CONFIG
|
|
198
|
-
? 'conductor'
|
|
199
|
-
: f.endsWith(VARIANT_SUFFIX)
|
|
200
|
-
? f.slice(0, -VARIANT_SUFFIX.length)
|
|
201
|
-
: undefined)
|
|
202
|
-
.filter((s) => s !== undefined)
|
|
203
|
-
.sort();
|
|
204
|
-
}
|
|
205
|
-
/** Map a conductor stem back to its file name. */
|
|
206
|
-
function fileForStem(stem) {
|
|
207
|
-
return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
|
|
211
|
-
* `.gaia/` dir holds at least one conductor config; return that `.gaia/` dir, or
|
|
212
|
-
* `undefined` if none is found up to the filesystem root.
|
|
213
|
-
*/
|
|
214
|
-
function findGaiaDir(cwd) {
|
|
215
|
-
let dir = resolve(cwd);
|
|
216
|
-
for (;;) {
|
|
217
|
-
const gaiaDir = join(dir, GAIA_DIR);
|
|
218
|
-
if (existsSync(gaiaDir) && configStems(gaiaDir).length > 0) {
|
|
219
|
-
return gaiaDir;
|
|
220
|
-
}
|
|
221
|
-
const parent = dirname(dir);
|
|
222
|
-
if (parent === dir) {
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
dir = parent;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
157
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* `<name>.conductor.config.js`; error listing the stems if absent).
|
|
237
|
-
* - No selector: `conductor.config.js` present → it is the default; else
|
|
238
|
-
* exactly one config → use it (back-compat); else → error naming the
|
|
239
|
-
* stems + the selector.
|
|
240
|
-
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
241
|
-
* (never leak a raw "Cannot find module" from a later import()).
|
|
158
|
+
* Load the ENGINE half of a conductor config (GAIA-201 split model). Reads
|
|
159
|
+
* remote/executor/agent/workspace + project/states/machine_id/label/prompt/
|
|
160
|
+
* scheduler/hooks — and deliberately NOT `site` or the auth `plugins`, which now
|
|
161
|
+
* live in a `gaia.config.js` connection config. The conductor command composes
|
|
162
|
+
* the two via `composeConductorConfig`. A legacy `conductor.config.js` still
|
|
163
|
+
* carrying `site`/`plugins` loads fine — those two keys are simply ignored here
|
|
164
|
+
* (the connection loader reads them, back-compat).
|
|
242
165
|
*/
|
|
243
|
-
export function resolveConfigPath(override, cwd = process.cwd(), conductorName) {
|
|
244
|
-
const explicit = override ?? process.env.GAIA_CONDUCTOR_CONFIG;
|
|
245
|
-
if (explicit) {
|
|
246
|
-
return explicit;
|
|
247
|
-
}
|
|
248
|
-
const gaiaDir = findGaiaDir(cwd);
|
|
249
|
-
if (gaiaDir === undefined) {
|
|
250
|
-
throw new Error(`no .gaia/conductor.config.js found from ${cwd} upward; run \`gaia init\``);
|
|
251
|
-
}
|
|
252
|
-
const stems = configStems(gaiaDir);
|
|
253
|
-
const name = conductorName ?? process.env.GAIA_CONDUCTOR;
|
|
254
|
-
if (name !== undefined && name !== '') {
|
|
255
|
-
const candidate = join(gaiaDir, fileForStem(name));
|
|
256
|
-
if (!existsSync(candidate)) {
|
|
257
|
-
throw new Error(`no conductor '${name}' in ${gaiaDir}; available: ${stems.join(', ')}`);
|
|
258
|
-
}
|
|
259
|
-
return candidate;
|
|
260
|
-
}
|
|
261
|
-
// No selector: the file named conductor.config.js is the default (AC-1, AC-2).
|
|
262
|
-
// Two conductor.config.js files cannot coexist in one dir, so ">1 default"
|
|
263
|
-
// (AC-5) is structurally impossible — no ambiguity branch is needed.
|
|
264
|
-
if (stems.includes('conductor')) {
|
|
265
|
-
return join(gaiaDir, DEFAULT_CONFIG);
|
|
266
|
-
}
|
|
267
|
-
// Back-compat: a lone config resolves even without the default name.
|
|
268
|
-
if (stems.length === 1) {
|
|
269
|
-
return join(gaiaDir, fileForStem(stems[0]));
|
|
270
|
-
}
|
|
271
|
-
throw new Error(`${stems.length} conductors in ${gaiaDir} (${stems.join(', ')}); ` +
|
|
272
|
-
'select one with --conductor <name> or $GAIA_CONDUCTOR');
|
|
273
|
-
}
|
|
274
166
|
export async function loadConductorConfig(configFile) {
|
|
275
167
|
const configPath = resolve(configFile);
|
|
276
168
|
const module = (await import(pathToFileURL(configPath).href));
|
|
@@ -278,14 +170,23 @@ export async function loadConductorConfig(configFile) {
|
|
|
278
170
|
if (!isRecord(raw)) {
|
|
279
171
|
throw new Error('conductor config default export must be an object');
|
|
280
172
|
}
|
|
173
|
+
// GAIA-218 (AC-6): the engine config must not carry connection/auth material.
|
|
174
|
+
// A legacy config still declaring `site`/`plugins` gets exactly ONE actionable
|
|
175
|
+
// warning naming the file + the offending key(s); the values are never merged
|
|
176
|
+
// (`ConductorEngineConfig` omits both structurally).
|
|
177
|
+
const leftover = ['site', 'plugins'].filter((k) => k in raw);
|
|
178
|
+
if (leftover.length > 0) {
|
|
179
|
+
console.warn(`warning: ${configPath} declares ${leftover.join('/')} — ignored; ` +
|
|
180
|
+
'auth/connection belong in the sibling gaia.config.js (GAIA-218)');
|
|
181
|
+
}
|
|
281
182
|
const config = raw;
|
|
282
|
-
const site = isRecord(config.site) ? config.site : {};
|
|
283
|
-
const baseUrl = requireNonEmptyString(site.base_url, 'site.base_url');
|
|
284
183
|
const project = requireNonEmptyString(config.project, 'project');
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const states = config.states
|
|
184
|
+
// `states` is optional. An empty (or absent) list means the conductor serves
|
|
185
|
+
// every claimable state in its project; a non-empty list narrows it to those
|
|
186
|
+
// states (GAIA-207). When present it must be an array of non-empty strings.
|
|
187
|
+
const states = Array.isArray(config.states)
|
|
188
|
+
? config.states.map((state) => requireNonEmptyString(state, 'states'))
|
|
189
|
+
: [];
|
|
289
190
|
// machine_id is required — the config MUST set it; there is no derived
|
|
290
191
|
// fallback. The id is defined in a single place (config.machine_id), which
|
|
291
192
|
// the CLI lifecycle commands and registration all read, never re-derive. The
|
|
@@ -298,21 +199,51 @@ export async function loadConductorConfig(configFile) {
|
|
|
298
199
|
const label = typeof config.label === 'string' && config.label.trim() !== ''
|
|
299
200
|
? config.label
|
|
300
201
|
: machineId;
|
|
202
|
+
// GAIA-215: discover the conductor `addons: []` surface (last-wins singletons,
|
|
203
|
+
// agent candidate list). A pre-existing per-slot descriptor still loads and
|
|
204
|
+
// WINS over a discovered contributor of the same kind (back-compat additive).
|
|
205
|
+
// GAIA-224: `discoverAddons` is surface-AGNOSTIC — it returns opaque
|
|
206
|
+
// accumulators. This is the single, reviewed narrowing seam where the engine
|
|
207
|
+
// reinterprets them as its own concrete conductor contributions; the runtime
|
|
208
|
+
// `.kind` guard inside `resolveConductorSlots` still fails loudly on a
|
|
209
|
+
// mis-declared contribution.
|
|
210
|
+
const discovered = narrowConductorContributions(Array.isArray(config.addons)
|
|
211
|
+
? await discoverAddons(config.addons, 'conductor', [
|
|
212
|
+
pathToFileURL(configPath).href,
|
|
213
|
+
pathToFileURL(`${process.cwd()}/`).href,
|
|
214
|
+
import.meta.url,
|
|
215
|
+
])
|
|
216
|
+
: emptyContributions());
|
|
217
|
+
const wired = resolveConductorSlots(discovered);
|
|
218
|
+
const remote = config.remote !== undefined
|
|
219
|
+
? await resolveSlot(config.remote, 'remote', configPath)
|
|
220
|
+
: wired.remote;
|
|
221
|
+
if (!remote) {
|
|
222
|
+
throw new Error('conductor config resolves no remote — name a remote addon in addons[] or set the remote slot');
|
|
223
|
+
}
|
|
224
|
+
const executor = config.executor !== undefined
|
|
225
|
+
? await resolveSlot(config.executor, 'executor', configPath)
|
|
226
|
+
: wired.executor;
|
|
227
|
+
if (!executor) {
|
|
228
|
+
throw new Error('conductor config resolves no executor — name an executor addon in addons[] or set the executor slot');
|
|
229
|
+
}
|
|
230
|
+
const workspace = config.workspace !== undefined
|
|
231
|
+
? await resolveSlot(config.workspace, 'workspace', configPath)
|
|
232
|
+
: wired.workspace;
|
|
233
|
+
if (!workspace) {
|
|
234
|
+
throw new Error('conductor config resolves no workspace — name a workspace addon in addons[] or set the workspace slot');
|
|
235
|
+
}
|
|
236
|
+
const agent = config.agent !== undefined
|
|
237
|
+
? await resolveAgents(config.agent, configPath)
|
|
238
|
+
: wired.agents;
|
|
239
|
+
if (Array.isArray(agent) && agent.length === 0) {
|
|
240
|
+
throw new Error('conductor config resolves no agent — name an agent addon in addons[] or set the agent slot');
|
|
241
|
+
}
|
|
301
242
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
? site.jsonapi_prefix
|
|
307
|
-
: '/jsonapi',
|
|
308
|
-
},
|
|
309
|
-
...(Array.isArray(config.plugins)
|
|
310
|
-
? { plugins: await resolvePlugins(config.plugins, configPath) }
|
|
311
|
-
: {}),
|
|
312
|
-
remote: await resolveSlot(config.remote, 'remote', configPath),
|
|
313
|
-
executor: await resolveSlot(config.executor, 'executor', configPath),
|
|
314
|
-
agent: await resolveAgents(config.agent, configPath),
|
|
315
|
-
workspace: await resolveSlot(config.workspace, 'workspace', configPath),
|
|
243
|
+
remote,
|
|
244
|
+
executor,
|
|
245
|
+
agent,
|
|
246
|
+
workspace,
|
|
316
247
|
label,
|
|
317
248
|
machine_id: machineId,
|
|
318
249
|
project,
|
|
@@ -327,3 +258,323 @@ export async function loadConductorConfig(configFile) {
|
|
|
327
258
|
config_path: configPath,
|
|
328
259
|
};
|
|
329
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Compose the full `ConductorFileConfig` the engine consumes from the engine
|
|
263
|
+
* half (`loadConductorConfig`) and the connection half (`loadGaiaConfig`). The
|
|
264
|
+
* connection supplies `site` + the auth `plugins`; `ensureAuthenticated` /
|
|
265
|
+
* `selectRemote` read them exactly as before — the fields just arrive from
|
|
266
|
+
* `gaia.config.js` now.
|
|
267
|
+
*/
|
|
268
|
+
export function composeConductorConfig(engine, connection) {
|
|
269
|
+
return {
|
|
270
|
+
...engine,
|
|
271
|
+
site: connection.site,
|
|
272
|
+
...(connection.plugins.length > 0 ? { plugins: connection.plugins } : {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const DEFAULT_CONFIG = 'conductor.config.js';
|
|
276
|
+
const VARIANT_SUFFIX = '.conductor.config.js';
|
|
277
|
+
/**
|
|
278
|
+
* The conductor **stems** in a `.gaia/` dir — the file naming predicate of
|
|
279
|
+
* GAIA-137: exactly `conductor.config.js` (stem `conductor`) or
|
|
280
|
+
* `<variant>.conductor.config.js` (stem `<variant>`). The resolution itself
|
|
281
|
+
* lives in `@gaia-ai/core` (re-exported above); this is the enumeration.
|
|
282
|
+
*/
|
|
283
|
+
export function configStems(gaiaDir) {
|
|
284
|
+
return readdirSync(gaiaDir)
|
|
285
|
+
.map((f) => f === DEFAULT_CONFIG
|
|
286
|
+
? 'conductor'
|
|
287
|
+
: f.endsWith(VARIANT_SUFFIX)
|
|
288
|
+
? f.slice(0, -VARIANT_SUFFIX.length)
|
|
289
|
+
: undefined)
|
|
290
|
+
.filter((s) => s !== undefined)
|
|
291
|
+
.sort();
|
|
292
|
+
}
|
|
293
|
+
/** Map a conductor stem back to its file name. */
|
|
294
|
+
export function fileForStem(stem) {
|
|
295
|
+
return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* List the conductor-config **file names** in a `.gaia/` dir, using the same
|
|
299
|
+
* naming predicate the loader uses (`configStems`). This is the only supported
|
|
300
|
+
* enumeration for the `@gaia/upgrade-project` states-strip: it visits the
|
|
301
|
+
* default `conductor.config.js` + every `<variant>.conductor.config.js`, and
|
|
302
|
+
* never an unrelated JS config (`vite.config.js`, the near-miss
|
|
303
|
+
* `myconductor.config.js`).
|
|
304
|
+
*/
|
|
305
|
+
export function listConductorConfigFiles(gaiaDir) {
|
|
306
|
+
return configStems(gaiaDir).map(fileForStem);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Remove the obsolete `states:` array property from a conductor-config JS
|
|
310
|
+
* source (GAIA-207 made `states` optional — empty ⇒ serve all claimable
|
|
311
|
+
* states — so `@gaia/upgrade-project` drops the now-dead key). Pure string
|
|
312
|
+
* transform: it strips the `states: [ ... ]` property (line form or inline in
|
|
313
|
+
* an object literal) with its trailing comma, and rewrites nothing else. A
|
|
314
|
+
* `substates:`/other key containing "states" is not matched.
|
|
315
|
+
*/
|
|
316
|
+
export function stripStatesFromConfigSource(source) {
|
|
317
|
+
return source.replace(/\n?[ \t]*(?<![A-Za-z0-9_$])states[ \t]*:[ \t]*\[[^\]]*\][ \t]*,?/g, '');
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// GAIA-218 (AC-7): strip legacy connection/auth from an ENGINE config source.
|
|
321
|
+
//
|
|
322
|
+
// Unlike `stripStatesFromConfigSource`'s flat `\[[^\]]*\]` regex (nested-unsafe),
|
|
323
|
+
// this is a balanced-delimiter, comment-aware source transform. The engine
|
|
324
|
+
// config's `plugins: [ … ]` array holds nested `{}` (oauth2 profiles AND
|
|
325
|
+
// `@gaia-ai/addon-essentials`), a sibling `agent: [ … ]` array, and the word
|
|
326
|
+
// "plugins" also appears in comments — so the removal keys on the REAL `site:` /
|
|
327
|
+
// `plugins:` *properties* of the default-export object, scanning each value from
|
|
328
|
+
// its opening delimiter to the matching close while skipping string/comment
|
|
329
|
+
// bytes. It also removes the now-orphaned connection preamble consts
|
|
330
|
+
// (`baseUrl`/`clientId`/`clientSecret`) and a connection-only `loadMachine`
|
|
331
|
+
// (with its `const machine = await loadMachine()`) when they become unreferenced.
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
const CLOSER = { '{': '}', '[': ']', '(': ')' };
|
|
334
|
+
/** Skip a `//`-line comment; return the index of the terminating newline (or EOF). */
|
|
335
|
+
function skipLineComment(src, i) {
|
|
336
|
+
const nl = src.indexOf('\n', i);
|
|
337
|
+
return nl === -1 ? src.length : nl;
|
|
338
|
+
}
|
|
339
|
+
/** Skip a `/* *\/` block comment; return the index just past it. */
|
|
340
|
+
function skipBlockComment(src, i) {
|
|
341
|
+
const end = src.indexOf('*/', i + 2);
|
|
342
|
+
return end === -1 ? src.length : end + 2;
|
|
343
|
+
}
|
|
344
|
+
/** Skip a string/template literal (honouring `\` escapes and `${ … }`
|
|
345
|
+
* interpolations in backtick strings); return the index just past the close. */
|
|
346
|
+
function skipString(src, i) {
|
|
347
|
+
const quote = src[i];
|
|
348
|
+
i++;
|
|
349
|
+
while (i < src.length) {
|
|
350
|
+
const c = src[i];
|
|
351
|
+
if (c === '\\') {
|
|
352
|
+
i += 2;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (quote === '`' && c === '$' && src[i + 1] === '{') {
|
|
356
|
+
i = matchDelimiter(src, i + 1) + 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (c === quote)
|
|
360
|
+
return i + 1;
|
|
361
|
+
i++;
|
|
362
|
+
}
|
|
363
|
+
return i;
|
|
364
|
+
}
|
|
365
|
+
/** Given the index of an opening `{`/`[`/`(`, return the index of its matching
|
|
366
|
+
* close, skipping nested delimiters, strings and comments. -1 if unbalanced. */
|
|
367
|
+
function matchDelimiter(src, openIdx) {
|
|
368
|
+
const stack = [CLOSER[src[openIdx]]];
|
|
369
|
+
let i = openIdx + 1;
|
|
370
|
+
while (i < src.length && stack.length > 0) {
|
|
371
|
+
const c = src[i];
|
|
372
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
373
|
+
i = skipLineComment(src, i);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
377
|
+
i = skipBlockComment(src, i);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
381
|
+
i = skipString(src, i);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (c === '{' || c === '[' || c === '(') {
|
|
385
|
+
stack.push(CLOSER[c]);
|
|
386
|
+
i++;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (c === '}' || c === ']' || c === ')') {
|
|
390
|
+
if (c === stack[stack.length - 1])
|
|
391
|
+
stack.pop();
|
|
392
|
+
i++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
return stack.length === 0 ? i - 1 : -1;
|
|
398
|
+
}
|
|
399
|
+
/** Skip whitespace + comments starting at `i` (bounded by `limit`). */
|
|
400
|
+
function skipTrivia(src, i, limit) {
|
|
401
|
+
while (i < limit) {
|
|
402
|
+
const c = src[i];
|
|
403
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
404
|
+
i++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
408
|
+
i = skipLineComment(src, i);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
412
|
+
i = skipBlockComment(src, i);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
return i;
|
|
418
|
+
}
|
|
419
|
+
/** Read a property key (bare identifier or quoted string) at `i`. */
|
|
420
|
+
function readKey(src, i) {
|
|
421
|
+
const c = src[i];
|
|
422
|
+
if (c === "'" || c === '"') {
|
|
423
|
+
const end = skipString(src, i);
|
|
424
|
+
return { name: src.slice(i + 1, end - 1), end };
|
|
425
|
+
}
|
|
426
|
+
const m = /^[A-Za-z0-9_$]+/.exec(src.slice(i));
|
|
427
|
+
if (m)
|
|
428
|
+
return { name: m[0], end: i + m[0].length };
|
|
429
|
+
return { end: i };
|
|
430
|
+
}
|
|
431
|
+
/** From `from`, scan to just past the next top-level `,` (or to `limit` when the
|
|
432
|
+
* value runs to the object close), skipping nested delimiters/strings/comments. */
|
|
433
|
+
function scanToTopLevelComma(src, from, limit) {
|
|
434
|
+
let i = from;
|
|
435
|
+
while (i < limit) {
|
|
436
|
+
const c = src[i];
|
|
437
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
438
|
+
i = skipLineComment(src, i);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
442
|
+
i = skipBlockComment(src, i);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
446
|
+
i = skipString(src, i);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (c === '{' || c === '[' || c === '(') {
|
|
450
|
+
i = matchDelimiter(src, i) + 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (c === ',')
|
|
454
|
+
return i + 1;
|
|
455
|
+
i++;
|
|
456
|
+
}
|
|
457
|
+
return limit;
|
|
458
|
+
}
|
|
459
|
+
/** Locate the `export default { … }` object; return the body span (exclusive of
|
|
460
|
+
* the braces) or undefined when the default export is not an object literal. */
|
|
461
|
+
function findDefaultExportObject(src) {
|
|
462
|
+
const m = /export\s+default\s*/.exec(src);
|
|
463
|
+
if (!m)
|
|
464
|
+
return undefined;
|
|
465
|
+
const braceIdx = m.index + m[0].length;
|
|
466
|
+
if (src[braceIdx] !== '{')
|
|
467
|
+
return undefined;
|
|
468
|
+
const end = matchDelimiter(src, braceIdx);
|
|
469
|
+
if (end === -1)
|
|
470
|
+
return undefined;
|
|
471
|
+
return { bodyStart: braceIdx + 1, bodyEnd: end };
|
|
472
|
+
}
|
|
473
|
+
/** Enumerate the top-level properties of the object body [bodyStart, bodyEnd). */
|
|
474
|
+
function scanTopLevelProperties(src, bodyStart, bodyEnd) {
|
|
475
|
+
const entries = [];
|
|
476
|
+
let i = bodyStart;
|
|
477
|
+
while (i < bodyEnd) {
|
|
478
|
+
const entryStart = i;
|
|
479
|
+
const keyPos = skipTrivia(src, i, bodyEnd);
|
|
480
|
+
if (keyPos >= bodyEnd)
|
|
481
|
+
break;
|
|
482
|
+
const key = readKey(src, keyPos);
|
|
483
|
+
let j = skipTrivia(src, key.end, bodyEnd);
|
|
484
|
+
if (src[j] !== ':') {
|
|
485
|
+
// Not a `key: value` property (spread/computed/shorthand) — keep it.
|
|
486
|
+
const term = scanToTopLevelComma(src, keyPos, bodyEnd);
|
|
487
|
+
entries.push({ key: undefined, start: entryStart, end: term });
|
|
488
|
+
i = term;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
j++;
|
|
492
|
+
const term = scanToTopLevelComma(src, j, bodyEnd);
|
|
493
|
+
entries.push({ key: key.name, start: entryStart, end: term });
|
|
494
|
+
i = term;
|
|
495
|
+
}
|
|
496
|
+
return entries;
|
|
497
|
+
}
|
|
498
|
+
/** Count word-boundary occurrences of an identifier in the source. */
|
|
499
|
+
function countIdent(src, name) {
|
|
500
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${name}(?![A-Za-z0-9_$])`, 'g');
|
|
501
|
+
return (src.match(re) ?? []).length;
|
|
502
|
+
}
|
|
503
|
+
/** Remove a single-line `const <name> = …;` declaration (no-op if absent). */
|
|
504
|
+
function removeConstDecl(src, name) {
|
|
505
|
+
const re = new RegExp(`^[ \\t]*const ${name}\\b[^\\n]*\\n`, 'm');
|
|
506
|
+
return src.replace(re, '');
|
|
507
|
+
}
|
|
508
|
+
/** Remove an `[async ]function <name>(…) { … }` declaration (balanced body). */
|
|
509
|
+
function removeFunctionDecl(src, name) {
|
|
510
|
+
const re = new RegExp(`(?:async[ \\t]+)?function[ \\t]+${name}[ \\t]*\\(`);
|
|
511
|
+
const m = re.exec(src);
|
|
512
|
+
if (!m)
|
|
513
|
+
return src;
|
|
514
|
+
const braceIdx = src.indexOf('{', m.index);
|
|
515
|
+
if (braceIdx === -1)
|
|
516
|
+
return src;
|
|
517
|
+
const end = matchDelimiter(src, braceIdx);
|
|
518
|
+
if (end === -1)
|
|
519
|
+
return src;
|
|
520
|
+
let e = end + 1;
|
|
521
|
+
if (src[e] === '\n')
|
|
522
|
+
e++;
|
|
523
|
+
return src.slice(0, m.index) + src.slice(e);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Is `machine` referenced anywhere OTHER than its own `const machine = …;` decl
|
|
527
|
+
* and inside the `loadMachine` function body? (The body's `…/machine.config.js`
|
|
528
|
+
* path string must not count as a live reference.) Used to decide whether a
|
|
529
|
+
* connection-only `loadMachine` has become orphaned.
|
|
530
|
+
*/
|
|
531
|
+
function machineReferencedElsewhere(src) {
|
|
532
|
+
let probe = removeFunctionDecl(src, 'loadMachine');
|
|
533
|
+
probe = removeConstDecl(probe, 'machine');
|
|
534
|
+
return countIdent(probe, 'machine') > 0;
|
|
535
|
+
}
|
|
536
|
+
/** Drop connection preamble that is orphaned once `site`/`plugins` are gone. */
|
|
537
|
+
function removeOrphanedPreamble(src) {
|
|
538
|
+
let text = src;
|
|
539
|
+
for (const name of ['baseUrl', 'clientId', 'clientSecret']) {
|
|
540
|
+
if (countIdent(text, name) <= 1)
|
|
541
|
+
text = removeConstDecl(text, name);
|
|
542
|
+
}
|
|
543
|
+
// A connection-only `loadMachine`: remove `const machine = await loadMachine()`
|
|
544
|
+
// + the function itself only when `machine` is otherwise unreferenced (so an
|
|
545
|
+
// engine config that still composes `machine_id` from `machine.user_id` keeps
|
|
546
|
+
// its loadMachine — the "connection-only" qualifier).
|
|
547
|
+
if (/\bconst machine\b/.test(text) &&
|
|
548
|
+
/\bfunction loadMachine\b/.test(text) &&
|
|
549
|
+
!machineReferencedElsewhere(text)) {
|
|
550
|
+
text = removeConstDecl(text, 'machine');
|
|
551
|
+
text = removeFunctionDecl(text, 'loadMachine');
|
|
552
|
+
}
|
|
553
|
+
return text;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Strip the `site:` and `plugins:` properties from an engine config's
|
|
557
|
+
* `export default { … }` (with their leading comment/whitespace), plus the
|
|
558
|
+
* now-orphaned connection preamble consts. Returns the rewritten source and the
|
|
559
|
+
* list of object keys removed (`[]` — source returned unchanged — when there is
|
|
560
|
+
* nothing connection-related to remove). remote/executor/agent/workspace/hooks/
|
|
561
|
+
* project/machine_id/loadLocal are left intact.
|
|
562
|
+
*/
|
|
563
|
+
export function stripLegacyConnectionFromConfigSource(source) {
|
|
564
|
+
const obj = findDefaultExportObject(source);
|
|
565
|
+
if (!obj)
|
|
566
|
+
return { text: source, removed: [] };
|
|
567
|
+
const entries = scanTopLevelProperties(source, obj.bodyStart, obj.bodyEnd);
|
|
568
|
+
const targets = new Set(['site', 'plugins']);
|
|
569
|
+
const toRemove = entries.filter((e) => e.key !== undefined && targets.has(e.key));
|
|
570
|
+
if (toRemove.length === 0)
|
|
571
|
+
return { text: source, removed: [] };
|
|
572
|
+
let text = source;
|
|
573
|
+
for (const e of [...toRemove].sort((a, b) => b.start - a.start)) {
|
|
574
|
+
text = text.slice(0, e.start) + text.slice(e.end);
|
|
575
|
+
}
|
|
576
|
+
text = removeOrphanedPreamble(text);
|
|
577
|
+
// Report in source order for a stable `site/plugins` message.
|
|
578
|
+
const removed = ['site', 'plugins'].filter((k) => toRemove.some((e) => e.key === k));
|
|
579
|
+
return { text, removed };
|
|
580
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { type AgentFootprint, emptyAgentFootprint, type GaiaAgent, } from './plugins/agent.js';
|
|
2
|
+
export type { ExecutorCapabilities, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput, } from './plugins/executor.js';
|
|
3
|
+
export type { AgentCandidate, AgentPlugin, ExecutorDeps, ExecutorPlugin, RemotePlugin, ResolvedAgent, WorkspacePlugin, } from './plugins/plugins.js';
|
|
4
|
+
export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
5
|
+
export { type ConductorAddonEntry, type ConductorContributions, narrowConductorContributions, type Preset, } from './plugins/preset.js';
|
|
6
|
+
export type { ActiveRun, ClaimedRun, ClaimOptions, ConductorRegistration, ConductorStatus, FinalizableRun, GaiaRemote, RunMetrics, RunWriteAttributes, Ticket, UncleanTicket, } from './plugins/remote.js';
|
|
7
|
+
export type { EnsuredWorkspace, GaiaWorkspace, } from './plugins/workspace.js';
|
|
8
|
+
export type { ConductorEngineConfig, ConductorFileConfig, ConductorSettings, } from './types.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// GAIA-224 (Finding 6): the CONDUCTOR-SURFACE CONTRACT, exposed as the
|
|
2
|
+
// runtime-light subpath `@gaia-ai/conductor/contract`.
|
|
3
|
+
//
|
|
4
|
+
// This is the module a conductor-surface addon (`addons/remote-drupal`,
|
|
5
|
+
// `addons/workspace-git`, `addons/herdr`, `addons/claude`, …) imports. It carries
|
|
6
|
+
// ONLY the surface interfaces, the config contract, the preset view, and the two
|
|
7
|
+
// tiny runtime helpers (`emptyAgentFootprint`, the `select*` slot selectors) — it
|
|
8
|
+
// imports NO engine, NO commander, NO dropsh runtime. Importing it therefore
|
|
9
|
+
// costs an addon nothing at boot, unlike the package main (`@gaia-ai/conductor`),
|
|
10
|
+
// which pulls the whole run engine + the `conductor` command plugin.
|
|
11
|
+
//
|
|
12
|
+
// The package main re-exports everything here too, so `import type { GaiaRemote }
|
|
13
|
+
// from '@gaia-ai/conductor'` keeps working for type-only consumers.
|
|
14
|
+
export { emptyAgentFootprint, } from './plugins/agent.js';
|
|
15
|
+
export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
16
|
+
export { narrowConductorContributions, } from './plugins/preset.js';
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ConductorLogger } from '@gaia-ai/core';
|
|
2
|
+
import type { GaiaExecutor } from '../plugins/executor.js';
|
|
3
|
+
import { type ResolvedAgent } from '../plugins/plugins.js';
|
|
4
|
+
import type { GaiaRemote } from '../plugins/remote.js';
|
|
5
|
+
import type { GaiaWorkspace } from '../plugins/workspace.js';
|
|
6
|
+
import type { ConductorFileConfig } from '../types.js';
|
|
2
7
|
/**
|
|
3
8
|
* Resolve the environment a run executes in (GAIA-99): parse the ticket's
|
|
4
9
|
* effective env_vars, drop any reserved key (loud warn — key NAME only, never
|
|
@@ -66,5 +71,15 @@ export declare class Conductor {
|
|
|
66
71
|
reap(): Promise<void>;
|
|
67
72
|
private dispatch;
|
|
68
73
|
serve(signal?: AbortSignal): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* True once this conductor's own checkout has been removed from disk (its
|
|
76
|
+
* worktree was reaped while the process kept running). Such a conductor is a
|
|
77
|
+
* pure liability: it still heartbeats and still claims runs, but every
|
|
78
|
+
* dispatch fails — no git command and no relative path can resolve from a
|
|
79
|
+
* deleted directory — so each claim burns one attempt and three of them park
|
|
80
|
+
* the ticket behind the circuit breaker. Observed as 30 orphans claiming and
|
|
81
|
+
* failing tickets they could never dispatch.
|
|
82
|
+
*/
|
|
83
|
+
private checkoutGone;
|
|
69
84
|
private pollLoop;
|
|
70
85
|
}
|