@clipboard-health/groundcrew 3.1.0 → 3.1.2

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
@@ -42,7 +42,7 @@ Eligibility
42
42
  - **Linear-native.** Polls a project, respects `agent-*` labels, honors blockers.
43
43
  - **One worktree per ticket.** Agents work in parallel without stepping on each other.
44
44
  - **Local-first sandboxing.** Safehouse on macOS, Docker Sandboxes on Linux, or an explicit `none` escape hatch.
45
- - **Multi-agent.** Ships with `claude` and `codex`; bring your own CLI by dropping a definition into `config.ts`.
45
+ - **Multi-agent.** Ships with `claude` and `codex`; bring your own CLI by dropping a definition into `crew.config.ts`.
46
46
 
47
47
  ## Install
48
48
 
@@ -64,11 +64,13 @@ Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitiv
64
64
 
65
65
  ```bash
66
66
  mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew"
67
- cp "$(npm root -g)/@clipboard-health/groundcrew/configExample.ts" \
68
- "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts"
69
- $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts"
67
+ cp "$(npm root -g)/@clipboard-health/groundcrew/crew.config.example.ts" \
68
+ "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts"
69
+ $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts"
70
70
  ```
71
71
 
72
+ Or drop `crew.config.ts` at the root of any repo you run `crew` from — `crew` discovers it via cosmiconfig project-walk. Any of `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, or `.config/crewrc{,.json}` work.
73
+
72
74
  Set `linear.projectSlug` (paste the trailing slug of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Defaults cover everything else.
73
75
 
74
76
  Then clone each repo before the first `crew run` — groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
@@ -134,7 +136,7 @@ Net effect: by the time the agent process exists, the values are gone from the e
134
136
  | `sdx` | Linux / WSL | [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) — required when the agent needs `docker`. |
135
137
  | `none` | — | Unsandboxed escape hatch. Never picked implicitly; doctor warns when configured. |
136
138
 
137
- For `sdx`: each model that runs under it needs a `sandbox: { agent: "<sbx-agent>" }` block in `config.ts`. Groundcrew names sandboxes `groundcrew-<agent>` (e.g. `groundcrew-claude`) and reuses one sandbox per agent across repos and tickets. First-time agent auth happens inside the sandbox the first time it launches. To bootstrap manually instead, run `sbx create --name groundcrew-<agent> <agent> <projectDir>` once.
139
+ For `sdx`: each model that runs under it needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew names sandboxes `groundcrew-<agent>` (e.g. `groundcrew-claude`) and reuses one sandbox per agent across repos and tickets. First-time agent auth happens inside the sandbox the first time it launches. To bootstrap manually instead, run `sbx create --name groundcrew-<agent> <agent> <projectDir>` once.
138
140
 
139
141
  <details>
140
142
  <summary>Safehouse clearance allowlist</summary>
@@ -167,7 +169,7 @@ Three keys are required; everything else has a default.
167
169
  | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
168
170
  | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
169
171
 
170
- `crew` resolves config as: `GROUNDCREW_CONFIG` if set → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` if it exists → a `config.ts` next to `crew`'s own source (only useful from a local checkout). The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username` — not configurable.
172
+ `crew` resolves config as: `GROUNDCREW_CONFIG` if set → project-walk from cwd (cosmiconfig: `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, `.config/crewrc{,.json}`) → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts` (also accepts legacy `config.ts` for one release). The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username` — not configurable.
171
173
 
172
174
  Agent selection uses Linear labels: `agent-claude`, `agent-codex`, `agent-<name>`. `crew run` without `--ticket` only fetches tickets carrying an `agent-*` label — the GraphQL query filters server-side, so unlabeled tickets are never returned by Linear and do not appear on the board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (falls back to `models.default`). `agent-any` routes to the model with the most available session capacity. Todo tickets blocked by non-terminal blockers are skipped until their blockers reach a terminal status.
173
175
 
@@ -180,7 +182,7 @@ For a personal workflow, keep the prompt next to your local config and load it w
180
182
  ```ts
181
183
  import { readFileSync } from "node:fs";
182
184
 
183
- export const config = {
185
+ export default {
184
186
  // ...
185
187
  prompts: {
186
188
  initial: readFileSync(new URL("./initial-prompt.md", import.meta.url), "utf8"),
@@ -193,31 +195,31 @@ This keeps package defaults portable while letting your private config reference
193
195
  <details>
194
196
  <summary>Full reference table</summary>
195
197
 
196
- | Key | Default | What it does |
197
- | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
- | `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `config.ts` self-documenting and the lookup survives project renames. |
199
- | `linear.statuses.todo` | `"Todo"` | Status name picked up for new work. |
200
- | `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`. |
201
- | `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup. |
202
- | `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
203
- | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
204
- | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
205
- | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
206
- | `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. |
207
- | `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
208
- | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
209
- | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
210
- | `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
211
- | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
212
- | `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
213
- | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
214
- | `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Omit to never gate. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. |
215
- | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name), `template`, `kits`, `setupCommand` (override for the inside-sandbox setup script). |
216
- | `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. |
217
- | `prompts.initial` | unattended template | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override this from `config.ts` for team-specific statuses, tools, plugins, or review loops. |
218
- | `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. |
219
- | `local.runner` | `"auto"` | Local isolation backend. `"auto"` → `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
220
- | `logging.file` | XDG state path | Append-mode log file. `log()` / `logEvent()` tee here in addition to stdout. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
198
+ | Key | Default | What it does |
199
+ | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
+ | `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `crew.config.ts` self-documenting and the lookup survives project renames. |
201
+ | `linear.statuses.todo` | `"Todo"` | Status name picked up for new work. |
202
+ | `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`. |
203
+ | `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup. |
204
+ | `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
205
+ | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
206
+ | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
207
+ | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
208
+ | `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. |
209
+ | `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
210
+ | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
211
+ | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
212
+ | `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
213
+ | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
214
+ | `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
215
+ | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
216
+ | `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Falls back to default when unset, with gating enabled for known models. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. Set to `{ disabled: true }` to disable usage gating. |
217
+ | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name), `template`, `kits`, `setupCommand` (override for the inside-sandbox setup script). |
218
+ | `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. |
219
+ | `prompts.initial` | unattended template | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override this from `crew.config.ts` for team-specific statuses, tools, plugins, or review loops. |
220
+ | `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. |
221
+ | `local.runner` | `"auto"` | Local isolation backend. `"auto"` → `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
222
+ | `logging.file` | XDG state path | Append-mode log file. `log()` / `logEvent()` tee here in addition to stdout. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
221
223
 
222
224
  </details>
223
225
 
@@ -227,8 +229,8 @@ This keeps package defaults portable while letting your private config reference
227
229
  Groundcrew ships `claude` and `codex` as default model definitions, additively merged into every resolved config. To stop probing one:
228
230
 
229
231
  ```ts
230
- // config.ts
231
- export const config = {
232
+ // crew.config.ts
233
+ export default {
232
234
  // …
233
235
  models: {
234
236
  default: "claude",
@@ -491,7 +493,7 @@ node --run crew -- doctor
491
493
  node --run crew:op -- run --watch
492
494
  ```
493
495
 
494
- Both forms read `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` by default; set `GROUNDCREW_CONFIG` to point elsewhere. The `crew:op` wrapper additionally reads `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env` (1Password env-file with `op://` references resolved at launch).
496
+ Both forms discover config via cosmiconfig — project-walk from cwd for `crew.config.ts` and friends, then `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts` (legacy `config.ts` is still accepted for one release). Set `GROUNDCREW_CONFIG` to point elsewhere. The `crew:op` wrapper additionally reads `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env` (1Password env-file with `op://` references resolved at launch).
495
497
 
496
498
  Logs land in `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log` by default (override via `logging.file`). The "Loaded config from …" line at startup tells you which config won.
497
499
 
@@ -59,9 +59,10 @@ raw.githubusercontent.com
59
59
  release-assets.githubusercontent.com
60
60
  results-receiver.actions.githubusercontent.com
61
61
 
62
- # npm registry
62
+ # npm registry + package website
63
63
  api.npmjs.org
64
64
  registry.npmjs.org
65
+ www.npmjs.com
65
66
 
66
67
  # Developer tooling
67
68
  buf.build
@@ -1,7 +1,7 @@
1
1
  import type { Config } from "./src/lib/config.js";
2
2
  // import { readFileSync } from "node:fs";
3
3
 
4
- export const config: Config = {
4
+ export default {
5
5
  linear: {
6
6
  // Project URL slug to scope polling. Copy the trailing segment of
7
7
  // your Linear project URL —
@@ -9,7 +9,7 @@ export const config: Config = {
9
9
  // — verbatim, for example "ai-strategy-5152195762f3". The 12-char hex
10
10
  // tail is the canonical ID groundcrew uses, so the orchestrator stays
11
11
  // resilient across project renames and across same-name projects in
12
- // different teams. The leading name segment keeps `config.ts`
12
+ // different teams. The leading name segment keeps the file
13
13
  // self-documenting at a glance.
14
14
  projectSlug: "your-project-name-0123456789ab",
15
15
  // statuses: { todo: "Todo", inProgress: "In Progress", done: "Done", terminal: ["Done"] },
@@ -76,4 +76,4 @@ export const config: Config = {
76
76
  // // evidence with it. Default: `${XDG_STATE_HOME:-~/.local/state}/groundcrew/groundcrew.log`.
77
77
  // file: "~/Library/Logs/groundcrew/groundcrew.log",
78
78
  // },
79
- };
79
+ } satisfies Config;
@@ -78,8 +78,16 @@ export interface ModelDefinition {
78
78
  * mirrors the runtime contract: an entry is either a pure overlay
79
79
  * (every concrete field optional, no `disabled` key) or a pure
80
80
  * disable directive (`{ disabled: true }` and nothing else).
81
+ *
82
+ * `usage` accepts an extra `{ disabled: true }` sentinel that strips the
83
+ * usage block from the merged definition — the only way to opt a shipped
84
+ * default out of codexbar gating without disabling the model entirely.
81
85
  */
82
- type EnabledUserModelDefinition = Partial<ModelDefinition> & {
86
+ type UserUsage = ModelDefinition["usage"] | {
87
+ disabled: true;
88
+ };
89
+ type EnabledUserModelDefinition = Partial<Omit<ModelDefinition, "usage">> & {
90
+ usage?: UserUsage;
83
91
  disabled?: never;
84
92
  };
85
93
  interface DisabledUserModelDefinition {
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { cosmiconfig } from "cosmiconfig";
6
7
  import { log, readEnvironmentVariable, setLogFile } from "./util.js";
7
8
  export { BUILD_SECRET_NAMES } from "./buildSecrets.js";
8
9
  /**
@@ -68,7 +69,8 @@ const DEFAULT_PROMPT_INITIAL = [
68
69
  "3. Run the repository's documented verification command. If no documented verification exists, run the smallest relevant test suite you can find. Fix failures you introduced before continuing.",
69
70
  "4. Review your own diff before stopping. Look for bugs, regressions, missing tests, security issues, and convention violations, then fix any issues you find.",
70
71
  "5. If this repository uses GitHub and the `gh` CLI is available and authenticated, open a pull request. If you cannot open one, leave the branch ready and record the blocker.",
71
- "6. Include a short continuation note in the PR body when you know how to reattach to this workspace. For the tmux backend, use `tmux attach -t groundcrew:{{ticket}}`.",
72
+ "6. Include `Closes {{ticket}}` in the PR description.",
73
+ "7. Include a short continuation note in the PR body when you know how to reattach to this workspace. For the tmux backend, use `tmux attach -t groundcrew:{{ticket}}`.",
72
74
  "",
73
75
  "Stop after the branch is ready or the PR is open.",
74
76
  ].join("\n");
@@ -79,10 +81,6 @@ const ALLOWED_PROMPT_PLACEHOLDERS = new Set([
79
81
  "{{description}}",
80
82
  ]);
81
83
  const PROMPT_PLACEHOLDER_RE = /{{[^{}]*}}/g;
82
- // import.meta.dirname is `<package>/src/lib`; the user's `config.ts` lives
83
- // at the package root (gitignored), two levels up. Last-resort fallback
84
- // when neither GROUNDCREW_CONFIG nor the XDG path resolves to a file.
85
- const PACKAGE_CONFIG_PATH = resolve(import.meta.dirname, "..", "..", "config.ts");
86
84
  const PERCENT_MIN_EXCLUSIVE = 0;
87
85
  const PERCENT_MAX = 100;
88
86
  function xdgBase(envName, fallbackSegments) {
@@ -101,17 +99,6 @@ function xdgStatePath(...segments) {
101
99
  function defaultLogFile() {
102
100
  return xdgStatePath("groundcrew", "groundcrew.log");
103
101
  }
104
- function resolveConfigPath() {
105
- const override = readEnvironmentVariable("GROUNDCREW_CONFIG");
106
- if (override !== undefined && override.length > 0) {
107
- return resolve(override);
108
- }
109
- const xdgPath = xdgConfigPath("groundcrew", "config.ts");
110
- if (existsSync(xdgPath)) {
111
- return xdgPath;
112
- }
113
- return PACKAGE_CONFIG_PATH;
114
- }
115
102
  function expandHome(p) {
116
103
  if (p === "~") {
117
104
  return homedir();
@@ -274,6 +261,9 @@ export function isShippedDefaultDisabled(config, name) {
274
261
  return (Object.hasOwn(DEFAULT_MODEL_DEFINITIONS, name) &&
275
262
  !Object.hasOwn(config.models.definitions, name));
276
263
  }
264
+ function isUsageDisableSentinel(usage) {
265
+ return isPlainObject(usage) && "disabled" in usage && usage.disabled;
266
+ }
277
267
  function mergeDefinitions(user) {
278
268
  if (user !== undefined && !isPlainObject(user)) {
279
269
  fail("models.definitions must be an object");
@@ -306,7 +296,12 @@ function mergeDefinitions(user) {
306
296
  candidate.color = override.color;
307
297
  }
308
298
  if (override.usage !== undefined) {
309
- candidate.usage = override.usage;
299
+ if (isUsageDisableSentinel(override.usage)) {
300
+ delete candidate.usage;
301
+ }
302
+ else {
303
+ candidate.usage = override.usage;
304
+ }
310
305
  }
311
306
  if (override.sandbox !== undefined) {
312
307
  candidate.sandbox = normalizeSandbox(override.sandbox, `models.definitions.${name}.sandbox`);
@@ -463,22 +458,100 @@ function validate(config) {
463
458
  validatePromptPlaceholders(config.prompts.initial);
464
459
  requireString(config.logging.file, "logging.file");
465
460
  }
461
+ const COSMICONFIG_MODULE_NAME = "crew";
462
+ const SEARCH_PLACES = [
463
+ "crew.config.ts",
464
+ "crew.config.mjs",
465
+ "crew.config.js",
466
+ "crew.config.json",
467
+ ".crewrc",
468
+ ".crewrc.json",
469
+ ".crewrc.ts",
470
+ ".config/crew.config.ts",
471
+ ".config/crew.config.json",
472
+ ".config/crewrc",
473
+ ".config/crewrc.json",
474
+ ];
475
+ // `config.ts` is the legacy single-name convention from the bespoke loader;
476
+ // kept for one release so existing users don't have to rename.
477
+ const XDG_FALLBACK_NAMES = [
478
+ "crew.config.ts",
479
+ "crew.config.mjs",
480
+ "crew.config.js",
481
+ "crew.config.json",
482
+ "config.ts",
483
+ ];
484
+ // cosmiconfig's built-in `.ts` loader requires the `typescript` package;
485
+ // we already rely on Node 24's native TS-stripping for `bin/run.js`, so
486
+ // doing the same here keeps the dependency footprint tiny.
487
+ const loadExecutableModule = async (filepath) => {
488
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- runtime fields are validated by applyDefaults/validate below
489
+ const module_ = (await import(__rewriteRelativeImportExtension(pathToFileURL(filepath).href)));
490
+ if (module_.default !== undefined) {
491
+ return module_.default;
492
+ }
493
+ if (module_.config !== undefined) {
494
+ log(`Config at ${filepath} uses the legacy \`export const config\` shape. Switch to \`export default\` — the legacy form will be removed in the next major.`);
495
+ return module_.config;
496
+ }
497
+ return null;
498
+ };
499
+ // One explorer per process. `loadConfig` caches its resolved result via
500
+ // the `cached` singleton below, so cosmiconfig's internal cache state
501
+ // is harmless across the at-most-two calls (search + maybe load).
502
+ const explorer = cosmiconfig(COSMICONFIG_MODULE_NAME, {
503
+ searchPlaces: [...SEARCH_PLACES],
504
+ searchStrategy: "project",
505
+ loaders: {
506
+ ".ts": loadExecutableModule,
507
+ ".mjs": loadExecutableModule,
508
+ ".js": loadExecutableModule,
509
+ },
510
+ });
511
+ async function loadAt(filepath) {
512
+ const result = await explorer.load(filepath);
513
+ if (result === null) {
514
+ fail(`${filepath} must export a config object (e.g. \`export default { ... } satisfies Config\`)`);
515
+ }
516
+ return result;
517
+ }
518
+ function findXdgConfigFile() {
519
+ return XDG_FALLBACK_NAMES.map((name) => xdgConfigPath("groundcrew", name)).find((path) => existsSync(path));
520
+ }
521
+ async function discoverUserConfig() {
522
+ const override = readEnvironmentVariable("GROUNDCREW_CONFIG");
523
+ if (override !== undefined && override.length > 0) {
524
+ const overridePath = resolve(override);
525
+ if (!existsSync(overridePath)) {
526
+ fail(`GROUNDCREW_CONFIG=${overridePath} not found`);
527
+ }
528
+ return await loadAt(overridePath);
529
+ }
530
+ const project = await explorer.search(process.cwd());
531
+ if (project !== null && project.isEmpty !== true) {
532
+ return project;
533
+ }
534
+ const xdgPath = findXdgConfigFile();
535
+ if (xdgPath !== undefined) {
536
+ return await loadAt(xdgPath);
537
+ }
538
+ // Throw directly so oxlint's `consistent-return` rule sees a
539
+ // terminating statement; it doesn't track `fail()`'s `never` return.
540
+ throw new Error(`groundcrew config: no crew config found. Create crew.config.ts in your project root, or ${xdgConfigPath("groundcrew", "crew.config.ts")}, or set GROUNDCREW_CONFIG.`);
541
+ }
466
542
  let cached;
467
543
  export async function loadConfig() {
468
544
  if (cached) {
469
545
  return cached;
470
546
  }
471
- const path = resolveConfigPath();
472
- if (!existsSync(path)) {
473
- fail(`${path} not found. Copy configExample.ts to ${xdgConfigPath("groundcrew", "config.ts")} (or set GROUNDCREW_CONFIG to a different path) and edit it.`);
474
- }
475
- log(`Loaded config from ${path}`);
476
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- user config is TS-typed against Config; runtime fields are validated below
477
- const module_ = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
478
- const { config: userConfig } = module_;
479
- if (!userConfig) {
480
- fail(`${path} must export a named "config" object (e.g. \`export const config: Config = { ... }\`)`);
547
+ const result = await discoverUserConfig();
548
+ const { filepath, isEmpty } = result;
549
+ const userConfig = result.config;
550
+ if (isEmpty === true || !isPlainObject(userConfig)) {
551
+ fail(`${filepath} must export a config object (e.g. \`export default { ... } satisfies Config\`)`);
481
552
  }
553
+ log(`Loaded config from ${filepath}`);
554
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- runtime fields are validated by applyDefaults/validate
482
555
  const resolved = applyDefaults(userConfig);
483
556
  validate(resolved);
484
557
  setLogFile(resolved.logging.file);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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",
@@ -24,7 +24,7 @@
24
24
  "README.md",
25
25
  "bin",
26
26
  "clearance-allow-hosts",
27
- "configExample.ts",
27
+ "crew.config.example.ts",
28
28
  "dist",
29
29
  "static"
30
30
  ],
@@ -45,7 +45,7 @@
45
45
  "access": "public"
46
46
  },
47
47
  "scripts": {
48
- "architecture:check": "depcruise src bin configExample.ts vitest.config.ts",
48
+ "architecture:check": "depcruise src bin crew.config.example.ts vitest.config.ts",
49
49
  "build": "tsgo --build --force tsconfig.lib.json",
50
50
  "build:dev": "tsgo --build tsconfig.lib.json",
51
51
  "cpd": "jscpd .",
@@ -69,6 +69,7 @@
69
69
  "dependencies": {
70
70
  "@clipboard-health/clearance": "1.0.8",
71
71
  "@linear/sdk": "84.0.0",
72
+ "cosmiconfig": "9.0.1",
72
73
  "tslib": "2.8.1"
73
74
  },
74
75
  "devDependencies": {