@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 +21 -0
- package/README.md +237 -122
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +3 -1
- package/dist/lib/dockerSandbox.d.ts +6 -7
- package/dist/lib/dockerSandbox.d.ts.map +1 -1
- package/dist/lib/dockerSandbox.js +6 -6
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +13 -8
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +34 -12
- package/package.json +1 -1
- package/static/groundcrew-wordmark-dark.svg +15 -0
- package/static/groundcrew-wordmark-light.svg +15 -0
- package/static/groundcrew.svg +0 -9
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|
|
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
|
-
|
|
59
|
+
2. **Pick an isolation runner.** See [Runners](#runners) — `auto` resolves to `safehouse` on macOS and `sdx` on Linux/WSL.
|
|
24
60
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary>Using 1Password (<code>op</code>) for the key</summary>
|
|
63
92
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
crew run --
|
|
103
|
+
crew doctor # check setup
|
|
104
|
+
crew run --dry-run # preview without provisioning
|
|
105
|
+
crew run --watch # poll forever
|
|
80
106
|
```
|
|
81
107
|
|
|
82
|
-
|
|
108
|
+
## Runners
|
|
83
109
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
143
|
+
Three keys are required; everything else has a default.
|
|
102
144
|
|
|
103
|
-
|
|
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.
|
|
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.
|
|
128
|
-
| `local.runner` | `"auto"` | Local isolation backend. `"auto"`
|
|
129
|
-
| `logging.file` | XDG state path | Append-mode log file
|
|
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
|
-
|
|
184
|
+
</details>
|
|
132
185
|
|
|
133
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
217
|
+
</details>
|
|
218
|
+
|
|
219
|
+
## Commands
|
|
164
220
|
|
|
165
221
|
```bash
|
|
166
|
-
crew doctor
|
|
167
|
-
crew
|
|
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
|
-
`
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
290
|
+
If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
|
|
247
291
|
|
|
248
|
-
|
|
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)
|
|
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
|
-
|
|
375
|
+
## License
|
|
261
376
|
|
|
262
|
-
|
|
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,
|
|
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"
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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_.
|
|
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;
|
|
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"}
|
package/dist/lib/workspaces.js
CHANGED
|
@@ -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("--
|
|
190
|
+
newWorkspaceArguments.push("--cwd", spec.cwd, "--command", spec.command);
|
|
188
191
|
}
|
|
189
192
|
else {
|
|
190
|
-
// Skip --
|
|
191
|
-
//
|
|
192
|
-
//
|
|
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
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
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;
|
|
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"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -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:
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// (modified/untracked files
|
|
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
|
|
186
|
-
throw
|
|
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
|
|
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
|
@@ -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>
|
package/static/groundcrew.svg
DELETED
|
@@ -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>
|