@clipboard-health/groundcrew 1.12.6 → 2.1.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/README.md +38 -84
- package/configExample.ts +1 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +0 -11
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +2 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +20 -24
- package/dist/commands/setupWorkspace.d.ts +1 -2
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +8 -77
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/lib/boardSource.d.ts +1 -5
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +6 -13
- package/dist/lib/config.d.ts +4 -22
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +4 -69
- package/dist/lib/launchCommand.d.ts +1 -14
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +3 -46
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +2 -2
- package/dist/lib/util.d.ts +7 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +14 -4
- package/dist/lib/worktrees.d.ts +8 -24
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +94 -353
- package/package.json +2 -2
- package/dist/commands/remoteSetup.d.ts +0 -54
- package/dist/commands/remoteSetup.d.ts.map +0 -1
- package/dist/commands/remoteSetup.js +0 -1032
- package/dist/lib/remoteSetupCommand.d.ts +0 -2
- package/dist/lib/remoteSetupCommand.d.ts.map +0 -1
- package/dist/lib/remoteSetupCommand.js +0 -31
- package/dist/lib/spriteRemoteRunnerProvider.d.ts +0 -76
- package/dist/lib/spriteRemoteRunnerProvider.d.ts.map +0 -1
- package/dist/lib/spriteRemoteRunnerProvider.js +0 -381
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
15
15
|
|
|
16
16
|
## Quickstart
|
|
17
17
|
|
|
18
|
-
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...).
|
|
18
|
+
1. **Install prereqs.** Node 24, `git`, `cmux` _or_ `tmux`, and the agent CLIs themselves (`claude`, `codex`, `cursor-agent`, ...). Groundcrew is **macOS-only** and requires [Safehouse](https://agent-safehouse.dev/) on `PATH`. Optional: `codexbar` for session-usage gating. The `workspaceKind` config key picks the workspace backend (`auto` resolves to cmux when installed, else tmux).
|
|
19
19
|
|
|
20
20
|
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.
|
|
21
21
|
|
|
@@ -49,24 +49,23 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
49
49
|
|
|
50
50
|
`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.
|
|
51
51
|
|
|
52
|
-
4. **Provide a Linear API key.** `crew`
|
|
52
|
+
4. **Provide a Linear API key.** `crew` reads the key from `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. Prefer `GROUNDCREW_LINEAR_API_KEY` so the value does not clash with other tools that consume `LINEAR_API_KEY`. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
# Direct
|
|
56
|
-
export
|
|
56
|
+
export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
|
|
57
57
|
crew doctor
|
|
58
58
|
|
|
59
59
|
# Via 1Password CLI (`op`), if you keep the key in a vault
|
|
60
|
-
echo "
|
|
60
|
+
echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
|
|
61
61
|
op run --env-file .env.1password -- crew doctor
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
- macOS local: `cmux` or `tmux` workspace, Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
|
|
66
|
-
- macOS remote: local `cmux` or `tmux` workspace that launches the configured remote runner.
|
|
67
|
-
- Linux/WSL remote: `cmux` or `tmux` workspace that launches the configured remote runner. Label tickets `agent-remote`; local execution is not supported.
|
|
64
|
+
`LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
|
|
68
65
|
|
|
69
|
-
|
|
66
|
+
5. **Prepare the runner and agent auth.** Groundcrew supports one runner: a `cmux` or `tmux` workspace on macOS, with Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
|
|
67
|
+
|
|
68
|
+
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.
|
|
70
69
|
|
|
71
70
|
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](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`:
|
|
72
71
|
|
|
@@ -84,36 +83,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
84
83
|
|
|
85
84
|
Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines and add only the domains your agents actually need.
|
|
86
85
|
|
|
87
|
-
7. **
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
crew remote setup crew-claude-1 \
|
|
91
|
-
--claude \
|
|
92
|
-
--codex \
|
|
93
|
-
--copy-local-codex-auth \
|
|
94
|
-
--datadog \
|
|
95
|
-
--github \
|
|
96
|
-
--git-name "Your Name" \
|
|
97
|
-
--git-email "you@users.noreply.github.com" \
|
|
98
|
-
--mcp linear \
|
|
99
|
-
--mcp slack \
|
|
100
|
-
--checkpoint
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the remote runner if needed, prepares `~/dev`, configures Git, runs selected auth flows, and adds selected MCP servers to Claude Code. With the Sprite provider, `--claude` detects Claude Code's dynamic localhost OAuth callback port inside the remote runner and temporarily runs `sprite proxy` for that port while login runs. Selected MCP servers authenticate through one interactive Claude Code session by default, and when combined with `--claude`, setup skips the separate headless Claude login and uses that one session for both Claude login and MCP auth while forwarding Claude localhost callback ports. `--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. `--datadog` installs a pinned `pup` release in the remote runner, verifies its checksum, installs the `dd-pup` guidance for Claude and Codex, verifies `pup auth status`, and when auth is missing temporarily runs `sprite proxy` for the OAuth callback while `pup auth login --read-only` runs in the remote runner. Open the Datadog URL printed by `pup` if your terminal does not open it automatically.
|
|
104
|
-
|
|
105
|
-
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 remote runner using `git.remote`, checks out the requested branch (creating it from the base branch when it does not exist on that remote), 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.
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
op run --env-file "${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env" -- \
|
|
109
|
-
crew remote bootstrap crew-claude-1 core-utils --branch rocky-team-123
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
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.
|
|
113
|
-
|
|
114
|
-
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 remote runner under `remote.worktreeRoot`, and asks the configured provider to launch the agent. The Sprite provider emits `sprite exec --tty`. Use `crew remote sessions [<runner-name>]` to inspect active remote sessions and `crew remote attach <session-id-or-command> [--runner <runner-name>]` to attach to one; both commands default to `remote.runnerName` when the runner name is omitted.
|
|
115
|
-
|
|
116
|
-
8. **Run.** Doctor first, then a dry run, then the real thing:
|
|
86
|
+
7. **Run.** Doctor first, then a dry run, then the real thing:
|
|
117
87
|
|
|
118
88
|
```bash
|
|
119
89
|
crew doctor
|
|
@@ -126,37 +96,31 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
126
96
|
|
|
127
97
|
Required fields are marked **required**; everything else has a default and can be omitted from `config.ts`.
|
|
128
98
|
|
|
129
|
-
| Key | Default
|
|
130
|
-
| --------------------------------------- |
|
|
131
|
-
| `linear.projectSlug` | **required**
|
|
132
|
-
| `linear.statuses.todo` | `"Todo"`
|
|
133
|
-
| `linear.statuses.inProgress` | `"In Progress"`
|
|
134
|
-
| `linear.statuses.done` | `"Done"`
|
|
135
|
-
| `linear.statuses.terminal` | `["Done"]`
|
|
136
|
-
| `git.remote` | `"origin"`
|
|
137
|
-
| `git.defaultBranch` | `"main"`
|
|
138
|
-
| `workspace.projectDir` | **required**
|
|
139
|
-
| `workspace.knownRepositories` | **required**
|
|
140
|
-
| `orchestrator.maximumInProgress` | `4`
|
|
141
|
-
| `orchestrator.pollIntervalMilliseconds` | `120_000`
|
|
142
|
-
| `orchestrator.sessionLimitPercentage` | `85`
|
|
143
|
-
| `models.default` | `"claude"`
|
|
144
|
-
| `models.definitions` | `{ claude, codex }`
|
|
145
|
-
| `models.definitions.<name>.cmd` | —
|
|
146
|
-
| `models.definitions.<name>.color` | —
|
|
147
|
-
| `models.definitions.<name>.usage` | optional
|
|
148
|
-
| `models.definitions.<name>.disabled` | optional
|
|
149
|
-
| `prompts.initial` | (template)
|
|
150
|
-
| `workspaceKind` | `"auto"`
|
|
151
|
-
| `
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
| `remote.repoRoot` | `"/home/sprite/dev"` | Shared clone root inside the remote runner. |
|
|
155
|
-
| `remote.worktreeRoot` | `"/home/sprite/groundcrew/worktrees"` | Per-ticket remote worktree root inside the remote runner. |
|
|
156
|
-
| `remote.secretNames` | `["NPM_TOKEN", "BUF_TOKEN"]` | Build-only env vars that may be uploaded for remote dependency setup, then unset before the agent process starts. |
|
|
157
|
-
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
158
|
-
|
|
159
|
-
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). Add `agent-remote` to run that ticket in the configured remote runner instead of locally; `agent-remote` is a modifier label, not a model. **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
|
|
99
|
+
| Key | Default | What it does |
|
|
100
|
+
| --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `config.ts` self-documenting and the lookup survives project renames. |
|
|
102
|
+
| `linear.statuses.todo` | `"Todo"` | Status name picked up for new work. |
|
|
103
|
+
| `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`. |
|
|
104
|
+
| `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup. |
|
|
105
|
+
| `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
|
|
106
|
+
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
|
|
107
|
+
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
|
|
108
|
+
| `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
|
|
109
|
+
| `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. |
|
|
110
|
+
| `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
|
|
111
|
+
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
112
|
+
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
113
|
+
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
114
|
+
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
115
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through Safehouse/clearance. `{{worktree}}` is replaced before launch and legacy `{{sandbox}}` expands to an empty string. |
|
|
116
|
+
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
117
|
+
| `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. |
|
|
118
|
+
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. See "Disabling a shipped default" below. |
|
|
119
|
+
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
120
|
+
| `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. |
|
|
121
|
+
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
122
|
+
|
|
123
|
+
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
|
|
160
124
|
|
|
161
125
|
### Disabling a shipped default
|
|
162
126
|
|
|
@@ -191,34 +155,24 @@ Rules:
|
|
|
191
155
|
## Manual commands
|
|
192
156
|
|
|
193
157
|
```bash
|
|
194
|
-
crew remote setup crew-claude-1 --claude --codex --copy-local-codex-auth --datadog --github --mcp linear --checkpoint
|
|
195
|
-
crew remote bootstrap crew-claude-1 core-utils --branch rocky-team-123
|
|
196
|
-
crew remote sessions
|
|
197
|
-
crew remote attach <session-id-or-command> --runner crew-claude-1
|
|
198
|
-
crew remote ps crew-claude-1
|
|
199
|
-
crew remote interrupt <process-group-id> --runner crew-claude-1
|
|
200
158
|
crew run --ticket <TICKET>
|
|
201
159
|
crew setup repos [--dry-run] [<repo>...]
|
|
202
160
|
crew cleanup <TICKET>
|
|
203
161
|
```
|
|
204
162
|
|
|
205
|
-
`crew run --ticket <TICKET>` provisions a single ticket the same way the orchestrator would: the repo is parsed from the ticket's Linear description
|
|
163
|
+
`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`.
|
|
206
164
|
|
|
207
165
|
## Gotchas
|
|
208
166
|
|
|
209
|
-
- **
|
|
167
|
+
- **Execution is macOS plus Safehouse only.** There is no `models.isolation` strategy and no direct local execution mode. Linux/WSL is not supported.
|
|
210
168
|
- **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.
|
|
211
169
|
- **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`.
|
|
212
|
-
- **Remote cleanup is also conservative.** `crew cleanup` removes tracked remote worktrees and branches, but it does not kill active remote sessions. Use `crew remote sessions [<runner-name>]` to inspect sessions and, with the Sprite provider, `sprite sessions kill -s <runner-name> <session-id>` when Git reports a worktree is busy.
|
|
213
170
|
- **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.
|
|
214
|
-
- **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 remote ps <runner-name>` to inspect the remote process tree; interrupt the tool's child `PGID`, not the agent session `PGID`, with `crew remote interrupt <PGID> --runner <runner-name>`.
|
|
215
|
-
- **Codex auth in remote runners may need auth-file copy.** If `crew remote setup <runner-name> --codex` finishes interactive login but `codex login status` still fails inside the remote runner, rerun with `--copy-local-codex-auth` after confirming local Codex auth works.
|
|
216
|
-
- **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.
|
|
217
171
|
- **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
|
|
218
172
|
- **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
|
|
219
173
|
- **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.
|
|
220
174
|
- **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`.
|
|
221
|
-
- **Claude launches in bypass-permissions mode by default.** Groundcrew creates isolated per-ticket worktrees
|
|
175
|
+
- **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.
|
|
222
176
|
- **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.
|
|
223
177
|
- **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.
|
|
224
178
|
- **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.
|
|
@@ -233,7 +187,7 @@ For developers working on the package itself, clone this repo, run `npm install`
|
|
|
233
187
|
cd ~/dev/c/groundcrew
|
|
234
188
|
node --run crew -- doctor
|
|
235
189
|
|
|
236
|
-
# With 1Password for
|
|
190
|
+
# With 1Password for GROUNDCREW_LINEAR_API_KEY:
|
|
237
191
|
node --run crew:op -- run --watch
|
|
238
192
|
```
|
|
239
193
|
|
package/configExample.ts
CHANGED
|
@@ -38,8 +38,7 @@ export const config: Config = {
|
|
|
38
38
|
// // Additive: defaults for `claude` and `codex` are merged in unless you
|
|
39
39
|
// // re-declare those keys here. Add a third agent (e.g. `cursor`) by
|
|
40
40
|
// // dropping it in this map and tagging tickets with `agent-cursor`.
|
|
41
|
-
// //
|
|
42
|
-
// // Linux/WSL users should label tickets `agent-remote` to use the remote runner.
|
|
41
|
+
// // Groundcrew runs agent commands through Safehouse/clearance unless already Safehouse-wrapped.
|
|
43
42
|
// definitions: {
|
|
44
43
|
// cursor: {
|
|
45
44
|
// cmd: "cursor-agent",
|
|
@@ -64,22 +63,6 @@ export const config: Config = {
|
|
|
64
63
|
// // session and lose status-pill painting (cmux-only feature).
|
|
65
64
|
// workspaceKind: "auto",
|
|
66
65
|
//
|
|
67
|
-
// remote: {
|
|
68
|
-
// // Provider implementation. Sprite is currently the only provider.
|
|
69
|
-
// provider: "sprite",
|
|
70
|
-
// // Tickets labeled `agent-remote` run through this shared remote runner.
|
|
71
|
-
// runnerName: "crew-claude-1",
|
|
72
|
-
// // Bare repository names are cloned as `${owner}/${repo}` inside the remote runner.
|
|
73
|
-
// owner: "ClipboardHealth",
|
|
74
|
-
// // Absolute paths inside the remote runner. Groundcrew creates one shared
|
|
75
|
-
// // clone per repo and one remote git worktree per ticket.
|
|
76
|
-
// repoRoot: "/home/sprite/dev",
|
|
77
|
-
// worktreeRoot: "/home/sprite/groundcrew/worktrees",
|
|
78
|
-
// // Build-only env vars forwarded for remote dependency setup, then
|
|
79
|
-
// // unset before the agent process starts.
|
|
80
|
-
// secretNames: ["NPM_TOKEN", "BUF_TOKEN"],
|
|
81
|
-
// },
|
|
82
|
-
//
|
|
83
66
|
// logging: {
|
|
84
67
|
// // Append-mode log file destination. `log()` / `logEvent()` tee here
|
|
85
68
|
// // in addition to stdout, so a vanished workspace doesn't take the
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA0HA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,6 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { cleanupWorkspaceCli } from "./commands/cleanupWorkspace.js";
|
|
3
3
|
import { doctor } from "./commands/doctor.js";
|
|
4
4
|
import { orchestrate } from "./commands/orchestrator.js";
|
|
5
|
-
import { remoteCli } from "./commands/remoteSetup.js";
|
|
6
5
|
import { setupReposCli } from "./commands/setupRepos.js";
|
|
7
6
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
8
7
|
import { errorMessage, writeError, writeOutput } from "./lib/util.js";
|
|
@@ -77,16 +76,6 @@ const SUBCOMMANDS = {
|
|
|
77
76
|
usage: "repos [--dry-run] [<repo>...]",
|
|
78
77
|
invoke: setupCli,
|
|
79
78
|
},
|
|
80
|
-
remote: {
|
|
81
|
-
summary: "Create, authenticate, bootstrap, and inspect a remote runner",
|
|
82
|
-
usage: "setup <runner-name> [--claude] [--codex] [--datadog] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
83
|
-
" → crew remote bootstrap <runner-name> <repo> [--branch <branch>]\n" +
|
|
84
|
-
" → crew remote sessions [<runner-name>]\n" +
|
|
85
|
-
" → crew remote attach <session-id-or-command> [--runner <runner-name>]\n" +
|
|
86
|
-
" → crew remote ps [<runner-name>]\n" +
|
|
87
|
-
" → crew remote interrupt <process-group-id> [--runner <runner-name>]",
|
|
88
|
-
invoke: remoteCli,
|
|
89
|
-
},
|
|
90
79
|
};
|
|
91
80
|
function printHelp() {
|
|
92
81
|
const width = Math.max(...Object.keys(SUBCOMMANDS).map((key) => key.length));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAmNjE"}
|
|
@@ -57,7 +57,6 @@ export function createDispatcher(deps) {
|
|
|
57
57
|
ticket: verdict.issue.id,
|
|
58
58
|
blockers: verdict.blockers,
|
|
59
59
|
model: verdict.model,
|
|
60
|
-
runner: verdict.issue.runner,
|
|
61
60
|
});
|
|
62
61
|
}
|
|
63
62
|
async function startEligibleIssue(start, dryRun, signal) {
|
|
@@ -68,14 +67,13 @@ export function createDispatcher(deps) {
|
|
|
68
67
|
if (dryRun) {
|
|
69
68
|
log(
|
|
70
69
|
/* v8 ignore next @preserve -- classifyTodo forces recovery=false in dry-run, so the resume branch can't fire here */
|
|
71
|
-
`[dry-run] Would ${recovery ? "resume" : "start"} ${issue.id} in ${issue.repository} (${issue.model}
|
|
70
|
+
`[dry-run] Would ${recovery ? "resume" : "start"} ${issue.id} in ${issue.repository} (${issue.model})`);
|
|
72
71
|
logEvent("dispatch", {
|
|
73
72
|
outcome: "skipped",
|
|
74
73
|
reason: "dry_run",
|
|
75
74
|
ticket: issue.id,
|
|
76
75
|
model: issue.model,
|
|
77
76
|
repository: issue.repository,
|
|
78
|
-
runner: issue.runner,
|
|
79
77
|
});
|
|
80
78
|
return;
|
|
81
79
|
}
|
|
@@ -88,7 +86,6 @@ export function createDispatcher(deps) {
|
|
|
88
86
|
repository: issue.repository,
|
|
89
87
|
ticket: issue.id,
|
|
90
88
|
model: issue.model,
|
|
91
|
-
runner: issue.runner,
|
|
92
89
|
};
|
|
93
90
|
await (signal === undefined
|
|
94
91
|
? setupWorkspace(config, setupOptions)
|
|
@@ -100,7 +97,6 @@ export function createDispatcher(deps) {
|
|
|
100
97
|
ticket: issue.id,
|
|
101
98
|
model: issue.model,
|
|
102
99
|
repository: issue.repository,
|
|
103
|
-
runner: issue.runner,
|
|
104
100
|
});
|
|
105
101
|
}
|
|
106
102
|
catch (error) {
|
|
@@ -110,7 +106,6 @@ export function createDispatcher(deps) {
|
|
|
110
106
|
ticket: issue.id,
|
|
111
107
|
model: issue.model,
|
|
112
108
|
repository: issue.repository,
|
|
113
|
-
runner: issue.runner,
|
|
114
109
|
error: errorMessage(error),
|
|
115
110
|
});
|
|
116
111
|
}
|
|
@@ -183,7 +178,7 @@ export function createDispatcher(deps) {
|
|
|
183
178
|
log(`${slots} slot(s) available, starting ${starts.length} ticket(s): ${starts.map(({ issue }) => `${issue.id}(${issue.model})`).join(", ")}`);
|
|
184
179
|
logEvent("dispatch", {
|
|
185
180
|
outcome: "starting",
|
|
186
|
-
tickets: starts.map(({ issue }) => `${issue.id}:${issue.model}
|
|
181
|
+
tickets: starts.map(({ issue }) => `${issue.id}:${issue.model}`),
|
|
187
182
|
});
|
|
188
183
|
for (const start of starts) {
|
|
189
184
|
// oxlint-disable-next-line no-await-in-loop -- one workspace at a time avoids racing on git
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync, statSync } from "node:fs";
|
|
6
6
|
import { loadConfig } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
8
|
-
import { errorMessage,
|
|
8
|
+
import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
|
|
9
9
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
10
10
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
11
11
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
@@ -23,14 +23,21 @@ async function checkCmd(cmd, required, hint) {
|
|
|
23
23
|
}
|
|
24
24
|
return result;
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
|
|
26
|
+
function checkLinearApiKey() {
|
|
27
|
+
const resolved = resolveLinearApiKey();
|
|
28
|
+
if (resolved !== undefined) {
|
|
29
|
+
return {
|
|
30
|
+
name: "linear api key",
|
|
31
|
+
ok: true,
|
|
32
|
+
required: true,
|
|
33
|
+
hint: `set via $${resolved.source}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
29
36
|
return {
|
|
30
|
-
name:
|
|
31
|
-
ok:
|
|
37
|
+
name: "linear api key",
|
|
38
|
+
ok: false,
|
|
32
39
|
required: true,
|
|
33
|
-
hint:
|
|
40
|
+
hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
function checkDir(path, label) {
|
|
@@ -89,8 +96,6 @@ function commandTokensToCheck(cmd) {
|
|
|
89
96
|
function gatherToolTokens(config) {
|
|
90
97
|
const all = new Set();
|
|
91
98
|
for (const definition of Object.values(config.models.definitions)) {
|
|
92
|
-
// Local runs execute the agent command on the host; remote runs need the
|
|
93
|
-
// same command in the remote runner, but doctor cannot know ticket labels in advance.
|
|
94
99
|
for (const token of commandTokensToCheck(definition.cmd)) {
|
|
95
100
|
all.add(token);
|
|
96
101
|
}
|
|
@@ -140,7 +145,7 @@ export async function doctor() {
|
|
|
140
145
|
const workspaceOutcome = resolveWorkspaceOutcome(config, host);
|
|
141
146
|
reportWorkspaceKind(config, workspaceOutcome);
|
|
142
147
|
const checks = [
|
|
143
|
-
|
|
148
|
+
checkLinearApiKey(),
|
|
144
149
|
await checkCmd("git", true, "https://git-scm.com/"),
|
|
145
150
|
...(await workspaceChecks(workspaceOutcome)),
|
|
146
151
|
checkDir(config.workspace.projectDir, "workspace.projectDir"),
|
|
@@ -150,9 +155,7 @@ export async function doctor() {
|
|
|
150
155
|
for (const token of toolTokens) {
|
|
151
156
|
const required = localCapability.ok;
|
|
152
157
|
// oxlint-disable-next-line no-await-in-loop -- doctor reports tools in deterministic order
|
|
153
|
-
const check = await checkCmd(token, required, required
|
|
154
|
-
? undefined
|
|
155
|
-
: "required for local runs; remote runs need this inside the remote runner");
|
|
158
|
+
const check = await checkCmd(token, required, required ? undefined : "required for local runs");
|
|
156
159
|
checks.push(check);
|
|
157
160
|
}
|
|
158
161
|
if (anyModelUsesUsage(config)) {
|
|
@@ -174,21 +177,14 @@ export async function doctor() {
|
|
|
174
177
|
return true;
|
|
175
178
|
}
|
|
176
179
|
function localCapabilityCheck(host) {
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
name: "local runner (macOS + Safehouse)",
|
|
180
|
-
ok: false,
|
|
181
|
-
required: false,
|
|
182
|
-
hint: "required for local runs; on Linux/WSL use agent-remote with the remote runner",
|
|
183
|
-
};
|
|
184
|
-
}
|
|
180
|
+
const supportsLocalRunner = host.isSafehouseSupported && host.hasSafehouse;
|
|
185
181
|
return {
|
|
186
182
|
name: "local runner (macOS + Safehouse)",
|
|
187
|
-
ok:
|
|
183
|
+
ok: supportsLocalRunner,
|
|
188
184
|
required: false,
|
|
189
|
-
hint:
|
|
185
|
+
hint: supportsLocalRunner
|
|
190
186
|
? "ready"
|
|
191
|
-
: "
|
|
187
|
+
: "groundcrew requires macOS with Safehouse on PATH (install from https://agent-safehouse.dev/)",
|
|
192
188
|
};
|
|
193
189
|
}
|
|
194
190
|
function reportLocalCapability(check) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ResolvedConfig
|
|
1
|
+
import { type ResolvedConfig } from "../lib/config.ts";
|
|
2
2
|
interface TicketDetails {
|
|
3
3
|
title: string;
|
|
4
4
|
description: string;
|
|
@@ -7,7 +7,6 @@ export interface SetupWorkspaceOptions {
|
|
|
7
7
|
ticket: string;
|
|
8
8
|
repository: string;
|
|
9
9
|
model: string;
|
|
10
|
-
runner?: WorkspaceRunner;
|
|
11
10
|
/** When provided, skip the Linear lookup for prompt-template fields. */
|
|
12
11
|
details?: TicketDetails;
|
|
13
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvF,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,CAoEf;AA6CD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -3,15 +3,14 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureClearance } from "@clipboard-health/clearance";
|
|
5
5
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
6
|
-
import { BUILD_SECRET_NAMES, loadConfig
|
|
6
|
+
import { BUILD_SECRET_NAMES, loadConfig } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities } from "../lib/host.js";
|
|
8
|
-
import { buildLaunchCommand,
|
|
8
|
+
import { buildLaunchCommand, shellSingleQuote } from "../lib/launchCommand.js";
|
|
9
9
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
10
10
|
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
11
|
-
import { getRemoteRunnerProvider } from "../lib/spriteRemoteRunnerProvider.js";
|
|
12
11
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
13
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
14
|
-
import {
|
|
13
|
+
import { worktrees } from "../lib/worktrees.js";
|
|
15
14
|
async function fetchTicket(ticket) {
|
|
16
15
|
const client = getLinearClient();
|
|
17
16
|
const issue = await client.issue(ticket.toUpperCase());
|
|
@@ -34,9 +33,9 @@ function renderPrompt(template, variables) {
|
|
|
34
33
|
* dir is `rm -rf`'d by the launch command (and rollback path), so cleanup
|
|
35
34
|
* is already handled.
|
|
36
35
|
*/
|
|
37
|
-
function stageBuildSecrets(promptDir
|
|
36
|
+
function stageBuildSecrets(promptDir) {
|
|
38
37
|
const lines = [];
|
|
39
|
-
for (const name of
|
|
38
|
+
for (const name of BUILD_SECRET_NAMES) {
|
|
40
39
|
const value = readEnvironmentVariable(name);
|
|
41
40
|
if (value === undefined || value.length === 0) {
|
|
42
41
|
continue;
|
|
@@ -71,23 +70,14 @@ function stagePrompt(input) {
|
|
|
71
70
|
}
|
|
72
71
|
export async function setupWorkspace(config, options, runOptions = {}) {
|
|
73
72
|
const { ticket, repository, model } = options;
|
|
74
|
-
const runner = options.runner ?? "local";
|
|
75
73
|
const { signal } = runOptions;
|
|
76
74
|
const definition = config.models.definitions[model];
|
|
77
75
|
if (!definition) {
|
|
78
76
|
throw new Error(`Unknown model: ${model}`);
|
|
79
77
|
}
|
|
80
|
-
if (runner === "remote") {
|
|
81
|
-
await setupRemoteWorkspace({
|
|
82
|
-
config,
|
|
83
|
-
options: { ...options, runner },
|
|
84
|
-
...(signal === undefined ? {} : { signal }),
|
|
85
|
-
});
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
78
|
assertLocalRunnerRequirements(await detectHostCapabilities(signal));
|
|
89
79
|
await ensureClearance({ logger: log });
|
|
90
|
-
const spec = { repository, ticket
|
|
80
|
+
const spec = { repository, ticket };
|
|
91
81
|
const created = signal === undefined
|
|
92
82
|
? await worktrees.create(config, spec)
|
|
93
83
|
: await worktrees.create(config, spec, signal);
|
|
@@ -136,64 +126,6 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
136
126
|
throw error;
|
|
137
127
|
}
|
|
138
128
|
}
|
|
139
|
-
async function resolveTicketDetails(options) {
|
|
140
|
-
if (options.details !== undefined) {
|
|
141
|
-
return options.details;
|
|
142
|
-
}
|
|
143
|
-
log(`Fetching ${options.ticket} from Linear...`);
|
|
144
|
-
return await fetchTicket(options.ticket);
|
|
145
|
-
}
|
|
146
|
-
async function setupRemoteWorkspace(arguments_) {
|
|
147
|
-
const { config, options, signal } = arguments_;
|
|
148
|
-
const { ticket, repository, model } = options;
|
|
149
|
-
const definition = config.models.definitions[model];
|
|
150
|
-
/* v8 ignore next 3 @preserve -- setupWorkspace validates the model before routing here */
|
|
151
|
-
if (definition === undefined) {
|
|
152
|
-
throw new Error(`Unknown model: ${model}`);
|
|
153
|
-
}
|
|
154
|
-
log(`Workspace runner: remote (${config.remote.provider}:${config.remote.runnerName})`);
|
|
155
|
-
const spec = { repository, ticket, model, runner: "remote" };
|
|
156
|
-
const created = signal === undefined
|
|
157
|
-
? await worktrees.create(config, spec)
|
|
158
|
-
: await worktrees.create(config, spec, signal);
|
|
159
|
-
const { branchName, dir: remoteWorktreeDir } = created;
|
|
160
|
-
const worktreeName = `${repository}-${ticket}`;
|
|
161
|
-
let promptDir;
|
|
162
|
-
try {
|
|
163
|
-
const ticketDetails = await resolveTicketDetails(options);
|
|
164
|
-
const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
|
|
165
|
-
promptDir = stagedPrompt.directory;
|
|
166
|
-
const secretsFile = stageBuildSecrets(promptDir, config.remote.secretNames);
|
|
167
|
-
const remotePromptFile = `/tmp/groundcrew-${ticket}-prompt.txt`;
|
|
168
|
-
const remoteSecretsFile = secretsFile === undefined ? undefined : `/tmp/groundcrew-${ticket}-secrets.env`;
|
|
169
|
-
const remoteLaunchCommand = buildRemoteLaunchCommand({
|
|
170
|
-
definition,
|
|
171
|
-
provider: getRemoteRunnerProvider(config.remote.provider),
|
|
172
|
-
remoteConfig: config.remote,
|
|
173
|
-
promptFile: stagedPrompt.file,
|
|
174
|
-
remotePromptFile,
|
|
175
|
-
worktreeDir: remoteWorktreeDir,
|
|
176
|
-
secretNames: config.remote.secretNames,
|
|
177
|
-
...(secretsFile === undefined ? {} : { secretsFile, remoteSecretsFile }),
|
|
178
|
-
});
|
|
179
|
-
const launchCmd = stageWorkspaceLaunchCommand(promptDir, remoteLaunchCommand);
|
|
180
|
-
log("Opening workspace...");
|
|
181
|
-
await workspaces.open(config, {
|
|
182
|
-
name: ticket,
|
|
183
|
-
cwd: repoDirFor(config, repository),
|
|
184
|
-
command: launchCmd,
|
|
185
|
-
status: { text: `${model}:remote`, color: definition.color, icon: "sparkle" },
|
|
186
|
-
}, signal);
|
|
187
|
-
log(`Workspace "${ticket}" launched (${model}, remote)`);
|
|
188
|
-
log(` Worktree: ${remoteWorktreeDir}`);
|
|
189
|
-
log(` Branch: ${branchName}`);
|
|
190
|
-
log(` Remote: ${config.remote.provider}:${config.remote.runnerName}`);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
await rollbackWorktree({ config, entry: created, promptDir });
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
129
|
async function rollbackWorktree(arguments_) {
|
|
198
130
|
log(`Setup failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.ticket}...`);
|
|
199
131
|
let result;
|
|
@@ -234,16 +166,15 @@ export async function setupWorkspaceCli(ticket, options = {}) {
|
|
|
234
166
|
const config = await loadConfig();
|
|
235
167
|
const client = getLinearClient();
|
|
236
168
|
const resolved = await fetchResolvedIssue({ client, config, ticket });
|
|
237
|
-
log(`Resolved ${ticket}: repository=${resolved.repository}, model=${resolved.model}
|
|
169
|
+
log(`Resolved ${ticket}: repository=${resolved.repository}, model=${resolved.model}`);
|
|
238
170
|
if (options.dryRun === true) {
|
|
239
|
-
log(`[dry-run] Would launch ${ticket} in ${resolved.repository} (${resolved.model}
|
|
171
|
+
log(`[dry-run] Would launch ${ticket} in ${resolved.repository} (${resolved.model})`);
|
|
240
172
|
return;
|
|
241
173
|
}
|
|
242
174
|
await setupWorkspace(config, {
|
|
243
175
|
ticket: ticket.toLowerCase(),
|
|
244
176
|
repository: resolved.repository,
|
|
245
177
|
model: resolved.model,
|
|
246
|
-
runner: resolved.runner,
|
|
247
178
|
details: { title: resolved.title, description: resolved.description },
|
|
248
179
|
});
|
|
249
180
|
await createLinearIssueStatusUpdater({ config, client }).markInProgress({
|