@clipboard-health/groundcrew 2.3.1 → 2.3.5

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,74 +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.
37
-
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`.
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.
39
73
 
40
- ```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:
74
+ Then clone each repo before the first `crew run` — groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
46
75
 
47
76
  ```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
+ ```
89
+
90
+ <details>
91
+ <summary>Using 1Password (<code>op</code>) for the key</summary>
63
92
 
64
- # Via 1Password CLI (`op`), if you keep the key in a vault
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 doctor --ticket TEAM-123
96
- crew run --dry-run
97
- crew run # one-shot
98
- crew run --watch # poll forever
99
- ```
120
+ <details>
121
+ <summary>Safehouse clearance allowlist</summary>
122
+
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:
124
+
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
100
142
 
101
- ## Config reference
143
+ Three keys are required; everything else has a default.
102
144
 
103
- Required fields are marked **required**; everything else has a default and can be omitted from `config.ts`.
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>
104
157
 
105
158
  | Key | Default | What it does |
106
159
  | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -122,17 +175,18 @@ Required fields are marked **required**; everything else has a default and can b
122
175
  | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
123
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. |
124
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). |
125
- | `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. |
126
179
  | `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
127
- | `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. |
128
- | `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. |
129
- | `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`. |
130
183
 
131
- 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>
132
185
 
133
- ### Disabling a shipped default
186
+ <details>
187
+ <summary>Disabling a shipped default</summary>
134
188
 
135
- 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:
136
190
 
137
191
  ```ts
138
192
  // config.ts
@@ -151,61 +205,34 @@ Effects:
151
205
 
152
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.
153
207
  - `agent-any` only resolves to enabled models.
154
- - 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.
155
209
 
156
210
  Rules:
157
211
 
158
- - `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.
159
213
  - `disabled` must be exactly the boolean `true`.
160
- - 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.
161
215
  - `models.default` must point at an enabled model.
162
216
 
163
- ## Manual commands
217
+ </details>
218
+
219
+ ## Commands
164
220
 
165
221
  ```bash
166
- crew doctor --ticket <TICKET>
167
- 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
168
227
  crew setup repos [--dry-run] [<repo>...]
169
- crew cleanup <TICKET>
228
+ crew cleanup <TICKET> # tear down every worktree carrying this ticket
170
229
  ```
171
230
 
172
- `crew doctor --ticket <TICKET>` diagnoses one Linear ticket without provisioning it. It checks the same resolution inputs that decide whether the orchestrator can see the ticket — Todo status, `agent-*` label, model resolution, repository mention, local clone — then checks blockers, model session usage, and available in-progress capacity.
173
-
174
- `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`.
175
232
 
176
233
  ### `crew doctor --ticket <ticket>`
177
234
 
178
- Diagnose why a ticket would or wouldn't be dispatched on the next orchestrator tick. Runs the same resolution and eligibility chain as the dispatcher, but for a single ticket, and prints a tree of pass/fail checks.
179
-
180
- ```bash
181
- crew doctor --ticket HRD-446
182
- ```
183
-
184
- Exits 0 if the ticket would dispatch, 1 otherwise. Useful when you've labelled a ticket with `agent-claude` and it doesn't show up on the board.
185
-
186
- Example output for a ticket that would dispatch:
187
-
188
- ```text
189
- groundcrew doctor --ticket HRD-446 (Add retry logic to the sync job)
190
- ────────────────────────────────────────────────────────────────────
191
-
192
- Resolution
193
- [ok] Ticket exists in Linear ("Add retry logic to the sync job")
194
- [ok] Status is Todo
195
- [ok] Has agent-* label (agent-claude)
196
- [ok] Model resolves from agent-* label (model "claude")
197
- [ok] Description mentions known repo (owner/repo)
198
- [ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
199
-
200
- Eligibility
201
- [ok] No active blockers
202
- [ok] Model "claude" usage under sessionLimitPercentage (12% (limit 85%))
203
- [ok] In-progress cap not hit (2/4 used)
204
-
205
- → would be dispatched on next tick
206
- ```
207
-
208
- Example output for a ticket that's not in the Todo status:
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:
209
236
 
210
237
  ```text
211
238
  groundcrew doctor --ticket HRD-447 (Refactor auth middleware)
@@ -225,27 +252,111 @@ Eligibility
225
252
  → ineligible: status is In Progress (need Todo)
226
253
  ```
227
254
 
228
- ## Gotchas
255
+ ## Troubleshooting
256
+
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.
258
+
259
+ <details>
260
+ <summary>Local execution picks one of safehouse / sdx / none</summary>
261
+
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>
229
286
 
230
- - **Ticket labeled but not on the board?** Run `crew doctor --ticket <ticket>` — it lists every check the dispatcher runs and flags the failing one.
231
- - **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.
232
- - **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.
233
- - **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`.
234
- - **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.
235
- - **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
236
- - **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
237
- - **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.
238
- - **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`.
239
- - **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.
240
- - **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.
241
- - **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.
242
- - **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.
243
- - **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
244
- - **`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.
287
+ <details>
288
+ <summary>Status names matter</summary>
245
289
 
246
- ## Hacking on groundcrew
290
+ If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
247
291
 
248
- 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.
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.
249
360
 
250
361
  ```bash
251
362
  cd ~/dev/c/groundcrew
@@ -255,8 +366,12 @@ node --run crew -- doctor
255
366
  node --run crew:op -- run --watch
256
367
  ```
257
368
 
258
- 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).
259
374
 
260
- 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
261
376
 
262
- 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)
@@ -1 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAoGf;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAuGf;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -122,7 +122,9 @@ export async function setupWorkspace(config, options, runOptions = {}) {
122
122
  const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
123
123
  promptDir = stagedPrompt.directory;
124
124
  const secretsFile = stageBuildSecrets(promptDir);
125
- const sandboxName = runner === "sdx" ? sandboxNameFor({ repository, model }) : undefined;
125
+ const sandboxName = runner === "sdx" && definition.sandbox !== undefined
126
+ ? sandboxNameFor({ agent: definition.sandbox.agent })
127
+ : undefined;
126
128
  if (runner === "sdx" && sandboxName !== undefined && definition.sandbox !== undefined) {
127
129
  await ensureSandbox({
128
130
  sandboxName,
@@ -1,15 +1,14 @@
1
1
  import type { SandboxDefinition } from "./config.ts";
2
2
  /**
3
- * Derive a deterministic sbx sandbox name from the repository + model
4
- * tuple so `crew sandbox auth <repo>` and the subsequent `crew local`
5
- * launch agree on which sandbox to target. Lowercased and reduced to the
6
- * sbx-safe charset (`a-z0-9.+-`) so unusual repo names still round-trip
7
- * cleanly. Keep the prefix stable — doctor and teardown use it to
3
+ * Derive a deterministic sbx sandbox name from the sbx agent so every
4
+ * groundcrew model that targets the same agent reuses one sandbox across
5
+ * repositories and tickets. Lowercased and reduced to the sbx-safe
6
+ * charset (`a-z0-9.+-`) so unusual agent names still round-trip cleanly.
7
+ * Keep the `groundcrew-` prefix stable — doctor and teardown use it to
8
8
  * identify groundcrew-owned sandboxes.
9
9
  */
10
10
  export declare function sandboxNameFor(arguments_: {
11
- repository: string;
12
- model: string;
11
+ agent: string;
13
12
  }): string;
14
13
  /**
15
14
  * Probe `sbx ls` to see whether a sandbox with `sandboxName` already
@@ -1 +1 @@
1
- {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMxF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
1
+ {"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -1,14 +1,14 @@
1
1
  import { runCommandAsync } from "./commandRunner.js";
2
2
  /**
3
- * Derive a deterministic sbx sandbox name from the repository + model
4
- * tuple so `crew sandbox auth <repo>` and the subsequent `crew local`
5
- * launch agree on which sandbox to target. Lowercased and reduced to the
6
- * sbx-safe charset (`a-z0-9.+-`) so unusual repo names still round-trip
7
- * cleanly. Keep the prefix stable — doctor and teardown use it to
3
+ * Derive a deterministic sbx sandbox name from the sbx agent so every
4
+ * groundcrew model that targets the same agent reuses one sandbox across
5
+ * repositories and tickets. Lowercased and reduced to the sbx-safe
6
+ * charset (`a-z0-9.+-`) so unusual agent names still round-trip cleanly.
7
+ * Keep the `groundcrew-` prefix stable — doctor and teardown use it to
8
8
  * identify groundcrew-owned sandboxes.
9
9
  */
10
10
  export function sandboxNameFor(arguments_) {
11
- const raw = `groundcrew-${arguments_.repository}-${arguments_.model}`.toLowerCase();
11
+ const raw = `groundcrew-${arguments_.agent}`.toLowerCase();
12
12
  return raw
13
13
  .replaceAll(/[^a-z0-9.+-]+/g, "-")
14
14
  .replaceAll(/-+/g, "-")
@@ -1 +1 @@
1
- {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAI1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAgU7C,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AA+ND,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SAAS,cAAc,QAAQ,MAAM,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhF,UAAU,SACN,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAI5C,CAAC"}
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAI1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAsU7C,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AA+ND,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SAAS,cAAc,QAAQ,MAAM,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhF,UAAU,SACN,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAI5C,CAAC"}
@@ -179,17 +179,20 @@ async function applyCmuxStatus(workspaceId, status, signal) {
179
179
  async function closeCmuxWorkspace(workspaceId, signal) {
180
180
  await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
181
181
  }
182
+ function isCmuxSetStatusUnsupported(error) {
183
+ return errorMessage(error).includes('unknown command "set-status"');
184
+ }
182
185
  const cmuxAdapter = {
183
186
  async open(spec, signal) {
184
187
  const inheritedRemote = await probeCurrentCmuxRemote(signal);
185
188
  const newWorkspaceArguments = ["--json", "new-workspace", "--name", spec.name];
186
189
  if (inheritedRemote === undefined) {
187
- newWorkspaceArguments.push("--working-directory", spec.cwd, "--command", spec.command);
190
+ newWorkspaceArguments.push("--cwd", spec.cwd, "--command", spec.command);
188
191
  }
189
192
  else {
190
- // Skip --working-directory: the path is on the SSH remote and would
191
- // fall back to $HOME (macOS) when cmux tries to chdir locally. The
192
- // wrapped ssh command does its own `cd` on the remote side.
193
+ // Skip --cwd: the path is on the SSH remote and would fall back to
194
+ // $HOME (macOS) when cmux tries to chdir locally. The wrapped ssh
195
+ // command does its own `cd` on the remote side.
193
196
  newWorkspaceArguments.push("--command", buildSshWrappedCommand(spec, inheritedRemote));
194
197
  }
195
198
  const output = await runWorkspaceCommand("cmux", newWorkspaceArguments, signal);
@@ -203,10 +206,12 @@ const cmuxAdapter = {
203
206
  await applyCmuxStatus(workspaceId, spec.status, signal);
204
207
  }
205
208
  catch (error) {
206
- // v2 cmux builds may not implement `set-status`; status pills are
207
- // a nice-to-have, not load-bearing. Log and keep the workspace
208
- // rather than tearing down a successful launch.
209
- log(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
209
+ // Status pills are best-effort. cmux v2+ dropped `set-status` entirely,
210
+ // so swallow that specific gap silently; surface anything else so a real
211
+ // regression doesn't hide behind the same swallow.
212
+ if (!isCmuxSetStatusUnsupported(error)) {
213
+ log(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
214
+ }
210
215
  }
211
216
  }
212
217
  },
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoSD,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;CAMrB,CAAC"}
1
+ {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoUD,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;CAMrB,CAAC"}
@@ -174,23 +174,31 @@ async function removeWorktree(config, entry, options) {
174
174
  await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
175
175
  }
176
176
  catch (error) {
177
- // git's `fatal: ... use --force to delete it` line goes to inherited
178
- // stderr, so the captured error is just "Exit status: 128". Probe the
179
- // worktree ourselves so the failure message explains the condition
180
- // (modified/untracked files) and points at `crew cleanup --force`.
177
+ // git's `fatal: ...` diagnostic goes to inherited stderr, so the
178
+ // captured error is just "Exit status: 128". Probe the worktree
179
+ // ourselves so the failure message names the condition — dirty
180
+ // (modified/untracked files, fixable with `crew cleanup --force`) or
181
+ // orphan (directory exists on disk but is not registered with the
182
+ // parent repo, requires manual inspection + `rm -rf`).
181
183
  if (options.force || options.signal?.aborted === true) {
182
184
  throw error;
183
185
  }
184
186
  const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
185
- if (dirtiness.kind !== "dirty") {
186
- throw error;
187
+ if (dirtiness.kind === "dirty") {
188
+ throw new Error(describeDirtyWorktree({
189
+ ticket: entry.ticket,
190
+ dir: entry.dir,
191
+ modified: dirtiness.modified,
192
+ untracked: dirtiness.untracked,
193
+ }), { cause: error });
194
+ }
195
+ if (dirtiness.kind === "unknown") {
196
+ const registration = await probeWorktreeRegistration(entry.dir, options.signal);
197
+ if (registration === "orphan") {
198
+ throw new Error(describeOrphanWorktree({ dir: entry.dir }), { cause: error });
199
+ }
187
200
  }
188
- throw new Error(describeDirtyWorktree({
189
- ticket: entry.ticket,
190
- dir: entry.dir,
191
- modified: dirtiness.modified,
192
- untracked: dirtiness.untracked,
193
- }), { cause: error });
201
+ throw error;
194
202
  }
195
203
  }
196
204
  else {
@@ -243,6 +251,20 @@ function describeDirtyWorktree(arguments_) {
243
251
  const pronoun = modified + untracked === 1 ? "it" : "them";
244
252
  return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
245
253
  }
254
+ async function probeWorktreeRegistration(worktreeDir, signal) {
255
+ let output;
256
+ try {
257
+ output = await runCommandAsync("git", ["-C", worktreeDir, "rev-parse", "--is-inside-work-tree"], signalProperty(signal));
258
+ }
259
+ catch {
260
+ return "unknown";
261
+ }
262
+ return output === "true" ? "registered" : "orphan";
263
+ }
264
+ function describeOrphanWorktree(arguments_) {
265
+ const { dir } = arguments_;
266
+ return `directory exists but is not a registered git worktree. If ${dir} has nothing of value, \`rm -rf\` ${dir} manually; otherwise inspect it before deleting.`;
267
+ }
246
268
  function list(config) {
247
269
  return listWorktrees(config);
248
270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "2.3.1",
3
+ "version": "2.3.5",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 120" width="620" height="120" role="img" aria-label="groundcrew">
3
+ <style>
4
+ .wordmark {
5
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
6
+ font-size: 92px;
7
+ font-weight: 700;
8
+ letter-spacing: -0.04em;
9
+ }
10
+ </style>
11
+ <text class="wordmark" x="20" y="92" textLength="540" lengthAdjust="spacingAndGlyphs" fill="#e4e4e7">groundcrew</text>
12
+ <rect x="572" y="24" width="30" height="72" fill="#77d94e">
13
+ <animate attributeName="opacity" values="1;0" dur="1.2s" calcMode="discrete" keyTimes="0;0.5" repeatCount="indefinite"/>
14
+ </rect>
15
+ </svg>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 120" width="620" height="120" role="img" aria-label="groundcrew">
3
+ <style>
4
+ .wordmark {
5
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
6
+ font-size: 92px;
7
+ font-weight: 700;
8
+ letter-spacing: -0.04em;
9
+ }
10
+ </style>
11
+ <text class="wordmark" x="20" y="92" textLength="540" lengthAdjust="spacingAndGlyphs" fill="#18181b">groundcrew</text>
12
+ <rect x="572" y="24" width="30" height="72" fill="#77d94e">
13
+ <animate attributeName="opacity" values="1;0" dur="1.2s" calcMode="discrete" keyTimes="0;0.5" repeatCount="indefinite"/>
14
+ </rect>
15
+ </svg>
@@ -1,9 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
- <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="225 230 575 530" width="1024.0pt" height="944.0pt">
4
- <path d="M 491.97 239.05 C 526.13 236.93 560.37 240.48 593.00 251.00 C 626.41 261.79 657.69 279.64 683.66 303.32 C 707.08 325.06 726.40 351.86 738.47 381.51 C 744.03 394.32 747.45 407.46 750.90 420.93 C 762.33 420.53 773.79 425.00 781.76 433.25 C 788.68 441.04 792.39 451.62 792.07 462.00 C 792.08 487.67 792.17 513.33 792.05 539.00 C 792.27 545.73 790.51 552.54 787.54 558.55 C 783.74 566.04 777.45 571.67 770.09 575.55 C 769.92 579.70 770.08 583.85 769.99 588.00 C 768.69 602.43 766.81 616.79 757.36 628.43 C 756.74 632.58 757.53 636.52 755.44 640.46 C 752.06 648.40 745.48 652.22 738.31 656.27 C 732.17 659.48 726.14 662.23 719.02 660.89 C 712.87 659.35 707.07 655.43 704.53 649.45 C 703.05 645.51 702.46 641.30 703.50 637.16 C 704.33 631.93 708.45 627.64 712.25 624.25 C 716.71 620.56 721.78 617.56 726.88 614.85 C 732.77 611.92 740.93 611.14 746.43 615.32 C 747.51 614.30 747.88 613.62 748.20 612.15 C 751.08 601.64 751.97 590.54 751.90 579.68 C 745.31 580.33 738.62 579.86 732.00 580.03 C 720.74 580.20 709.30 575.59 703.14 565.80 C 699.76 560.81 698.29 554.97 697.99 549.01 C 698.02 514.60 697.92 480.42 698.18 446.00 C 698.56 440.21 699.47 434.75 703.08 430.01 C 706.08 425.60 710.41 423.81 715.11 421.80 C 706.69 386.13 686.06 353.49 659.23 328.77 C 646.53 316.55 632.06 306.71 616.51 298.46 C 593.44 285.91 568.21 277.82 542.16 274.50 C 536.07 273.94 530.13 272.79 524.00 272.97 C 517.66 272.59 511.34 272.32 504.99 272.77 C 499.95 273.21 495.01 272.70 490.00 273.58 C 454.29 276.95 418.96 289.01 389.27 309.26 C 362.61 326.89 340.47 351.37 325.30 379.49 C 319.33 390.66 314.19 402.41 310.72 414.61 C 309.94 417.28 308.92 419.78 308.91 422.62 C 318.36 424.43 324.44 433.67 324.21 443.00 C 324.27 477.65 324.12 512.34 324.20 547.00 C 324.14 550.61 324.24 554.28 323.83 557.87 C 321.66 565.85 315.98 572.12 308.64 575.78 C 300.05 580.18 291.39 580.22 282.00 580.01 C 267.62 580.66 253.23 576.52 243.12 565.88 C 234.68 557.31 231.16 545.84 230.97 534.00 C 231.02 512.33 230.96 490.67 231.00 469.00 C 231.03 464.43 230.69 460.00 231.64 455.49 C 233.10 443.19 240.79 431.64 251.69 425.73 C 258.00 422.14 265.62 420.75 272.81 420.86 C 278.14 395.31 287.67 370.76 301.76 348.76 C 308.87 337.50 316.93 326.73 326.09 317.05 C 330.25 312.27 334.65 307.58 339.54 303.53 C 365.80 279.49 398.14 261.74 432.05 251.14 C 451.47 244.59 471.61 240.98 491.97 239.05 Z" fill="#78716c" />
5
- <path d="M 356.00 444.19 C 452.27 444.23 548.55 444.15 644.82 444.23 C 649.66 444.23 654.22 444.56 657.84 448.15 C 661.26 451.27 662.52 455.45 662.47 459.99 C 662.31 525.35 662.51 590.59 662.40 656.00 C 662.38 661.55 662.63 667.04 661.94 672.56 C 659.55 678.86 653.83 683.18 647.00 683.04 C 547.66 683.01 448.31 683.05 348.96 683.02 C 340.58 682.87 333.60 675.32 333.95 667.00 C 333.95 599.00 333.96 531.00 333.94 463.00 C 333.95 460.46 333.79 457.81 334.22 455.29 C 335.23 451.68 337.14 448.37 340.50 446.48 C 345.39 443.64 350.60 444.17 356.00 444.19 Z" fill="#78716c" />
6
- <path d="M 389.00 483.91 C 392.46 483.66 394.50 486.02 397.14 487.82 C 416.17 501.83 435.17 515.92 454.42 529.63 C 456.37 531.09 458.13 532.56 458.89 534.98 C 459.10 537.05 459.34 539.53 458.56 541.50 C 457.15 543.90 454.68 545.81 452.49 547.46 C 434.16 560.87 415.99 574.32 397.64 587.70 C 395.69 589.05 393.38 590.56 391.00 590.95 C 386.00 590.89 381.37 587.28 381.55 581.96 C 381.26 576.63 385.28 574.69 388.98 571.94 C 404.61 560.64 420.06 549.02 435.97 538.11 C 419.10 525.62 402.11 513.26 385.25 500.75 C 382.37 498.55 379.93 495.86 380.05 491.99 C 380.79 487.65 384.18 483.40 389.00 483.91 Z" fill="#77d94e" />
7
- <path d="M 672.78 521.32 C 677.82 520.86 682.12 520.21 686.27 523.73 C 691.33 527.85 690.31 533.17 690.62 539.00 C 690.46 604.02 690.92 668.98 690.42 734.01 C 690.17 739.13 688.58 744.11 684.46 747.42 C 681.30 750.32 677.21 751.32 673.00 751.12 C 577.34 751.12 481.66 751.13 386.00 751.08 C 377.59 751.55 369.36 743.47 369.97 735.00 C 370.03 721.31 369.99 707.63 369.97 693.94 C 460.31 694.02 550.66 693.99 641.00 693.96 C 652.00 694.60 663.63 688.89 669.36 679.38 C 672.64 674.56 672.40 668.57 672.51 663.00 C 672.55 618.67 672.51 574.33 672.55 530.00 C 672.50 527.10 672.50 524.21 672.78 521.32 Z" fill="#78716c" />
8
- <path d="M 473.99 577.90 C 490.65 577.77 507.33 577.94 524.00 577.84 C 527.49 577.86 531.25 577.49 534.68 578.19 C 539.56 579.86 541.50 585.54 539.02 590.00 C 537.21 593.42 533.72 594.68 530.03 594.77 C 512.35 594.96 494.68 594.68 477.00 594.87 C 473.37 594.93 470.04 594.29 467.64 591.35 C 465.40 589.27 465.80 585.98 466.16 583.24 C 467.44 580.11 470.43 577.60 473.99 577.90 Z" fill="#77d94e" />
9
- </svg>