@gaia-ai/conductor 0.4.2 → 0.4.4
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/dist/src/cli/gaia.d.ts +2 -0
- package/dist/src/cli/gaia.js +56 -10
- package/dist/src/config.d.ts +31 -12
- package/dist/src/config.js +95 -46
- package/dist/src/core/conductor.d.ts +16 -18
- package/dist/src/core/conductor.js +38 -37
- package/package.json +4 -4
package/dist/src/cli/gaia.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ConductorFileConfig, type ConductorLogger, type GaiaAgent, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace } from '@gaia-ai/core';
|
|
2
|
+
import { Command } from 'commander';
|
|
2
3
|
/** Test seam: inject any subset of dependencies. */
|
|
3
4
|
export interface GaiaCliDeps {
|
|
4
5
|
remote?: GaiaRemote;
|
|
@@ -15,5 +16,6 @@ export declare function parseHerdrJson(output: string, command: string): unknown
|
|
|
15
16
|
* when unauthenticated.
|
|
16
17
|
*/
|
|
17
18
|
export declare function ensureAuthenticated(config: ConductorFileConfig, logger: ConductorLogger): Promise<boolean>;
|
|
19
|
+
export declare function buildProgram(deps: GaiaCliDeps): Command;
|
|
18
20
|
export declare function runGaiaCli(argv: string[], deps?: GaiaCliDeps): Promise<void>;
|
|
19
21
|
export declare function main(argv: string[]): Promise<void>;
|
package/dist/src/cli/gaia.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CommandRunner, createLogger, exec, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
5
6
|
import { selectAgent, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
6
7
|
import { Command } from 'commander';
|
|
7
8
|
import { authStatus, buildProgram as buildDropshProgram } from 'dropsh';
|
|
@@ -31,13 +32,17 @@ function checkoutRootOf(config) {
|
|
|
31
32
|
return dirname(config.config_path);
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* The conductor's stable identity (gaia_conductor.machine_id).
|
|
35
|
-
*
|
|
36
|
-
* commands read it here
|
|
37
|
-
*
|
|
35
|
+
* The conductor's stable identity (gaia_conductor.machine_id). machine_id is
|
|
36
|
+
* required — the loader (loadConductorConfig) throws without it — so lifecycle
|
|
37
|
+
* commands read it here, never re-derive. A config not built by the loader that
|
|
38
|
+
* somehow lacks it fails loudly rather than silently minting a divergent id.
|
|
38
39
|
*/
|
|
39
40
|
function conductorIdOf(config) {
|
|
40
|
-
|
|
41
|
+
const id = config.machine_id;
|
|
42
|
+
if (id === undefined || id.trim() === '') {
|
|
43
|
+
throw new Error('conductor config has no machine_id');
|
|
44
|
+
}
|
|
45
|
+
return id;
|
|
41
46
|
}
|
|
42
47
|
/** Read the `conductor`-level --log-level / --log-sink flags from a subcommand. */
|
|
43
48
|
function logOptsOf(cmd) {
|
|
@@ -368,9 +373,50 @@ async function promptSecret() {
|
|
|
368
373
|
}
|
|
369
374
|
}
|
|
370
375
|
// --- program ----------------------------------------------------------------
|
|
371
|
-
|
|
376
|
+
// Report the CLI's own package version. Walks up from this module to the
|
|
377
|
+
// nearest @gaia-ai/conductor package.json so it resolves both from the
|
|
378
|
+
// compiled dist/src/cli/gaia.js (3 dirs up) and the src/cli/gaia.ts vitest
|
|
379
|
+
// runs (2 dirs up). Same release tag across packages ⇒ == @gaia-ai/gaia.
|
|
380
|
+
function resolveCliVersion() {
|
|
381
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
382
|
+
for (let i = 0; i < 6; i++) {
|
|
383
|
+
try {
|
|
384
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
385
|
+
if (pkg.name === '@gaia-ai/conductor')
|
|
386
|
+
return pkg.version ?? '0.0.0';
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// no package.json here — keep walking up
|
|
390
|
+
}
|
|
391
|
+
dir = dirname(dir);
|
|
392
|
+
}
|
|
393
|
+
return '0.0.0';
|
|
394
|
+
}
|
|
395
|
+
export function buildProgram(deps) {
|
|
372
396
|
const program = new Command();
|
|
373
|
-
program
|
|
397
|
+
program
|
|
398
|
+
.name('gaia')
|
|
399
|
+
.description('GAIA conductor + client CLI')
|
|
400
|
+
.option('--conductor <name>', 'select a named .gaia/<name>.config.js (default: the sole config; env $GAIA_CONDUCTOR)');
|
|
401
|
+
// GAIA-126: a repo may hold several named .gaia/<stem>.config.js conductors.
|
|
402
|
+
// resolveConfigPath already honours $GAIA_CONDUCTOR; thread the global
|
|
403
|
+
// --conductor flag into it (flag wins over env) so every command selects the
|
|
404
|
+
// named config without per-command wiring. A per-invocation flag beats any
|
|
405
|
+
// ambient env for this run.
|
|
406
|
+
program.hook('preAction', () => {
|
|
407
|
+
const name = program.opts().conductor;
|
|
408
|
+
if (typeof name === 'string' && name !== '') {
|
|
409
|
+
process.env.GAIA_CONDUCTOR = name;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const cliVersion = resolveCliVersion();
|
|
413
|
+
program.version(cliVersion); // -V, --version
|
|
414
|
+
program
|
|
415
|
+
.command('version')
|
|
416
|
+
.description('output the gaia CLI version')
|
|
417
|
+
.action(() => {
|
|
418
|
+
console.log(cliVersion);
|
|
419
|
+
});
|
|
374
420
|
const conductor = program
|
|
375
421
|
.command('conductor')
|
|
376
422
|
.description('node-agent lifecycle + local registry')
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ConductorFileConfig } from '@gaia-ai/core';
|
|
2
2
|
/**
|
|
3
3
|
* Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
|
|
4
4
|
* the agent must stop instead of running the whole flow in one session. The run
|
|
@@ -12,21 +12,40 @@ import { type ConductorFileConfig } from '@gaia-ai/core';
|
|
|
12
12
|
*
|
|
13
13
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
14
14
|
* ticket content into a single typed pane line overran the PTY canonical line
|
|
15
|
-
* cap
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* cap and truncated the dispatch command. Instead the prompt is a bounded,
|
|
16
|
+
* constant-size pointer and the agent reads the ticket + all comments at run
|
|
17
|
+
* start via `gaia dropsh read … --include comments`.
|
|
18
|
+
*
|
|
19
|
+
* HINT — the canonical cap is platform-specific: MAX_CANON is 4096 B on Linux
|
|
20
|
+
* but only 1024 B on macOS (a whole line >= 1024 B is silently DROPPED there).
|
|
21
|
+
* herdr types env-prefix + this prompt + flags as ONE line, so keep the total
|
|
22
|
+
* well under 1024 B — i.e. keep this prompt short (~a few hundred bytes). Do NOT
|
|
23
|
+
* grow it back toward the old ~1 KB, or macOS dispatch truncates silently again.
|
|
24
|
+
*
|
|
25
|
+
* The prompt is a SINGLE LINE — no newlines, no control chars (GAIA-128). herdr
|
|
26
|
+
* types it into the pane as one line, so a newline would submit early and any
|
|
27
|
+
* control char would force bash-only `$'…'` quoting that fish can't parse.
|
|
28
|
+
* Being single-line + bounded, plain `'…'` shell-quoting suffices (fish-safe)
|
|
29
|
+
* and the typed line stays well under the cap (1024 B on macOS, 4096 B on Linux)
|
|
30
|
+
* — so NO base64/`bash -c` wrapper and NO multiline handling are needed (that
|
|
31
|
+
* wrapper, GAIA-118, was the thing that re-inflated the line past the cap and
|
|
32
|
+
* truncated it mid-quote).
|
|
18
33
|
* Placeholders: `{identifier}`, `{state}`, `{runUuid}` ({state} falls back to
|
|
19
34
|
* `triage` for unclassified tickets).
|
|
20
35
|
*/
|
|
21
36
|
export declare const DEFAULT_AGENT_PROMPT: string;
|
|
22
37
|
/**
|
|
23
|
-
* Resolve the conductor config path from `cwd`.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the nearest
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* error
|
|
38
|
+
* Resolve the conductor config path from `cwd`.
|
|
39
|
+
*
|
|
40
|
+
* 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
|
|
41
|
+
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding `*.config.js`
|
|
42
|
+
* — so any subdirectory of a project/worktree resolves the same dir.
|
|
43
|
+
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves
|
|
44
|
+
* `<gaiaDir>/<name>.config.js` (error listing the stems if absent).
|
|
45
|
+
* - No selector: exactly one config → use it (back-compat: the lone
|
|
46
|
+
* `conductor.config.js`); many → error naming the stems + the selector.
|
|
47
|
+
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
48
|
+
* (never leak a raw "Cannot find module" from a later import()).
|
|
30
49
|
*/
|
|
31
|
-
export declare function resolveConfigPath(override?: string, cwd?: string): string;
|
|
50
|
+
export declare function resolveConfigPath(override?: string, cwd?: string, conductorName?: string): string;
|
|
32
51
|
export declare function loadConductorConfig(configFile: string): Promise<ConductorFileConfig>;
|
package/dist/src/config.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { conductorId, } from '@gaia-ai/core';
|
|
6
5
|
/**
|
|
7
6
|
* Default agent prompt - the GAIA run contract. One run works EXACTLY one state;
|
|
8
7
|
* the agent must stop instead of running the whole flow in one session. The run
|
|
@@ -16,26 +15,34 @@ import { conductorId, } from '@gaia-ai/core';
|
|
|
16
15
|
*
|
|
17
16
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
18
17
|
* ticket content into a single typed pane line overran the PTY canonical line
|
|
19
|
-
* cap
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* cap and truncated the dispatch command. Instead the prompt is a bounded,
|
|
19
|
+
* constant-size pointer and the agent reads the ticket + all comments at run
|
|
20
|
+
* start via `gaia dropsh read … --include comments`.
|
|
21
|
+
*
|
|
22
|
+
* HINT — the canonical cap is platform-specific: MAX_CANON is 4096 B on Linux
|
|
23
|
+
* but only 1024 B on macOS (a whole line >= 1024 B is silently DROPPED there).
|
|
24
|
+
* herdr types env-prefix + this prompt + flags as ONE line, so keep the total
|
|
25
|
+
* well under 1024 B — i.e. keep this prompt short (~a few hundred bytes). Do NOT
|
|
26
|
+
* grow it back toward the old ~1 KB, or macOS dispatch truncates silently again.
|
|
27
|
+
*
|
|
28
|
+
* The prompt is a SINGLE LINE — no newlines, no control chars (GAIA-128). herdr
|
|
29
|
+
* types it into the pane as one line, so a newline would submit early and any
|
|
30
|
+
* control char would force bash-only `$'…'` quoting that fish can't parse.
|
|
31
|
+
* Being single-line + bounded, plain `'…'` shell-quoting suffices (fish-safe)
|
|
32
|
+
* and the typed line stays well under the cap (1024 B on macOS, 4096 B on Linux)
|
|
33
|
+
* — so NO base64/`bash -c` wrapper and NO multiline handling are needed (that
|
|
34
|
+
* wrapper, GAIA-118, was the thing that re-inflated the line past the cap and
|
|
35
|
+
* truncated it mid-quote).
|
|
22
36
|
* Placeholders: `{identifier}`, `{state}`, `{runUuid}` ({state} falls back to
|
|
23
37
|
* `triage` for unclassified tickets).
|
|
24
38
|
*/
|
|
25
39
|
export const DEFAULT_AGENT_PROMPT = `You are a GAIA agent working ticket {identifier}, current state: {state}. ` +
|
|
26
|
-
`Invoke the gaia skill and run \`ticket:run {identifier} {state}\` — it
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
30
|
-
`
|
|
31
|
-
`
|
|
32
|
-
`blocked or you need a human, stop and surface the blocker. After you have ` +
|
|
33
|
-
`written the ticket's next state, run /exit to end this session.\n\n` +
|
|
34
|
-
`First, read the ticket + all comments in one call:\n\n` +
|
|
35
|
-
` gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments\n\n` +
|
|
36
|
-
`The latest \`summary\` comment is review feedback you MUST address; any ` +
|
|
37
|
-
`\`spec\`/\`diagnose\` comment is the agreed plan. Treat them as in-scope ` +
|
|
38
|
-
`acceptance criteria.`;
|
|
40
|
+
`Invoke the gaia skill and run \`ticket:run {identifier} {state}\` — it first ` +
|
|
41
|
+
`reads the ticket + all comments ` +
|
|
42
|
+
`(gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
|
|
43
|
+
`renders the intake splash, 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. When done, or if you are blocked, STOP and run /exit.`;
|
|
39
46
|
function requirePlugin(value, kind) {
|
|
40
47
|
if (!isRecord(value) || value.kind !== kind) {
|
|
41
48
|
throw new Error(`conductor config requires a ${kind} plugin in the "${kind}" slot`);
|
|
@@ -115,13 +122,19 @@ async function resolveSlot(value, kind, configPath) {
|
|
|
115
122
|
/**
|
|
116
123
|
* Resolve the `plugins[]` array: descriptor entries are constructed, already-
|
|
117
124
|
* constructed entries pass through. No kind-guard (plugins are not slotted).
|
|
125
|
+
*
|
|
126
|
+
* A factory may return one plugin OR an array of plugins — an aggregator built
|
|
127
|
+
* with dropsh's `composePlugins(a(), b())` returns the flat child list. dropsh's
|
|
128
|
+
* own `loadConfig` flattens such a nested entry one level; we mirror that here so
|
|
129
|
+
* `buildProgram({ plugins })` — which reads each entry's hooks/renderers per
|
|
130
|
+
* top-level element and does NOT recurse — sees every child.
|
|
118
131
|
*/
|
|
119
132
|
async function resolvePlugins(raw, configPath) {
|
|
120
133
|
if (!Array.isArray(raw)) {
|
|
121
134
|
return [];
|
|
122
135
|
}
|
|
123
136
|
const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadNamedPlugin(entry, configPath) : entry));
|
|
124
|
-
return resolved;
|
|
137
|
+
return resolved.flat();
|
|
125
138
|
}
|
|
126
139
|
function isRecord(value) {
|
|
127
140
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -141,35 +154,70 @@ function optionalPositiveInteger(value, fallback, key) {
|
|
|
141
154
|
}
|
|
142
155
|
return value;
|
|
143
156
|
}
|
|
144
|
-
/**
|
|
145
|
-
const
|
|
157
|
+
/** A repo's config files live in this dir, one `<stem>.config.js` per conductor. */
|
|
158
|
+
const GAIA_DIR = '.gaia';
|
|
159
|
+
/** List the `*.config.js` stems in a `.gaia/` dir (e.g. `shop.config.js` → `shop`). */
|
|
160
|
+
function configStems(gaiaDir) {
|
|
161
|
+
return readdirSync(gaiaDir)
|
|
162
|
+
.filter((f) => f.endsWith('.config.js'))
|
|
163
|
+
.map((f) => f.slice(0, -'.config.js'.length))
|
|
164
|
+
.sort();
|
|
165
|
+
}
|
|
146
166
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* the nearest ancestor holding `.gaia/conductor.config.js` and return that
|
|
151
|
-
* absolute path — so any subdirectory of a project/worktree resolves the same
|
|
152
|
-
* config. If none is found up to the filesystem root, throw a clear, actionable
|
|
153
|
-
* error instead of leaking a raw "Cannot find module" from a later import().
|
|
167
|
+
* Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
|
|
168
|
+
* `.gaia/` dir holds at least one `*.config.js`; return that `.gaia/` dir, or
|
|
169
|
+
* `undefined` if none is found up to the filesystem root.
|
|
154
170
|
*/
|
|
155
|
-
|
|
156
|
-
const explicit = override ?? process.env.GAIA_CONDUCTOR_CONFIG;
|
|
157
|
-
if (explicit) {
|
|
158
|
-
return explicit;
|
|
159
|
-
}
|
|
171
|
+
function findGaiaDir(cwd) {
|
|
160
172
|
let dir = resolve(cwd);
|
|
161
173
|
for (;;) {
|
|
162
|
-
const
|
|
163
|
-
if (existsSync(
|
|
164
|
-
return
|
|
174
|
+
const gaiaDir = join(dir, GAIA_DIR);
|
|
175
|
+
if (existsSync(gaiaDir) && configStems(gaiaDir).length > 0) {
|
|
176
|
+
return gaiaDir;
|
|
165
177
|
}
|
|
166
178
|
const parent = dirname(dir);
|
|
167
179
|
if (parent === dir) {
|
|
168
|
-
|
|
180
|
+
return undefined;
|
|
169
181
|
}
|
|
170
182
|
dir = parent;
|
|
171
183
|
}
|
|
172
|
-
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the conductor config path from `cwd`.
|
|
187
|
+
*
|
|
188
|
+
* 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
|
|
189
|
+
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding `*.config.js`
|
|
190
|
+
* — so any subdirectory of a project/worktree resolves the same dir.
|
|
191
|
+
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves
|
|
192
|
+
* `<gaiaDir>/<name>.config.js` (error listing the stems if absent).
|
|
193
|
+
* - No selector: exactly one config → use it (back-compat: the lone
|
|
194
|
+
* `conductor.config.js`); many → error naming the stems + the selector.
|
|
195
|
+
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
196
|
+
* (never leak a raw "Cannot find module" from a later import()).
|
|
197
|
+
*/
|
|
198
|
+
export function resolveConfigPath(override, cwd = process.cwd(), conductorName) {
|
|
199
|
+
const explicit = override ?? process.env.GAIA_CONDUCTOR_CONFIG;
|
|
200
|
+
if (explicit) {
|
|
201
|
+
return explicit;
|
|
202
|
+
}
|
|
203
|
+
const gaiaDir = findGaiaDir(cwd);
|
|
204
|
+
if (gaiaDir === undefined) {
|
|
205
|
+
throw new Error(`no .gaia/conductor.config.js found from ${cwd} upward; run \`gaia init\``);
|
|
206
|
+
}
|
|
207
|
+
const stems = configStems(gaiaDir);
|
|
208
|
+
const name = conductorName ?? process.env.GAIA_CONDUCTOR;
|
|
209
|
+
if (name !== undefined && name !== '') {
|
|
210
|
+
const candidate = join(gaiaDir, `${name}.config.js`);
|
|
211
|
+
if (!existsSync(candidate)) {
|
|
212
|
+
throw new Error(`no conductor '${name}' in ${gaiaDir}; available: ${stems.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
return candidate;
|
|
215
|
+
}
|
|
216
|
+
if (stems.length === 1) {
|
|
217
|
+
return join(gaiaDir, `${stems[0]}.config.js`);
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`${stems.length} conductors in ${gaiaDir} (${stems.join(', ')}); ` +
|
|
220
|
+
'select one with --conductor <name> or $GAIA_CONDUCTOR');
|
|
173
221
|
}
|
|
174
222
|
export async function loadConductorConfig(configFile) {
|
|
175
223
|
const configPath = resolve(configFile);
|
|
@@ -186,14 +234,15 @@ export async function loadConductorConfig(configFile) {
|
|
|
186
234
|
throw new Error('conductor config requires non-empty states');
|
|
187
235
|
}
|
|
188
236
|
const states = config.states.map((state) => requireNonEmptyString(state, 'states'));
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
237
|
+
// machine_id is required — the config MUST set it; there is no derived
|
|
238
|
+
// fallback. The id is defined in a single place (config.machine_id), which
|
|
239
|
+
// the CLI lifecycle commands and registration all read, never re-derive. The
|
|
240
|
+
// conductor label defaults to it (a conductor is identified by it). A repo's
|
|
241
|
+
// committed config composes it from the machine context
|
|
242
|
+
// (`${user_id}-${machine_id}-${project}`); with several named configs in one
|
|
243
|
+
// .gaia/ (GAIA-126) each composes its own from its own project, so co-located
|
|
244
|
+
// conductors get distinct identities with no filename-based fallback.
|
|
245
|
+
const machineId = requireNonEmptyString(config.machine_id, 'machine_id');
|
|
197
246
|
const label = typeof config.label === 'string' && config.label.trim() !== ''
|
|
198
247
|
? config.label
|
|
199
248
|
: machineId;
|
|
@@ -38,27 +38,25 @@ export declare class Conductor {
|
|
|
38
38
|
* flag via {@link GaiaRemote.fetchUncleanedTickets}: every ticket that is
|
|
39
39
|
* finished (state=done OR closed) but not yet cleaned. The SAME reconciliation
|
|
40
40
|
* runs on every tick AND standalone as `gaia conductor reap`, and the query is
|
|
41
|
-
*
|
|
42
|
-
* structurally cannot (
|
|
43
|
-
* `done`, crashed mid-cleanup
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* with no host enumeration.
|
|
41
|
+
* scoped to THIS conductor (GAIA-121) — so it catches the orphans a
|
|
42
|
+
* live-tick-only path structurally cannot (the conductor was down when the
|
|
43
|
+
* ticket hit `done`, or crashed mid-cleanup) for its OWN tickets. A foreign
|
|
44
|
+
* (non-gaia) worktree has no gaia ticket, so it is never on the work list; a
|
|
45
|
+
* ticket assigned to another conductor is filtered out at the query — both are
|
|
46
|
+
* left untouched with no host enumeration.
|
|
47
47
|
*
|
|
48
48
|
* Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
|
|
49
49
|
* is closed at once as a lifecycle step, independent of whether its worktree
|
|
50
|
-
* teardown then succeeds.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* one
|
|
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.
|
|
50
|
+
* teardown then succeeds. Because the list is conductor-scoped, every ticket's
|
|
51
|
+
* worktree belongs on THIS host: `removeWorktree` returning `false` means the
|
|
52
|
+
* directory was already deleted out of band (nothing left to remove), which is
|
|
53
|
+
* itself a completed teardown → flag `cleaned_up`. There is no cross-host defer
|
|
54
|
+
* (GAIA-121) — the manufactured ambiguity that required it is gone once the
|
|
55
|
+
* query no longer loads foreign tickets. A teardown that THROWS logs loudly and
|
|
56
|
+
* leaves `cleaned_up=0`, so the next reconciliation retries; one miss never
|
|
57
|
+
* orphans the workspace (RC1). Re-running on an already-cleaned ticket is a
|
|
58
|
+
* no-op — it is off the list (idempotent). Each ticket is isolated in a
|
|
59
|
+
* try/catch so one failure never aborts the rest.
|
|
62
60
|
*
|
|
63
61
|
* The `after_done` hook runs through the executor (GAIA-84), which owns the
|
|
64
62
|
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
@@ -194,8 +194,22 @@ export class Conductor {
|
|
|
194
194
|
const log = r.worktreePath
|
|
195
195
|
? await this.agent.getRunLog(r.worktreePath)
|
|
196
196
|
: '';
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Footprint (GAIA-132): the agent parses its own transcript for the
|
|
198
|
+
// effort metrics (transcript format is agent-specific); the conductor
|
|
199
|
+
// adds duration_s from started_at → now. The conductor owns the run, so
|
|
200
|
+
// this write always lands (the agent session could not).
|
|
201
|
+
const parsed = this.agent.parseFootprint(log);
|
|
202
|
+
const now = Math.floor(Date.now() / 1000);
|
|
203
|
+
const metrics = {
|
|
204
|
+
tokens: parsed.tokens,
|
|
205
|
+
duration_s: r.startedAt !== undefined ? Math.max(0, now - r.startedAt) : 0,
|
|
206
|
+
agent_turns: parsed.agent_turns,
|
|
207
|
+
tool_calls: parsed.tool_calls,
|
|
208
|
+
user_prompts: parsed.user_prompts,
|
|
209
|
+
user_prompt_words: parsed.user_prompt_words,
|
|
210
|
+
};
|
|
211
|
+
await this.remote.finalizeRun(r.runUuid, log, metrics);
|
|
212
|
+
this.logger.info({ run: r.runUuid, footprint: metrics }, 'run finalised');
|
|
199
213
|
}
|
|
200
214
|
catch (err) {
|
|
201
215
|
this.logger.warn({ run: r.runUuid, err: String(err) }, 'run finalise failed');
|
|
@@ -208,34 +222,32 @@ export class Conductor {
|
|
|
208
222
|
* flag via {@link GaiaRemote.fetchUncleanedTickets}: every ticket that is
|
|
209
223
|
* finished (state=done OR closed) but not yet cleaned. The SAME reconciliation
|
|
210
224
|
* runs on every tick AND standalone as `gaia conductor reap`, and the query is
|
|
211
|
-
*
|
|
212
|
-
* structurally cannot (
|
|
213
|
-
* `done`, crashed mid-cleanup
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* with no host enumeration.
|
|
225
|
+
* scoped to THIS conductor (GAIA-121) — so it catches the orphans a
|
|
226
|
+
* live-tick-only path structurally cannot (the conductor was down when the
|
|
227
|
+
* ticket hit `done`, or crashed mid-cleanup) for its OWN tickets. A foreign
|
|
228
|
+
* (non-gaia) worktree has no gaia ticket, so it is never on the work list; a
|
|
229
|
+
* ticket assigned to another conductor is filtered out at the query — both are
|
|
230
|
+
* left untouched with no host enumeration.
|
|
217
231
|
*
|
|
218
232
|
* Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
|
|
219
233
|
* is closed at once as a lifecycle step, independent of whether its worktree
|
|
220
|
-
* teardown then succeeds.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* one
|
|
230
|
-
* ticket is a no-op — it is off the list (idempotent). Each ticket is isolated
|
|
231
|
-
* in a try/catch so one failure never aborts the rest.
|
|
234
|
+
* teardown then succeeds. Because the list is conductor-scoped, every ticket's
|
|
235
|
+
* worktree belongs on THIS host: `removeWorktree` returning `false` means the
|
|
236
|
+
* directory was already deleted out of band (nothing left to remove), which is
|
|
237
|
+
* itself a completed teardown → flag `cleaned_up`. There is no cross-host defer
|
|
238
|
+
* (GAIA-121) — the manufactured ambiguity that required it is gone once the
|
|
239
|
+
* query no longer loads foreign tickets. A teardown that THROWS logs loudly and
|
|
240
|
+
* leaves `cleaned_up=0`, so the next reconciliation retries; one miss never
|
|
241
|
+
* orphans the workspace (RC1). Re-running on an already-cleaned ticket is a
|
|
242
|
+
* no-op — it is off the list (idempotent). Each ticket is isolated in a
|
|
243
|
+
* try/catch so one failure never aborts the rest.
|
|
232
244
|
*
|
|
233
245
|
* The `after_done` hook runs through the executor (GAIA-84), which owns the
|
|
234
246
|
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
235
247
|
* the core needs no per-call-site try/catch around it.
|
|
236
248
|
*/
|
|
237
249
|
async reap() {
|
|
238
|
-
const tickets = await this.remote.fetchUncleanedTickets();
|
|
250
|
+
const tickets = await this.remote.fetchUncleanedTickets(this.id);
|
|
239
251
|
for (const t of tickets) {
|
|
240
252
|
try {
|
|
241
253
|
// Lifecycle: close a done+unclosed ticket at once, decoupled from the
|
|
@@ -259,9 +271,12 @@ export class Conductor {
|
|
|
259
271
|
});
|
|
260
272
|
}
|
|
261
273
|
if (this.executor.capabilities().persistent) {
|
|
262
|
-
let removed;
|
|
263
274
|
try {
|
|
264
|
-
|
|
275
|
+
// The work list is scoped to this conductor (GAIA-121), so its
|
|
276
|
+
// worktree belongs on THIS host. Whether removeWorktree tears one
|
|
277
|
+
// down or finds it already gone (`false`), teardown is complete —
|
|
278
|
+
// the boolean is irrelevant, only a THROW (a real failure) matters.
|
|
279
|
+
await this.executor.removeWorktree(t.branchName, t.worktreePath || undefined);
|
|
265
280
|
}
|
|
266
281
|
catch (err) {
|
|
267
282
|
this.logger.warn({
|
|
@@ -272,20 +287,6 @@ export class Conductor {
|
|
|
272
287
|
}, 'worktree teardown failed');
|
|
273
288
|
continue; // leave cleaned_up=0 → the next reconciliation retries.
|
|
274
289
|
}
|
|
275
|
-
// A recorded worktree path that was NOT present on this host belongs
|
|
276
|
-
// to another conductor's host (the query is not machine-scoped, so we
|
|
277
|
-
// see every finished ticket). Do NOT flag it cleaned — that would be a
|
|
278
|
-
// cross-host false-teardown, orphaning a still-live workspace. Defer:
|
|
279
|
-
// the host that physically holds it reaps + flags it. A ticket with no
|
|
280
|
-
// recorded path (nothing hosted to locate) falls through to cleaned.
|
|
281
|
-
if (t.worktreePath && !removed) {
|
|
282
|
-
this.logger.info({
|
|
283
|
-
ticket: t.ticketUuid,
|
|
284
|
-
branch: t.branchName,
|
|
285
|
-
worktreePath: t.worktreePath,
|
|
286
|
-
}, 'worktree not on this host — deferring teardown to its host');
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
290
|
}
|
|
290
291
|
// Teardown verified locally (or nothing hosted to tear down): flag it
|
|
291
292
|
// cleaned so it drops off the work list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/conductor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"directory": "conductor/packages/conductor"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gaia-ai/core": "^0.4.
|
|
28
|
+
"@gaia-ai/core": "^0.4.4",
|
|
29
29
|
"@dropsh/plugin-oauth2": "^0.4.1",
|
|
30
|
-
"@dropsh/plugin-jsonapi-schema": "^0.5.
|
|
30
|
+
"@dropsh/plugin-jsonapi-schema": "^0.5.6",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
|
-
"dropsh": "^0.5.
|
|
32
|
+
"dropsh": "^0.5.6"
|
|
33
33
|
}
|
|
34
34
|
}
|