@gaia-ai/conductor 0.4.3 → 0.4.5
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 +26 -8
- package/dist/src/config.d.ts +13 -9
- package/dist/src/config.js +71 -30
- 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
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { CommandRunner,
|
|
5
|
+
import { CommandRunner, createLogger, exec, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
6
6
|
import { selectAgent, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { authStatus, buildProgram as buildDropshProgram } from 'dropsh';
|
|
@@ -32,13 +32,17 @@ function checkoutRootOf(config) {
|
|
|
32
32
|
return dirname(config.config_path);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* The conductor's stable identity (gaia_conductor.machine_id).
|
|
36
|
-
*
|
|
37
|
-
* commands read it here
|
|
38
|
-
*
|
|
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.
|
|
39
39
|
*/
|
|
40
40
|
function conductorIdOf(config) {
|
|
41
|
-
|
|
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;
|
|
42
46
|
}
|
|
43
47
|
/** Read the `conductor`-level --log-level / --log-sink flags from a subcommand. */
|
|
44
48
|
function logOptsOf(cmd) {
|
|
@@ -388,9 +392,23 @@ function resolveCliVersion() {
|
|
|
388
392
|
}
|
|
389
393
|
return '0.0.0';
|
|
390
394
|
}
|
|
391
|
-
function buildProgram(deps) {
|
|
395
|
+
export function buildProgram(deps) {
|
|
392
396
|
const program = new Command();
|
|
393
|
-
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
|
+
});
|
|
394
412
|
const cliVersion = resolveCliVersion();
|
|
395
413
|
program.version(cliVersion); // -V, --version
|
|
396
414
|
program
|
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
|
|
@@ -28,13 +28,17 @@ import { type ConductorFileConfig } from '@gaia-ai/core';
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const DEFAULT_AGENT_PROMPT: string;
|
|
30
30
|
/**
|
|
31
|
-
* Resolve the conductor config path from `cwd`.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* the nearest
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* error
|
|
31
|
+
* Resolve the conductor config path from `cwd`.
|
|
32
|
+
*
|
|
33
|
+
* 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
|
|
34
|
+
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding `*.config.js`
|
|
35
|
+
* — so any subdirectory of a project/worktree resolves the same dir.
|
|
36
|
+
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves
|
|
37
|
+
* `<gaiaDir>/<name>.config.js` (error listing the stems if absent).
|
|
38
|
+
* - No selector: exactly one config → use it (back-compat: the lone
|
|
39
|
+
* `conductor.config.js`); many → error naming the stems + the selector.
|
|
40
|
+
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
41
|
+
* (never leak a raw "Cannot find module" from a later import()).
|
|
38
42
|
*/
|
|
39
|
-
export declare function resolveConfigPath(override?: string, cwd?: string): string;
|
|
43
|
+
export declare function resolveConfigPath(override?: string, cwd?: string, conductorName?: string): string;
|
|
40
44
|
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
|
|
@@ -123,13 +122,19 @@ async function resolveSlot(value, kind, configPath) {
|
|
|
123
122
|
/**
|
|
124
123
|
* Resolve the `plugins[]` array: descriptor entries are constructed, already-
|
|
125
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.
|
|
126
131
|
*/
|
|
127
132
|
async function resolvePlugins(raw, configPath) {
|
|
128
133
|
if (!Array.isArray(raw)) {
|
|
129
134
|
return [];
|
|
130
135
|
}
|
|
131
136
|
const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadNamedPlugin(entry, configPath) : entry));
|
|
132
|
-
return resolved;
|
|
137
|
+
return resolved.flat();
|
|
133
138
|
}
|
|
134
139
|
function isRecord(value) {
|
|
135
140
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -149,35 +154,70 @@ function optionalPositiveInteger(value, fallback, key) {
|
|
|
149
154
|
}
|
|
150
155
|
return value;
|
|
151
156
|
}
|
|
152
|
-
/**
|
|
153
|
-
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
|
+
}
|
|
154
166
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* the nearest ancestor holding `.gaia/conductor.config.js` and return that
|
|
159
|
-
* absolute path — so any subdirectory of a project/worktree resolves the same
|
|
160
|
-
* config. If none is found up to the filesystem root, throw a clear, actionable
|
|
161
|
-
* 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.
|
|
162
170
|
*/
|
|
163
|
-
|
|
164
|
-
const explicit = override ?? process.env.GAIA_CONDUCTOR_CONFIG;
|
|
165
|
-
if (explicit) {
|
|
166
|
-
return explicit;
|
|
167
|
-
}
|
|
171
|
+
function findGaiaDir(cwd) {
|
|
168
172
|
let dir = resolve(cwd);
|
|
169
173
|
for (;;) {
|
|
170
|
-
const
|
|
171
|
-
if (existsSync(
|
|
172
|
-
return
|
|
174
|
+
const gaiaDir = join(dir, GAIA_DIR);
|
|
175
|
+
if (existsSync(gaiaDir) && configStems(gaiaDir).length > 0) {
|
|
176
|
+
return gaiaDir;
|
|
173
177
|
}
|
|
174
178
|
const parent = dirname(dir);
|
|
175
179
|
if (parent === dir) {
|
|
176
|
-
|
|
180
|
+
return undefined;
|
|
177
181
|
}
|
|
178
182
|
dir = parent;
|
|
179
183
|
}
|
|
180
|
-
|
|
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');
|
|
181
221
|
}
|
|
182
222
|
export async function loadConductorConfig(configFile) {
|
|
183
223
|
const configPath = resolve(configFile);
|
|
@@ -194,14 +234,15 @@ export async function loadConductorConfig(configFile) {
|
|
|
194
234
|
throw new Error('conductor config requires non-empty states');
|
|
195
235
|
}
|
|
196
236
|
const states = config.states.map((state) => requireNonEmptyString(state, 'states'));
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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');
|
|
205
246
|
const label = typeof config.label === 'string' && config.label.trim() !== ''
|
|
206
247
|
? config.label
|
|
207
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.5",
|
|
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.5",
|
|
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
|
}
|