@clipboard-health/groundcrew 4.17.0 → 4.18.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.
package/README.md CHANGED
@@ -114,6 +114,9 @@ import type { Config } from "@clipboard-health/groundcrew";
114
114
  export default {
115
115
  workspace: {
116
116
  projectDir: "~/dev",
117
+ // Optional: all worktrees go here regardless of where each repo lives.
118
+ // worktreeDir: "~/dev/worktrees",
119
+ // Strings live under projectDir; use { name, projectDirOverride } to override per repo.
117
120
  knownRepositories: ["OWNER/REPO"],
118
121
  },
119
122
  models: {
@@ -131,6 +134,11 @@ export default {
131
134
  } satisfies Config;
132
135
  ```
133
136
 
137
+ Changing `workspace.worktreeDir` only affects worktrees discovered under the new
138
+ root. Clean up existing worktrees before switching it, or temporarily unset
139
+ `worktreeDir` when you need `crew cleanup` to find worktrees created beside the
140
+ repos.
141
+
134
142
  There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`.
135
143
 
136
144
  ## Reference
@@ -13,12 +13,17 @@ export default {
13
13
  // Opt a ticket in: assign it to yourself and add an `agent-<model>`
14
14
  // label (e.g. `agent-claude`, `agent-any`).
15
15
  workspace: {
16
- // Parent directory under which groundcrew clones repositories and
17
- // creates per-ticket worktrees.
16
+ // Parent directory under which groundcrew clones repositories and (by
17
+ // default) creates per-ticket worktrees.
18
18
  projectDir: "~/dev/groundcrew",
19
+ // Optional: collect ALL worktrees here instead of beside each repo. Useful
20
+ // when your repos live in more than one place. Defaults to projectDir.
21
+ // worktreeDir: "~/dev/worktrees",
19
22
  // Repositories groundcrew is allowed to set up worktrees in. Add
20
- // `<owner>/<repo>` or bare `<repo>` entries; the orchestrator scopes
21
- // tickets to these and refuses unknown repos by default.
23
+ // `<owner>/<repo>` or bare `<repo>` strings; the orchestrator scopes
24
+ // tickets to these and refuses unknown repos by default. Use the object
25
+ // form to point a repo at a different parent directory:
26
+ // { name: "other-org/other-repo", projectDirOverride: "~/work" }
22
27
  knownRepositories: ["your-org/your-repo"],
23
28
  },
24
29
  models: {
@@ -63,6 +68,9 @@ export default {
63
68
  // inReview: ["Code Review"],
64
69
  // },
65
70
  // },
71
+ // // Optional: disable the built-in Linear source entirely for shell-only
72
+ // // setups (no Linear API key needed). Replaces the block above.
73
+ // // { kind: "linear", enabled: false },
66
74
  // {
67
75
  // kind: "shell",
68
76
  // name: "jira",
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ const REMOVED_SANDBOX_COMMAND_MESSAGE = [
17
17
  const requireFromCli = createRequire(import.meta.url);
18
18
  const SETUP_REPOS_REMOVED_MESSAGE = [
19
19
  "crew setup repos was removed.",
20
- "Clone repositories manually with git clone into workspace.projectDir.",
20
+ "Clone repositories manually with git clone into workspace.projectDir (or each repo's workspace.knownRepositories `projectDirOverride`).",
21
21
  "See README.md#manual-repository-bootstrap for the replacement workflow.",
22
22
  ].join(" ");
23
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAgF/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAmF/C"}
@@ -5,7 +5,7 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { createBoard } from "../lib/board.js";
7
7
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
8
- import { loadConfigWithSource, } from "../lib/config.js";
8
+ import { loadConfigWithSource, worktreeBaseDir, } from "../lib/config.js";
9
9
  import { detectHostCapabilities, which } from "../lib/host.js";
10
10
  import { resolveLocalRunner } from "../lib/localRunner.js";
11
11
  import { gatedModels } from "../lib/usage.js";
@@ -184,6 +184,9 @@ export async function doctor() {
184
184
  await checkCmd("git", true, "https://git-scm.com/"),
185
185
  ...(await workspaceChecks(workspaceOutcome)),
186
186
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
187
+ ...(config.workspace.worktreeDir === undefined
188
+ ? []
189
+ : [checkDir(worktreeBaseDir(config), "workspace.worktreeDir")]),
187
190
  localCapability,
188
191
  ];
189
192
  const toolTargets = gatherToolTargets(config);
@@ -7,6 +7,7 @@
7
7
  import { z } from "zod";
8
8
  export declare const linearAdapterConfigSchema: z.ZodObject<{
9
9
  kind: z.ZodLiteral<"linear">;
10
+ enabled: z.ZodOptional<z.ZodBoolean>;
10
11
  name: z.ZodOptional<z.ZodString>;
11
12
  statuses: z.ZodOptional<z.ZodObject<{
12
13
  inProgress: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,yBAAyB;;;;;;;iBAYpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,yBAAyB;;;;;;;;iBAkBpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -8,6 +8,12 @@ import { z } from "zod";
8
8
  const statusNamesSchema = z.array(z.string().trim().min(1)).min(1);
9
9
  export const linearAdapterConfigSchema = z.object({
10
10
  kind: z.literal("linear"),
11
+ /**
12
+ * Opt-out sentinel. Set `enabled: false` to disable the built-in Linear
13
+ * source entirely (no adapter is constructed, no API key required) — see
14
+ * `sourcesFromConfig` in buildSources.ts. Omitted/`true` keeps Linear active.
15
+ */
16
+ enabled: z.boolean().optional(),
11
17
  name: z
12
18
  .string()
13
19
  .regex(/^[a-z][a-z0-9-]*$/, "name must be kebab-case (lowercase letters, digits, hyphens)")
@@ -22,9 +22,15 @@ export declare function buildSourcesWith(registry: Record<string, AdapterDefinit
22
22
  * implicit Linear source (Linear is always active under the post-#110
23
23
  * model — viewer + agent-* label filtering happens at the GraphQL layer)
24
24
  * and appends any user-declared `sources`. The implicit source is omitted
25
- * when the user already declared a Linear source (by `kind` or by runtime
26
- * name "linear") so they can override its `name` / construction without
27
- * spawning a duplicate adapter.
25
+ * when the user already declared a Linear source by `kind: "linear"`, or by
26
+ * a surviving (non-disabled) source whose runtime name is "linear" so they
27
+ * can override its `name` / construction without spawning a duplicate adapter.
28
+ *
29
+ * Users opt out of Linear entirely with the sentinel
30
+ * `{ kind: "linear", enabled: false }`: it still counts as an explicit Linear
31
+ * declaration (so the implicit source is suppressed) and is itself filtered
32
+ * out (so no Linear adapter is constructed and no API key is required). Any
33
+ * other source with `enabled: false` is likewise dropped from the result.
28
34
  */
29
35
  export declare function sourcesFromConfig(config: ResolvedConfig): readonly unknown[];
30
36
  //# sourceMappingURL=buildSources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,YAAY,EAAE,CAchB;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAM5E"}
1
+ {"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,YAAY,EAAE,CAchB;AA6DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAa5E"}
@@ -35,7 +35,40 @@ export function buildSourcesWith(registry, rawConfigs, context) {
35
35
  const sourceShape = z.looseObject({
36
36
  name: z.string().optional(),
37
37
  kind: z.string().optional(),
38
+ enabled: z.boolean().optional(),
38
39
  });
40
+ /**
41
+ * Read the structural `name` / `kind` / `enabled` fields off a raw source.
42
+ * `looseObject()` with all-optional fields only fails to parse non-object
43
+ * inputs (null, primitives); those are rejected downstream by the per-adapter
44
+ * Zod schema in buildSourcesWith, so we treat a non-object entry as an empty
45
+ * field set ("no opinion") rather than branching on it at every call site.
46
+ */
47
+ function sourceFields(raw) {
48
+ const parsed = sourceShape.safeParse(raw);
49
+ /* v8 ignore next 3 @preserve -- non-object inputs never reach here in practice (see above); the guard exists only for type-narrowing. */
50
+ if (!parsed.success) {
51
+ return {};
52
+ }
53
+ return parsed.data;
54
+ }
55
+ /**
56
+ * True when `raw` carries the opt-out sentinel `enabled: false`. Used to drop
57
+ * a source the user explicitly disabled — most importantly a
58
+ * `{ kind: "linear", enabled: false }` entry — so its adapter is never
59
+ * constructed.
60
+ */
61
+ function isSourceDisabled(raw) {
62
+ return sourceFields(raw).enabled === false;
63
+ }
64
+ /**
65
+ * True when `raw` declares `kind: "linear"`, regardless of `name` or `enabled`.
66
+ * This is what lets the `{ kind: "linear", enabled: false }` opt-out suppress
67
+ * the implicit Linear source even though the entry itself is filtered out.
68
+ */
69
+ function isLinearKindSource(raw) {
70
+ return sourceFields(raw).kind === "linear";
71
+ }
39
72
  /**
40
73
  * True when `raw` is an explicitly-declared Linear source. Matches either a
41
74
  * `kind: "linear"` entry — regardless of any `name` override — or any entry
@@ -50,26 +83,34 @@ const sourceShape = z.looseObject({
50
83
  * downstream.
51
84
  */
52
85
  function isExplicitLinearSource(raw) {
53
- const parsed = sourceShape.safeParse(raw);
54
- /* v8 ignore next 3 @preserve -- looseObject() with all-optional fields only fails to parse non-object inputs (null, primitives); the same input would be rejected by the per-adapter Zod schema in buildSourcesWith, so this guard never fires in practice. */
55
- if (!parsed.success) {
56
- return false;
57
- }
58
- return parsed.data.kind === "linear" || (parsed.data.name ?? parsed.data.kind) === "linear";
86
+ const { kind, name } = sourceFields(raw);
87
+ return kind === "linear" || (name ?? kind) === "linear";
59
88
  }
60
89
  /**
61
90
  * Build the runtime source list from a ResolvedConfig: synthesizes the
62
91
  * implicit Linear source (Linear is always active under the post-#110
63
92
  * model — viewer + agent-* label filtering happens at the GraphQL layer)
64
93
  * and appends any user-declared `sources`. The implicit source is omitted
65
- * when the user already declared a Linear source (by `kind` or by runtime
66
- * name "linear") so they can override its `name` / construction without
67
- * spawning a duplicate adapter.
94
+ * when the user already declared a Linear source by `kind: "linear"`, or by
95
+ * a surviving (non-disabled) source whose runtime name is "linear" so they
96
+ * can override its `name` / construction without spawning a duplicate adapter.
97
+ *
98
+ * Users opt out of Linear entirely with the sentinel
99
+ * `{ kind: "linear", enabled: false }`: it still counts as an explicit Linear
100
+ * declaration (so the implicit source is suppressed) and is itself filtered
101
+ * out (so no Linear adapter is constructed and no API key is required). Any
102
+ * other source with `enabled: false` is likewise dropped from the result.
68
103
  */
69
104
  export function sourcesFromConfig(config) {
70
- const hasExplicitLinear = config.sources.some(isExplicitLinearSource);
105
+ const kept = config.sources.filter((source) => !isSourceDisabled(source));
106
+ // A `kind: "linear"` entry suppresses the implicit source even when it is the
107
+ // disabled opt-out sentinel — it's removed from `kept` above, leaving Linear
108
+ // off entirely. A source that's Linear only by *name* (e.g. a shell source
109
+ // named "linear") suppresses the implicit source only while it survives the
110
+ // filter, so disabling such an entry doesn't silently drop Linear.
111
+ const hasExplicitLinear = config.sources.some(isLinearKindSource) || kept.some(isExplicitLinearSource);
71
112
  if (hasExplicitLinear) {
72
- return [...config.sources];
113
+ return kept;
73
114
  }
74
- return [{ kind: "linear" }, ...config.sources];
115
+ return [{ kind: "linear" }, ...kept];
75
116
  }
@@ -127,6 +127,15 @@ type UserModelDefinition = EnabledUserModelDefinition;
127
127
  * Linear's default "In Progress" / "In Review" status names disambiguate
128
128
  * `started` workflow states; unmatched statuses fall back to `state.type`.
129
129
  */
130
+ /**
131
+ * A configured repository. The bare-string form keeps the repo under
132
+ * `workspace.projectDir`; the object form's optional `projectDirOverride`
133
+ * overrides that parent directory so repos can live in more than one place.
134
+ */
135
+ export interface KnownRepository {
136
+ name: string;
137
+ projectDirOverride?: string;
138
+ }
130
139
  export interface Config {
131
140
  /**
132
141
  * Additional pluggable ticket sources beyond the built-in Linear adapter
@@ -135,6 +144,11 @@ export interface Config {
135
144
  * an external system (Jira, plan-keeper, etc.) by pointing at command
136
145
  * templates that emit/consume JSON.
137
146
  *
147
+ * The implicit Linear source can be turned off with the opt-out sentinel
148
+ * `{ kind: "linear", enabled: false }` — useful for shell-only setups with
149
+ * no Linear API key, where a failing Linear probe would otherwise take down
150
+ * the whole queue.
151
+ *
138
152
  * Per-source Zod validation runs at `buildSources` time — config.ts only
139
153
  * verifies the structural shape (array of objects with a string `kind`).
140
154
  */
@@ -151,7 +165,12 @@ export interface Config {
151
165
  };
152
166
  workspace: {
153
167
  projectDir: string;
154
- knownRepositories: string[];
168
+ /**
169
+ * Parent directory all per-ticket worktrees are created under. Defaults
170
+ * to `projectDir` when unset, so single-directory setups are unchanged.
171
+ */
172
+ worktreeDir?: string;
173
+ knownRepositories: (string | KnownRepository)[];
155
174
  };
156
175
  defaults?: {
157
176
  hooks?: HookCommands;
@@ -216,7 +235,12 @@ export interface ResolvedConfig {
216
235
  };
217
236
  workspace: {
218
237
  projectDir: string;
238
+ /** Resolved worktree root; unset means "use projectDir". */
239
+ worktreeDir?: string;
240
+ /** Repository names only — the union's `projectDirOverride`s are lifted out. */
219
241
  knownRepositories: string[];
242
+ /** name -> resolved parent dir, only for entries that override projectDir. */
243
+ repositoryDirs?: Record<string, string>;
220
244
  };
221
245
  defaults: {
222
246
  hooks: HookCommands;
@@ -250,6 +274,17 @@ export interface ResolvedConfig {
250
274
  file: string;
251
275
  };
252
276
  }
277
+ /**
278
+ * Parent directory under which a repository's clone lives. The per-repo
279
+ * `projectDirOverride` wins; otherwise repos sit under `projectDir`.
280
+ */
281
+ export declare function repositoryBaseDir(config: ResolvedConfig, repository: string): string;
282
+ /**
283
+ * Parent directory all worktrees are created under, independent of where the
284
+ * source repositories live. Falls back to `projectDir` when `worktreeDir` is
285
+ * unset.
286
+ */
287
+ export declare function worktreeBaseDir(config: ResolvedConfig): string;
253
288
  export type ConfigSourceKind = "env" | "project" | "xdg";
254
289
  export interface ConfigSource {
255
290
  kind: ConfigSourceKind;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAMrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAMrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,KAAK,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC;AAC/D,KAAK,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,GAAG;IAC1E,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AACF,KAAK,mBAAmB,GAAG,0BAA0B,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB;;;;WAIG;QACH,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,YAAY,CAAC;KACtB,CAAC;IACF,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;KACjC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;;;;;WAKG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;KACnD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;;OAIG;IACH,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,kBAAkB,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACR;;;;;WAKG;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,GAAG,EAAE;QACH,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,QAAQ,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;KACrB,CAAC;IACF,YAAY,EAAE;QACZ,iBAAiB,EAAE,MAAM,CAAC;QAC1B,wBAAwB,EAAE,MAAM,CAAC;QACjC,sBAAsB,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;KAC9C,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF;;;OAGG;IACH,aAAa,EAAE,oBAAoB,CAAC;IACpC;;;;OAIG;IACH,KAAK,EAAE;QACL,MAAM,EAAE,kBAAkB,CAAC;KAC5B,CAAC;IACF,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACjC,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;CAChC;AAsND;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG,OAAO,CAE1F;AA6FD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAKT;AA6bD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CA2B5E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAGpE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,21 @@ export const LOCAL_RUNNER_SETTINGS = [
27
27
  "sdx",
28
28
  "none",
29
29
  ];
30
+ /**
31
+ * Parent directory under which a repository's clone lives. The per-repo
32
+ * `projectDirOverride` wins; otherwise repos sit under `projectDir`.
33
+ */
34
+ export function repositoryBaseDir(config, repository) {
35
+ return config.workspace.repositoryDirs?.[repository] ?? config.workspace.projectDir;
36
+ }
37
+ /**
38
+ * Parent directory all worktrees are created under, independent of where the
39
+ * source repositories live. Falls back to `projectDir` when `worktreeDir` is
40
+ * unset.
41
+ */
42
+ export function worktreeBaseDir(config) {
43
+ return config.workspace.worktreeDir ?? config.workspace.projectDir;
44
+ }
30
45
  const DEFAULT_GIT = {
31
46
  remote: "origin",
32
47
  defaultBranch: "main",
@@ -428,6 +443,65 @@ function normalizeSources(raw) {
428
443
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- structural validation above guarantees array of {kind: string} entries; per-source Zod validation lives in buildSources
429
444
  return raw;
430
445
  }
446
+ /**
447
+ * Resolve one `knownRepositories` entry to its name and (optional) resolved
448
+ * base dir. Bare strings live under `projectDir`; the object form's
449
+ * `projectDirOverride` overrides that parent directory. This is the seam later
450
+ * per-repo options hang off — add new `KnownRepository` fields here.
451
+ */
452
+ function normalizeKnownRepository(entry, index) {
453
+ if (typeof entry === "string") {
454
+ return { name: entry };
455
+ }
456
+ requireObject(entry, `workspace.knownRepositories[${index}]`);
457
+ requireString(entry.name, `workspace.knownRepositories[${index}].name`);
458
+ if (entry.projectDirOverride === undefined) {
459
+ return { name: entry.name };
460
+ }
461
+ requireString(entry.projectDirOverride, `workspace.knownRepositories[${index}].projectDirOverride`);
462
+ return { name: entry.name, projectDirOverride: expandHome(entry.projectDirOverride) };
463
+ }
464
+ /**
465
+ * Flatten the loose `(string | KnownRepository)[]` union into the strict
466
+ * resolved shape: a `string[]` of names every downstream consumer reads, plus
467
+ * a separate `repositoryDirs` map holding only the entries that override
468
+ * `projectDir`. Types are validated here, at the resolution edge, before any
469
+ * `expandHome` runs (which would otherwise throw a raw TypeError on a
470
+ * non-string `worktreeDir`).
471
+ */
472
+ function normalizeWorkspace(workspace) {
473
+ requireObject(workspace, "workspace");
474
+ requireString(workspace.projectDir, "workspace.projectDir");
475
+ // Track the first index each name was seen at so a duplicate (which would
476
+ // silently overwrite its `projectDirOverride` in `repositoryDirs`) fails
477
+ // loudly instead of resolving order-dependently.
478
+ const seen = new Map();
479
+ const repositoryDirs = {};
480
+ const entries = Array.isArray(workspace.knownRepositories) ? workspace.knownRepositories : [];
481
+ entries.forEach((entry, index) => {
482
+ const { name, projectDirOverride } = normalizeKnownRepository(entry, index);
483
+ const previous = seen.get(name);
484
+ if (previous !== undefined) {
485
+ fail(`workspace.knownRepositories[${index}] duplicates ${JSON.stringify(name)} from workspace.knownRepositories[${previous}]. Configure distinct repository names.`);
486
+ }
487
+ seen.set(name, index);
488
+ if (projectDirOverride !== undefined) {
489
+ repositoryDirs[name] = projectDirOverride;
490
+ }
491
+ });
492
+ const names = [...seen.keys()];
493
+ let worktreeDir;
494
+ if (workspace.worktreeDir !== undefined) {
495
+ requireString(workspace.worktreeDir, "workspace.worktreeDir");
496
+ worktreeDir = expandHome(workspace.worktreeDir);
497
+ }
498
+ return {
499
+ projectDir: expandHome(workspace.projectDir),
500
+ ...(worktreeDir === undefined ? {} : { worktreeDir }),
501
+ knownRepositories: names,
502
+ ...(Object.keys(repositoryDirs).length === 0 ? {} : { repositoryDirs }),
503
+ };
504
+ }
431
505
  function applyDefaults(user) {
432
506
  // Guard the top-level shape before reading nested fields, so a
433
507
  // malformed runtime config produces a `groundcrew config: ...` error
@@ -458,10 +532,7 @@ function applyDefaults(user) {
458
532
  ...user.git,
459
533
  ...(branchPrefix === undefined ? {} : { branchPrefix }),
460
534
  },
461
- workspace: {
462
- projectDir: expandHome(user.workspace.projectDir),
463
- knownRepositories: user.workspace.knownRepositories,
464
- },
535
+ workspace: normalizeWorkspace(user.workspace),
465
536
  defaults: normalizeDefaults(user.defaults),
466
537
  orchestrator: { ...DEFAULT_ORCHESTRATOR, ...user.orchestrator },
467
538
  models: {
@@ -1,4 +1,4 @@
1
- import type { ModelDefinition, ResolvedConfig } from "./config.ts";
1
+ import { type ModelDefinition, type ResolvedConfig } from "./config.ts";
2
2
  export interface StagedSrtLaunch {
3
3
  /** Dedicated temp dir holding the settings files (and any relocated config home). */
4
4
  directory: string;
@@ -1 +1 @@
1
- {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKnE,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA8ClB"}
1
+ {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAqB,MAAM,aAAa,CAAC;AAK3F,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA8ClB"}
@@ -2,6 +2,7 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { collectAllowedDomains } from "./clearanceHosts.js";
5
+ import { repositoryBaseDir } from "./config.js";
5
6
  import { inferAgentCommandName } from "./launchCommand.js";
6
7
  import { agentConfigRelocation, buildSrtSettings } from "./srtPolicy.js";
7
8
  import { readEnvironmentVariable } from "./util.js";
@@ -27,7 +28,7 @@ import { readEnvironmentVariable } from "./util.js";
27
28
  export function buildAndStageSrtLaunch(input) {
28
29
  const agent = inferAgentCommandName(input.definition.cmd);
29
30
  const homeDir = input.homeDir ?? os.homedir();
30
- const repoDir = path.resolve(input.config.workspace.projectDir, input.repository);
31
+ const repoDir = path.resolve(repositoryBaseDir(input.config, input.repository), input.repository);
31
32
  const base = {
32
33
  worktreeDir: input.worktreeDir,
33
34
  gitCommonDir: path.join(repoDir, ".git"),
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Worktree lifecycle — manages host git worktrees for tickets.
3
3
  *
4
- * A worktree is a `git worktree add`'d sibling at
5
- * `<projectDir>/<repo>-<TICKET>/`. Callers go through the `worktrees`
6
- * namespace; the module owns creation, listing, removal, and teardown
7
- * (workspace-close + worktree-remove paired) so callers don't reach into
8
- * git directly.
4
+ * A worktree is a `git worktree add`'d directory at
5
+ * `<worktreeDir>/<repo>-<TICKET>/` (where `worktreeDir` defaults to
6
+ * `projectDir`). The source repo it is cut from may live under a different
7
+ * per-repo `projectDirOverride`. Callers go through the `worktrees` namespace;
8
+ * the module owns creation, listing, removal, and teardown (workspace-close +
9
+ * worktree-remove paired) so callers don't reach into git directly.
9
10
  */
10
- import type { ResolvedConfig } from "./config.ts";
11
+ import { type ResolvedConfig } from "./config.ts";
11
12
  import { type WorkspaceProbe } from "./workspaces.ts";
12
13
  export type WorktreeKind = "host";
13
14
  export declare class WorktreeAlreadyExistsError extends 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;AAGlD,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,sCAAsC;IACtC,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;AAkBD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AAmQD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,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;AAyBD,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,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
1
+ {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,EAAE,KAAK,cAAc,EAAsC,MAAM,aAAa,CAAC;AAGtF,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,sCAAsC;IACtC,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;AAkBD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AAgQD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,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;AAyBD,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,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Worktree lifecycle — manages host git worktrees for tickets.
3
3
  *
4
- * A worktree is a `git worktree add`'d sibling at
5
- * `<projectDir>/<repo>-<TICKET>/`. Callers go through the `worktrees`
6
- * namespace; the module owns creation, listing, removal, and teardown
7
- * (workspace-close + worktree-remove paired) so callers don't reach into
8
- * git directly.
4
+ * A worktree is a `git worktree add`'d directory at
5
+ * `<worktreeDir>/<repo>-<TICKET>/` (where `worktreeDir` defaults to
6
+ * `projectDir`). The source repo it is cut from may live under a different
7
+ * per-repo `projectDirOverride`. Callers go through the `worktrees` namespace;
8
+ * the module owns creation, listing, removal, and teardown (workspace-close +
9
+ * worktree-remove paired) so callers don't reach into git directly.
9
10
  */
10
11
  import { existsSync, readdirSync, rmSync } from "node:fs";
11
12
  import { userInfo } from "node:os";
12
13
  import path from "node:path";
13
14
  import { runCommandAsync } from "./commandRunner.js";
15
+ import { repositoryBaseDir, worktreeBaseDir } from "./config.js";
14
16
  import { resolveDefaultBranch } from "./defaultBranch.js";
15
17
  import { debug, errorMessage, isVerbose } from "./util.js";
16
18
  import { workspaces } from "./workspaces.js";
@@ -46,7 +48,7 @@ function repoDirFor(config, repository) {
46
48
  if (!config.workspace.knownRepositories.includes(repository)) {
47
49
  throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
48
50
  }
49
- const repoDir = path.resolve(config.workspace.projectDir, repository);
51
+ const repoDir = path.resolve(repositoryBaseDir(config, repository), repository);
50
52
  if (!existsSync(repoDir)) {
51
53
  throw new Error(`Repository not found: ${repoDir}`);
52
54
  }
@@ -59,12 +61,10 @@ function basePaths(config, repository, ticket) {
59
61
  if (!TICKET_RE.test(ticket)) {
60
62
  throw new Error(`Invalid ticket "${ticket}": must be a plain ticket id`);
61
63
  }
62
- const projectDir = path.resolve(config.workspace.projectDir);
63
64
  const repoDir = repoDirFor(config, repository);
64
65
  const hostWorktreeName = `${repository}-${ticket}`;
65
- const hostWorktreeDir = path.resolve(projectDir, hostWorktreeName);
66
+ const hostWorktreeDir = path.resolve(worktreeBaseDir(config), hostWorktreeName);
66
67
  return {
67
- projectDir,
68
68
  repoDir,
69
69
  ticket,
70
70
  branchName: branchNameForTicket(config, ticket),
@@ -126,17 +126,18 @@ async function createWorktree(config, spec, signal) {
126
126
  };
127
127
  }
128
128
  function listWorktrees(config) {
129
- const projectDir = path.resolve(config.workspace.projectDir);
129
+ const worktreeRoot = path.resolve(worktreeBaseDir(config));
130
130
  const entries = [];
131
- // Worktrees live at `projectDir/<repository>-<ticket>`. When `repository`
131
+ // Worktrees live at `worktreeRoot/<repository>-<ticket>`. When `repository`
132
132
  // contains a slash (e.g. "owner/repo"), `path.resolve()` nests one level
133
- // deeper, so the worktree path is `projectDir/owner/repo-<ticket>`.
134
- // Scan each known repository's parent directory rather than the project
135
- // root, so nested worktrees are discovered alongside bare ones.
133
+ // deeper, so the worktree path is `worktreeRoot/owner/repo-<ticket>`.
134
+ // Scan each known repository's parent directory under the worktree root
135
+ // rather than the root itself, so nested worktrees are discovered alongside
136
+ // bare ones.
136
137
  const reposByParent = new Map();
137
138
  for (const repository of config.workspace.knownRepositories) {
138
139
  const lastSlash = repository.lastIndexOf("/");
139
- const parentDir = lastSlash === -1 ? projectDir : path.resolve(projectDir, repository.slice(0, lastSlash));
140
+ const parentDir = lastSlash === -1 ? worktreeRoot : path.resolve(worktreeRoot, repository.slice(0, lastSlash));
140
141
  const basename = lastSlash === -1 ? repository : repository.slice(lastSlash + 1);
141
142
  let repoByBasename = reposByParent.get(parentDir);
142
143
  if (repoByBasename === undefined) {
@@ -182,8 +183,7 @@ function listWorktrees(config) {
182
183
  return entries;
183
184
  }
184
185
  async function removeWorktree(config, entry, options) {
185
- const projectDir = path.resolve(config.workspace.projectDir);
186
- const repoDir = path.resolve(projectDir, entry.repository);
186
+ const repoDir = path.resolve(repositoryBaseDir(config, entry.repository), entry.repository);
187
187
  if (existsSync(entry.dir)) {
188
188
  debug(`Removing worktree ${entry.dir}${options.force ? " (--force)" : ""}...`);
189
189
  const removeArguments = ["-C", repoDir, "worktree", "remove"];
@@ -317,7 +317,7 @@ function describeOrphanWorktree(arguments_) {
317
317
  return `directory exists but is not a registered git worktree. Run \`crew cleanup --force ${ticket}\` to remove ${dir}, or inspect it first if it may contain valuable files.`;
318
318
  }
319
319
  function expectedHostWorktreeDir(config, entry) {
320
- return path.resolve(config.workspace.projectDir, `${entry.repository}-${entry.ticket}`);
320
+ return path.resolve(worktreeBaseDir(config), `${entry.repository}-${entry.ticket}`);
321
321
  }
322
322
  function isInsideDirectory(parentDir, childDir) {
323
323
  const childRelativePath = path.relative(parentDir, childDir);
@@ -326,10 +326,10 @@ function isInsideDirectory(parentDir, childDir) {
326
326
  !path.isAbsolute(childRelativePath));
327
327
  }
328
328
  function removeOrphanWorktreeDirectory(config, entry) {
329
- const projectDir = path.resolve(config.workspace.projectDir);
329
+ const worktreeRoot = path.resolve(worktreeBaseDir(config));
330
330
  const expectedDir = expectedHostWorktreeDir(config, entry);
331
331
  const targetDir = path.resolve(entry.dir);
332
- if (targetDir !== expectedDir || !isInsideDirectory(projectDir, targetDir)) {
332
+ if (targetDir !== expectedDir || !isInsideDirectory(worktreeRoot, targetDir)) {
333
333
  throw new Error(`Refusing to force-delete ${entry.dir}: expected groundcrew worktree path ${expectedDir}.`);
334
334
  }
335
335
  debug(`Removing orphaned worktree directory ${entry.dir} (--force)...`);
@@ -4,7 +4,8 @@ Workspace settings and at least one enabled model are required; everything else
4
4
 
5
5
  | Key | What |
6
6
  | ----------------------------- | ---------------------------------------------------------------------- |
7
- | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
7
+ | `workspace.projectDir` | Parent dir for cloned repos and the default ticket worktree root. |
8
+ | `workspace.worktreeDir` | Optional parent dir for ticket worktrees. |
8
9
  | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
9
10
  | `models.definitions` | Enabled model set. Built-in presets can be enabled with `{}`. |
10
11
 
@@ -12,7 +13,10 @@ The branch prefix (`<prefix>-<TICKET>`) defaults to `os.userInfo().username`; ov
12
13
 
13
14
  ## Repository Layout
14
15
 
15
- Groundcrew never clones repositories for you. `crew init --repo OWNER/REPO` prints the clone command to run. If you are cloning manually, clone each `workspace.knownRepositories` entry into `workspace.projectDir` using the same relative path the config uses.
16
+ Groundcrew never clones repositories for you. `crew init --repo OWNER/REPO`
17
+ prints the clone command to run. If you are cloning manually, clone each string
18
+ `workspace.knownRepositories` entry into `workspace.projectDir` using the same
19
+ relative path the config uses.
16
20
 
17
21
  ```bash
18
22
  PROJECT_DIR="$HOME/dev"
@@ -20,7 +24,18 @@ mkdir -p "$PROJECT_DIR/OWNER"
20
24
  git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
21
25
  ```
22
26
 
23
- Bare-name entries have no owner, so pick the remote URL yourself and clone to `$PROJECT_DIR/<name>`.
27
+ Bare-name entries have no owner, so pick the remote URL yourself and clone to
28
+ `$PROJECT_DIR/<name>`. To keep a repo clone somewhere else, use
29
+ `{ name: "OWNER/REPO", projectDirOverride: "~/other" }` and clone it under that
30
+ parent dir.
31
+
32
+ By default, ticket worktrees are created beside the repos under
33
+ `workspace.projectDir`. Set `workspace.worktreeDir` to collect worktrees under a
34
+ separate root, regardless of where each source repo clone lives. Changing
35
+ `workspace.worktreeDir` only affects worktrees discovered under the new root.
36
+ Clean up existing worktrees before switching it, or temporarily unset
37
+ `worktreeDir` when you need `crew cleanup` to find worktrees created beside the
38
+ repos.
24
39
 
25
40
  ## Config Discovery
26
41
 
@@ -68,6 +83,23 @@ export default {
68
83
 
69
84
  Configured names replace the default for that status; omitted fields keep their defaults. Matching is case-insensitive and trims surrounding whitespace.
70
85
 
86
+ Linear is implicit-on, but you can turn it off entirely with the opt-out sentinel `{ kind: "linear", enabled: false }`. This suppresses the built-in Linear source so no adapter is constructed and no API key is required — useful for shell-only setups, where a failing Linear probe would otherwise mark the whole queue unavailable:
87
+
88
+ ```ts
89
+ export default {
90
+ sources: [
91
+ { kind: "linear", enabled: false },
92
+ {
93
+ kind: "shell",
94
+ name: "plans",
95
+ commands: {
96
+ fetch: "~/.config/groundcrew/plans-fetch.sh",
97
+ },
98
+ },
99
+ ],
100
+ };
101
+ ```
102
+
71
103
  ## Enabling Model Presets
72
104
 
73
105
  Groundcrew ships built-in presets for `claude` and `codex`, but models are not enabled by default. List the models you want in `models.definitions`:
@@ -148,12 +180,13 @@ and hook contract.
148
180
 
149
181
  | Key | Default | What it does |
150
182
  | ---------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
151
- | `sources` | `[]` | Additional pluggable ticket sources, dispatched alongside the built-in Linear adapter. Built-in kinds: `shell`, `linear`. Declare `{ kind: "linear", statuses: { ... } }` only to override Linear status names used for `in-progress` / `in-review` disambiguation. |
183
+ | `sources` | `[]` | Additional pluggable ticket sources, dispatched alongside the built-in Linear adapter. Built-in kinds: `shell`, `linear`. Declare `{ kind: "linear", statuses: { ... } }` only to override Linear status names used for `in-progress` / `in-review` disambiguation. Disable the implicit Linear source with `{ kind: "linear", enabled: false }` (no API key required) — useful for shell-only setups. |
152
184
  | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
153
185
  | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
154
186
  | `git.branchPrefix` | OS username | Prefix groundcrew puts before the ticket id when naming a worktree branch (`<branchPrefix>-<ticket>`). Must be a slash-free slug of letters, digits, `.`, `_`, or `-`. Defaults to the OS account username. Changing it only affects newly created worktrees; existing local branches keep their original names until cleaned up. Prefer a per-user config for personal prefixes — a committed `git.branchPrefix` gives every contributor the same branch prefix. |
155
- | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
156
- | `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
187
+ | `workspace.projectDir` | **required** | Parent dir for cloned repos and the default ticket worktree root. |
188
+ | `workspace.worktreeDir` | optional | Parent dir for ticket worktrees. When unset, worktrees are created under `workspace.projectDir`. Changing this only affects worktrees discovered under the new root; clean up existing worktrees before switching it, or temporarily unset it when you need `crew cleanup` to find old worktrees. |
189
+ | `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. Entries can be strings under `workspace.projectDir` or `{ name, projectDirOverride }` objects when a repo clone lives under a different parent dir. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
157
190
  | `defaults.hooks.prepareWorktree` | optional | Fallback repo-preparation command used only when the worktree does not define `.groundcrew/config.json` `hooks.prepareWorktree`. The hook runs after worktree creation and before the agent starts. Repo-local config wins. |
158
191
  | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
159
192
  | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.17.0",
3
+ "version": "4.18.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",