@clipboard-health/groundcrew 2.1.0 → 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;
@@ -1 +1 @@
1
- {"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;AA+LD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmDzB"}
1
+ {"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;AAmMD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmDzB"}
@@ -163,7 +163,11 @@ function escapeRegex(value) {
163
163
  // must beat `api` when both are configured. `\b` treats `-` as a word
164
164
  // boundary, so without this ordering `api` would win on `api-admin`.
165
165
  function buildRepositoryRegex(config) {
166
- const alternation = config.workspace.knownRepositories
166
+ const candidates = config.workspace.knownRepositories.flatMap((repo) => {
167
+ const slashIndex = repo.indexOf("/");
168
+ return slashIndex === -1 ? [repo] : [repo, repo.slice(slashIndex + 1)];
169
+ });
170
+ const alternation = candidates
167
171
  .toSorted((a, b) => b.length - a.length)
168
172
  .map(escapeRegex)
169
173
  .join("|");
@@ -223,14 +227,29 @@ function parseRepository(arguments_) {
223
227
  repositories: config.workspace.knownRepositories,
224
228
  });
225
229
  }
226
- const repository = repositoryRegex.exec(description)?.[1];
227
- if (repository === undefined) {
230
+ const matched = repositoryRegex.exec(description)?.[1];
231
+ if (matched === undefined) {
228
232
  throw new RepositoryResolutionError({
229
233
  ticket,
230
234
  repositories: config.workspace.knownRepositories,
231
235
  });
232
236
  }
233
- return repository;
237
+ // Resolve the match to a known repo. The regex may capture a bare repo name
238
+ // (no org prefix) when only that appears in the description; the filter
239
+ // handles both full "owner/repo" and bare "repo" matches. Reject if
240
+ // ambiguous (same bare name under multiple orgs).
241
+ const candidates = config.workspace.knownRepositories.filter((r) => r === matched || r.endsWith(`/${matched}`));
242
+ if (candidates.length !== 1) {
243
+ throw new RepositoryResolutionError({
244
+ ticket,
245
+ repositories: config.workspace.knownRepositories,
246
+ });
247
+ }
248
+ /* v8 ignore next 3 @preserve -- candidates.length===1 guarantees [0] is defined */
249
+ if (candidates[0] === undefined) {
250
+ throw new Error("unreachable");
251
+ }
252
+ return candidates[0];
234
253
  }
235
254
  function parseAgentLabels(labels, config) {
236
255
  const agentLabels = labels.filter((label) => label.name.startsWith(AGENT_LABEL_PREFIX));
@@ -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;AAqND,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() {
@@ -159,7 +170,28 @@ async function removeWorktree(config, entry, options) {
159
170
  removeArguments.push("--force");
160
171
  }
161
172
  removeArguments.push(entry.dir);
162
- await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
173
+ try {
174
+ await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
175
+ }
176
+ catch (error) {
177
+ // git's `fatal: ... use --force to delete it` line goes to inherited
178
+ // stderr, so the captured error is just "Exit status: 128". Probe the
179
+ // worktree ourselves so the failure message explains the condition
180
+ // (modified/untracked files) and points at `crew cleanup --force`.
181
+ if (options.force || options.signal?.aborted === true) {
182
+ throw error;
183
+ }
184
+ const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
185
+ if (dirtiness.kind !== "dirty") {
186
+ throw error;
187
+ }
188
+ throw new Error(describeDirtyWorktree({
189
+ ticket: entry.ticket,
190
+ dir: entry.dir,
191
+ modified: dirtiness.modified,
192
+ untracked: dirtiness.untracked,
193
+ }), { cause: error });
194
+ }
163
195
  }
164
196
  else {
165
197
  log(`Worktree directory ${entry.dir} not found, pruning stale refs...`);
@@ -172,6 +204,45 @@ async function removeWorktree(config, entry, options) {
172
204
  ...signalProperty(options.signal),
173
205
  });
174
206
  }
207
+ async function probeWorktreeDirtiness(worktreeDir, signal) {
208
+ let output;
209
+ try {
210
+ output = await runCommandAsync("git", ["-C", worktreeDir, "status", "--porcelain"], signalProperty(signal));
211
+ }
212
+ catch {
213
+ return { kind: "unknown" };
214
+ }
215
+ let modified = 0;
216
+ let untracked = 0;
217
+ for (const line of output.split("\n")) {
218
+ if (line.length === 0) {
219
+ continue;
220
+ }
221
+ if (line.startsWith("??")) {
222
+ untracked += 1;
223
+ }
224
+ else {
225
+ modified += 1;
226
+ }
227
+ }
228
+ if (modified === 0 && untracked === 0) {
229
+ return { kind: "clean" };
230
+ }
231
+ return { kind: "dirty", modified, untracked };
232
+ }
233
+ function describeDirtyWorktree(arguments_) {
234
+ const { ticket, dir, modified, untracked } = arguments_;
235
+ const parts = [];
236
+ if (modified > 0) {
237
+ parts.push(`${modified} modified file${modified === 1 ? "" : "s"}`);
238
+ }
239
+ if (untracked > 0) {
240
+ parts.push(`${untracked} untracked file${untracked === 1 ? "" : "s"}`);
241
+ }
242
+ const summary = parts.join(" and ");
243
+ const pronoun = modified + untracked === 1 ? "it" : "them";
244
+ return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
245
+ }
175
246
  function list(config) {
176
247
  return listWorktrees(config);
177
248
  }
@@ -179,11 +250,9 @@ function findByTicket(config, ticket) {
179
250
  return list(config).filter((entry) => entry.ticket === ticket);
180
251
  }
181
252
  async function create(config, spec, signal) {
182
- const existing = findByTicket(config, spec.ticket).filter((entry) => entry.repository === spec.repository);
183
- if (existing.length > 0) {
184
- const [first] = existing;
185
- /* v8 ignore next @preserve -- length>0 guarantees [0] is defined */
186
- 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);
187
256
  }
188
257
  return await createWorktree(config, spec, signal);
189
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "2.1.0",
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",