@gaia-ai/addon-herdr 0.6.3 → 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.
@@ -1,4 +1,4 @@
1
- import type { ExecutorCapabilities, ExecutorPlugin, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput } from '@gaia-ai/conductor/contract';
1
+ import type { ExecutorCapabilities, ExecutorPlugin, GaiaExecutor, HookContext, HookName, HookResult, SpawnedSession, SpawnRunInput } from '@gaia-ai/conductor/contract';
2
2
  import type { ConductorLogger } from '@gaia-ai/core';
3
3
  import { type HerdrExecutorOptions } from './config.js';
4
4
  import { type HerdrExec } from './pane-layout.js';
@@ -26,8 +26,14 @@ export declare class HerdrExecutor implements GaiaExecutor {
26
26
  * silent no-op. A failing command → log loudly (hook + worktree + ticket +
27
27
  * err) and return. NEVER throws, so a hook failure can't abort dispatch or
28
28
  * wedge a run.
29
+ *
30
+ * GAIA-237: it also RETURNS what happened ({@link HookResult}). Not throwing
31
+ * kept a hook from wedging a run, but swallowing the outcome entirely left the
32
+ * caller unable to tell a provisioned worktree from a broken one — so an agent
33
+ * was dispatched into a worktree whose bootstrap had aborted, with nothing but
34
+ * a log line (which, on a TTY, went nowhere) to say so.
29
35
  */
30
- runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
36
+ runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<HookResult>;
31
37
  /**
32
38
  * List worktrees, ALWAYS anchored to the configured parent repo root
33
39
  * (GAIA-211). `herdr worktree list` is machine-global, but run WITHOUT
package/dist/src/index.js CHANGED
@@ -217,17 +217,25 @@ export class HerdrExecutor {
217
217
  * silent no-op. A failing command → log loudly (hook + worktree + ticket +
218
218
  * err) and return. NEVER throws, so a hook failure can't abort dispatch or
219
219
  * wedge a run.
220
+ *
221
+ * GAIA-237: it also RETURNS what happened ({@link HookResult}). Not throwing
222
+ * kept a hook from wedging a run, but swallowing the outcome entirely left the
223
+ * caller unable to tell a provisioned worktree from a broken one — so an agent
224
+ * was dispatched into a worktree whose bootstrap had aborted, with nothing but
225
+ * a log line (which, on a TTY, went nowhere) to say so.
220
226
  */
221
227
  async runHook(name, cwd, ctx, env) {
222
228
  const command = this.options.hooks?.[name];
223
229
  if (!command) {
224
- return;
230
+ return { hook: name, ran: false, ok: true };
225
231
  }
226
232
  try {
227
233
  await this.runShell(command, cwd, env);
234
+ return { hook: name, ran: true, ok: true };
228
235
  }
229
236
  catch (err) {
230
237
  this.logger.error({ hook: name, worktree: cwd, ticket: ctx.ticket, err: String(err) }, 'lifecycle hook failed');
238
+ return { hook: name, ran: true, ok: false, error: String(err) };
231
239
  }
232
240
  }
233
241
  /**
@@ -71,6 +71,11 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
71
71
  private readonly worktreeDir;
72
72
  private readonly open;
73
73
  private readonly logger;
74
+ /**
75
+ * Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
76
+ * awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
77
+ */
78
+ private readonly pendingRootPanes;
74
79
  /** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
75
80
  readonly agentHost: AgentLaunchHost;
76
81
  constructor(options: HerdrWorkspaceOptions);
@@ -92,12 +97,37 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
92
97
  */
93
98
  private verifyRemoteRef;
94
99
  /**
95
- * Ensure the branch's worktree + herdr workspace exist. `identifier` is the
96
- * ticket's human id: it is NOT used to locate the worktree (the branch is),
97
- * but it IS the value the open layout's `{identifier}` renders to (GAIA-234),
98
- * which is why it is taken as-passed rather than parsed back out of `branch`.
100
+ * Ensure the branch's worktree + herdr workspace exist.
101
+ *
102
+ * `_identifier` is unused HERE: the worktree is located by branch, and since
103
+ * GAIA-237 the open layout the one thing that consumed the identifier, to
104
+ * render `{identifier}` (GAIA-234) — is applied by {@link applyOpenLayout},
105
+ * which takes it on its own `ctx`. The parameter stays because it is the
106
+ * `GaiaWorkspace.ensure` contract position (the git workspace does use it).
107
+ * It is still taken as-passed rather than parsed back out of `branch`, which
108
+ * remains the reason the contract carries it at all.
109
+ */
110
+ ensure(_identifier: string, branch?: string, baseRefOverride?: string): Promise<EnsuredWorkspace>;
111
+ /**
112
+ * Apply the open layout to the root pane captured by the most recent
113
+ * {@link ensure} create for `path` — by default (GAIA-234) the ticket's
114
+ * `gaia ui` deep link, so the tab shows the ticket instead of a bare shell.
115
+ * The pane's cwd is the worktree checkout, passed explicitly on the splits
116
+ * (the root pane's cwd is already the checkout).
117
+ *
118
+ * NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
119
+ * took the reuse branch, there is no fresh root pane, and re-applying would
120
+ * start a duplicate editor process on reattach (GAIA-139). The capture is
121
+ * one-shot — consumed here — so a repeat call cannot double-apply and the map
122
+ * cannot grow without bound.
123
+ *
124
+ * Best-effort — mirrors the `fetchBaseRef` pattern: a broken layout is logged
125
+ * and swallowed so a startup-command failure can NEVER wedge dispatch.
99
126
  */
100
- ensure(identifier: string, branch?: string, baseRefOverride?: string): Promise<EnsuredWorkspace>;
127
+ applyOpenLayout(path: string, ctx: {
128
+ identifier: string;
129
+ branch: string;
130
+ }): Promise<void>;
101
131
  }
102
132
  export declare function herdrWorkspace(opts?: {
103
133
  /**
@@ -90,6 +90,11 @@ export class HerdrWorkspace {
90
90
  worktreeDir;
91
91
  open;
92
92
  logger;
93
+ /**
94
+ * Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
95
+ * awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
96
+ */
97
+ pendingRootPanes = new Map();
93
98
  /** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
94
99
  agentHost;
95
100
  constructor(options) {
@@ -168,12 +173,17 @@ export class HerdrWorkspace {
168
173
  }
169
174
  }
170
175
  /**
171
- * Ensure the branch's worktree + herdr workspace exist. `identifier` is the
172
- * ticket's human id: it is NOT used to locate the worktree (the branch is),
173
- * but it IS the value the open layout's `{identifier}` renders to (GAIA-234),
174
- * which is why it is taken as-passed rather than parsed back out of `branch`.
176
+ * Ensure the branch's worktree + herdr workspace exist.
177
+ *
178
+ * `_identifier` is unused HERE: the worktree is located by branch, and since
179
+ * GAIA-237 the open layout the one thing that consumed the identifier, to
180
+ * render `{identifier}` (GAIA-234) — is applied by {@link applyOpenLayout},
181
+ * which takes it on its own `ctx`. The parameter stays because it is the
182
+ * `GaiaWorkspace.ensure` contract position (the git workspace does use it).
183
+ * It is still taken as-passed rather than parsed back out of `branch`, which
184
+ * remains the reason the contract carries it at all.
175
185
  */
176
- async ensure(identifier, branch, baseRefOverride) {
186
+ async ensure(_identifier, branch, baseRefOverride) {
177
187
  if (branch === undefined) {
178
188
  throw new Error('herdr workspace requires a branch name');
179
189
  }
@@ -237,37 +247,65 @@ export class HerdrWorkspace {
237
247
  '--json',
238
248
  ]);
239
249
  const path = parseWorktreePath(createOutput, 'worktree create');
240
- // GAIA-139: on a FRESH create, apply the open layout to the initial root
241
- // pane by default (GAIA-234) the ticket's `gaia ui` deep link, so the tab
242
- // shows the ticket instead of a bare shell. The pane's cwd is the worktree
243
- // checkout, passed explicitly on the splits (the root pane's cwd is already
244
- // the checkout). Best-effort mirrors the `fetchBaseRef` pattern above: a
245
- // broken layout is logged and swallowed so a startup-command failure can
246
- // NEVER wedge worktree creation / run dispatch.
247
- try {
248
- const rootPaneId = parseWorktreeRootPane(createOutput);
249
- if (rootPaneId) {
250
- // Render the ROOT command here (spec D2). `applyPaneLayout` types its
251
- // root command verbatim for the executor's brace-bearing env-prefixed
252
- // agent command, so templating it there would corrupt that; the split
253
- // commands it already renders from the same `vars`.
254
- const vars = { identifier, branch, worktreePath: path };
255
- const layout = {
256
- ...this.open,
257
- ...(this.open.command !== undefined
258
- ? { command: renderTemplate(this.open.command, vars) }
259
- : {}),
260
- };
261
- await applyPaneLayout(this.execHerdr, rootPaneId, layout, path, vars);
262
- }
263
- }
264
- catch (err) {
265
- this.logger.warn({ branch, worktree: path, err: String(err) }, 'herdr open-layout failed (best-effort, ignored)');
250
+ // GAIA-237: CAPTURE the fresh root pane here, but do NOT lay it out yet.
251
+ // The layout's startup command names the gaia CLI, which the `after_create`
252
+ // hook builds and that hook runs after `ensure()` returns. Applying the
253
+ // layout here fired the command before its binary existed, which is why the
254
+ // pane command needed a sentinel wait loop. The conductor now calls
255
+ // `applyOpenLayout` after the hook; a create with no root pane in its output
256
+ // stores nothing, so the layout is a no-op exactly as before.
257
+ const rootPaneId = parseWorktreeRootPane(createOutput);
258
+ if (rootPaneId) {
259
+ this.pendingRootPanes.set(path, rootPaneId);
266
260
  }
267
261
  // The `after_create` hook is run by the executor (GAIA-84), not here — the
268
262
  // workspace only reports that it created a fresh worktree.
269
263
  return { path, instructions: loadInstructions(path), created: true };
270
264
  }
265
+ /**
266
+ * Apply the open layout to the root pane captured by the most recent
267
+ * {@link ensure} create for `path` — by default (GAIA-234) the ticket's
268
+ * `gaia ui` deep link, so the tab shows the ticket instead of a bare shell.
269
+ * The pane's cwd is the worktree checkout, passed explicitly on the splits
270
+ * (the root pane's cwd is already the checkout).
271
+ *
272
+ * NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
273
+ * took the reuse branch, there is no fresh root pane, and re-applying would
274
+ * start a duplicate editor process on reattach (GAIA-139). The capture is
275
+ * one-shot — consumed here — so a repeat call cannot double-apply and the map
276
+ * cannot grow without bound.
277
+ *
278
+ * Best-effort — mirrors the `fetchBaseRef` pattern: a broken layout is logged
279
+ * and swallowed so a startup-command failure can NEVER wedge dispatch.
280
+ */
281
+ async applyOpenLayout(path, ctx) {
282
+ const rootPaneId = this.pendingRootPanes.get(path);
283
+ if (!rootPaneId) {
284
+ return;
285
+ }
286
+ this.pendingRootPanes.delete(path);
287
+ try {
288
+ // Render the ROOT command here (spec D2). `applyPaneLayout` types its
289
+ // root command verbatim for the executor's brace-bearing env-prefixed
290
+ // agent command, so templating it there would corrupt that; the split
291
+ // commands it already renders from the same `vars`.
292
+ const vars = {
293
+ identifier: ctx.identifier,
294
+ branch: ctx.branch,
295
+ worktreePath: path,
296
+ };
297
+ const layout = {
298
+ ...this.open,
299
+ ...(this.open.command !== undefined
300
+ ? { command: renderTemplate(this.open.command, vars) }
301
+ : {}),
302
+ };
303
+ await applyPaneLayout(this.execHerdr, rootPaneId, layout, path, vars);
304
+ }
305
+ catch (err) {
306
+ this.logger.warn({ branch: ctx.branch, worktree: path, err: String(err) }, 'herdr open-layout failed (best-effort, ignored)');
307
+ }
308
+ }
271
309
  }
272
310
  export function herdrWorkspace(opts = {}) {
273
311
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-herdr",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "GAIA herdr integration: spawn executor + per-ticket worktree workspace.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "directory": "gaia-cli/addons/herdr"
21
21
  },
22
22
  "dependencies": {
23
- "@gaia-ai/addon-workspace-git": "^0.6.3"
23
+ "@gaia-ai/addon-workspace-git": "^0.6.4"
24
24
  },
25
25
  "peerDependencies": {
26
- "@gaia-ai/conductor": "^0.6.3",
27
- "@gaia-ai/core": "^0.6.3"
26
+ "@gaia-ai/conductor": "^0.6.4",
27
+ "@gaia-ai/core": "^0.6.4"
28
28
  }
29
29
  }