@clipboard-health/groundcrew 2.1.1 → 2.2.0

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 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAoEf;AA6CD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA6Ef;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -10,7 +10,7 @@ import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
10
10
  import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
11
11
  import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
12
12
  import { workspaces } from "../lib/workspaces.js";
13
- import { worktrees } from "../lib/worktrees.js";
13
+ import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
14
14
  async function fetchTicket(ticket) {
15
15
  const client = getLinearClient();
16
16
  const issue = await client.issue(ticket.toUpperCase());
@@ -78,9 +78,19 @@ export async function setupWorkspace(config, options, runOptions = {}) {
78
78
  assertLocalRunnerRequirements(await detectHostCapabilities(signal));
79
79
  await ensureClearance({ logger: log });
80
80
  const spec = { repository, ticket };
81
- const created = signal === undefined
82
- ? await worktrees.create(config, spec)
83
- : await worktrees.create(config, spec, signal);
81
+ let created;
82
+ try {
83
+ created =
84
+ signal === undefined
85
+ ? await worktrees.create(config, spec)
86
+ : await worktrees.create(config, spec, signal);
87
+ }
88
+ catch (error) {
89
+ if (isWorktreeAlreadyExistsError(error)) {
90
+ await logAccessHintForExistingWorkspace({ config, ticket, signal });
91
+ }
92
+ throw error;
93
+ }
84
94
  const { branchName, dir: launchDir } = created;
85
95
  const worktreeName = `${repository}-${ticket}`;
86
96
  // Anything that fails after the worktree is on disk must roll it back
@@ -120,12 +130,43 @@ export async function setupWorkspace(config, options, runOptions = {}) {
120
130
  log(`Workspace "${ticket}" launched (${model})`);
121
131
  log(` Worktree: ${launchDir}`);
122
132
  log(` Branch: ${branchName}`);
133
+ await logWorkspaceAccessHint({ config, ticket, signal });
123
134
  }
124
135
  catch (error) {
125
136
  await rollbackWorktree({ config, entry: created, promptDir });
126
137
  throw error;
127
138
  }
128
139
  }
140
+ /**
141
+ * Probe the workspace backend and, if a workspace for `ticket` is still
142
+ * live, log the access hint. Used on the pre-launch error path (e.g. the
143
+ * worktree already exists from a prior run) so the user can find the
144
+ * still-running session instead of being told only that the worktree is
145
+ * in the way. Silent when the probe is unavailable or the workspace is
146
+ * gone — we don't want to point at a window that doesn't exist.
147
+ */
148
+ async function logAccessHintForExistingWorkspace(arguments_) {
149
+ const { config, ticket, signal } = arguments_;
150
+ const accessHint = await workspaces.accessHint(config, ticket, signal);
151
+ if (accessHint === undefined) {
152
+ return;
153
+ }
154
+ const probe = await workspaces.probe(config, signal);
155
+ if (probe.kind !== "ok" || !probe.names.has(ticket)) {
156
+ return;
157
+ }
158
+ logAccessHint(accessHint);
159
+ }
160
+ async function logWorkspaceAccessHint(arguments_) {
161
+ const accessHint = await workspaces.accessHint(arguments_.config, arguments_.ticket, arguments_.signal);
162
+ if (accessHint === undefined) {
163
+ return;
164
+ }
165
+ logAccessHint(accessHint);
166
+ }
167
+ function logAccessHint(accessHint) {
168
+ log(` Attach: ${accessHint.command}`);
169
+ }
129
170
  async function rollbackWorktree(arguments_) {
130
171
  log(`Setup failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.ticket}...`);
131
172
  let result;
@@ -16,6 +16,10 @@ export interface WorkspaceStatus {
16
16
  color?: string;
17
17
  icon?: string;
18
18
  }
19
+ export interface WorkspaceAccessHint {
20
+ kind: "attachCommand";
21
+ command: string;
22
+ }
19
23
  export interface OpenSpec {
20
24
  /** Ticket id; becomes the workspace's name. */
21
25
  name: string;
@@ -53,6 +57,7 @@ export declare const workspaces: {
53
57
  open(config: ResolvedConfig, spec: OpenSpec, signal?: AbortSignal): Promise<void>;
54
58
  probe: typeof probeWorkspaces;
55
59
  close(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<void>;
60
+ accessHint(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceAccessHint | undefined>;
56
61
  };
57
62
  export {};
58
63
  //# sourceMappingURL=workspaces.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAoL7C,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAwND,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SAAS,cAAc,QAAQ,MAAM,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAIvF,CAAC"}
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA+L7C,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AA+ND,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SAAS,cAAc,QAAQ,MAAM,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhF,UAAU,SACN,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAI5C,CAAC"}
@@ -146,6 +146,12 @@ const cmuxAdapter = {
146
146
  throw error;
147
147
  }
148
148
  },
149
+ accessHint(_name) {
150
+ // cmux is a TUI; users surface workspaces by launching the cmux app,
151
+ // not a shell command. No useful hint to emit.
152
+ // oxlint-disable-next-line unicorn/no-useless-undefined -- explicit signal that the backend has no hint
153
+ return undefined;
154
+ },
149
155
  };
150
156
  export function resolveWorkspaceKind(arguments_) {
151
157
  const { config, host } = arguments_;
@@ -186,6 +192,9 @@ const TMUX_SESSION = "groundcrew";
186
192
  // sentinel and filter it out — it stays around as a placeholder so the
187
193
  // session doesn't collapse when the last ticket window closes.
188
194
  const TMUX_IDLE_WINDOW = "_groundcrew_idle";
195
+ function tmuxTarget(name) {
196
+ return `${TMUX_SESSION}:${name}`;
197
+ }
189
198
  function isTmuxNotFoundError(error) {
190
199
  // runCommand surfaces the child's stderr in error.message, so the "no
191
200
  // server" / "missing session" / "can't find window" signatures are visible
@@ -265,7 +274,7 @@ function parseTmuxWindows(output) {
265
274
  const tmuxAdapter = {
266
275
  async open(spec, signal) {
267
276
  await ensureTmuxSession(signal);
268
- const target = `${TMUX_SESSION}:${spec.name}`;
277
+ const target = tmuxTarget(spec.name);
269
278
  const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
270
279
  const keepDeadWindows = keepDeadWindowsEnv !== undefined && keepDeadWindowsEnv.length > 0;
271
280
  await runWorkspaceCommand("tmux", [
@@ -307,7 +316,7 @@ const tmuxAdapter = {
307
316
  },
308
317
  async close(name, signal) {
309
318
  try {
310
- await runWorkspaceCommand("tmux", ["kill-window", "-t", `${TMUX_SESSION}:${name}`], signal);
319
+ await runWorkspaceCommand("tmux", ["kill-window", "-t", tmuxTarget(name)], signal);
311
320
  }
312
321
  catch (error) {
313
322
  if (isSignalAborted(signal)) {
@@ -319,6 +328,9 @@ const tmuxAdapter = {
319
328
  throw error;
320
329
  }
321
330
  },
331
+ accessHint(name) {
332
+ return { kind: "attachCommand", command: `tmux attach -t ${tmuxTarget(name)}` };
333
+ },
322
334
  };
323
335
  // Per-config cache: production resolves the adapter once at first use
324
336
  // (loadConfig returns a frozen, cached instance); each test uses a fresh
@@ -364,4 +376,8 @@ export const workspaces = {
364
376
  const adapter = await adapterFor(config, signal);
365
377
  await adapter.close(name, signal);
366
378
  },
379
+ async accessHint(config, name, signal) {
380
+ const adapter = await adapterFor(config, signal);
381
+ return adapter.accessHint(name);
382
+ },
367
383
  };
@@ -10,6 +10,11 @@
10
10
  import type { ResolvedConfig } from "./config.ts";
11
11
  import { type WorkspaceProbe } from "./workspaces.ts";
12
12
  export type WorktreeKind = "host";
13
+ export declare class WorktreeAlreadyExistsError extends Error {
14
+ readonly dir: string;
15
+ constructor(dir: string);
16
+ }
17
+ export declare function isWorktreeAlreadyExistsError(error: unknown): error is WorktreeAlreadyExistsError;
13
18
  export interface WorktreeEntry {
14
19
  repository: string;
15
20
  /** Linear ticket id, lowercased — e.g. "team-220". */
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoSD,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAUxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;CAMrB,CAAC"}
1
+ {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoSD,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;CAMrB,CAAC"}
@@ -14,6 +14,17 @@ import { runCommandAsync } from "./commandRunner.js";
14
14
  import { errorMessage, log } from "./util.js";
15
15
  import { workspaces } from "./workspaces.js";
16
16
  const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
17
+ export class WorktreeAlreadyExistsError extends Error {
18
+ dir;
19
+ constructor(dir) {
20
+ super(`Worktree already exists: ${dir}`);
21
+ this.dir = dir;
22
+ this.name = "WorktreeAlreadyExistsError";
23
+ }
24
+ }
25
+ export function isWorktreeAlreadyExistsError(error) {
26
+ return error instanceof WorktreeAlreadyExistsError;
27
+ }
17
28
  const TICKET_RE = /^[a-z][\da-z]*-\d+$/;
18
29
  const TICKET_DIR_RE = /^(.+)-([a-z][\da-z]*-\d+)$/;
19
30
  function branchPrefix() {
@@ -239,11 +250,9 @@ function findByTicket(config, ticket) {
239
250
  return list(config).filter((entry) => entry.ticket === ticket);
240
251
  }
241
252
  async function create(config, spec, signal) {
242
- const existing = findByTicket(config, spec.ticket).filter((entry) => entry.repository === spec.repository);
243
- if (existing.length > 0) {
244
- const [first] = existing;
245
- /* v8 ignore next @preserve -- length>0 guarantees [0] is defined */
246
- throw new Error(`Worktree already exists: ${first?.dir}`);
253
+ const existing = findByTicket(config, spec.ticket).find((entry) => entry.repository === spec.repository);
254
+ if (existing !== undefined) {
255
+ throw new WorktreeAlreadyExistsError(existing.dir);
247
256
  }
248
257
  return await createWorktree(config, spec, signal);
249
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",