@clipboard-health/groundcrew 2.0.0 → 2.1.1

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/README.md CHANGED
@@ -49,18 +49,20 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
49
49
 
50
50
  `crew` resolves the config path as: `GROUNDCREW_CONFIG` if set → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` if it exists → a `config.ts` sitting next to `crew`'s own source files (only useful from a local checkout; see [Hacking on groundcrew](#hacking-on-groundcrew)). Set `GROUNDCREW_CONFIG` only when you want to override the XDG location.
51
51
 
52
- 4. **Provide a Linear API key.** `crew` expects `LINEAR_API_KEY` in its environment. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
52
+ 4. **Provide a Linear API key.** `crew` reads the key from `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. Prefer `GROUNDCREW_LINEAR_API_KEY` so the value does not clash with other tools that consume `LINEAR_API_KEY`. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
53
53
 
54
54
  ```bash
55
55
  # Direct
56
- export LINEAR_API_KEY="lin_api_..."
56
+ export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
57
57
  crew doctor
58
58
 
59
59
  # Via 1Password CLI (`op`), if you keep the key in a vault
60
- echo "LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
60
+ echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
61
61
  op run --env-file .env.1password -- crew doctor
62
62
  ```
63
63
 
64
+ `LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
65
+
64
66
  5. **Prepare the runner and agent auth.** Groundcrew supports one runner: a `cmux` or `tmux` workspace on macOS, with Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
65
67
 
66
68
  Setup fails before creating a worktree when the host is not macOS or `safehouse` is missing. `models.isolation`, per-model `isolation`, and per-model `sandbox` are legacy keys and now fail config validation.
@@ -185,7 +187,7 @@ For developers working on the package itself, clone this repo, run `npm install`
185
187
  cd ~/dev/c/groundcrew
186
188
  node --run crew -- doctor
187
189
 
188
- # With 1Password for LINEAR_API_KEY:
190
+ # With 1Password for GROUNDCREW_LINEAR_API_KEY:
189
191
  node --run crew:op -- run --watch
190
192
  ```
191
193
 
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
@@ -5,7 +5,7 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { loadConfig } from "../lib/config.js";
7
7
  import { detectHostCapabilities, which } from "../lib/host.js";
8
- import { errorMessage, readEnvironmentVariable, writeOutput } from "../lib/util.js";
8
+ import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
9
9
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
10
10
  // Tokenization stops after this many non-flag tokens. Two is enough to
11
11
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
@@ -23,14 +23,21 @@ async function checkCmd(cmd, required, hint) {
23
23
  }
24
24
  return result;
25
25
  }
26
- function checkEnvironment(name) {
27
- const value = readEnvironmentVariable(name);
28
- const set = value !== undefined && value.length > 0;
26
+ function checkLinearApiKey() {
27
+ const resolved = resolveLinearApiKey();
28
+ if (resolved !== undefined) {
29
+ return {
30
+ name: "linear api key",
31
+ ok: true,
32
+ required: true,
33
+ hint: `set via $${resolved.source}`,
34
+ };
35
+ }
29
36
  return {
30
- name: `$${name}`,
31
- ok: set,
37
+ name: "linear api key",
38
+ ok: false,
32
39
  required: true,
33
- hint: set ? "set" : "export the variable in your shell",
40
+ hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
34
41
  };
35
42
  }
36
43
  function checkDir(path, label) {
@@ -138,7 +145,7 @@ export async function doctor() {
138
145
  const workspaceOutcome = resolveWorkspaceOutcome(config, host);
139
146
  reportWorkspaceKind(config, workspaceOutcome);
140
147
  const checks = [
141
- checkEnvironment("LINEAR_API_KEY"),
148
+ checkLinearApiKey(),
142
149
  await checkCmd("git", true, "https://git-scm.com/"),
143
150
  ...(await workspaceChecks(workspaceOutcome)),
144
151
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
@@ -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"}
@@ -41,7 +41,7 @@ async function verifyProject(client, config) {
41
41
  const { projects } = response.data;
42
42
  const [project] = projects.nodes;
43
43
  if (!project) {
44
- throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that LINEAR_API_KEY can access this workspace.`);
44
+ throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that your Linear API key can access this workspace.`);
45
45
  }
46
46
  log(`Resolved Linear project: ${project.name} (slugId ${project.slugId})`);
47
47
  }
@@ -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));
@@ -8,6 +8,13 @@ export declare function log(message: string): void;
8
8
  type LogEventFieldValue = boolean | number | string | readonly string[] | undefined;
9
9
  export declare function logEvent(event: string, fields: Record<string, LogEventFieldValue>): void;
10
10
  export declare function readEnvironmentVariable(name: string): string | undefined;
11
+ declare const LINEAR_API_KEY_SOURCES: readonly ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
12
+ export type LinearApiKeySource = (typeof LINEAR_API_KEY_SOURCES)[number];
13
+ export interface ResolvedLinearApiKey {
14
+ value: string;
15
+ source: LinearApiKeySource;
16
+ }
17
+ export declare function resolveLinearApiKey(): ResolvedLinearApiKey | undefined;
11
18
  export declare function getLinearClient(): LinearClient;
12
19
  export declare function errorMessage(error: unknown): string;
13
20
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAM9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,QAAA,MAAM,sBAAsB,YAAI,2BAA2B,EAAE,gBAAgB,CAAU,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,GAAG,SAAS,CAQtE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
package/dist/lib/util.js CHANGED
@@ -87,12 +87,22 @@ export function readEnvironmentVariable(name) {
87
87
  // oxlint-disable-next-line node/no-process-env -- Centralized environment accessor.
88
88
  return process.env[name];
89
89
  }
90
+ const LINEAR_API_KEY_SOURCES = ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
91
+ export function resolveLinearApiKey() {
92
+ for (const source of LINEAR_API_KEY_SOURCES) {
93
+ const value = readEnvironmentVariable(source);
94
+ if (value !== undefined && value.length > 0) {
95
+ return { value, source };
96
+ }
97
+ }
98
+ return undefined;
99
+ }
90
100
  export function getLinearClient() {
91
- const apiKey = readEnvironmentVariable("LINEAR_API_KEY");
92
- if (apiKey === undefined || apiKey.length === 0) {
93
- throw new Error("LINEAR_API_KEY not set. Add it to your environment.");
101
+ const resolved = resolveLinearApiKey();
102
+ if (resolved === undefined) {
103
+ throw new Error("Linear API key not set. Set GROUNDCREW_LINEAR_API_KEY or LINEAR_API_KEY in your environment.");
94
104
  }
95
- return new LinearClient({ apiKey });
105
+ return new LinearClient({ apiKey: resolved.value });
96
106
  }
97
107
  export function errorMessage(error) {
98
108
  if (error instanceof Error) {
@@ -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,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"}
@@ -159,7 +159,28 @@ async function removeWorktree(config, entry, options) {
159
159
  removeArguments.push("--force");
160
160
  }
161
161
  removeArguments.push(entry.dir);
162
- await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
162
+ try {
163
+ await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
164
+ }
165
+ catch (error) {
166
+ // git's `fatal: ... use --force to delete it` line goes to inherited
167
+ // stderr, so the captured error is just "Exit status: 128". Probe the
168
+ // worktree ourselves so the failure message explains the condition
169
+ // (modified/untracked files) and points at `crew cleanup --force`.
170
+ if (options.force || options.signal?.aborted === true) {
171
+ throw error;
172
+ }
173
+ const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
174
+ if (dirtiness.kind !== "dirty") {
175
+ throw error;
176
+ }
177
+ throw new Error(describeDirtyWorktree({
178
+ ticket: entry.ticket,
179
+ dir: entry.dir,
180
+ modified: dirtiness.modified,
181
+ untracked: dirtiness.untracked,
182
+ }), { cause: error });
183
+ }
163
184
  }
164
185
  else {
165
186
  log(`Worktree directory ${entry.dir} not found, pruning stale refs...`);
@@ -172,6 +193,45 @@ async function removeWorktree(config, entry, options) {
172
193
  ...signalProperty(options.signal),
173
194
  });
174
195
  }
196
+ async function probeWorktreeDirtiness(worktreeDir, signal) {
197
+ let output;
198
+ try {
199
+ output = await runCommandAsync("git", ["-C", worktreeDir, "status", "--porcelain"], signalProperty(signal));
200
+ }
201
+ catch {
202
+ return { kind: "unknown" };
203
+ }
204
+ let modified = 0;
205
+ let untracked = 0;
206
+ for (const line of output.split("\n")) {
207
+ if (line.length === 0) {
208
+ continue;
209
+ }
210
+ if (line.startsWith("??")) {
211
+ untracked += 1;
212
+ }
213
+ else {
214
+ modified += 1;
215
+ }
216
+ }
217
+ if (modified === 0 && untracked === 0) {
218
+ return { kind: "clean" };
219
+ }
220
+ return { kind: "dirty", modified, untracked };
221
+ }
222
+ function describeDirtyWorktree(arguments_) {
223
+ const { ticket, dir, modified, untracked } = arguments_;
224
+ const parts = [];
225
+ if (modified > 0) {
226
+ parts.push(`${modified} modified file${modified === 1 ? "" : "s"}`);
227
+ }
228
+ if (untracked > 0) {
229
+ parts.push(`${untracked} untracked file${untracked === 1 ? "" : "s"}`);
230
+ }
231
+ const summary = parts.join(" and ");
232
+ const pronoun = modified + untracked === 1 ? "it" : "them";
233
+ return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
234
+ }
175
235
  function list(config) {
176
236
  return listWorktrees(config);
177
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
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",