@gaia-ai/conductor 0.6.2 → 0.6.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/commands/conductor.js +7 -6
- package/dist/src/contract.d.ts +1 -1
- package/dist/src/core/conductor.js +46 -4
- package/dist/src/plugins/executor.d.ts +22 -1
- package/dist/src/plugins/plugins.d.ts +16 -3
- package/dist/src/plugins/plugins.js +8 -3
- package/dist/src/plugins/workspace.d.ts +30 -0
- package/package.json +2 -2
|
@@ -192,7 +192,7 @@ async function cmdPoll(deps, log = {}) {
|
|
|
192
192
|
return;
|
|
193
193
|
const remote = await resolveRemote(deps, config);
|
|
194
194
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
195
|
-
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
195
|
+
const workspace = deps.workspace ?? (await selectWorkspace(config, logger));
|
|
196
196
|
const agents = deps.agents ?? (await selectAgents(config));
|
|
197
197
|
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
198
198
|
await conductor.start();
|
|
@@ -206,7 +206,7 @@ async function cmdReap(deps, log = {}) {
|
|
|
206
206
|
return;
|
|
207
207
|
const remote = await resolveRemote(deps, config);
|
|
208
208
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
209
|
-
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
209
|
+
const workspace = deps.workspace ?? (await selectWorkspace(config, logger));
|
|
210
210
|
const agents = deps.agents ?? (await selectAgents(config));
|
|
211
211
|
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
212
212
|
await conductor.reap();
|
|
@@ -219,7 +219,7 @@ async function cmdStartForeground(deps, log = {}) {
|
|
|
219
219
|
return;
|
|
220
220
|
const remote = await resolveRemote(deps, config);
|
|
221
221
|
const executor = deps.executor ?? (await selectExecutor(config, logger));
|
|
222
|
-
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
222
|
+
const workspace = deps.workspace ?? (await selectWorkspace(config, logger));
|
|
223
223
|
const agents = deps.agents ?? (await selectAgents(config));
|
|
224
224
|
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
225
225
|
await conductor.start();
|
|
@@ -383,13 +383,14 @@ export function createConductorCommand(deps) {
|
|
|
383
383
|
const conductor = new Command('conductor')
|
|
384
384
|
.description('node-agent lifecycle + local registry')
|
|
385
385
|
.option('--log-level <level>', 'log verbosity: debug | info | warn | error (overrides GAIA_LOG_LEVEL)')
|
|
386
|
-
.option('--log-sink <sink>', 'log sink: stdout | file (overrides GAIA_CONDUCTOR_LOG)')
|
|
386
|
+
.option('--log-sink <sink>', 'log sink: stdout | file | both (overrides GAIA_CONDUCTOR_LOG)')
|
|
387
387
|
.addHelpText('after', `
|
|
388
388
|
Logging (precedence: CLI flag > env var > default):
|
|
389
389
|
--log-level <level> debug | info (default) | warn | error.
|
|
390
390
|
Set debug to see per-poll ticks, claims, and idle cycles.
|
|
391
|
-
--log-sink <sink> stdout | file (
|
|
392
|
-
Default:
|
|
391
|
+
--log-sink <sink> stdout | file | both (file/both write <checkoutRoot>/log.txt).
|
|
392
|
+
Default: both on a TTY, file otherwise (GAIA-237 — a run in a
|
|
393
|
+
herdr pane is on a TTY, and its diagnostics must outlive the pane).
|
|
393
394
|
GAIA_LOG_LEVEL env fallback for --log-level.
|
|
394
395
|
GAIA_CONDUCTOR_LOG env fallback for --log-sink.
|
|
395
396
|
|
package/dist/src/contract.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type AgentFootprint, emptyAgentFootprint, type GaiaAgent, } from './plugins/agent.js';
|
|
2
|
-
export type { ExecutorCapabilities, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput, } from './plugins/executor.js';
|
|
2
|
+
export type { ExecutorCapabilities, GaiaExecutor, HookContext, HookName, HookResult, SpawnedSession, SpawnRunInput, } from './plugins/executor.js';
|
|
3
3
|
export type { AgentCandidate, AgentPlugin, ExecutorDeps, ExecutorPlugin, RemotePlugin, ResolvedAgent, WorkspacePlugin, } from './plugins/plugins.js';
|
|
4
4
|
export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
5
5
|
export { type ConductorAddonEntry, type ConductorContributions, narrowConductorContributions, type Preset, } from './plugins/preset.js';
|
|
@@ -380,11 +380,53 @@ export class Conductor {
|
|
|
380
380
|
}
|
|
381
381
|
// Lifecycle hooks are executor-owned + best-effort (GAIA-84): runHook never
|
|
382
382
|
// throws, so neither call can abort dispatch or wedge a run in `claimed`.
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
383
|
+
// Both get the resolved run env so a setup/pre hook sees the same vars (e.g.
|
|
384
|
+
// a DB DSN) the agent does (GAIA-99).
|
|
385
|
+
//
|
|
386
|
+
// GAIA-237: `after_create` runs on EVERY dispatch, not only when
|
|
387
|
+
// `ws.created`. `created` answers "was this worktree born just now?", which
|
|
388
|
+
// is not the question dispatch needs answered — a worktree whose bootstrap
|
|
389
|
+
// aborted, an interrupted earlier run, or a hand-made `git worktree add` all
|
|
390
|
+
// exist WITHOUT being provisioned, and under the creation gate they never got
|
|
391
|
+
// another chance. Provisioning is therefore convergent: run the hook and let
|
|
392
|
+
// the hook decide. Keeping the repeat cheap is the HOOK's job (gaia's
|
|
393
|
+
// `.ddev/worktree-post.sh` short-circuits on its own sentinel) — the
|
|
394
|
+
// conductor deliberately learns no project-specific "is it provisioned?"
|
|
395
|
+
// marker, which is exactly why the gate cannot live here.
|
|
396
|
+
const provisioning = await this.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
|
|
397
|
+
// AC-6: an agent is never dispatched into a worktree whose bootstrap failed
|
|
398
|
+
// without that being visible. Dispatch is NOT withheld — runHook's
|
|
399
|
+
// never-throw contract exists so a hook cannot wedge a run, and withholding
|
|
400
|
+
// would re-introduce that wedge — so the failure is recorded against the run
|
|
401
|
+
// instead (ticket + run + hook + workspace + err) and the run proceeds.
|
|
402
|
+
// Boundedness stays with the existing circuit breaker.
|
|
403
|
+
if (!provisioning.ok) {
|
|
404
|
+
this.logger.error({
|
|
405
|
+
hook: provisioning.hook,
|
|
406
|
+
ticket: t.identifier,
|
|
407
|
+
run: run.runUuid,
|
|
408
|
+
workspace: ws.path,
|
|
409
|
+
err: provisioning.error,
|
|
410
|
+
}, 'lifecycle hook failed');
|
|
411
|
+
}
|
|
412
|
+
// GAIA-237: apply the workspace's open layout HERE — after `after_create`,
|
|
413
|
+
// before the run starts. The layout's startup command names the gaia CLI
|
|
414
|
+
// that `after_create` builds; applied inside `workspace.ensure()` (where it
|
|
415
|
+
// used to live) it necessarily fired first, so a self-hosting checkout had
|
|
416
|
+
// to paper over the ordering with a sentinel wait loop inside the pane
|
|
417
|
+
// command. Ordering the two correctly removes the need for any handshake.
|
|
418
|
+
//
|
|
419
|
+
// Gated on `ws.created` — DELIBERATELY narrower than the `after_create` gate
|
|
420
|
+
// right above it, which is convergent (a worktree can exist without ever
|
|
421
|
+
// having been provisioned). Only a fresh create has an initial pane to lay
|
|
422
|
+
// out; re-applying on reuse would start a duplicate editor on reattach.
|
|
423
|
+
// Optional on the contract, so a workspace with no layout concept is a
|
|
424
|
+
// no-op. Best-effort by contract: implementations swallow their own errors.
|
|
386
425
|
if (ws.created) {
|
|
387
|
-
await this.
|
|
426
|
+
await this.workspace.applyOpenLayout?.(ws.path, {
|
|
427
|
+
identifier: t.identifier,
|
|
428
|
+
branch: t.branchName,
|
|
429
|
+
});
|
|
388
430
|
}
|
|
389
431
|
await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
|
|
390
432
|
// Clear a leftover tab for THIS run id in a reused workspace before
|
|
@@ -34,6 +34,24 @@ export interface HookContext {
|
|
|
34
34
|
/** The ticket identifier/uuid the hook is running for (log context). */
|
|
35
35
|
ticket: string;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* What a lifecycle hook invocation DID (GAIA-237). `runHook` still never throws,
|
|
39
|
+
* but it no longer swallows the outcome silently: it reports it, so a caller can
|
|
40
|
+
* record a failed provisioning hook against the run it is about to dispatch
|
|
41
|
+
* instead of relying on a log line to survive (AC-6). Reporting — rather than
|
|
42
|
+
* throwing — keeps the never-wedge property that the never-throw contract exists
|
|
43
|
+
* for; boundedness stays with the conductor's circuit breaker.
|
|
44
|
+
*/
|
|
45
|
+
export interface HookResult {
|
|
46
|
+
/** The hook slot that was invoked. */
|
|
47
|
+
hook: HookName;
|
|
48
|
+
/** false when no command is configured for the slot — nothing ran. */
|
|
49
|
+
ran: boolean;
|
|
50
|
+
/** false ONLY when a configured command actually failed. */
|
|
51
|
+
ok: boolean;
|
|
52
|
+
/** The stringified failure; present iff `ok` is false. */
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
37
55
|
export interface GaiaExecutor {
|
|
38
56
|
id: string;
|
|
39
57
|
capabilities(): ExecutorCapabilities;
|
|
@@ -47,13 +65,16 @@ export interface GaiaExecutor {
|
|
|
47
65
|
* `claimed`.
|
|
48
66
|
* - The underlying shell command still fails honestly (a non-zero exit
|
|
49
67
|
* rejects); only the executor catches + logs + continues.
|
|
68
|
+
* - MUST report the outcome as a {@link HookResult} (GAIA-237): never throwing
|
|
69
|
+
* is not the same as never telling. The caller decides what a failure means
|
|
70
|
+
* for the run it is dispatching; the executor only refuses to abort.
|
|
50
71
|
*
|
|
51
72
|
* `env`, when given, is the run's resolved environment (per-ticket env_vars
|
|
52
73
|
* merged with the core GAIA_* vars, GAIA-99), merged over the hook process's
|
|
53
74
|
* inherited env. Values may be sensitive — implementations must never log
|
|
54
75
|
* them (log key names only).
|
|
55
76
|
*/
|
|
56
|
-
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<
|
|
77
|
+
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<HookResult>;
|
|
57
78
|
startRun(input: SpawnRunInput): Promise<SpawnedSession>;
|
|
58
79
|
/**
|
|
59
80
|
* Signal the run's agent to stop. Called by the conductor's run-finalise pass
|
|
@@ -26,7 +26,15 @@ export interface WorkspacePlugin extends DropSHPlugin {
|
|
|
26
26
|
readonly kind: 'workspace';
|
|
27
27
|
readonly id: string;
|
|
28
28
|
readonly requiredModules: string[];
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* GAIA-234: `deps` is **optional** so every existing workspace plugin
|
|
31
|
+
* (`addon-workspace-git`, `addon-fake`) keeps compiling untouched — a
|
|
32
|
+
* workspace that logs nothing simply ignores it. It reuses {@link ExecutorDeps}
|
|
33
|
+
* rather than minting a `WorkspaceDeps` alias: that interface is already the
|
|
34
|
+
* contract's name for "runtime deps handed to a plugin factory", and a second
|
|
35
|
+
* identical shape would be two names for one thing.
|
|
36
|
+
*/
|
|
37
|
+
createWorkspace(config: ConductorFileConfig, deps?: ExecutorDeps): Promise<GaiaWorkspace>;
|
|
30
38
|
}
|
|
31
39
|
export interface AgentPlugin extends DropSHPlugin {
|
|
32
40
|
readonly kind: 'agent';
|
|
@@ -38,8 +46,13 @@ export interface AgentPlugin extends DropSHPlugin {
|
|
|
38
46
|
export declare function selectRemote(config: ConductorFileConfig): Promise<GaiaRemote>;
|
|
39
47
|
/** Resolves the executor from its named config slot, injecting the logger. */
|
|
40
48
|
export declare function selectExecutor(config: ConductorFileConfig, logger: ConductorLogger): Promise<GaiaExecutor>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the workspace from its named config slot, injecting the logger when
|
|
51
|
+
* the caller has one — mirroring {@link selectExecutor}. Without it the herdr
|
|
52
|
+
* workspace's best-effort open-layout warn would land on a noop logger
|
|
53
|
+
* (GAIA-234).
|
|
54
|
+
*/
|
|
55
|
+
export declare function selectWorkspace(config: ConductorFileConfig, logger?: ConductorLogger): Promise<GaiaWorkspace>;
|
|
43
56
|
/** A config-side agent choice: an agent plugin + an optional static priority over the ticket. */
|
|
44
57
|
export interface AgentCandidate {
|
|
45
58
|
agent: AgentPlugin;
|
|
@@ -6,9 +6,14 @@ export async function selectRemote(config) {
|
|
|
6
6
|
export async function selectExecutor(config, logger) {
|
|
7
7
|
return config.executor.createExecutor(config, { logger });
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the workspace from its named config slot, injecting the logger when
|
|
11
|
+
* the caller has one — mirroring {@link selectExecutor}. Without it the herdr
|
|
12
|
+
* workspace's best-effort open-layout warn would land on a noop logger
|
|
13
|
+
* (GAIA-234).
|
|
14
|
+
*/
|
|
15
|
+
export async function selectWorkspace(config, logger) {
|
|
16
|
+
return config.workspace.createWorkspace(config, logger ? { logger } : undefined);
|
|
12
17
|
}
|
|
13
18
|
/** A missing `priority` scores strictly below any real number (AC-4). */
|
|
14
19
|
const DEFAULT_PRIORITY = Number.NEGATIVE_INFINITY;
|
|
@@ -32,4 +32,34 @@ export interface GaiaWorkspace {
|
|
|
32
32
|
* anymore — the executor owns all hooks (GAIA-84).
|
|
33
33
|
*/
|
|
34
34
|
ensure(identifier: string, branch?: string, baseRef?: string): Promise<EnsuredWorkspace>;
|
|
35
|
+
/**
|
|
36
|
+
* Apply the workspace's "open" layout — for a hosted workspace, the startup
|
|
37
|
+
* command(s) of the freshly created workspace's initial pane.
|
|
38
|
+
*
|
|
39
|
+
* OPTIONAL: a workspace with no layout concept (git, fake) simply omits it and
|
|
40
|
+
* the conductor calls it with `?.`.
|
|
41
|
+
*
|
|
42
|
+
* GAIA-237 — why this is a SEPARATE call and not part of `ensure()`. The
|
|
43
|
+
* layout's startup command typically names the gaia CLI, which the
|
|
44
|
+
* `after_create` lifecycle hook builds. Applied inside `ensure()` it
|
|
45
|
+
* necessarily ran BEFORE that hook, so in a self-hosting checkout the command
|
|
46
|
+
* fired minutes before the binary it names existed. The only remedy available
|
|
47
|
+
* to a config author was a cross-process handshake — a sentinel wait loop
|
|
48
|
+
* inside the pane command itself. Splitting the call lets the conductor order
|
|
49
|
+
* the two correctly (`after_create` → `applyOpenLayout`), which removes the
|
|
50
|
+
* need for any handshake.
|
|
51
|
+
*
|
|
52
|
+
* The conductor calls this ONLY on a freshly created workspace
|
|
53
|
+
* ({@link EnsuredWorkspace.created}), because only a fresh create has an
|
|
54
|
+
* initial pane to lay out; re-applying on reuse would start a duplicate editor
|
|
55
|
+
* process on every reattach. Deliberately a NARROWER gate than the one on
|
|
56
|
+
* `after_create`, which is convergent and runs on every dispatch.
|
|
57
|
+
*
|
|
58
|
+
* Best-effort by contract: implementations log and swallow a failing layout so
|
|
59
|
+
* it can never wedge dispatch.
|
|
60
|
+
*/
|
|
61
|
+
applyOpenLayout?(path: string, ctx: {
|
|
62
|
+
identifier: string;
|
|
63
|
+
branch: string;
|
|
64
|
+
}): Promise<void>;
|
|
35
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/conductor",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"directory": "gaia-cli/conductor"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@gaia-ai/core": "^0.6.
|
|
31
|
+
"@gaia-ai/core": "^0.6.4",
|
|
32
32
|
"@dropsh/plugin-oauth2": "^0.5.7",
|
|
33
33
|
"@dropsh/plugin-jsonapi-schema": "^0.5.8",
|
|
34
34
|
"commander": "^12.1.0",
|