@clipboard-health/groundcrew 1.4.0 → 1.6.0
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/CONTEXT.md +10 -7
- package/README.md +23 -36
- package/configExample.ts +2 -15
- package/package.json +2 -2
- package/src/cli.js +6 -8
- package/src/cli.js.map +1 -1
- package/src/commands/doctor.js +32 -93
- package/src/commands/doctor.js.map +1 -1
- package/src/commands/setupWorkspace.js +13 -19
- package/src/commands/setupWorkspace.js.map +1 -1
- package/src/commands/spriteSetup.d.ts +18 -0
- package/src/commands/spriteSetup.js +116 -3
- package/src/commands/spriteSetup.js.map +1 -1
- package/src/lib/config.d.ts +11 -76
- package/src/lib/config.js +23 -85
- package/src/lib/config.js.map +1 -1
- package/src/lib/host.d.ts +6 -9
- package/src/lib/host.js +4 -6
- package/src/lib/host.js.map +1 -1
- package/src/lib/launchCommand.d.ts +2 -6
- package/src/lib/launchCommand.js +13 -38
- package/src/lib/launchCommand.js.map +1 -1
- package/src/lib/localRunner.d.ts +2 -0
- package/src/lib/localRunner.js +9 -0
- package/src/lib/localRunner.js.map +1 -0
- package/src/lib/workspaces.js +6 -2
- package/src/lib/workspaces.js.map +1 -1
- package/src/lib/worktrees.d.ts +7 -14
- package/src/lib/worktrees.js +17 -112
- package/src/lib/worktrees.js.map +1 -1
- package/src/commands/sandboxAuth.d.ts +0 -8
- package/src/commands/sandboxAuth.js +0 -76
- package/src/commands/sandboxAuth.js.map +0 -1
- package/src/lib/isolation.d.ts +0 -26
- package/src/lib/isolation.js +0 -56
- package/src/lib/isolation.js.map +0 -1
- package/src/lib/sandbox.d.ts +0 -10
- package/src/lib/sandbox.js +0 -25
- package/src/lib/sandbox.js.map +0 -1
package/CONTEXT.md
CHANGED
|
@@ -7,13 +7,13 @@ Six nouns that are collision-prone in this codebase. Use them precisely; don't s
|
|
|
7
7
|
The directory an agent works in for a single ticket. Two kinds, one concept:
|
|
8
8
|
|
|
9
9
|
- **Host worktree** — a `git worktree add`'d sibling at `<projectDir>/<repo>-<TICKET>/`. Visible to the host's `git worktree list --porcelain`.
|
|
10
|
-
- **
|
|
10
|
+
- **Sprite worktree** — a remote git worktree under `remote.sprite.worktreeRoot` on the Sprite runner. Tracked by groundcrew in `sprite-worktrees.json`; not visible to the host's `git worktree list --porcelain`.
|
|
11
11
|
|
|
12
12
|
Lifecycle and lookup live in `src/lib/worktrees.ts`. Callers ask `worktrees.create(spec)` / `worktrees.findByTicket(...)` / `worktrees.remove(entry)` / `worktrees.teardown(entries)` and never branch on the kind themselves — the module dispatches.
|
|
13
13
|
|
|
14
|
-
Branch name is `<os-username>-<ticket-lowercased
|
|
14
|
+
Branch name is `<os-username>-<ticket-lowercased>`. One ticket can have at most one worktree per kind; local host and Sprite remote worktrees can coexist when a ticket moves between runners. `list()` returns host and Sprite worktrees, and intentionally ignores legacy `.sbx` directories.
|
|
15
15
|
|
|
16
|
-
`teardown(entries)` is the destructive lifecycle for a Worktree paired with its Workspace. It closes the live Workspace (deduped per ticket so host
|
|
16
|
+
`teardown(entries)` is the destructive lifecycle for a Worktree paired with its Workspace. It closes the live Workspace (deduped per ticket so host and Sprite entries share one close) before removing each Worktree, and survives per-entry failures, returning a structured result. The order is non-negotiable: the Workspace must close while its underlying directory and branch still exist, or the user is left with a zombie Workspace. Cleaner's per-iteration sweep, the `crew cleanup` CLI, and `setupWorkspace`'s rollback path all route through this one operation.
|
|
17
17
|
|
|
18
18
|
## Workspace
|
|
19
19
|
|
|
@@ -22,17 +22,20 @@ The host-side terminal session that runs an agent for one ticket. Two kinds, one
|
|
|
22
22
|
- **cmux workspace** — a pane/tab in [cmux](https://github.com/clayton-cole/cmux). macOS-only.
|
|
23
23
|
- **tmux workspace** — a window inside a dedicated `groundcrew` tmux session. Linux/macOS.
|
|
24
24
|
|
|
25
|
-
Every provisioned ticket gets one workspace, named with the ticket id (`TEAM-220`). Tracked by ticket, not by worktree kind — there is one workspace per ticket regardless of host vs
|
|
25
|
+
Every provisioned ticket gets one workspace, named with the ticket id (`TEAM-220`). Tracked by ticket, not by worktree kind — there is one workspace per ticket regardless of host vs Sprite runner.
|
|
26
26
|
|
|
27
27
|
Lifecycle and lookup live in `src/lib/workspaces.ts`. Callers ask `workspaces.open(spec)` / `workspaces.probe()` / `workspaces.close(name)` and never branch on the kind themselves — the module dispatches via the resolved adapter (`workspaceKind` config + host capabilities). `probe()` returns a typed `WorkspaceProbe` (`{ kind: "ok"; names }` or `{ kind: "unavailable"; error? }`) so callers don't re-invent a sentinel when the adapter binary is flaky.
|
|
28
28
|
|
|
29
29
|
`groundcrew` opens workspaces in `setupWorkspace`, closes them in `cleaner.runOnce`. Distinct from `worktrees`; do not call a workspace a "worktree."
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Runner
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The environment that executes the agent command for a ticket.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
- **Local runner** — macOS only. Groundcrew requires `safehouse` on `PATH`, starts `clearance`, and launches the model command through `safehouse-clearance` inside the host worktree.
|
|
36
|
+
- **Sprite runner** — remote Linux. Tickets labeled `agent-remote` keep the workspace local but run repository setup and the agent command inside the configured Sprite.
|
|
37
|
+
|
|
38
|
+
There is no `models.isolation` strategy, Docker Sandboxes runner, or direct local runner. Linux/WSL users run tickets through Sprite. Legacy `.sbx` worktrees and persistent Docker Sandboxes state are no longer discovered or cleaned up by groundcrew; users remove old state manually with `sbx` if needed.
|
|
36
39
|
|
|
37
40
|
## Dispatcher
|
|
38
41
|
|
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ Watch a Linear project and farm out ready tickets to coding-agent CLIs running i
|
|
|
8
8
|
npm install -g @clipboard-health/groundcrew
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
This installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitively and provides the `clearance` / `clearance-ensure` bins used by Safehouse
|
|
11
|
+
This installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitively and provides the `clearance` / `clearance-ensure` bins used by local Safehouse execution.
|
|
12
12
|
|
|
13
13
|
## Quickstart
|
|
14
14
|
|
|
15
|
-
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the
|
|
15
|
+
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...). Local runs require macOS with [Safehouse](https://agent-safehouse.dev/) on `PATH`; Linux/WSL hosts must run tickets remotely through Sprite by adding the `agent-remote` label. Optional: `codexbar` for session-usage gating. The `workspaceKind` config key picks the workspace backend (`auto` resolves to cmux when installed, else tmux).
|
|
16
16
|
|
|
17
17
|
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.
|
|
18
18
|
|
|
@@ -41,28 +41,14 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
41
41
|
op run --env-file .env.1password -- crew doctor
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
5. **Prepare
|
|
44
|
+
5. **Prepare the runner and agent auth.** Groundcrew supports one local runner and one remote runner:
|
|
45
|
+
- macOS local: `cmux` or `tmux` workspace, Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
|
|
46
|
+
- macOS remote: local `cmux` or `tmux` workspace that launches a Sprite session.
|
|
47
|
+
- Linux/WSL remote: `tmux` workspace that launches a Sprite session. Label tickets `agent-remote`; local execution is not supported.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
Local setup fails before creating a worktree when the host is not macOS or `safehouse` is missing. `models.isolation`, per-model `isolation`, and per-model `sandbox` are legacy keys and now fail config validation.
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
sbx daemon start
|
|
50
|
-
sbx login
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Then prepare each repo/model sandbox once. For Claude, this opens the agent with no ticket prompt so you can complete `/login` without losing task context:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
crew sandbox auth <repo> --model claude
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
For Codex, groundcrew starts Docker's host-side OpenAI OAuth flow before preparing the sandbox:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
crew sandbox auth <repo> --model codex
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
6. **Set the clearance allowlist (Safehouse only).** When the resolved isolation strategy is 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](../clearance/README.md) for the proxy's env vars, log paths, and DNS rules. The shortest path is to set the env before `crew run`:
|
|
51
|
+
6. **Set the clearance allowlist for local macOS runs.** 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](../clearance/README.md) for the proxy's env vars, log paths, and DNS rules. The shortest path is to set the env before `crew run`:
|
|
66
52
|
|
|
67
53
|
```bash
|
|
68
54
|
CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
|
|
@@ -95,7 +81,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
95
81
|
|
|
96
82
|
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the Sprite if needed, prepares `~/dev`, configures Git, runs selected auth flows, adds selected MCP servers to Claude Code, and then opens Claude so you can run `/mcp` and authenticate only those selected servers. `--copy-local-codex-auth` copies `${CODEX_HOME:-$HOME/.codex}/auth.json` into `/home/sprite/.codex/auth.json` and then verifies `codex login status`; it never prints the file contents. Use `--skip-mcp-auth` when you only want to add MCP definitions, and run the `/mcp` step later.
|
|
97
83
|
|
|
98
|
-
Repo setup is separate from runner setup and should run after the ticket branch exists, immediately before launching an agent. It clones/fetches the repo in the Sprite, checks out the requested branch (creating it from the base branch when it does not exist on origin), forwards only build-time secrets for the dependency install, removes the temporary secret file, clears those env vars, and then exits. It
|
|
84
|
+
Repo setup is separate from runner setup and should run after the ticket branch exists, immediately before launching an agent. It clones/fetches the repo in the Sprite, checks out the requested branch (creating it from the base branch when it does not exist on origin), forwards only build-time secrets for the dependency install, removes the temporary secret file, clears those env vars, and then exits. It uses groundcrew's remote setup command.
|
|
99
85
|
|
|
100
86
|
```bash
|
|
101
87
|
op run --env-file "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env" -- \
|
|
@@ -104,7 +90,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
104
90
|
|
|
105
91
|
By default bootstrap forwards any locally set `NPM_TOKEN` and `BUF_TOKEN`. Use repeated `--secret <ENV_NAME>` to require a specific set, or `--no-secrets` for public installs. Do not checkpoint after repo bootstrap; dependency state is branch-specific and should be refreshed per ticket.
|
|
106
92
|
|
|
107
|
-
To run a ticket remotely through the orchestrator or `crew run --ticket`, label it with `agent-remote` plus the agent label you want, for example `agent-claude` or `agent-codex`. `agent-remote` alone uses `models.default`. Groundcrew keeps cmux/tmux local, creates a per-ticket git worktree in the Sprite under `/home/sprite/groundcrew/worktrees`, and runs the agent with `sprite exec --tty`.
|
|
93
|
+
To run a ticket remotely through the orchestrator or `crew run --ticket`, label it with `agent-remote` plus the agent label you want, for example `agent-claude` or `agent-codex`. `agent-remote` alone uses `models.default`. Groundcrew keeps cmux/tmux local, creates a per-ticket git worktree in the Sprite under `/home/sprite/groundcrew/worktrees`, and runs the agent with `sprite exec --tty`. Use `crew sprite sessions [<sprite-name>]` to inspect active Sprite sessions and `crew sprite attach <session-id-or-command> [--sprite <sprite-name>]` to attach to one; both commands default to `remote.sprite.spriteName` when the Sprite name is omitted.
|
|
108
94
|
|
|
109
95
|
8. **Run.** Doctor first, then a dry run, then the real thing:
|
|
110
96
|
|
|
@@ -128,17 +114,15 @@ Required fields are marked **required**; everything else has a default and can b
|
|
|
128
114
|
| `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
|
|
129
115
|
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
|
|
130
116
|
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
|
|
131
|
-
| `workspace.projectDir` | **required** | Parent dir for cloned repos.
|
|
117
|
+
| `workspace.projectDir` | **required** | Parent dir for cloned repos and local sibling ticket worktrees. Sprite ticket worktrees live in `remote.sprite.worktreeRoot` on the remote runner. |
|
|
132
118
|
| `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
|
|
133
119
|
| `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
|
|
134
120
|
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
135
121
|
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
136
122
|
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew setup <TICKET>` for unlabeled tickets. `crew run` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
137
|
-
| `models.isolation` | `"auto"` | Isolation strategy. `"auto"` picks Safehouse on macOS, else Docker Sandboxes when the model has a sandbox config. Safehouse or a model sandbox config is required; if neither is available, setup fails. Set `"none"` explicitly to run directly. |
|
|
138
123
|
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
139
|
-
| `models.definitions.<name>.cmd` | — | Shell command launched for the model.
|
|
124
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Local macOS runs execute in the worktree through Safehouse/clearance; Sprite runs execute inside the remote worktree. `{{worktree}}` is replaced before launch and legacy `{{sandbox}}` expands to an empty string. |
|
|
140
125
|
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
141
|
-
| `models.definitions.<name>.sandbox` | `{ agent }` | Optional Docker Sandboxes backing. Defaults set `claude` → `agent: "claude"` and `codex` → `agent: "codex"`. Set `sandbox: false` on an override to run the command outside Docker Sandboxes. |
|
|
142
126
|
| `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. |
|
|
143
127
|
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
144
128
|
| `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. |
|
|
@@ -154,30 +138,33 @@ The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.us
|
|
|
154
138
|
## Manual commands
|
|
155
139
|
|
|
156
140
|
```bash
|
|
157
|
-
crew sandbox auth <repo> --model claude
|
|
158
|
-
crew sandbox auth <repo> --model codex
|
|
159
141
|
crew sprite setup crew-claude-1 --claude --codex --copy-local-codex-auth --github --mcp linear --checkpoint
|
|
160
142
|
crew sprite bootstrap crew-claude-1 core-utils --branch rocky-team-123
|
|
143
|
+
crew sprite sessions
|
|
144
|
+
crew sprite attach <session-id-or-command> --sprite crew-claude-1
|
|
145
|
+
crew sprite ps crew-claude-1
|
|
146
|
+
crew sprite interrupt <process-group-id> --sprite crew-claude-1
|
|
161
147
|
crew run --ticket <TICKET>
|
|
162
148
|
crew cleanup <TICKET>
|
|
163
149
|
```
|
|
164
150
|
|
|
165
|
-
`crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description, the model comes from the ticket's `agent-*` label, and `agent-remote` is honored. 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 worktree carrying that ticket id (host
|
|
151
|
+
`crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description, the model comes from the ticket's `agent-*` label, and `agent-remote` is honored. 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 (host and Sprite kinds, across repos) and tears them all down. To inspect remote sessions, run `crew sprite sessions` or pass an explicit Sprite name. To attach to a listed session id or command selector, run `crew sprite attach <session-id-or-command>`. If an attached agent appears stuck in a long-running shell tool, use `crew sprite ps <sprite>` to find the child process group (`PGID`) under the agent, then use `crew sprite interrupt <PGID> --sprite <sprite>` to send SIGINT to that child command without killing the agent session. If cleanup cannot remove a remote worktree because the agent is still running, stop that session with `sprite sessions kill -s crew-claude-1 <session-id>` and retry cleanup. To inspect codexbar session windows directly, run `codexbar usage`; the orchestrator already gates on this internally via `orchestrator.sessionLimitPercentage`.
|
|
166
152
|
|
|
167
153
|
## Gotchas
|
|
168
154
|
|
|
169
|
-
- **
|
|
155
|
+
- **Local execution is macOS plus Safehouse only.** There is no `models.isolation` strategy and no direct local execution mode. On Linux/WSL, label tickets `agent-remote` and run them through Sprite.
|
|
170
156
|
- **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.
|
|
171
|
-
- **
|
|
172
|
-
- **
|
|
173
|
-
- **
|
|
157
|
+
- **Legacy Docker Sandboxes state is unmanaged.** Groundcrew no longer discovers or cleans `.sbx` worktrees or persistent Docker Sandboxes containers. If you have old state, inspect and remove it manually with `sbx`.
|
|
158
|
+
- **Sprite cleanup is also conservative.** `crew cleanup` removes tracked remote worktrees and branches, but it does not kill active Sprite sessions. Use `crew sprite sessions [<sprite>]` to inspect sessions and `sprite sessions kill -s <sprite> <session-id>` when Git reports a worktree is busy.
|
|
159
|
+
- **Long-running remote shell tools block agent input.** Claude Code and similar TUI agents cannot accept a new prompt while one of their shell tools is still running. Use `crew sprite ps <sprite>` to inspect the remote process tree; interrupt the tool's child `PGID`, not the agent session `PGID`, with `crew sprite interrupt <PGID> --sprite <sprite>`.
|
|
174
160
|
- **Codex auth in Sprites may need auth-file copy.** If `crew sprite setup <sprite> --codex` finishes interactive login but `codex login status` still fails inside the Sprite, rerun with `--copy-local-codex-auth` after confirming local Codex auth works.
|
|
175
161
|
- **Usage source defaults are OS-aware.** `codexbar` usage uses `--source auto` on macOS so CodexBar can prefer account/web sources and fall back as it supports. On Linux/WSL it uses `--source cli`, so install the CodexBar Linux CLI and authenticate the provider CLIs inside that environment.
|
|
176
162
|
- **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
|
|
177
163
|
- **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
|
|
178
164
|
- **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.
|
|
179
165
|
- **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`.
|
|
180
|
-
- **
|
|
166
|
+
- **Claude launches in bypass-permissions mode by default.** Groundcrew creates isolated per-ticket worktrees and unattended remote sessions, 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.
|
|
167
|
+
- **Doctor's command introspection is shallow.** Doctor reports whether the host can run local tickets with macOS plus Safehouse, 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.
|
|
181
168
|
- **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
|
|
182
169
|
|
|
183
170
|
## Hacking on groundcrew
|
package/configExample.ts
CHANGED
|
@@ -35,28 +35,15 @@ export const config: Config = {
|
|
|
35
35
|
//
|
|
36
36
|
// models: {
|
|
37
37
|
// default: "claude",
|
|
38
|
-
// // How model launch commands are wrapped. "auto" picks safehouse on a
|
|
39
|
-
// // supported host (currently macOS) when the binary is installed, then
|
|
40
|
-
// // Docker Sandboxes when configured. If neither isolated runner is
|
|
41
|
-
// // available, setup fails; set "none" explicitly to run directly.
|
|
42
|
-
// // Override to "safehouse", "docker", or "none" to pin a strategy.
|
|
43
|
-
// isolation: "auto",
|
|
44
38
|
// // Additive: defaults for `claude` and `codex` are merged in unless you
|
|
45
39
|
// // re-declare those keys here. Add a third agent (e.g. `cursor`) by
|
|
46
40
|
// // dropping it in this map and tagging tickets with `agent-cursor`.
|
|
41
|
+
// // Local runs on macOS are always wrapped with Safehouse/clearance.
|
|
42
|
+
// // Linux/WSL users should label tickets `agent-remote` to use Sprite.
|
|
47
43
|
// definitions: {
|
|
48
44
|
// cursor: {
|
|
49
45
|
// cmd: "cursor-agent",
|
|
50
46
|
// color: "#929292",
|
|
51
|
-
// // Optional per-model override of `models.isolation`:
|
|
52
|
-
// // isolation: "docker",
|
|
53
|
-
// // Optional for Docker Sandboxes-backed agents:
|
|
54
|
-
// // sandbox: {
|
|
55
|
-
// // agent: "cursor",
|
|
56
|
-
// // template: "groundcrew-node24:latest",
|
|
57
|
-
// // kits: ["./.sbx/kit"],
|
|
58
|
-
// // setupCommand: "npm clean-install",
|
|
59
|
-
// // },
|
|
60
47
|
// },
|
|
61
48
|
// },
|
|
62
49
|
// },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Linear-driven orchestrator that launches AI coding agents in
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle, Sprite remote runners, and usage tracking.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"ai",
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { cleanupWorkspaceCli } from "./commands/cleanupWorkspace.js";
|
|
2
2
|
import { doctor } from "./commands/doctor.js";
|
|
3
3
|
import { orchestrate } from "./commands/orchestrator.js";
|
|
4
|
-
import { sandboxAuthCli } from "./commands/sandboxAuth.js";
|
|
5
4
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
6
5
|
import { spriteCli } from "./commands/spriteSetup.js";
|
|
7
6
|
import { errorMessage, writeError, writeOutput } from "./lib/util.js";
|
|
@@ -59,15 +58,14 @@ const SUBCOMMANDS = {
|
|
|
59
58
|
usage: "[--force] <ticket>",
|
|
60
59
|
invoke: cleanupWorkspaceCli,
|
|
61
60
|
},
|
|
62
|
-
sandbox: {
|
|
63
|
-
summary: "Prepare persistent Docker Sandboxes auth",
|
|
64
|
-
usage: "auth <repo> [--model <name>]",
|
|
65
|
-
invoke: sandboxAuthCli,
|
|
66
|
-
},
|
|
67
61
|
sprite: {
|
|
68
|
-
summary: "Create, authenticate, and
|
|
62
|
+
summary: "Create, authenticate, bootstrap, and inspect a remote Sprite runner",
|
|
69
63
|
usage: "setup <sprite-name> [--claude] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
70
|
-
" → crew sprite bootstrap <sprite-name> <repo> [--branch <branch>]"
|
|
64
|
+
" → crew sprite bootstrap <sprite-name> <repo> [--branch <branch>]\n" +
|
|
65
|
+
" → crew sprite sessions [<sprite-name>]\n" +
|
|
66
|
+
" → crew sprite attach <session-id-or-command> [--sprite <sprite-name>]\n" +
|
|
67
|
+
" → crew sprite ps [<sprite-name>]\n" +
|
|
68
|
+
" → crew sprite interrupt <process-group-id> [--sprite <sprite-name>]",
|
|
71
69
|
invoke: spriteCli,
|
|
72
70
|
},
|
|
73
71
|
};
|
package/src/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../../../packages/groundcrew/src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../../../packages/groundcrew/src/cli.ts"],"names":[],"mappings":"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"}
|
package/src/commands/doctor.js
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
* Returns true if every required check passes; false otherwise.
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, statSync } from "node:fs";
|
|
6
|
-
import { runCommandAsync } from "../lib/commandRunner.js";
|
|
7
6
|
import { loadConfig } from "../lib/config.js";
|
|
8
7
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
9
|
-
import { resolveIsolationStrategy } from "../lib/isolation.js";
|
|
10
8
|
import { errorMessage, readEnvironmentVariable, writeOutput } from "../lib/util.js";
|
|
11
9
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
12
10
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
13
11
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
14
|
-
// `sbx run` is handled specially because `run` is a subcommand, not a
|
|
15
|
-
// dependency the user installs separately.
|
|
16
12
|
const MAX_TOKENS_PER_CMD = 2;
|
|
17
13
|
async function checkCmd(cmd, required, hint) {
|
|
18
14
|
const path = await which(cmd);
|
|
@@ -27,25 +23,6 @@ async function checkCmd(cmd, required, hint) {
|
|
|
27
23
|
}
|
|
28
24
|
return result;
|
|
29
25
|
}
|
|
30
|
-
async function checkSbxDiagnose() {
|
|
31
|
-
try {
|
|
32
|
-
await runCommandAsync("sbx", ["diagnose"]);
|
|
33
|
-
return {
|
|
34
|
-
name: "sbx diagnose",
|
|
35
|
-
ok: true,
|
|
36
|
-
required: true,
|
|
37
|
-
hint: "Docker Sandboxes ready",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return {
|
|
42
|
-
name: "sbx diagnose",
|
|
43
|
-
ok: false,
|
|
44
|
-
required: true,
|
|
45
|
-
hint: "run `sbx daemon start` and `sbx login`, then retry",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
26
|
function checkEnvironment(name) {
|
|
50
27
|
const value = readEnvironmentVariable(name);
|
|
51
28
|
const set = value !== undefined && value.length > 0;
|
|
@@ -109,22 +86,11 @@ function commandTokensToCheck(cmd) {
|
|
|
109
86
|
}
|
|
110
87
|
return result;
|
|
111
88
|
}
|
|
112
|
-
function gatherToolTokens(config
|
|
89
|
+
function gatherToolTokens(config) {
|
|
113
90
|
const all = new Set();
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (outcome.resolution.resolved === "docker") {
|
|
120
|
-
all.add("sbx");
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (outcome.resolution.resolved === "safehouse") {
|
|
124
|
-
all.add("safehouse");
|
|
125
|
-
}
|
|
126
|
-
// The agent command (claude, codex, …) still has to be on PATH for the
|
|
127
|
-
// host strategies, so tokenize it regardless.
|
|
91
|
+
for (const definition of Object.values(config.models.definitions)) {
|
|
92
|
+
// Local runs execute the agent command on the host; Sprite runs need the
|
|
93
|
+
// same command remotely, but doctor cannot know ticket labels in advance.
|
|
128
94
|
for (const token of commandTokensToCheck(definition.cmd)) {
|
|
129
95
|
all.add(token);
|
|
130
96
|
}
|
|
@@ -169,8 +135,8 @@ export async function doctor() {
|
|
|
169
135
|
writeOutput(`[--] host: ${errorMessage(error)}`);
|
|
170
136
|
return false;
|
|
171
137
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
138
|
+
const localCapability = localCapabilityCheck(host);
|
|
139
|
+
reportLocalCapability(localCapability);
|
|
174
140
|
const workspaceOutcome = resolveWorkspaceOutcome(config, host);
|
|
175
141
|
reportWorkspaceKind(config, workspaceOutcome);
|
|
176
142
|
const checks = [
|
|
@@ -178,29 +144,22 @@ export async function doctor() {
|
|
|
178
144
|
await checkCmd("git", true, "https://git-scm.com/"),
|
|
179
145
|
...(await workspaceChecks(workspaceOutcome)),
|
|
180
146
|
checkDir(config.workspace.projectDir, "workspace.projectDir"),
|
|
181
|
-
|
|
147
|
+
localCapability,
|
|
182
148
|
];
|
|
183
|
-
const toolTokens = gatherToolTokens(config
|
|
184
|
-
let sbxAvailable = false;
|
|
149
|
+
const toolTokens = gatherToolTokens(config);
|
|
185
150
|
for (const token of toolTokens) {
|
|
186
|
-
|
|
187
|
-
if (token === "safehouse") {
|
|
188
|
-
hint = "macOS-only sandbox; edit config.ts to remove if you're not on macOS";
|
|
189
|
-
}
|
|
151
|
+
const required = localCapability.ok;
|
|
190
152
|
// oxlint-disable-next-line no-await-in-loop -- doctor reports tools in deterministic order
|
|
191
|
-
const check = await checkCmd(token,
|
|
153
|
+
const check = await checkCmd(token, required, required ? undefined : "required for local runs; Sprite runs need this inside the Sprite");
|
|
192
154
|
checks.push(check);
|
|
193
|
-
if (token === "sbx" && check.ok) {
|
|
194
|
-
sbxAvailable = true;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (sbxAvailable) {
|
|
198
|
-
checks.push(await checkSbxDiagnose());
|
|
199
155
|
}
|
|
200
156
|
if (anyModelUsesUsage(config)) {
|
|
201
157
|
checks.push(await checkCmd("codexbar", false, "optional — only used for usage gating"));
|
|
202
158
|
}
|
|
203
159
|
for (const check of checks) {
|
|
160
|
+
if (check === localCapability) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
204
163
|
writeOutput(format(check));
|
|
205
164
|
}
|
|
206
165
|
const failed = checks.filter((check) => !check.ok && check.required);
|
|
@@ -212,49 +171,29 @@ export async function doctor() {
|
|
|
212
171
|
writeOutput("All required checks passed.");
|
|
213
172
|
return true;
|
|
214
173
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const resolution = resolveIsolationStrategy({ config, model: name, host });
|
|
220
|
-
out.set(name, { kind: "ok", resolution });
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
const requested = definition.isolation ?? config.models.isolation;
|
|
224
|
-
out.set(name, { kind: "error", requested, reason: errorMessage(error) });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
function isolationChecks(resolutions) {
|
|
230
|
-
const checks = [];
|
|
231
|
-
for (const [name, outcome] of resolutions) {
|
|
232
|
-
if (outcome.kind === "ok") {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
checks.push({
|
|
236
|
-
name: `isolation[${name}]`,
|
|
174
|
+
function localCapabilityCheck(host) {
|
|
175
|
+
if (!host.isMacOS) {
|
|
176
|
+
return {
|
|
177
|
+
name: "local runner (macOS + Safehouse)",
|
|
237
178
|
ok: false,
|
|
238
|
-
required:
|
|
239
|
-
hint:
|
|
240
|
-
}
|
|
179
|
+
required: false,
|
|
180
|
+
hint: "required for local runs; on Linux/WSL use agent-remote with Sprite",
|
|
181
|
+
};
|
|
241
182
|
}
|
|
242
|
-
return
|
|
183
|
+
return {
|
|
184
|
+
name: "local runner (macOS + Safehouse)",
|
|
185
|
+
ok: host.hasSafehouse,
|
|
186
|
+
required: false,
|
|
187
|
+
hint: host.hasSafehouse
|
|
188
|
+
? "ready"
|
|
189
|
+
: "required for local runs; install Safehouse from https://agent-safehouse.dev/ and ensure `safehouse` is on PATH",
|
|
190
|
+
};
|
|
243
191
|
}
|
|
244
|
-
function
|
|
192
|
+
function reportLocalCapability(check) {
|
|
245
193
|
writeOutput();
|
|
246
|
-
writeOutput("
|
|
247
|
-
writeOutput("
|
|
248
|
-
writeOutput(
|
|
249
|
-
for (const [name, outcome] of resolutions) {
|
|
250
|
-
if (outcome.kind === "ok") {
|
|
251
|
-
const { requested, resolved, reason } = outcome.resolution;
|
|
252
|
-
writeOutput(`[ok] ${name}: requested=${requested}, resolved=${resolved} (${reason})`);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
writeOutput(`[--] ${name}: requested=${outcome.requested} — ${outcome.reason}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
194
|
+
writeOutput("Local runner");
|
|
195
|
+
writeOutput("------------");
|
|
196
|
+
writeOutput(format(check));
|
|
258
197
|
}
|
|
259
198
|
function resolveWorkspaceOutcome(config, host) {
|
|
260
199
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../../../packages/groundcrew/src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../../../packages/groundcrew/src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,8 +5,8 @@ import { ensureClearance } from "@clipboard-health/clearance";
|
|
|
5
5
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
6
6
|
import { BUILD_SECRET_NAMES, loadConfig, } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities } from "../lib/host.js";
|
|
8
|
-
import { resolveIsolationStrategy } from "../lib/isolation.js";
|
|
9
8
|
import { buildLaunchCommand, buildSpriteLaunchCommand, shellSingleQuote, } from "../lib/launchCommand.js";
|
|
9
|
+
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
10
10
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
11
11
|
import { workspaces } from "../lib/workspaces.js";
|
|
12
12
|
import { repoDirFor, worktrees } from "../lib/worktrees.js";
|
|
@@ -48,6 +48,11 @@ function stageBuildSecrets(promptDir, secretNames = BUILD_SECRET_NAMES) {
|
|
|
48
48
|
writeFileSync(secretsFile, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
49
49
|
return secretsFile;
|
|
50
50
|
}
|
|
51
|
+
function stageLaunchScript(promptDir, command) {
|
|
52
|
+
const launcherFile = join(promptDir, "launch.sh");
|
|
53
|
+
writeFileSync(launcherFile, `#!/usr/bin/env bash\n${command}\n`, { mode: 0o700 });
|
|
54
|
+
return launcherFile;
|
|
55
|
+
}
|
|
51
56
|
export async function setupWorkspace(config, options, runOptions = {}) {
|
|
52
57
|
const { ticket, repository, model } = options;
|
|
53
58
|
const runner = options.runner ?? "local";
|
|
@@ -64,20 +69,13 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
64
69
|
});
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
host: await detectHostCapabilities(signal),
|
|
71
|
-
});
|
|
72
|
-
log(`Isolation strategy: ${strategy} (${reason})`);
|
|
73
|
-
if (strategy === "safehouse") {
|
|
74
|
-
await ensureClearance({ logger: log });
|
|
75
|
-
}
|
|
76
|
-
const spec = { repository, ticket, model, strategy, runner };
|
|
72
|
+
assertLocalRunnerRequirements(await detectHostCapabilities(signal));
|
|
73
|
+
await ensureClearance({ logger: log });
|
|
74
|
+
const spec = { repository, ticket, model, runner };
|
|
77
75
|
const created = signal === undefined
|
|
78
76
|
? await worktrees.create(config, spec)
|
|
79
77
|
: await worktrees.create(config, spec, signal);
|
|
80
|
-
const { branchName, dir: launchDir
|
|
78
|
+
const { branchName, dir: launchDir } = created;
|
|
81
79
|
const worktreeName = `${repository}-${ticket}`;
|
|
82
80
|
// Anything that fails after the worktree is on disk must roll it back
|
|
83
81
|
// (the worktree and the just-created branch). `workspaces.open` cleans
|
|
@@ -109,8 +107,6 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
109
107
|
definition,
|
|
110
108
|
promptFile,
|
|
111
109
|
worktreeDir: launchDir,
|
|
112
|
-
sandboxName,
|
|
113
|
-
strategy,
|
|
114
110
|
secretsFile,
|
|
115
111
|
});
|
|
116
112
|
log("Opening workspace...");
|
|
@@ -123,9 +119,6 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
123
119
|
log(`Workspace "${ticket}" launched (${model})`);
|
|
124
120
|
log(` Worktree: ${launchDir}`);
|
|
125
121
|
log(` Branch: ${branchName}`);
|
|
126
|
-
if (sandboxName !== undefined) {
|
|
127
|
-
log(` sbx: ${sandboxName}`);
|
|
128
|
-
}
|
|
129
122
|
}
|
|
130
123
|
catch (error) {
|
|
131
124
|
await rollbackWorktree({ config, entry: created, promptDir });
|
|
@@ -148,7 +141,7 @@ async function setupSpriteWorkspace(arguments_) {
|
|
|
148
141
|
throw new Error(`Unknown model: ${model}`);
|
|
149
142
|
}
|
|
150
143
|
log(`Workspace runner: sprite (${config.remote.sprite.spriteName})`);
|
|
151
|
-
const spec = { repository, ticket, model,
|
|
144
|
+
const spec = { repository, ticket, model, runner: "sprite" };
|
|
152
145
|
const created = signal === undefined
|
|
153
146
|
? await worktrees.create(config, spec)
|
|
154
147
|
: await worktrees.create(config, spec, signal);
|
|
@@ -168,7 +161,7 @@ async function setupSpriteWorkspace(arguments_) {
|
|
|
168
161
|
const secretsFile = stageBuildSecrets(promptDir, config.remote.sprite.secretNames);
|
|
169
162
|
const remotePromptFile = `/tmp/groundcrew-${ticket}-prompt.txt`;
|
|
170
163
|
const remoteSecretsFile = secretsFile === undefined ? undefined : `/tmp/groundcrew-${ticket}-secrets.env`;
|
|
171
|
-
const
|
|
164
|
+
const spriteLaunchCommand = buildSpriteLaunchCommand({
|
|
172
165
|
definition,
|
|
173
166
|
spriteName: config.remote.sprite.spriteName,
|
|
174
167
|
promptFile,
|
|
@@ -177,6 +170,7 @@ async function setupSpriteWorkspace(arguments_) {
|
|
|
177
170
|
secretNames: config.remote.sprite.secretNames,
|
|
178
171
|
...(secretsFile === undefined ? {} : { secretsFile, remoteSecretsFile }),
|
|
179
172
|
});
|
|
173
|
+
const launchCmd = `bash ${shellSingleQuote(stageLaunchScript(promptDir, spriteLaunchCommand))}`;
|
|
180
174
|
log("Opening workspace...");
|
|
181
175
|
await workspaces.open(config, {
|
|
182
176
|
name: ticket,
|