@clipboard-health/groundcrew 4.0.0 → 4.0.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
@@ -17,24 +17,31 @@
17
17
  </p>
18
18
 
19
19
  ```text
20
- $ crew doctor --ticket HRD-446
21
- groundcrew doctor --ticket HRD-446 (Add retry logic to the sync job)
22
- ────────────────────────────────────────────────────────────────────
23
-
24
- Resolution
25
- [ok] Ticket exists in Linear ("Add retry logic to the sync job")
26
- [ok] Status is Todo
27
- [ok] Has agent-* label (agent-claude)
28
- [ok] Model resolves from agent-* label (model "claude")
29
- [ok] Description mentions known repo (owner/repo)
30
- [ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
31
-
32
- Eligibility
33
- [ok] No active blockers
34
- [ok] Model "claude" usage under sessionLimitPercentage (12% (limit 85%))
35
- [ok] In-progress cap not hit (2/4 used)
36
-
37
- → would be dispatched on next tick
20
+ $ crew status HRD-446
21
+ groundcrew status HRD-446
22
+ ========================
23
+ ticket: hrd-446
24
+
25
+ Config snapshot
26
+ ---------------
27
+ projectDir: /dev/workspaces
28
+ repositories: owner/repo
29
+ git: remote=origin; defaultBranch=main
30
+ workspaceKind: auto
31
+
32
+ Worktree state
33
+ --------------
34
+ - owner/repo host
35
+ branch: rocky-hrd-446
36
+ git: dirty (2 modified, 1 untracked)
37
+
38
+ Workspace probe
39
+ ---------------
40
+ live: yes
41
+
42
+ Last Linear status
43
+ ------------------
44
+ In Progress (state.type=started) — Add retry logic to the sync job
38
45
  ```
39
46
 
40
47
  ## Why
@@ -42,92 +49,106 @@ Eligibility
42
49
  - **Linear-native.** Polls issues assigned to the API key's viewer with `agent-*` labels, honors blockers.
43
50
  - **One worktree per ticket.** Agents work in parallel without stepping on each other.
44
51
  - **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 `crew.config.ts`.
46
-
47
- ## Install
48
-
49
- ```bash
50
- npm install -g @clipboard-health/groundcrew
51
- ```
52
-
53
- Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitively and provides the `clearance` / `clearance-ensure` bins used by Safehouse runs.
52
+ - **Multi-agent.** Ships with `claude` and `codex`; bring your own CLI via `crew.config.ts`.
54
53
 
55
54
  ## Quickstart
56
55
 
57
- 1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, …). Optional: `codexbar` for session-usage gating.
58
-
59
- 2. **Pick an isolation runner.** See [Runners](#runners) — `auto` resolves to `safehouse` on macOS and `sdx` on Linux/WSL.
60
-
61
- 3. **Prepare tickets in Linear.** Assign tickets to yourself and add an `agent-*` label. Groundcrew picks them up across all visible teams and projects.
62
-
63
- 4. **Configure.** Create a `crew.config.ts` you can edit:
56
+ ```bash
57
+ # 1. Install Node ≥ 24, git, cmux or tmux, and the agent CLIs you'll use (claude, codex, ...).
64
58
 
65
- ```bash
66
- # Write into the current folder:
67
- crew init && $EDITOR crew.config.ts
59
+ # 2. Install groundcrew
60
+ npm install -g @clipboard-health/groundcrew
68
61
 
69
- # ...or into the XDG config dir:
70
- crew init --global && $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts"
71
- ```
62
+ # 3. Scaffold a config and edit workspace.projectDir + workspace.knownRepositories
63
+ crew init && $EDITOR crew.config.ts
72
64
 
73
- `crew init` refuses to overwrite an existing config; pass `--force` to replace it, or `--dry-run` to preview the destination path.
65
+ # 4. Clone the repos referenced in your config
66
+ crew setup repos
74
67
 
75
- `crew` discovers the config via cosmiconfig project-walk, so dropping it at the root of any repo you run `crew` from works too. Any of `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, or `.config/crewrc{,.json}` are recognized.
68
+ # 5. Export your Linear API key
69
+ export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
76
70
 
77
- Set `workspace.projectDir` and `workspace.knownRepositories`. Defaults cover everything else. There is no `linear` config block — groundcrew picks up every Linear issue assigned to your API key's viewer that carries an `agent-*` label, across every project and team you can see, governed by a single `orchestrator.maximumInProgress` budget.
71
+ # 6. Verify setup, then dispatch
72
+ crew doctor
73
+ crew run --watch
74
+ ```
78
75
 
79
- Then clone each repo before the first `crew run` groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
76
+ In Linear, assign tickets to yourself and add an `agent-*` label (`agent-claude`, `agent-codex`, or `agent-any`). Groundcrew picks them up across every team and project your API key can see.
80
77
 
81
- ```bash
82
- crew setup repos # clone all missing entries via gh
83
- crew setup repos --dry-run # preview
84
- crew setup repos owner/repo # restrict to one entry
85
- ```
78
+ `crew init --global` writes the config into `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/` instead of the cwd. Both forms refuse to overwrite — pass `--force` to replace, `--dry-run` to preview.
86
79
 
87
- `crew setup repos` is idempotent; already-cloned repos report `[exists]`. Bare-name entries (no `owner/`) are skipped — clone them manually into `<projectDir>/<name>`.
80
+ ## Commands
88
81
 
89
- 5. **Export a Linear API key.** `crew` reads `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`.
82
+ ```bash
83
+ crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
84
+ crew doctor # check setup
85
+ crew status [<TICKET>] # inspect current state or one ticket
86
+ crew run # one-shot dispatch
87
+ crew run --watch # poll forever
88
+ crew run --ticket <TICKET> # dispatch one ticket
89
+ crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
90
+ crew interrupt <TICKET> [--reason <text>] # stop workspace, keep worktree
91
+ crew resume <TICKET> # reopen a paused ticket
92
+ crew cleanup <TICKET> # tear down every worktree for a ticket
93
+ ```
90
94
 
91
- ```bash
92
- export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
93
- ```
95
+ ## Configuration
94
96
 
95
- <details>
96
- <summary>Using 1Password (<code>op</code>) for the key</summary>
97
+ Two keys are required; everything else has a default.
97
98
 
98
- ```bash
99
- echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
100
- op run --env-file .env.1password -- crew doctor
101
- ```
99
+ | Key | What |
100
+ | ----------------------------- | ---------------------------------------------------------------------- |
101
+ | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
102
+ | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
102
103
 
103
- </details>
104
+ The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username` and isn't configurable. There is no `linear` config block — groundcrew picks up every issue assigned to your API key's viewer that carries an `agent-*` label across every visible team and project, governed by a single `orchestrator.maximumInProgress` budget.
104
105
 
105
- 6. **Run.**
106
+ <details>
107
+ <summary>Agent label routing</summary>
106
108
 
107
- ```bash
108
- crew doctor # check setup
109
- crew run --dry-run # preview without provisioning
110
- crew run --watch # poll forever
111
- ```
109
+ - `agent-claude`, `agent-codex`, `agent-<name>` → that model.
110
+ - `agent-any` the model with the most available session capacity.
111
+ - Unknown `agent-<name>` falls back to `models.default` with a warning.
112
+ - No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew run --ticket <TICKET>` (also falls back to `models.default`).
113
+ - Todo tickets blocked by non-terminal blockers are skipped until their blockers reach a terminal status.
112
114
 
113
- ## Secrets
115
+ Status classification uses Linear's workflow `state.type` (`unstarted`, `started`, `completed`, `canceled`, `duplicate`), so renamed status columns work without configuration. Parent issues with children are ignored — sub-issues are the work items.
114
116
 
115
- Groundcrew forwards a small allowlist of build-time secrets from your shell into the setup phase (so `npm install` can authenticate against private registries) and then strips them before the agent runs. The agent process never inherits these values in its environment.
117
+ </details>
116
118
 
117
- **Recognized names.** Defined in [`BUILD_SECRET_NAMES`](./src/lib/buildSecrets.ts):
119
+ <details>
120
+ <summary>Config discovery</summary>
118
121
 
119
- - `NPM_TOKEN`
120
- - `BUF_TOKEN`
122
+ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any of `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` (legacy `config.ts` accepted for one release). The "Loaded config from …" line at startup tells you which won.
121
123
 
122
- Set them in the shell you run `crew` from. Anything not in this list is ignored by the secret-shuttling path.
124
+ </details>
123
125
 
124
- **Flow.** For each ticket:
126
+ <details>
127
+ <summary>Full configuration reference</summary>
125
128
 
126
- 1. If any recognized var is set and non-empty, groundcrew writes `secrets.env` (mode `0600`) into the ticket's temp prompt dir as `KEY='value'` lines — see `stageBuildSecrets` in [`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts).
127
- 2. The launch script sources `secrets.env` with `set -a` so the values are exported into the setup phase only (and under `sdx`, forwarded into the sandbox via `-e KEY` flags).
128
- 3. After setup completes, the script `unset`s every name in `BUILD_SECRET_NAMES` and then `rm -rf`s the entire prompt dir (including `secrets.env`) before `exec`'ing the agent. See `sourceSecretsLine` / `unsetSecretsLine` and the `rm -rf` / `exec` lines in [`src/lib/launchCommand.ts`](./src/lib/launchCommand.ts). The rollback path on setup failure ([`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts)) wipes the prompt dir too.
129
+ | Key | Default | What it does |
130
+ | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
+ | `sources` | `[]` | Additional pluggable ticket sources. Extra sources are verified at startup; the built-in Linear adapter remains the dispatch read path until the canonical consumer refactor. Built-in kinds: `shell`, `linear`. |
132
+ | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
133
+ | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
134
+ | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
135
+ | `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. |
136
+ | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
137
+ | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
138
+ | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
139
+ | `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`. |
140
+ | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
141
+ | `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. |
142
+ | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
143
+ | `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. |
144
+ | `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). |
145
+ | `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. |
146
+ | `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. |
147
+ | `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. |
148
+ | `local.runner` | `"auto"` | Local isolation backend. `"auto"` → `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
149
+ | `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`. |
129
150
 
130
- Net effect: by the time the agent process exists, the values are gone from the environment and the file is gone from disk.
151
+ </details>
131
152
 
132
153
  ## Runners
133
154
 
@@ -139,8 +160,6 @@ Net effect: by the time the agent process exists, the values are gone from the e
139
160
  | `sdx` | Linux / WSL | [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) — required when the agent needs `docker`. |
140
161
  | `none` | — | Unsandboxed escape hatch. Never picked implicitly; doctor warns when configured. |
141
162
 
142
- 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.
143
-
144
163
  <details>
145
164
  <summary>Safehouse clearance allowlist</summary>
146
165
 
@@ -160,24 +179,152 @@ crew run --watch
160
179
 
161
180
  Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines and add only the domains your agents actually need.
162
181
 
182
+ `@clipboard-health/clearance` is pulled in transitively when you install groundcrew and provides the `clearance` / `clearance-ensure` bins used by Safehouse runs.
183
+
163
184
  </details>
164
185
 
165
- ## Configuration
186
+ <details>
187
+ <summary>Docker Sandboxes (sdx) setup</summary>
166
188
 
167
- Two keys are required; everything else has a default.
189
+ Each model that runs under `sdx` 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.
168
190
 
169
- | Key | What |
170
- | ----------------------------- | ---------------------------------------------------------------------- |
171
- | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
172
- | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
191
+ Groundcrew auto-creates sandboxes when missing but never deletes them — they persist across tickets and `crew cleanup`. Auth state lives inside the sandbox, so deleting it forces a re-login. Manage with `sbx ls` / `sbx rm`.
173
192
 
174
- There is **no** `linear` config block. Groundcrew's built-in Linear adapter picks up every Linear issue assigned to your API key's viewer that carries an `agent-*` label — across every project and team you can see. State classification is driven by Linear's workflow `state.type` (`unstarted` → todo, `started` → in progress, `completed`/`canceled`/`duplicate` → terminal), so renamed status columns Just Work without any per-team configuration.
193
+ </details>
175
194
 
176
- `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.
195
+ ## Inspecting status
177
196
 
178
- Agent selection uses Linear labels: `agent-claude`, `agent-codex`, `agent-<name>`. `crew run` without `--ticket` only fetches tickets carrying an `agent-*` label AND assigned to the API key's viewer — the GraphQL query filters server-side, so unlabeled or unassigned 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.
197
+ `crew status <TICKET>` prints a read-only snapshot for one ticket: resolved config, matching worktrees, workspace probe result, recorded run state, recent log lines for that ticket, and the latest Linear status. It does not fetch, recover, tear down, resume, or mutate any local/remote state.
179
198
 
180
- ### Pluggable ticket sources
199
+ `crew status` with no ticket prints the current inventory: known worktrees with workspace/run-state presence plus live workspaces reported by the configured backend.
200
+
201
+ Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKET>` to reopen preserved work. Status is intentionally informational only.
202
+
203
+ ## Doctor
204
+
205
+ `crew doctor` checks host prerequisites only: config validity, Linear reachability, required binaries on PATH, workspace backend availability, workspace.projectDir, local runner capability, and enabled model commands.
206
+
207
+ <details>
208
+ <summary>Sample ticket status output</summary>
209
+
210
+ ```text
211
+ groundcrew status HRD-442
212
+ =========================
213
+ ticket: hrd-442
214
+
215
+ Run state
216
+ ---------
217
+ running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
218
+
219
+ Worktree
220
+ --------
221
+ - herds-social host
222
+ branch: paul-hrd-442
223
+ dir: /Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442
224
+ git: dirty (0 modified, 1 untracked)
225
+
226
+ Workspace
227
+ ---------
228
+ live: yes
229
+
230
+ Last Linear status
231
+ ------------------
232
+ In Progress (state.type=started) — Multi-event extractor: year inference can produce date_start > date_end
233
+ ```
234
+
235
+ </details>
236
+
237
+ ### `crew interrupt <TICKET>`
238
+
239
+ Stops a live workspace pane while preserving the ticket worktree and branch. The manual pause button for cases where you need terminal capacity back, want to stop an agent that's going in the wrong direction, or need to inspect the diff before letting another agent continue.
240
+
241
+ ```bash
242
+ crew interrupt HRD-442 --reason "wrong implementation direction"
243
+ crew status HRD-442
244
+ crew resume HRD-442
245
+ ```
246
+
247
+ The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, interrupt records that fact so status can show the preserved branch.
248
+
249
+ ### `crew resume <TICKET>`
250
+
251
+ Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew run --ticket <ticket>`.
252
+
253
+ The resume prompt tells the agent to inspect git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
254
+
255
+ ## Secrets
256
+
257
+ Groundcrew forwards a small allowlist of build-time secrets from your shell into the setup phase (so `npm install` can authenticate against private registries) and strips them before the agent runs. The agent process never inherits these values.
258
+
259
+ Recognized names, defined in [`BUILD_SECRET_NAMES`](./src/lib/buildSecrets.ts):
260
+
261
+ - `NPM_TOKEN`
262
+ - `BUF_TOKEN`
263
+
264
+ Set them in the shell you run `crew` from. Anything not in this list is ignored.
265
+
266
+ <details>
267
+ <summary>How the secret shuttle works</summary>
268
+
269
+ For each ticket:
270
+
271
+ 1. If any recognized var is set and non-empty, groundcrew writes `secrets.env` (mode `0600`) into the ticket's temp prompt dir as `KEY='value'` lines — see `stageBuildSecrets` in [`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts).
272
+ 2. The launch script sources `secrets.env` with `set -a` so the values are exported into the setup phase only (and under `sdx`, forwarded into the sandbox via `-e KEY` flags).
273
+ 3. After setup completes, the script `unset`s every name in `BUILD_SECRET_NAMES` and then `rm -rf`s the entire prompt dir (including `secrets.env`) before `exec`'ing the agent. See `sourceSecretsLine` / `unsetSecretsLine` and the `rm -rf` / `exec` lines in [`src/lib/launchCommand.ts`](./src/lib/launchCommand.ts). The rollback path on setup failure ([`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts)) wipes the prompt dir too.
274
+
275
+ Net effect: by the time the agent process exists, the values are gone from the environment and the file is gone from disk.
276
+
277
+ </details>
278
+
279
+ ## Per-repo setup hook
280
+
281
+ If `.groundcrew/setup.sh` exists in the repo root, groundcrew runs `bash .groundcrew/setup.sh --deps-only` before each agent launch; otherwise nothing runs. Same convention applies inside the sdx sandbox (overridable per-model via `models.definitions.<name>.sandbox.setupCommand`). No implicit `npm install`, `uv sync`, or anything else — groundcrew is language-agnostic, so opt in by adding the script.
282
+
283
+ The `--deps-only` flag tells the script "you're being called by an automated system before an agent launches — skip anything interactive or one-time-only." The same script handles both modes; branch on `$1`:
284
+
285
+ - **With `--deps-only`**: do the cheap recurring work this worktree needs (lockfile install, generate types, etc.). No prompts, no global installs, no `nvm` / `pyenv` bootstrap.
286
+ - **Without the flag**: full interactive bootstrap. Use this when an engineer runs the script by hand for first-time onboarding, or when wiring it into another tool's SessionStart hook.
287
+
288
+ Setup failures are advisory — groundcrew logs the non-zero exit and still launches the agent so a flaky network or stale lockfile doesn't block the session.
289
+
290
+ <details>
291
+ <summary>Examples</summary>
292
+
293
+ **Python (uv):**
294
+
295
+ ```bash
296
+ #!/usr/bin/env bash
297
+ set -euo pipefail
298
+ if [ "${1:-}" = "--deps-only" ]; then
299
+ uv sync --dev
300
+ else
301
+ uv sync --dev
302
+ # ... extra one-time bootstrap (e.g., pre-commit install, db seed) ...
303
+ fi
304
+ ```
305
+
306
+ **Node (npm):**
307
+
308
+ ```bash
309
+ #!/usr/bin/env bash
310
+ set -euo pipefail
311
+ if [ "${1:-}" = "--deps-only" ]; then
312
+ npm clean-install
313
+ else
314
+ npm clean-install
315
+ # ... extra one-time bootstrap (e.g., husky install, codegen, link local packages) ...
316
+ fi
317
+ ```
318
+
319
+ **Docs-only or polyglot repo with no install step:** omit the script. With nothing at `.groundcrew/setup.sh`, groundcrew skips the hook silently.
320
+
321
+ For a comprehensive real-world example (nvm bootstrap, hash-based skip-on-no-changes caching, portable SHA-256 detection), see [this repo's own `.groundcrew/setup.sh`](./.groundcrew/setup.sh). It's also symlinked at `.claude/setup.sh` so the same script doubles as a Claude Code SessionStart hook for this repo — that symlink is local convenience, not part of groundcrew's contract.
322
+
323
+ To scaffold `.groundcrew/setup.sh` with a coding agent (Claude Code, Cursor, etc.), see [docs/setup-hook-agent-prompt.md](./docs/setup-hook-agent-prompt.md) — it encodes the contract above as a copy-pasteable prompt.
324
+
325
+ </details>
326
+
327
+ ## Pluggable ticket sources
181
328
 
182
329
  `sources` declares extra ticket-system adapters. The current release verifies configured extra sources during `crew run` startup; the dispatch loop still reads Linear directly through the built-in Linear adapter until the canonical consumer refactor lands. This lets you validate shell/Jira/local-plan integrations without changing existing Linear behavior.
183
330
 
@@ -224,7 +371,7 @@ export default {
224
371
 
225
372
  Allowed `status` values are `todo`, `in-progress`, `in-review`, `done`, and `other`. Use `null` for `repository` or `model` when a ticket should not be groundcrew-eligible. `hasMoreBlockers` is optional and defaults to `false`; `sourceRef` is opaque data that groundcrew passes back to your writeback command.
226
373
 
227
- ### Prompt customization
374
+ ## Prompt customization
228
375
 
229
376
  Groundcrew ships one model-agnostic unattended prompt by default. It tells the agent to make reasonable assumptions, follow repository instructions, run documented verification, review its diff, open a PR when GitHub/`gh` is available, and include a workspace continuation hint when known.
230
377
 
@@ -243,35 +390,7 @@ export default {
243
390
 
244
391
  This keeps package defaults portable while letting your private config reference team-specific statuses, tools, plugins, or review loops.
245
392
 
246
- <details>
247
- <summary>Full reference table</summary>
248
-
249
- | Key | Default | What it does |
250
- | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
251
- | `sources` | `[]` | Additional pluggable ticket sources. Extra sources are verified at startup; the built-in Linear adapter remains the dispatch read path until the canonical consumer refactor. Built-in kinds: `shell`, `linear`. |
252
- | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
253
- | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
254
- | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
255
- | `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. |
256
- | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
257
- | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
258
- | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
259
- | `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`. |
260
- | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
261
- | `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. |
262
- | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
263
- | `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. |
264
- | `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). |
265
- | `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. |
266
- | `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. |
267
- | `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. |
268
- | `local.runner` | `"auto"` | Local isolation backend. `"auto"` → `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
269
- | `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`. |
270
-
271
- </details>
272
-
273
- <details>
274
- <summary>Disabling a shipped default</summary>
393
+ ## Disabling a shipped default model
275
394
 
276
395
  Groundcrew ships `claude` and `codex` as default model definitions, additively merged into every resolved config. To stop probing one:
277
396
 
@@ -301,171 +420,18 @@ Rules:
301
420
  - It cannot be combined with `cmd`, `color`, or `usage` in the same entry.
302
421
  - `models.default` must point at an enabled model.
303
422
 
304
- </details>
305
-
306
- ## Per-repo setup hook
307
-
308
- When groundcrew launches a worktree, if `.groundcrew/setup.sh` exists in the repo root it's invoked as `bash .groundcrew/setup.sh --deps-only` before the agent starts; otherwise nothing runs. The same convention applies inside the sdx sandbox (overridable per-model via `models.definitions.<name>.sandbox.setupCommand`). No implicit `npm install`, `uv sync`, or anything else — groundcrew is language-agnostic, so opt in by adding the script.
309
-
310
- ### The `--deps-only` contract
311
-
312
- The flag tells the script "you're being called by an automated system before an agent launches — skip anything interactive or one-time-only." The same script handles both modes; branch on `$1`. The name is historical and Node-flavored, but the semantic is language-neutral:
313
-
314
- - **With `--deps-only`**: do the cheap recurring work this worktree needs (lockfile install, generate types, etc.). No prompts, no global installs, no `nvm` / `pyenv` bootstrap that the host should already have.
315
- - **Without the flag**: full interactive bootstrap. Use this path when an engineer runs the script by hand for first-time onboarding, or when wiring it into another tool's SessionStart hook.
316
-
317
- Setup failures are advisory — groundcrew logs the non-zero exit and still launches the agent so a flaky network or stale lockfile doesn't block the session.
318
-
319
- ### Examples
320
-
321
- **Python (uv):**
322
-
323
- ```bash
324
- #!/usr/bin/env bash
325
- set -euo pipefail
326
- if [ "${1:-}" = "--deps-only" ]; then
327
- uv sync --dev
328
- else
329
- uv sync --dev
330
- # ... extra one-time bootstrap (e.g., pre-commit install, db seed) ...
331
- fi
332
- ```
333
-
334
- **Node (npm):**
335
-
336
- ```bash
337
- #!/usr/bin/env bash
338
- set -euo pipefail
339
- if [ "${1:-}" = "--deps-only" ]; then
340
- npm clean-install
341
- else
342
- npm clean-install
343
- # ... extra one-time bootstrap (e.g., husky install, codegen, link local packages) ...
344
- fi
345
- ```
346
-
347
- **Docs-only or polyglot repo with no install step:**
348
-
349
- Omit the script. With nothing at `.groundcrew/setup.sh`, groundcrew skips the hook silently — fine for documentation repos, polyglot monorepos where setup happens per-package, or anywhere the per-worktree work is genuinely zero.
350
-
351
- For a more comprehensive real-world example (nvm bootstrap, hash-based skip-on-no-changes caching, portable SHA-256 detection), see [this repo's own `.groundcrew/setup.sh`](./.groundcrew/setup.sh). It's also symlinked at `.claude/setup.sh` so the same script doubles as a Claude Code SessionStart hook for this repo — that symlink is local convenience, not part of groundcrew's contract.
352
-
353
- ### Generating it with an agent
354
-
355
- To have a coding agent (Claude Code, Cursor, etc.) scaffold `.groundcrew/setup.sh` for a repo you're onboarding, see [docs/setup-hook-agent-prompt.md](./docs/setup-hook-agent-prompt.md) — it encodes the contract above as a copy-pasteable prompt.
356
-
357
- ## Commands
358
-
359
- ```bash
360
- crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
361
- crew doctor # full setup check
362
- crew doctor --ticket <TICKET> [--no-linear] [--no-fetch] # full ticket lifecycle (dispatch + recovery)
363
- crew run # one-shot dispatch
364
- crew run --watch # poll forever
365
- crew run --ticket <TICKET> # provision one ticket and exit
366
- crew setup repos [--dry-run] [<repo>...]
367
- crew interrupt <TICKET> [--reason <text>] # stop the live workspace, keep the worktree
368
- crew resume <TICKET> # reopen an existing ticket worktree
369
- crew cleanup <TICKET> # tear down every worktree carrying this ticket
370
- ```
371
-
372
- `crew doctor --ticket <TICKET>` covers the full per-ticket lifecycle: pre-dispatch eligibility (Todo status, `agent-*` label, model resolution, repository mention, local clone, blockers, model session usage, in-progress capacity) **and** post-dispatch local-state recovery (recorded run state, host worktree, workspace pane, local branch, remote branch, open PR). Verdict precedence starts with PR outcomes (`pr-open` > `pr-merged`). Recorded failed launches report before ordinary local recovery, interrupted runs report concrete recoverable git work first when it exists and otherwise report `interrupted`, and ordinary post-dispatch cases report `in-flight` before `recoverable`. If none of those apply, doctor falls through to `unresolvable` > `ineligible` > `would-dispatch` > `lost`. Exits 0 on `would-dispatch`, `pr-open`, or `pr-merged`; any other verdict exits 1. `--watch` and `--ticket` are mutually exclusive. To inspect codexbar session windows directly, run `codexbar usage`.
373
-
374
- ### `crew doctor --ticket <ticket>`
375
-
376
- Diagnose where a ticket is in its lifecycle and what to do next. Runs the same resolution and eligibility chain as the dispatcher, plus probes recorded run state, host worktree, workspace pane, local branch, remote branch, and PR; prints a single verdict with a copy-pasteable recovery step when one applies.
377
-
378
- Flags:
379
-
380
- - `--no-linear` — skip the Linear GraphQL call. Resolution and Eligibility sections are skipped; verdicts that need only local state (`in-flight`, `recoverable`, `pr-open`, `pr-merged`, `lost`) still fire.
381
- - `--no-fetch` — skip the upfront `git fetch origin <branch>` before checking remote presence.
382
-
383
- The Workspace section appends an attach hint to the pane name when the workspace backend exposes one (e.g. `tmux attach -t <session>:<pane>` or `cmux attach <name>`), so the verdict line is immediately actionable. The hero above shows a passing pre-dispatch run; here's the same command on a ticket that's already past dispatch:
384
-
385
- ```text
386
- groundcrew doctor --ticket HRD-442 (Multi-event extractor: year inference can produce date_start > date_end)
387
- ────────────────────────────────────────────────────────────────────────────────────────────────────────────
388
-
389
- Resolution
390
- [ok] Ticket exists in Linear ("Multi-event extractor: year inference can produce date_start > date_end")
391
- [ok] Status is Todo
392
- (skipped — post-dispatch — pre-dispatch checks are irrelevant)
393
-
394
- Eligibility
395
- (skipped — post-dispatch — pre-dispatch checks are irrelevant)
396
-
397
- Run state
398
- [ok] Local run state (running)
399
- [ok] Recorded model (claude)
400
- [ok] Recorded worktree (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
401
- [ok] Recorded branch (paul-hrd-442)
402
- [ok] Resume count (0)
403
-
404
- Worktree
405
- [ok] Host worktree exists (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
406
- [--] Working tree clean (0 modified, 1 untracked)
407
- [ok] Branch checked out (paul-hrd-442)
408
-
409
- Workspace
410
- [ok] Workspace pane open (hrd-442 — attach: `tmux attach -t groundcrew:hrd-442`)
411
-
412
- Local branch
413
- [ok] Local branch exists (paul-hrd-442, 2 ahead / 0 behind origin/main)
414
-
415
- Remote branch
416
- [ok] Branch present on origin
417
-
418
- Pull request
419
- [ok] Open PR for this branch (#224 https://github.com/herds-social/herds/pull/224)
420
-
421
- → pr-open: https://github.com/herds-social/herds/pull/224 (#224)
422
- ```
423
-
424
- #### Recovering a stranded ticket
425
-
426
- The verdict on the last line maps to a recovery action:
427
-
428
- | Verdict | What to do |
429
- | ---------------- | --------------------------------------------------------------------------------------------- |
430
- | `pr-open` | Nothing — the PR is the source of truth. |
431
- | `pr-merged` | Done. |
432
- | `in-flight` | The ticket is still being worked on; the verdict line names the workspace pane to attach to. |
433
- | `recoverable` | Run the printed `nextStep` exactly. |
434
- | `interrupted` | Resume the preserved worktree with `crew resume <ticket>` or inspect it by hand. |
435
- | `failed-launch` | Fix the launch failure, then run `crew resume <ticket>` or `crew cleanup <ticket>`. |
436
- | `would-dispatch` | Pre-dispatch checks pass; the orchestrator will pick the ticket up on its next tick. |
437
- | `ineligible` | A resolution or eligibility check failed; the reason after the colon names the failing check. |
438
- | `unresolvable` | The Linear ticket couldn't be fetched; the reason after the colon names the error. |
439
- | `lost` | No trace exists. Re-dispatch via `crew run --ticket <ticket>`. |
440
-
441
- ### `crew interrupt <ticket>`
423
+ ## Using 1Password for the API key
442
424
 
443
- Stop a live workspace pane while preserving the ticket worktree and branch. This is the manual pause button for cases where you need terminal capacity back, want to stop an agent that is going in the wrong direction, or need to inspect the diff before letting another agent continue.
425
+ `crew` reads `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. To resolve from 1Password:
444
426
 
445
427
  ```bash
446
- crew interrupt HRD-442 --reason "wrong implementation direction"
447
- crew doctor --ticket HRD-442
448
- crew resume HRD-442
428
+ echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
429
+ op run --env-file .env.1password -- crew doctor
449
430
  ```
450
431
 
451
- The command closes the cmux/tmux workspace when it exists, records local run state under the groundcrew state directory, and never tears down the worktree. If the workspace was already gone but the worktree is still present, interrupt records that fact so doctor can point at the preserved branch instead of reporting a mystery ticket.
452
-
453
- ### `crew resume <ticket>`
454
-
455
- Reopen an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists, it fails and leaves re-dispatch to `crew run --ticket <ticket>`.
456
-
457
- The resume prompt tells the agent to inspect current git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
458
-
459
432
  ## Troubleshooting
460
433
 
461
- First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
462
-
463
- <details>
464
- <summary>Local execution picks one of safehouse / sdx / none</summary>
465
-
466
- `local.runner: "auto"` resolves to `safehouse` on macOS and `sdx` (Docker Sandboxes) on Linux/WSL. Override with `local.runner: "safehouse" | "sdx" | "none"`. There is no per-model `isolation` knob — the runner is global. `sdx` requires a per-model `sandbox: { agent }` block so groundcrew can map the model to an sbx agent.
467
-
468
- </details>
434
+ First stop for "what exists locally right now": `crew status <ticket>` shows the ticket's worktrees, workspace presence, run state, logs, and latest Linear status. Use `crew doctor` when you need to verify host setup.
469
435
 
470
436
  <details>
471
437
  <summary>Safehouse-already-wrapped commands are not re-wrapped</summary>
@@ -474,13 +440,6 @@ If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew
474
440
 
475
441
  </details>
476
442
 
477
- <details>
478
- <summary>Sandbox lifecycle is create-only</summary>
479
-
480
- Groundcrew auto-creates the sandbox for an sbx agent (`groundcrew-<agent>`) when missing, but never deletes one — sandboxes persist across tickets and across `crew cleanup`. Auth state lives inside the sandbox, so deleting it forces a re-login. Inspect or remove them manually with `sbx ls` / `sbx rm`.
481
-
482
- </details>
483
-
484
443
  <details>
485
444
  <summary>Dead tmux windows vanish by default</summary>
486
445
 
@@ -488,20 +447,6 @@ When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm i
488
447
 
489
448
  </details>
490
449
 
491
- <details>
492
- <summary>Status names don't matter</summary>
493
-
494
- Groundcrew classifies tickets by Linear's workflow `state.type` (`unstarted`, `started`, `completed`, `canceled`, `duplicate`), not by status name. Teams that rename "Todo" to "To Do" or "Done" to "Shipped" need no configuration — the orchestrator still classifies correctly.
495
-
496
- </details>
497
-
498
- <details>
499
- <summary>Leaf-only</summary>
500
-
501
- Parent issues with children are ignored — sub-issues are the work items.
502
-
503
- </details>
504
-
505
450
  <details>
506
451
  <summary>Tickets stay in-progress until something else moves them</summary>
507
452
 
@@ -509,13 +454,6 @@ Groundcrew sets a ticket to `Started` (the first workflow state with `type === "
509
454
 
510
455
  </details>
511
456
 
512
- <details>
513
- <summary>Cross-team boards work out of the box</summary>
514
-
515
- Groundcrew picks up tickets across every team your API key's viewer can see. The "mark in progress" writeback looks up each ticket's own team workflow and uses that team's `started` state, so teams with different state names coexist without any per-team configuration.
516
-
517
- </details>
518
-
519
457
  <details>
520
458
  <summary>Claude launches in auto mode by default</summary>
521
459
 
@@ -533,7 +471,7 @@ Doctor reports the resolved local runner (safehouse / sdx / none) and whether it
533
471
  <details>
534
472
  <summary>Doctor checks every enabled model</summary>
535
473
 
536
- `models.definitions` includes both shipped defaults (`claude`, `codex`) by default via additive merge. If you only intend to label tickets `agent-claude` and don't have `codex` installed, set `models.definitions.codex: { disabled: true }` (see "Disabling a shipped default" above). Without that, doctor exits non-zero on a missing `codex` binary even though `crew run` would never route to it.
474
+ `models.definitions` includes both shipped defaults (`claude`, `codex`) by default via additive merge. If you only intend to label tickets `agent-claude` and don't have `codex` installed, set `models.definitions.codex: { disabled: true }`. Without that, doctor exits non-zero on a missing `codex` binary even though `crew run` would never route to it.
537
475
 
538
476
  </details>
539
477
 
@@ -572,7 +510,7 @@ node --run crew:op -- run --watch
572
510
 
573
511
  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).
574
512
 
575
- 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.
513
+ Logs land in `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log` by default (override via `logging.file`).
576
514
 
577
515
  Source edits in `src/**` are picked up on the next invocation. Requires Node ≥ 24.3 (native `.ts` type stripping).
578
516