@clipboard-health/groundcrew 2.3.0 → 2.3.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clipboard Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,9 +1,48 @@
1
- <h1 align="center">groundcrew</h1>
2
1
  <p align="center">
3
- <img alt="Groundcrew logo." height="250px" src="./static/groundcrew.svg">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./static/groundcrew-wordmark-dark.svg">
4
+ <img alt="groundcrew" src="./static/groundcrew-wordmark-light.svg" height="96">
5
+ </picture>
4
6
  </p>
5
7
 
6
- Watch a Linear project and farm out ready tickets to coding-agent CLIs running in workspaces backed by git worktrees. Workspaces are [`cmux`](https://github.com/clayton-cole/cmux) panes or `tmux` windows.
8
+ <p align="center">
9
+ Dispatch your Linear backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/@clipboard-health/groundcrew"><img alt="npm" src="https://img.shields.io/npm/v/@clipboard-health/groundcrew?style=flat-square&label=npm&color=77d94e&labelColor=18181b"></a>
14
+ <a href="https://www.npmjs.com/package/@clipboard-health/groundcrew"><img alt="downloads" src="https://img.shields.io/npm/dw/@clipboard-health/groundcrew?style=flat-square&label=downloads&color=18181b&labelColor=18181b"></a>
15
+ <a href="https://github.com/ClipboardHealth/groundcrew/actions/workflows/ci.yml"><img alt="ci" src="https://img.shields.io/github/actions/workflow/status/ClipboardHealth/groundcrew/ci.yml?style=flat-square&label=ci&color=77d94e&labelColor=18181b"></a>
16
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@clipboard-health/groundcrew?style=flat-square&label=license&color=18181b&labelColor=18181b"></a>
17
+ </p>
18
+
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
38
+ ```
39
+
40
+ ## Why
41
+
42
+ - **Linear-native.** Polls a project, respects `agent-*` labels, honors blockers.
43
+ - **One worktree per ticket.** Agents work in parallel without stepping on each other.
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`.
7
46
 
8
47
  ## Install
9
48
 
@@ -11,20 +50,17 @@ Watch a Linear project and farm out ready tickets to coding-agent CLIs running i
11
50
  npm install -g @clipboard-health/groundcrew
12
51
  ```
13
52
 
14
- This installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitively and provides the `clearance` / `clearance-ensure` bins used by local Safehouse execution.
53
+ Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitively and provides the `clearance` / `clearance-ensure` bins used by Safehouse runs.
15
54
 
16
55
  ## Quickstart
17
56
 
18
- 1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...). Pick a local isolation backend below depending on platform and what the agent needs to do. Optional: `codexbar` for session-usage gating. The `workspaceKind` config key picks the workspace backend (`auto` resolves to cmux when installed, else tmux).
19
- - **`safehouse`** (macOS default) — [Safehouse](https://agent-safehouse.dev/) on `PATH`. The fastest local backend; cannot safely give the agent Docker.
20
- - **`sdx`** (Linux default, macOS opt-in) — [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) on `PATH`. Required when a ticket needs the agent to use Docker (`docker build`, `docker run`, integration tests). Each model that should run under sdx needs a `sandbox: { agent: "<sbx-agent>" }` block in `config.ts` so groundcrew knows which sbx agent to address.
21
- - **`none`** — explicit unsandboxed escape hatch. Never picked implicitly; `crew doctor` warns when it is configured.
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.
22
58
 
23
- Groundcrew resolves `local.runner` per platform: macOS `safehouse`, Linux/WSL `sdx`. Set `local.runner` to `"safehouse" | "sdx" | "none"` to override; leave at `"auto"` for the platform default.
59
+ 2. **Pick an isolation runner.** See [Runners](#runners) `auto` resolves to `safehouse` on macOS and `sdx` on Linux/WSL.
24
60
 
25
- 2. **Create a Linear project to scope your work.** Any team works — make a project inside it and drop tickets in. The orchestrator polls by project, not by team, so you don't need a dedicated team.
61
+ 3. **Create a Linear project to scope your work.** Any team works — make a project inside it and drop tickets in. The orchestrator polls by project, not by team.
26
62
 
27
- 3. **Create your config.** Copy the shipped example into the XDG config path and edit it:
63
+ 4. **Configure.** Copy the shipped example into XDG config and edit:
28
64
 
29
65
  ```bash
30
66
  mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew"
@@ -33,73 +69,91 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
33
69
  $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts"
34
70
  ```
35
71
 
36
- At minimum set `linear.projectSlug` (paste the trailing segment of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Everything else has a default.
72
+ 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.
37
73
 
38
- Create `workspace.projectDir` if it does not exist, and clone each repo in `workspace.knownRepositories` into `<projectDir>/<repo>` before the first `crew run`. Groundcrew creates per-ticket worktrees from these clones; it does not auto-clone. Use the literal `knownRepositories` string as the path under `projectDir` — `"owner/repo"` lives at `<projectDir>/owner/repo`, bare `"repo"` lives at `<projectDir>/repo`.
74
+ Then clone each repo before the first `crew run` groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
39
75
 
40
76
  ```bash
41
- mkdir -p ~/dev/groundcrew-workspaces
42
- gh repo clone owner/repo ~/dev/groundcrew-workspaces/owner/repo
43
- ```
44
-
45
- Or let `crew` clone every missing `owner/repo` entry for you using your `gh` login:
46
-
47
- ```bash
48
- crew setup repos # clone all missing entries
49
- crew setup repos --dry-run # preview what would be cloned
77
+ crew setup repos # clone all missing entries via gh
78
+ crew setup repos --dry-run # preview
50
79
  crew setup repos owner/repo # restrict to one entry
51
80
  ```
52
81
 
53
- `crew setup repos` is idempotent already-cloned repos are reported `[exists]` and untouched. Bare-name entries (no `owner/`) are skipped with an instruction to clone manually, since groundcrew can't safely guess the org. The command fails fast with an install hint when `gh` is not on `PATH`.
54
-
55
- `crew` resolves the config path as: `GROUNDCREW_CONFIG` if set → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` if it exists → a `config.ts` sitting next to `crew`'s own source files (only useful from a local checkout; see [Hacking on groundcrew](#hacking-on-groundcrew)). Set `GROUNDCREW_CONFIG` only when you want to override the XDG location.
82
+ `crew setup repos` is idempotent; already-cloned repos report `[exists]`. Bare-name entries (no `owner/`) are skipped clone them manually into `<projectDir>/<name>`.
56
83
 
57
- 4. **Provide a Linear API key.** `crew` reads the key from `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. Prefer `GROUNDCREW_LINEAR_API_KEY` so the value does not clash with other tools that consume `LINEAR_API_KEY`. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
84
+ 5. **Export a Linear API key.** `crew` reads `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`.
58
85
 
59
86
  ```bash
60
- # Direct
61
87
  export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
62
- crew doctor
88
+ ```
63
89
 
64
- # Via 1Password CLI (`op`), if you keep the key in a vault
90
+ <details>
91
+ <summary>Using 1Password (<code>op</code>) for the key</summary>
92
+
93
+ ```bash
65
94
  echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
66
95
  op run --env-file .env.1password -- crew doctor
67
96
  ```
68
97
 
69
- `LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
70
-
71
- 5. **Prepare the runner and agent auth.** Groundcrew uses a `cmux` or `tmux` workspace hosting the resolved local backend (`safehouse`, `sdx`, or `none`) plus locally authenticated agent CLIs. Setup fails fast when the resolved backend's binary or platform requirement is missing — `safehouse` requires macOS + `safehouse` on PATH; `sdx` requires macOS or Linux + `sbx` on PATH. `models.isolation` and per-model `isolation` are legacy keys and fail config validation. Per-model `sandbox` blocks are accepted again and used by the `sdx` runner.
98
+ </details>
72
99
 
73
- For sdx, sandboxes are created automatically on first launch. Groundcrew names them deterministically as `groundcrew-<repository>-<model>`, probes `sbx ls`, and runs `sbx create [--template ...] [--kit ...] <agent> <projectDir>` when missing — `projectDir` is the mount so every per-ticket worktree (created as a sibling of the bare clone) is visible inside the sandbox. First-time agent auth happens inside the sandbox the first time the agent runs via `sbx exec`. To bootstrap manually instead, run `sbx create --name groundcrew-<repo>-<model> <agent> <projectDir>` once.
74
-
75
- 6. **Set the clearance allowlist for local Safehouse runs.** Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` from `@clipboard-health/clearance` on `http://127.0.0.1:19999` (skipping the launch if something is already listening) and runs the agent through the bundled `safehouse-clearance` wrapper. Clearance refuses to start without an allowlist — see [its README](https://github.com/ClipboardHealth/core-utils/tree/main/packages/clearance) for the proxy's env vars, log paths, and DNS rules. The shortest path is to set the env before `crew run`:
100
+ 6. **Run.**
76
101
 
77
102
  ```bash
78
- CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
79
- crew run --watch
103
+ crew doctor # check setup
104
+ crew run --dry-run # preview without provisioning
105
+ crew run --watch # poll forever
80
106
  ```
81
107
 
82
- Groundcrew also ships a starter allowlist file covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`. Point clearance at it (and optionally a personal file) via `CLEARANCE_ALLOW_HOSTS_FILES`:
108
+ ## Runners
83
109
 
84
- ```bash
85
- CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts:$HOME/.config/clearance/personal-allow-hosts" \
86
- crew run --watch
87
- ```
110
+ `local.runner` picks the local isolation backend. `auto` resolves per platform.
88
111
 
89
- Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines and add only the domains your agents actually need.
112
+ | Runner | Default on | Backend |
113
+ | ----------- | ----------- | -------------------------------------------------------------------------------------------------------- |
114
+ | `safehouse` | macOS | [Safehouse](https://agent-safehouse.dev/) — fastest local; cannot safely give the agent Docker. |
115
+ | `sdx` | Linux / WSL | [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) — required when the agent needs `docker`. |
116
+ | `none` | — | Unsandboxed escape hatch. Never picked implicitly; doctor warns when configured. |
90
117
 
91
- 7. **Run.** Doctor first, then a dry run, then the real thing:
118
+ 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.
92
119
 
93
- ```bash
94
- crew doctor
95
- crew run --dry-run
96
- crew run # one-shot
97
- crew run --watch # poll forever
98
- ```
120
+ <details>
121
+ <summary>Safehouse clearance allowlist</summary>
99
122
 
100
- ## Config reference
123
+ Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Clearance refuses to start without an allowlist — see [its README](https://github.com/ClipboardHealth/core-utils/tree/main/packages/clearance) for proxy env vars, log paths, and DNS rules. Shortest path:
101
124
 
102
- Required fields are marked **required**; everything else has a default and can be omitted from `config.ts`.
125
+ ```bash
126
+ CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
127
+ crew run --watch
128
+ ```
129
+
130
+ Groundcrew ships a starter file covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`. Point clearance at it (and optionally a personal file) via `CLEARANCE_ALLOW_HOSTS_FILES`:
131
+
132
+ ```bash
133
+ CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts:$HOME/.config/clearance/personal-allow-hosts" \
134
+ crew run --watch
135
+ ```
136
+
137
+ Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines and add only the domains your agents actually need.
138
+
139
+ </details>
140
+
141
+ ## Configuration
142
+
143
+ Three keys are required; everything else has a default.
144
+
145
+ | Key | What |
146
+ | ----------------------------- | -------------------------------------------------------------------------- |
147
+ | `linear.projectSlug` | Trailing slug of the Linear project URL (e.g. `ai-strategy-5152195762f3`). |
148
+ | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
149
+ | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
150
+
151
+ `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.
152
+
153
+ 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.
154
+
155
+ <details>
156
+ <summary>Full reference table</summary>
103
157
 
104
158
  | Key | Default | What it does |
105
159
  | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -121,17 +175,18 @@ Required fields are marked **required**; everything else has a default and can b
121
175
  | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
122
176
  | `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 `auto` on macOS and `cli` elsewhere. |
123
177
  | `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). |
124
- | `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. See "Disabling a shipped default" below. |
178
+ | `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. |
125
179
  | `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
126
- | `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. tmux windows live in a dedicated `groundcrew` session. |
127
- | `local.runner` | `"auto"` | Local isolation backend. `"auto"` resolves to `safehouse` on macOS and `sdx` on Linux/WSL. Explicit values: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly — doctor warns when it is configured. |
128
- | `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
180
+ | `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. |
181
+ | `local.runner` | `"auto"` | Local isolation backend. `"auto"` `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
182
+ | `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
183
 
130
- The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
184
+ </details>
131
185
 
132
- ### Disabling a shipped default
186
+ <details>
187
+ <summary>Disabling a shipped default</summary>
133
188
 
134
- Groundcrew ships `claude` and `codex` as default model definitions, additively merged into every resolved config. If you only ever route work through one of them, set `disabled: true` on the other so doctor stops probing for the unused CLI:
189
+ Groundcrew ships `claude` and `codex` as default model definitions, additively merged into every resolved config. To stop probing one:
135
190
 
136
191
  ```ts
137
192
  // config.ts
@@ -150,45 +205,158 @@ Effects:
150
205
 
151
206
  - `crew doctor` does not probe the disabled model's CLI. `crew doctor || exit 1` becomes viable as a CI gate when you only have one agent installed.
152
207
  - `agent-any` only resolves to enabled models.
153
- - An `agent-<disabled>` label on a ticket (e.g. `agent-codex` after disabling codex) falls back to `models.default` with a warning in the log, so the ticket still runs and you can see the mismatch.
208
+ - An `agent-<disabled>` label on a ticket falls back to `models.default` with a warning in the log.
154
209
 
155
210
  Rules:
156
211
 
157
- - `disabled` only accepts shipped-default keys (`claude`, `codex`). A typo on the key fails loudly at config load instead of silently disabling nothing.
212
+ - `disabled` only accepts shipped-default keys (`claude`, `codex`). A typo fails loudly at config load.
158
213
  - `disabled` must be exactly the boolean `true`.
159
- - It cannot be combined with `cmd`, `color`, or `usage` in the same entry — disable a model or override its fields, not both.
214
+ - It cannot be combined with `cmd`, `color`, or `usage` in the same entry.
160
215
  - `models.default` must point at an enabled model.
161
216
 
162
- ## Manual commands
217
+ </details>
218
+
219
+ ## Commands
163
220
 
164
221
  ```bash
165
- crew run --ticket <TICKET>
222
+ crew doctor # full setup check
223
+ crew doctor --ticket <TICKET> # diagnose one ticket
224
+ crew run # one-shot dispatch
225
+ crew run --watch # poll forever
226
+ crew run --ticket <TICKET> # provision one ticket and exit
166
227
  crew setup repos [--dry-run] [<repo>...]
167
- crew cleanup <TICKET>
228
+ crew cleanup <TICKET> # tear down every worktree carrying this ticket
168
229
  ```
169
230
 
170
- `crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description and the model comes from the ticket's `agent-*` label (manual setup falls back to `models.default` for unlabeled tickets). If the description does not mention a repo from `workspace.knownRepositories`, setup fails before provisioning. `--watch` and `--ticket` are mutually exclusive — `--watch` drives the orchestrator loop; `--ticket` provisions one ticket and exits. `crew cleanup <TICKET>` resolves to every tracked worktree carrying that ticket id (across repos) and tears them all down. To inspect codexbar session windows directly, run `codexbar usage`; the orchestrator already gates on this internally via `orchestrator.sessionLimitPercentage`.
231
+ `--watch` and `--ticket` are mutually exclusive. To inspect codexbar session windows directly, run `codexbar usage`.
232
+
233
+ ### `crew doctor --ticket <ticket>`
234
+
235
+ Diagnose why a ticket would or wouldn't be dispatched on the next tick. Runs the same resolution and eligibility chain as the dispatcher. Exits 0 if the ticket would dispatch, 1 otherwise. The hero above shows a passing run; here's a failing one:
236
+
237
+ ```text
238
+ groundcrew doctor --ticket HRD-447 (Refactor auth middleware)
239
+ ─────────────────────────────────────────────────────────────
240
+
241
+ Resolution
242
+ [ok] Ticket exists in Linear ("Refactor auth middleware")
243
+ [--] Status is Todo (current: In Progress)
244
+ [ok] Has agent-* label (agent-claude)
245
+ [ok] Model resolves from agent-* label (model "claude")
246
+ [ok] Description mentions known repo (owner/repo)
247
+ [ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
248
+
249
+ Eligibility
250
+ (skipped — resolution checks failed)
251
+
252
+ → ineligible: status is In Progress (need Todo)
253
+ ```
171
254
 
172
- ## Gotchas
255
+ ## Troubleshooting
173
256
 
174
- - **Local execution picks one of safehouse/sdx/none.** `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 anymore — the runner is global. `sdx` requires a per-model `sandbox: { agent }` block so groundcrew can map the model to an sbx agent.
175
- - **Safehouse-already-wrapped commands are not re-wrapped.** If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew assumes that command owns its Safehouse flags and does not add the `safehouse-clearance` wrapper a second time. Changing the proxy's allowlist after it's running requires killing the PID in `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.pid` so the next launch picks up the new env.
176
- - **Sandbox lifecycle is create-only.** Groundcrew auto-creates the sandbox for a `<repository, model>` pair 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`.
177
- - **Dead tmux windows vanish by default.** When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` (any non-empty value works) in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. Close it manually with `tmux kill-window -t groundcrew:<ticket>` after diagnosis. tmux backend only.
178
- - **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
179
- - **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
180
- - **Tickets stay in the in-progress status until something else moves them.** Groundcrew sets a ticket to `inProgress` when it provisions a workspace and never advances it. The next transition (typically "in review" when a PR opens) is left to your team's Linear automation rules.
181
- - **Project must be on a single Linear team in practice.** Cross-team projects work — the orchestrator caches the in-progress state ID per team — but every team in the project must use the same status name for `linear.statuses.inProgress`.
182
- - **Claude launches in bypass-permissions mode by default.** Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode bypassPermissions` to avoid workspace-trust and tool-permission prompts blocking automation. Override `models.definitions.claude.cmd` if you want a stricter mode.
183
- - **Doctor's command introspection is shallow.** Doctor reports the resolved local runner (safehouse / sdx / none) and whether its prerequisites are met, then tokenizes model `cmd` and checks the first two non-flag tokens against PATH (so `safehouse claude --foo` checks both `safehouse` and `claude`). Boolean flags without values, env-var assignments (`FOO=1`), shell pipelines, and subshells are not parsed — verify those manually. In particular, `npx -y claude` and `env FOO=1 claude` only check the wrapper, not the wrapped CLI. When `local.runner` is `"none"`, doctor surfaces a single WARNING line so the unsandboxed launch is visible at a glance.
184
- - **Doctor checks every enabled model, including shipped defaults you didn't disable.** `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" under "Config reference"). Without that, doctor exits non-zero on a missing `codex` binary even though `crew run` would never route to it.
185
- - **Switch to tmux if cmux is misbehaving.** Set `workspaceKind: "tmux"` to force the tmux backend when cmux's CLI/socket bridge is flaky (symptoms: `cmux --json list-workspaces` returning `Failed to write to socket (Broken pipe)` or `Socket not found at ...cmux.sock` on every tick). tmux is more reliable — just a unix socket, no GUI app — at the cost of losing cmux's status pills, notifications, and vertical-tab sidebar.
186
- - **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
187
- - **`crew setup repos` only auto-clones `owner/repo` entries.** Bare-name entries in `workspace.knownRepositories` (e.g. `"api"` rather than `"clipboardhealth/api"`) are skipped with a hint to clone manually — the command refuses to guess the owner. After a partial setup, the exit code is non-zero so CI gates notice; rerun is idempotent once you clone the bare ones into `<projectDir>/<name>` yourself. Adding a new repo to `knownRepositories` later? Just rerun `crew setup repos`; already-present entries report `[exists]` and are untouched.
257
+ First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
188
258
 
189
- ## Hacking on groundcrew
259
+ <details>
260
+ <summary>Local execution picks one of safehouse / sdx / none</summary>
190
261
 
191
- For developers working on the package itself, clone this repo, run `npm install`, and the repo's `crew` / `crew:op` scripts execute groundcrew straight from TypeScript source no build step. Package dependencies, including `@clipboard-health/clearance`, resolve through normal npm package exports.
262
+ `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.
263
+
264
+ </details>
265
+
266
+ <details>
267
+ <summary>Safehouse-already-wrapped commands are not re-wrapped</summary>
268
+
269
+ If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew assumes that command owns its Safehouse flags and does not add the `safehouse-clearance` wrapper a second time. Changing the proxy's allowlist after it's running requires killing the PID in `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.pid` so the next launch picks up the new env.
270
+
271
+ </details>
272
+
273
+ <details>
274
+ <summary>Sandbox lifecycle is create-only</summary>
275
+
276
+ 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`.
277
+
278
+ </details>
279
+
280
+ <details>
281
+ <summary>Dead tmux windows vanish by default</summary>
282
+
283
+ When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. Close it manually with `tmux kill-window -t groundcrew:<ticket>` after diagnosis. tmux backend only.
284
+
285
+ </details>
286
+
287
+ <details>
288
+ <summary>Status names matter</summary>
289
+
290
+ If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
291
+
292
+ </details>
293
+
294
+ <details>
295
+ <summary>Leaf-only</summary>
296
+
297
+ Parent issues with children are ignored — sub-issues are the work items.
298
+
299
+ </details>
300
+
301
+ <details>
302
+ <summary>Tickets stay in-progress until something else moves them</summary>
303
+
304
+ Groundcrew sets a ticket to `inProgress` when it provisions a workspace and never advances it. The next transition (typically "in review" when a PR opens) is left to your Linear automation rules.
305
+
306
+ </details>
307
+
308
+ <details>
309
+ <summary>Project must be on a single Linear team in practice</summary>
310
+
311
+ Cross-team projects work — the orchestrator caches the in-progress state ID per team — but every team in the project must use the same status name for `linear.statuses.inProgress`.
312
+
313
+ </details>
314
+
315
+ <details>
316
+ <summary>Claude launches in bypass-permissions mode by default</summary>
317
+
318
+ Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode bypassPermissions` to avoid workspace-trust and tool-permission prompts blocking automation. Override `models.definitions.claude.cmd` for a stricter mode.
319
+
320
+ </details>
321
+
322
+ <details>
323
+ <summary>Doctor's command introspection is shallow</summary>
324
+
325
+ Doctor reports the resolved local runner (safehouse / sdx / none) and whether its prerequisites are met, then tokenizes model `cmd` and checks the first two non-flag tokens against PATH. Boolean flags without values, env-var assignments (`FOO=1`), shell pipelines, and subshells are not parsed — verify those manually. When `local.runner` is `"none"`, doctor surfaces a single WARNING line.
326
+
327
+ </details>
328
+
329
+ <details>
330
+ <summary>Doctor checks every enabled model</summary>
331
+
332
+ `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.
333
+
334
+ </details>
335
+
336
+ <details>
337
+ <summary>Switch to tmux if cmux is misbehaving</summary>
338
+
339
+ Set `workspaceKind: "tmux"` to force the tmux backend when cmux's CLI/socket bridge is flaky (symptoms: `cmux --json list-workspaces` returning `Failed to write to socket (Broken pipe)` or `Socket not found at ...cmux.sock` on every tick). tmux is more reliable — just a unix socket, no GUI app — at the cost of losing cmux's status pills, notifications, and sidebar.
340
+
341
+ </details>
342
+
343
+ <details>
344
+ <summary>Agent CLI must accept a positional prompt</summary>
345
+
346
+ The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
347
+
348
+ </details>
349
+
350
+ <details>
351
+ <summary><code>crew setup repos</code> only auto-clones <code>owner/repo</code> entries</summary>
352
+
353
+ Bare-name entries in `workspace.knownRepositories` (e.g. `"api"` rather than `"clipboardhealth/api"`) are skipped with a hint to clone manually — the command refuses to guess the owner. After a partial setup, the exit code is non-zero so CI gates notice; rerun is idempotent once you clone the bare ones into `<projectDir>/<name>` yourself.
354
+
355
+ </details>
356
+
357
+ ## Development
358
+
359
+ Clone the repo and the `crew` / `crew:op` scripts execute straight from TypeScript source — no build step needed.
192
360
 
193
361
  ```bash
194
362
  cd ~/dev/c/groundcrew
@@ -198,8 +366,12 @@ node --run crew -- doctor
198
366
  node --run crew:op -- run --watch
199
367
  ```
200
368
 
201
- 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) — symlink it there if you keep yours elsewhere; the path is not configurable.
369
+ 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).
370
+
371
+ 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.
372
+
373
+ Source edits in `src/**` are picked up on the next invocation. Requires Node ≥ 24.3 (native `.ts` type stripping).
202
374
 
203
- Logs land in `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log` by default (override via `logging.file` in your config). The "Loaded config from …" line at startup tells you which config won.
375
+ ## License
204
376
 
205
- Source edits in `src/**` are picked up on the next invocation. Requires Node ≥ 24.3 (the version with native `.ts` type stripping enabled by default).
377
+ [MIT](./LICENSE)
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA0HA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyIA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
package/dist/cli.js CHANGED
@@ -51,6 +51,24 @@ async function runCli(argv) {
51
51
  }
52
52
  await setupWorkspaceCli(ticket, { dryRun });
53
53
  }
54
+ async function doctorCli(argv) {
55
+ let ticket;
56
+ for (let index = 0; index < argv.length; index += 1) {
57
+ const argument = argv[index];
58
+ if (argument === "--ticket") {
59
+ const value = argv[index + 1];
60
+ if (value === undefined || value.length === 0 || value.startsWith("-")) {
61
+ throw new Error("crew doctor --ticket: ticket id is required");
62
+ }
63
+ ticket = value;
64
+ index += 1;
65
+ continue;
66
+ }
67
+ throw new Error(`crew doctor: unknown argument: ${argument}`);
68
+ }
69
+ const ok = ticket === undefined ? await doctor() : await doctor({ ticket });
70
+ process.exitCode = ok ? process.exitCode : 1;
71
+ }
54
72
  const SUBCOMMANDS = {
55
73
  run: {
56
74
  summary: "Run the orchestrator (one-shot by default), or provision one ticket with --ticket",
@@ -58,13 +76,9 @@ const SUBCOMMANDS = {
58
76
  invoke: runCli,
59
77
  },
60
78
  doctor: {
61
- summary: "Verify prereqs against the resolved config",
62
- invoke: async () => {
63
- const ok = await doctor();
64
- if (!ok) {
65
- process.exitCode = 1;
66
- }
67
- },
79
+ summary: "Verify prereqs, or diagnose one ticket with --ticket",
80
+ usage: "[--ticket <ticket>]",
81
+ invoke: doctorCli,
68
82
  },
69
83
  cleanup: {
70
84
  summary: "Tear down a worktree",
@@ -87,9 +101,7 @@ function printHelp() {
87
101
  writeOutput("Commands:");
88
102
  for (const [name, command] of Object.entries(SUBCOMMANDS)) {
89
103
  writeOutput(` ${name.padEnd(width)} ${command.summary}`);
90
- if (command.usage !== undefined) {
91
- writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
92
- }
104
+ writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
93
105
  }
94
106
  writeOutput("\nSee README.md for full configuration and behavior.");
95
107
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAmNjE"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAuLjE"}
@@ -10,42 +10,16 @@ import { isGroundcrewIssue } from "../lib/boardSource.js";
10
10
  import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
11
11
  import { errorMessage, log, logEvent } from "../lib/util.js";
12
12
  import { workspaces } from "../lib/workspaces.js";
13
- import { classifyBlockers, classifyEligibility, } from "./eligibility.js";
13
+ import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, } from "./eligibility.js";
14
14
  import { setupWorkspace } from "./setupWorkspace.js";
15
- const PERCENT_FRACTION_DIVISOR = 100;
16
- const DAYS_PER_WEEK = 7;
17
- const MINUTES_PER_DAY = 24 * 60;
18
- const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
19
15
  export function createDispatcher(deps) {
20
16
  const { config, client } = deps;
21
17
  const issueStatusUpdater = createLinearIssueStatusUpdater({ config, client });
22
18
  function buildExhaustedSet(usage) {
23
19
  const exhausted = new Set();
24
- const sessionLimit = config.orchestrator.sessionLimitPercentage;
25
- for (const [model, snapshot] of Object.entries(usage)) {
26
- if (snapshot.session !== null && snapshot.session * PERCENT_FRACTION_DIVISOR > sessionLimit) {
27
- exhausted.add(model);
28
- const pct = (snapshot.session * PERCENT_FRACTION_DIVISOR).toFixed(0);
29
- const mins = snapshot.sessionEndDuration ?? "?";
30
- log(`${model} session at ${pct}% (> ${sessionLimit}%), resets in ${mins}m — skipping its tickets`);
31
- }
32
- // Weekly gate paces total weekly usage against day buckets from the
33
- // weekly reset. Day 1's budget is available immediately after rollover,
34
- // then each later day opens another 1/7 of the weekly budget. Skipped when:
35
- // - weekly is null (no codexbar secondary window this tick)
36
- // - weekly is non-finite (EXHAUSTED_USAGE — session gate above
37
- // already pins it to Infinity)
38
- // - weekEndDuration is null (can't compute where we are in week)
39
- if (snapshot.weekly !== null &&
40
- Number.isFinite(snapshot.weekly) &&
41
- snapshot.weekEndDuration !== null) {
42
- const usedPct = snapshot.weekly * PERCENT_FRACTION_DIVISOR;
43
- const allowedPct = weeklyPacedBudgetPercentage(snapshot.weekEndDuration);
44
- if (usedPct > allowedPct) {
45
- exhausted.add(model);
46
- log(`${model} weekly at ${usedPct.toFixed(1)}% (> ${allowedPct.toFixed(1)}% paced budget), resets in ${snapshot.weekEndDuration}m — skipping its tickets`);
47
- }
48
- }
20
+ for (const exhaustion of classifyUsageExhaustion(config, usage)) {
21
+ exhausted.add(exhaustion.model);
22
+ log(formatUsageExhaustion(exhaustion));
49
23
  }
50
24
  return exhausted;
51
25
  }
@@ -187,9 +161,10 @@ export function createDispatcher(deps) {
187
161
  }
188
162
  return { runOnce };
189
163
  }
190
- function weeklyPacedBudgetPercentage(weekEndDuration) {
191
- const elapsedMinutes = Math.min(MINUTES_PER_WEEK, Math.max(0, MINUTES_PER_WEEK - weekEndDuration));
192
- const elapsedDayCount = Math.ceil(elapsedMinutes / MINUTES_PER_DAY);
193
- const budgetDayCount = Math.min(DAYS_PER_WEEK, Math.max(1, elapsedDayCount));
194
- return (budgetDayCount / DAYS_PER_WEEK) * PERCENT_FRACTION_DIVISOR;
164
+ function formatUsageExhaustion(exhaustion) {
165
+ if (exhaustion.kind === "session") {
166
+ const mins = exhaustion.resetMinutes ?? "?";
167
+ return `${exhaustion.model} session at ${exhaustion.usedPercentage.toFixed(0)}% (> ${exhaustion.limitPercentage}%), resets in ${mins}m — skipping its tickets`;
168
+ }
169
+ return `${exhaustion.model} weekly at ${exhaustion.usedPercentage.toFixed(1)}% (> ${exhaustion.allowedPercentage.toFixed(1)}% paced budget), resets in ${exhaustion.resetMinutes}m — skipping its tickets`;
195
170
  }
@@ -2,5 +2,8 @@
2
2
  * doctor — verify groundcrew prerequisites against the resolved config.
3
3
  * Returns true if every required check passes; false otherwise.
4
4
  */
5
- export declare function doctor(): Promise<boolean>;
5
+ export interface DoctorOptions {
6
+ ticket?: string;
7
+ }
8
+ export declare function doctor(options?: DoctorOptions): Promise<boolean>;
6
9
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8IH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAkE/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA2BH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsHD,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E"}