@gaia-ai/conductor 0.6.3 → 0.6.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.
@@ -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 (writes <checkoutRoot>/log.txt).
392
- Default: stdout on a TTY, file otherwise.
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
 
@@ -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
- // after_create only on a freshly-created worktree; before_run always. Both
384
- // get the resolved run env so a setup/pre hook sees the same vars (e.g. a DB
385
- // DSN) the agent does (GAIA-99).
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.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
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<void>;
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
@@ -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",
3
+ "version": "0.6.5",
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.3",
31
+ "@gaia-ai/core": "^0.6.5",
32
32
  "@dropsh/plugin-oauth2": "^0.5.7",
33
33
  "@dropsh/plugin-jsonapi-schema": "^0.5.8",
34
34
  "commander": "^12.1.0",