@clipboard-health/groundcrew 2.2.0 → 2.3.1
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 +72 -8
- package/configExample.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -10
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +10 -35
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -13
- package/dist/commands/eligibility.d.ts +14 -0
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +44 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +23 -4
- package/dist/commands/ticketDoctor.d.ts +48 -0
- package/dist/commands/ticketDoctor.d.ts.map +1 -0
- package/dist/commands/ticketDoctor.js +402 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/boardSource.d.ts +55 -0
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +171 -26
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +75 -7
- package/dist/lib/dockerSandbox.d.ts +40 -0
- package/dist/lib/dockerSandbox.d.ts.map +1 -0
- package/dist/lib/dockerSandbox.js +58 -0
- package/dist/lib/host.d.ts +10 -0
- package/dist/lib/host.d.ts.map +1 -1
- package/dist/lib/host.js +8 -3
- package/dist/lib/launchCommand.d.ts +17 -3
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +66 -8
- package/dist/lib/localRunner.d.ts +22 -1
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +48 -5
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +138 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,12 @@ 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`, ...). Pick a local isolation backend below depending on platform and what the agent needs to do. Optional: `codexbar` for session-usage gating. The `workspaceKind` config key picks the workspace backend (`auto` resolves to cmux when installed, else tmux).
|
|
19
|
+
- **`safehouse`** (macOS default) — [Safehouse](https://agent-safehouse.dev/) on `PATH`. The fastest local backend; cannot safely give the agent Docker.
|
|
20
|
+
- **`sdx`** (Linux default, macOS opt-in) — [Docker Sandboxes](https://docs.docker.com/sandboxes/) (`sbx`) on `PATH`. Required when a ticket needs the agent to use Docker (`docker build`, `docker run`, integration tests). Each model that should run under sdx needs a `sandbox: { agent: "<sbx-agent>" }` block in `config.ts` so groundcrew knows which sbx agent to address.
|
|
21
|
+
- **`none`** — explicit unsandboxed escape hatch. Never picked implicitly; `crew doctor` warns when it is configured.
|
|
22
|
+
|
|
23
|
+
Groundcrew resolves `local.runner` per platform: macOS → `safehouse`, Linux/WSL → `sdx`. Set `local.runner` to `"safehouse" | "sdx" | "none"` to override; leave at `"auto"` for the platform default.
|
|
19
24
|
|
|
20
25
|
2. **Create a Linear project to scope your work.** Any team works — make a project inside it and drop tickets in. The orchestrator polls by project, not by team, so you don't need a dedicated team.
|
|
21
26
|
|
|
@@ -63,11 +68,11 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
63
68
|
|
|
64
69
|
`LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
|
|
65
70
|
|
|
66
|
-
5. **Prepare the runner and agent auth.** Groundcrew
|
|
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.
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
For sdx, sandboxes are created automatically on first launch. Groundcrew names them deterministically as `groundcrew-<repository>-<model>`, probes `sbx ls`, and runs `sbx create [--template ...] [--kit ...] <agent> <projectDir>` when missing — `projectDir` is the mount so every per-ticket worktree (created as a sibling of the bare clone) is visible inside the sandbox. First-time agent auth happens inside the sandbox the first time the agent runs via `sbx exec`. To bootstrap manually instead, run `sbx create --name groundcrew-<repo>-<model> <agent> <projectDir>` once.
|
|
69
74
|
|
|
70
|
-
6. **Set the clearance allowlist for local
|
|
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`:
|
|
71
76
|
|
|
72
77
|
```bash
|
|
73
78
|
CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
|
|
@@ -87,6 +92,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
87
92
|
|
|
88
93
|
```bash
|
|
89
94
|
crew doctor
|
|
95
|
+
crew doctor --ticket TEAM-123
|
|
90
96
|
crew run --dry-run
|
|
91
97
|
crew run # one-shot
|
|
92
98
|
crew run --watch # poll forever
|
|
@@ -112,12 +118,14 @@ Required fields are marked **required**; everything else has a default and can b
|
|
|
112
118
|
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
113
119
|
| `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
120
|
| `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
|
|
121
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
|
|
116
122
|
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
117
123
|
| `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
|
+
| `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). |
|
|
118
125
|
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. See "Disabling a shipped default" below. |
|
|
119
126
|
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
120
127
|
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. tmux windows live in a dedicated `groundcrew` session. |
|
|
128
|
+
| `local.runner` | `"auto"` | Local isolation backend. `"auto"` resolves to `safehouse` on macOS and `sdx` on Linux/WSL. Explicit values: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly — doctor warns when it is configured. |
|
|
121
129
|
| `logging.file` | XDG state path | Append-mode log file destination. `log()` / `logEvent()` tee here in addition to stdout, so a vanished workspace doesn't take the evidence with it. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
122
130
|
|
|
123
131
|
The branch prefix (`<prefix>-<TICKET>`) is derived from your OS username (`os.userInfo().username`), not configured. Agent selection looks for a top-level Linear label named `agent-<model>` (e.g. `agent-claude`, `agent-codex`). **`crew run` without `--ticket` only fetches tickets with an `agent-*` label** — the GraphQL query filters them server-side, so unlabeled tickets are never returned by Linear's API and do not appear in the rendered board. Use `crew run --ticket <TICKET>` to provision an unlabeled ticket on demand (manual setup falls back to `models.default`). The reserved label `agent-any` routes the ticket to the configured model with the most available session capacity (lowest codexbar session-used percent), skipping any model already over `sessionLimitPercentage`. With no usage data, `agent-any` resolves to `models.default`. The name `any` cannot be used in `models.definitions`. Todo tickets blocked by Linear issues that are not in `linear.statuses.terminal` are skipped until their blockers reach a terminal status.
|
|
@@ -155,25 +163,81 @@ Rules:
|
|
|
155
163
|
## Manual commands
|
|
156
164
|
|
|
157
165
|
```bash
|
|
166
|
+
crew doctor --ticket <TICKET>
|
|
158
167
|
crew run --ticket <TICKET>
|
|
159
168
|
crew setup repos [--dry-run] [<repo>...]
|
|
160
169
|
crew cleanup <TICKET>
|
|
161
170
|
```
|
|
162
171
|
|
|
172
|
+
`crew doctor --ticket <TICKET>` diagnoses one Linear ticket without provisioning it. It checks the same resolution inputs that decide whether the orchestrator can see the ticket — Todo status, `agent-*` label, model resolution, repository mention, local clone — then checks blockers, model session usage, and available in-progress capacity.
|
|
173
|
+
|
|
163
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`.
|
|
164
175
|
|
|
176
|
+
### `crew doctor --ticket <ticket>`
|
|
177
|
+
|
|
178
|
+
Diagnose why a ticket would or wouldn't be dispatched on the next orchestrator tick. Runs the same resolution and eligibility chain as the dispatcher, but for a single ticket, and prints a tree of pass/fail checks.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
crew doctor --ticket HRD-446
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Exits 0 if the ticket would dispatch, 1 otherwise. Useful when you've labelled a ticket with `agent-claude` and it doesn't show up on the board.
|
|
185
|
+
|
|
186
|
+
Example output for a ticket that would dispatch:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
groundcrew doctor --ticket HRD-446 (Add retry logic to the sync job)
|
|
190
|
+
────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
Resolution
|
|
193
|
+
[ok] Ticket exists in Linear ("Add retry logic to the sync job")
|
|
194
|
+
[ok] Status is Todo
|
|
195
|
+
[ok] Has agent-* label (agent-claude)
|
|
196
|
+
[ok] Model resolves from agent-* label (model "claude")
|
|
197
|
+
[ok] Description mentions known repo (owner/repo)
|
|
198
|
+
[ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
|
|
199
|
+
|
|
200
|
+
Eligibility
|
|
201
|
+
[ok] No active blockers
|
|
202
|
+
[ok] Model "claude" usage under sessionLimitPercentage (12% (limit 85%))
|
|
203
|
+
[ok] In-progress cap not hit (2/4 used)
|
|
204
|
+
|
|
205
|
+
→ would be dispatched on next tick
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Example output for a ticket that's not in the Todo status:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
groundcrew doctor --ticket HRD-447 (Refactor auth middleware)
|
|
212
|
+
─────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
Resolution
|
|
215
|
+
[ok] Ticket exists in Linear ("Refactor auth middleware")
|
|
216
|
+
[--] Status is Todo (current: In Progress)
|
|
217
|
+
[ok] Has agent-* label (agent-claude)
|
|
218
|
+
[ok] Model resolves from agent-* label (model "claude")
|
|
219
|
+
[ok] Description mentions known repo (owner/repo)
|
|
220
|
+
[ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
|
|
221
|
+
|
|
222
|
+
Eligibility
|
|
223
|
+
(skipped — resolution checks failed)
|
|
224
|
+
|
|
225
|
+
→ ineligible: status is In Progress (need Todo)
|
|
226
|
+
```
|
|
227
|
+
|
|
165
228
|
## Gotchas
|
|
166
229
|
|
|
167
|
-
- **
|
|
230
|
+
- **Ticket labeled but not on the board?** Run `crew doctor --ticket <ticket>` — it lists every check the dispatcher runs and flags the failing one.
|
|
231
|
+
- **Local execution picks one of safehouse/sdx/none.** `local.runner: "auto"` resolves to `safehouse` on macOS and `sdx` (Docker Sandboxes) on Linux/WSL. Override with `local.runner: "safehouse" | "sdx" | "none"`. There is no per-model `isolation` knob anymore — the runner is global. `sdx` requires a per-model `sandbox: { agent }` block so groundcrew can map the model to an sbx agent.
|
|
168
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.
|
|
169
|
-
- **
|
|
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`.
|
|
170
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.
|
|
171
235
|
- **Status names matter.** If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
|
|
172
236
|
- **Leaf-only.** Parent issues with children are ignored — sub-issues are the work items.
|
|
173
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.
|
|
174
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`.
|
|
175
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.
|
|
176
|
-
- **Doctor's command introspection is shallow.** Doctor reports
|
|
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.
|
|
177
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.
|
|
178
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.
|
|
179
243
|
- **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
|
package/configExample.ts
CHANGED
|
@@ -44,9 +44,18 @@ export const config: Config = {
|
|
|
44
44
|
// cmd: "cursor-agent",
|
|
45
45
|
// color: "#929292",
|
|
46
46
|
// },
|
|
47
|
+
// // To run a model under the sdx (Docker Sandboxes) runner, bind it to
|
|
48
|
+
// // an sbx agent. Required when `local.runner` resolves to `sdx`.
|
|
49
|
+
// // claude: { sandbox: { agent: "claude" } },
|
|
47
50
|
// },
|
|
48
51
|
// },
|
|
49
52
|
//
|
|
53
|
+
// // Local isolation backend. Defaults to `"auto"` — macOS → safehouse,
|
|
54
|
+
// // Linux → sdx (Docker Sandboxes). `"none"` is an explicit unsandboxed
|
|
55
|
+
// // escape hatch and is never picked implicitly. Switch to `"sdx"` on
|
|
56
|
+
// // macOS when you need an agent to use Docker safely.
|
|
57
|
+
// local: { runner: "auto" },
|
|
58
|
+
//
|
|
50
59
|
// prompts: {
|
|
51
60
|
// initial: [
|
|
52
61
|
// "Begin work on {{ticket}} ({{title}}) in the {{worktree}} wt subdirectory.",
|
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":"AAyIA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,24 @@ async function runCli(argv) {
|
|
|
51
51
|
}
|
|
52
52
|
await setupWorkspaceCli(ticket, { dryRun });
|
|
53
53
|
}
|
|
54
|
+
async function doctorCli(argv) {
|
|
55
|
+
let ticket;
|
|
56
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
57
|
+
const argument = argv[index];
|
|
58
|
+
if (argument === "--ticket") {
|
|
59
|
+
const value = argv[index + 1];
|
|
60
|
+
if (value === undefined || value.length === 0 || value.startsWith("-")) {
|
|
61
|
+
throw new Error("crew doctor --ticket: ticket id is required");
|
|
62
|
+
}
|
|
63
|
+
ticket = value;
|
|
64
|
+
index += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`crew doctor: unknown argument: ${argument}`);
|
|
68
|
+
}
|
|
69
|
+
const ok = ticket === undefined ? await doctor() : await doctor({ ticket });
|
|
70
|
+
process.exitCode = ok ? process.exitCode : 1;
|
|
71
|
+
}
|
|
54
72
|
const SUBCOMMANDS = {
|
|
55
73
|
run: {
|
|
56
74
|
summary: "Run the orchestrator (one-shot by default), or provision one ticket with --ticket",
|
|
@@ -58,13 +76,9 @@ const SUBCOMMANDS = {
|
|
|
58
76
|
invoke: runCli,
|
|
59
77
|
},
|
|
60
78
|
doctor: {
|
|
61
|
-
summary: "Verify prereqs
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!ok) {
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
}
|
|
67
|
-
},
|
|
79
|
+
summary: "Verify prereqs, or diagnose one ticket with --ticket",
|
|
80
|
+
usage: "[--ticket <ticket>]",
|
|
81
|
+
invoke: doctorCli,
|
|
68
82
|
},
|
|
69
83
|
cleanup: {
|
|
70
84
|
summary: "Tear down a worktree",
|
|
@@ -87,9 +101,7 @@ function printHelp() {
|
|
|
87
101
|
writeOutput("Commands:");
|
|
88
102
|
for (const [name, command] of Object.entries(SUBCOMMANDS)) {
|
|
89
103
|
writeOutput(` ${name.padEnd(width)} ${command.summary}`);
|
|
90
|
-
|
|
91
|
-
writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
|
|
92
|
-
}
|
|
104
|
+
writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
|
|
93
105
|
}
|
|
94
106
|
writeOutput("\nSee README.md for full configuration and behavior.");
|
|
95
107
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAuLjE"}
|
|
@@ -10,42 +10,16 @@ import { isGroundcrewIssue } from "../lib/boardSource.js";
|
|
|
10
10
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
11
11
|
import { errorMessage, log, logEvent } from "../lib/util.js";
|
|
12
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
13
|
-
import { classifyBlockers, classifyEligibility, } from "./eligibility.js";
|
|
13
|
+
import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, } from "./eligibility.js";
|
|
14
14
|
import { setupWorkspace } from "./setupWorkspace.js";
|
|
15
|
-
const PERCENT_FRACTION_DIVISOR = 100;
|
|
16
|
-
const DAYS_PER_WEEK = 7;
|
|
17
|
-
const MINUTES_PER_DAY = 24 * 60;
|
|
18
|
-
const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
|
|
19
15
|
export function createDispatcher(deps) {
|
|
20
16
|
const { config, client } = deps;
|
|
21
17
|
const issueStatusUpdater = createLinearIssueStatusUpdater({ config, client });
|
|
22
18
|
function buildExhaustedSet(usage) {
|
|
23
19
|
const exhausted = new Set();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
exhausted.add(model);
|
|
28
|
-
const pct = (snapshot.session * PERCENT_FRACTION_DIVISOR).toFixed(0);
|
|
29
|
-
const mins = snapshot.sessionEndDuration ?? "?";
|
|
30
|
-
log(`${model} session at ${pct}% (> ${sessionLimit}%), resets in ${mins}m — skipping its tickets`);
|
|
31
|
-
}
|
|
32
|
-
// Weekly gate paces total weekly usage against day buckets from the
|
|
33
|
-
// weekly reset. Day 1's budget is available immediately after rollover,
|
|
34
|
-
// then each later day opens another 1/7 of the weekly budget. Skipped when:
|
|
35
|
-
// - weekly is null (no codexbar secondary window this tick)
|
|
36
|
-
// - weekly is non-finite (EXHAUSTED_USAGE — session gate above
|
|
37
|
-
// already pins it to Infinity)
|
|
38
|
-
// - weekEndDuration is null (can't compute where we are in week)
|
|
39
|
-
if (snapshot.weekly !== null &&
|
|
40
|
-
Number.isFinite(snapshot.weekly) &&
|
|
41
|
-
snapshot.weekEndDuration !== null) {
|
|
42
|
-
const usedPct = snapshot.weekly * PERCENT_FRACTION_DIVISOR;
|
|
43
|
-
const allowedPct = weeklyPacedBudgetPercentage(snapshot.weekEndDuration);
|
|
44
|
-
if (usedPct > allowedPct) {
|
|
45
|
-
exhausted.add(model);
|
|
46
|
-
log(`${model} weekly at ${usedPct.toFixed(1)}% (> ${allowedPct.toFixed(1)}% paced budget), resets in ${snapshot.weekEndDuration}m — skipping its tickets`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
20
|
+
for (const exhaustion of classifyUsageExhaustion(config, usage)) {
|
|
21
|
+
exhausted.add(exhaustion.model);
|
|
22
|
+
log(formatUsageExhaustion(exhaustion));
|
|
49
23
|
}
|
|
50
24
|
return exhausted;
|
|
51
25
|
}
|
|
@@ -187,9 +161,10 @@ export function createDispatcher(deps) {
|
|
|
187
161
|
}
|
|
188
162
|
return { runOnce };
|
|
189
163
|
}
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
164
|
+
function formatUsageExhaustion(exhaustion) {
|
|
165
|
+
if (exhaustion.kind === "session") {
|
|
166
|
+
const mins = exhaustion.resetMinutes ?? "?";
|
|
167
|
+
return `${exhaustion.model} session at ${exhaustion.usedPercentage.toFixed(0)}% (> ${exhaustion.limitPercentage}%), resets in ${mins}m — skipping its tickets`;
|
|
168
|
+
}
|
|
169
|
+
return `${exhaustion.model} weekly at ${exhaustion.usedPercentage.toFixed(1)}% (> ${exhaustion.allowedPercentage.toFixed(1)}% paced budget), resets in ${exhaustion.resetMinutes}m — skipping its tickets`;
|
|
195
170
|
}
|
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
* doctor — verify groundcrew prerequisites against the resolved config.
|
|
3
3
|
* Returns true if every required check passes; false otherwise.
|
|
4
4
|
*/
|
|
5
|
-
export
|
|
5
|
+
export interface DoctorOptions {
|
|
6
|
+
ticket?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function doctor(options?: DoctorOptions): Promise<boolean>;
|
|
6
9
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA2BH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsHD,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,10 +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 { loadConfig } from "../lib/config.js";
|
|
6
|
+
import { loadConfig, } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
8
|
+
import { resolveLocalRunner } from "../lib/localRunner.js";
|
|
8
9
|
import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
|
|
9
10
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
11
|
+
import { runTicketDoctor } from "./ticketDoctor.js";
|
|
10
12
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
11
13
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
12
14
|
const MAX_TOKENS_PER_CMD = 2;
|
|
@@ -120,7 +122,26 @@ function format(check) {
|
|
|
120
122
|
const hint = check.hint !== undefined && check.hint.length > 0 ? ` — ${check.hint}` : "";
|
|
121
123
|
return `${tag}${check.name}${hint}`;
|
|
122
124
|
}
|
|
123
|
-
export async function doctor() {
|
|
125
|
+
export async function doctor(options = {}) {
|
|
126
|
+
if (options.ticket !== undefined) {
|
|
127
|
+
return await doctorTicket(options.ticket);
|
|
128
|
+
}
|
|
129
|
+
return await doctorHost();
|
|
130
|
+
}
|
|
131
|
+
async function doctorTicket(ticket) {
|
|
132
|
+
try {
|
|
133
|
+
return await runTicketDoctor(ticket);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const displayTicket = ticket.toUpperCase();
|
|
137
|
+
const header = `groundcrew doctor --ticket ${displayTicket}`;
|
|
138
|
+
writeOutput(header);
|
|
139
|
+
writeOutput("=".repeat(header.length));
|
|
140
|
+
writeOutput(`[--] config: ${errorMessage(error)}`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function doctorHost() {
|
|
124
145
|
writeOutput("groundcrew doctor");
|
|
125
146
|
writeOutput("=================");
|
|
126
147
|
let config;
|
|
@@ -140,8 +161,13 @@ export async function doctor() {
|
|
|
140
161
|
writeOutput(`[--] host: ${errorMessage(error)}`);
|
|
141
162
|
return false;
|
|
142
163
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
164
|
+
const resolvedRunner = resolveLocalRunner(config.local.runner, host);
|
|
165
|
+
const localCapability = localCapabilityCheck(host, resolvedRunner);
|
|
166
|
+
reportLocalCapability({
|
|
167
|
+
check: localCapability,
|
|
168
|
+
setting: config.local.runner,
|
|
169
|
+
resolved: resolvedRunner,
|
|
170
|
+
});
|
|
145
171
|
const workspaceOutcome = resolveWorkspaceOutcome(config, host);
|
|
146
172
|
reportWorkspaceKind(config, workspaceOutcome);
|
|
147
173
|
const checks = [
|
|
@@ -176,22 +202,43 @@ export async function doctor() {
|
|
|
176
202
|
writeOutput("All required checks passed.");
|
|
177
203
|
return true;
|
|
178
204
|
}
|
|
179
|
-
function localCapabilityCheck(host) {
|
|
180
|
-
|
|
205
|
+
function localCapabilityCheck(host, resolved) {
|
|
206
|
+
if (resolved === "safehouse") {
|
|
207
|
+
const ok = host.isSafehouseSupported && host.hasSafehouse;
|
|
208
|
+
return {
|
|
209
|
+
name: "local runner (safehouse)",
|
|
210
|
+
ok,
|
|
211
|
+
required: false,
|
|
212
|
+
hint: ok
|
|
213
|
+
? "ready"
|
|
214
|
+
: "safehouse runner requires macOS with `safehouse` on PATH (install from https://agent-safehouse.dev/)",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (resolved === "sdx") {
|
|
218
|
+
const ok = host.isSdxSupported && host.hasSbx;
|
|
219
|
+
return {
|
|
220
|
+
name: "local runner (sdx)",
|
|
221
|
+
ok,
|
|
222
|
+
required: false,
|
|
223
|
+
hint: ok
|
|
224
|
+
? "ready"
|
|
225
|
+
: "sdx runner requires `sbx` (Docker Sandboxes) on PATH (install from https://docs.docker.com/sandboxes/)",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// resolved === "none"
|
|
181
229
|
return {
|
|
182
|
-
name: "local runner (
|
|
183
|
-
ok:
|
|
230
|
+
name: "local runner (none)",
|
|
231
|
+
ok: true,
|
|
184
232
|
required: false,
|
|
185
|
-
hint:
|
|
186
|
-
? "ready"
|
|
187
|
-
: "groundcrew requires macOS with Safehouse on PATH (install from https://agent-safehouse.dev/)",
|
|
233
|
+
hint: "WARNING: local.runner='none' — agent runs unsandboxed on the host. Only use this when you understand the implications.",
|
|
188
234
|
};
|
|
189
235
|
}
|
|
190
|
-
function reportLocalCapability(
|
|
236
|
+
function reportLocalCapability(arguments_) {
|
|
191
237
|
writeOutput();
|
|
192
238
|
writeOutput("Local runner");
|
|
193
239
|
writeOutput("------------");
|
|
194
|
-
writeOutput(
|
|
240
|
+
writeOutput(`requested: ${arguments_.setting} → resolved: ${arguments_.resolved}`);
|
|
241
|
+
writeOutput(format(arguments_.check));
|
|
195
242
|
}
|
|
196
243
|
function resolveWorkspaceOutcome(config, host) {
|
|
197
244
|
try {
|
|
@@ -37,6 +37,19 @@ export interface SkipVerdict {
|
|
|
37
37
|
model?: string;
|
|
38
38
|
}
|
|
39
39
|
type Verdict = StartVerdict | SkipVerdict;
|
|
40
|
+
export type ModelUsageExhaustion = {
|
|
41
|
+
kind: "session";
|
|
42
|
+
model: string;
|
|
43
|
+
usedPercentage: number;
|
|
44
|
+
limitPercentage: number;
|
|
45
|
+
resetMinutes: number | null;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "weekly";
|
|
48
|
+
model: string;
|
|
49
|
+
usedPercentage: number;
|
|
50
|
+
allowedPercentage: number;
|
|
51
|
+
resetMinutes: number;
|
|
52
|
+
};
|
|
40
53
|
export interface ClassifyArguments {
|
|
41
54
|
config: ResolvedConfig;
|
|
42
55
|
/**
|
|
@@ -68,6 +81,7 @@ interface BlockerClassification {
|
|
|
68
81
|
* falls back to the default predictably.
|
|
69
82
|
*/
|
|
70
83
|
export declare function pickBestModel(config: ResolvedConfig, usage: UsageByModel, exhausted: Set<string>): string | undefined;
|
|
84
|
+
export declare function classifyUsageExhaustion(config: ResolvedConfig, usage: UsageByModel): ModelUsageExhaustion[];
|
|
71
85
|
/**
|
|
72
86
|
* Cheap pre-pass — partitions Todo into unblocked issues and blocker
|
|
73
87
|
* skip verdicts. Runs before the dispatcher fetches usage or probes the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAgB,KAAK,eAAe,EAAoB,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAgB,KAAK,eAAe,EAAoB,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOzD,KAAK,UAAU,GACX,SAAS,GACT,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,4BAA4B,GAC5B,mBAAmB,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,WAAW,EAAE,UAAU,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,OAAO,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,qBAAqB;IAC7B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAqCD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,MAAM,GAAG,SAAS,CAepB;AAaD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,GAClB,oBAAoB,EAAE,CAmCxB;AA4CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,qBAAqB,CAYvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,EAAE,CAgE5E"}
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { isTerminalStatus } from "../lib/boardSource.js";
|
|
10
10
|
import { AGENT_ANY_MODEL } from "../lib/config.js";
|
|
11
|
+
const PERCENT_FRACTION_DIVISOR = 100;
|
|
12
|
+
const DAYS_PER_WEEK = 7;
|
|
13
|
+
const MINUTES_PER_DAY = 24 * 60;
|
|
14
|
+
const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
|
|
11
15
|
function blockerSummary(blocker) {
|
|
12
16
|
return `${blocker.id}:${blocker.status ?? "missing"}`;
|
|
13
17
|
}
|
|
@@ -59,6 +63,46 @@ export function pickBestModel(config, usage, exhausted) {
|
|
|
59
63
|
return best;
|
|
60
64
|
}).name;
|
|
61
65
|
}
|
|
66
|
+
function weeklyPacedBudgetPercentage(weekEndDuration) {
|
|
67
|
+
const elapsedMinutes = Math.min(MINUTES_PER_WEEK, Math.max(0, MINUTES_PER_WEEK - weekEndDuration));
|
|
68
|
+
const elapsedDayCount = Math.ceil(elapsedMinutes / MINUTES_PER_DAY);
|
|
69
|
+
const budgetDayCount = Math.min(DAYS_PER_WEEK, Math.max(1, elapsedDayCount));
|
|
70
|
+
return (budgetDayCount / DAYS_PER_WEEK) * PERCENT_FRACTION_DIVISOR;
|
|
71
|
+
}
|
|
72
|
+
export function classifyUsageExhaustion(config, usage) {
|
|
73
|
+
const exhausted = [];
|
|
74
|
+
const sessionLimit = config.orchestrator.sessionLimitPercentage;
|
|
75
|
+
for (const [model, snapshot] of Object.entries(usage)) {
|
|
76
|
+
if (snapshot.session !== null && snapshot.session * PERCENT_FRACTION_DIVISOR > sessionLimit) {
|
|
77
|
+
exhausted.push({
|
|
78
|
+
kind: "session",
|
|
79
|
+
model,
|
|
80
|
+
usedPercentage: snapshot.session * PERCENT_FRACTION_DIVISOR,
|
|
81
|
+
limitPercentage: sessionLimit,
|
|
82
|
+
resetMinutes: snapshot.sessionEndDuration,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Weekly gate paces total weekly usage against day buckets from the
|
|
86
|
+
// weekly reset. Day 1's budget is available immediately after rollover,
|
|
87
|
+
// then each later day opens another 1/7 of the weekly budget.
|
|
88
|
+
if (snapshot.weekly !== null &&
|
|
89
|
+
Number.isFinite(snapshot.weekly) &&
|
|
90
|
+
snapshot.weekEndDuration !== null) {
|
|
91
|
+
const usedPercentage = snapshot.weekly * PERCENT_FRACTION_DIVISOR;
|
|
92
|
+
const allowedPercentage = weeklyPacedBudgetPercentage(snapshot.weekEndDuration);
|
|
93
|
+
if (usedPercentage > allowedPercentage) {
|
|
94
|
+
exhausted.push({
|
|
95
|
+
kind: "weekly",
|
|
96
|
+
model,
|
|
97
|
+
usedPercentage,
|
|
98
|
+
allowedPercentage,
|
|
99
|
+
resetMinutes: snapshot.weekEndDuration,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return exhausted;
|
|
105
|
+
}
|
|
62
106
|
// Stale worktrees with no matching live workspace are filtered out here so
|
|
63
107
|
// they don't permanently block later tickets in the Todo queue.
|
|
64
108
|
function classifyRecovery(arguments_) {
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAoGf;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
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
|
+
import { ensureSandbox, sandboxNameFor } from "../lib/dockerSandbox.js";
|
|
7
8
|
import { detectHostCapabilities } from "../lib/host.js";
|
|
8
9
|
import { buildLaunchCommand, shellSingleQuote } from "../lib/launchCommand.js";
|
|
9
10
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
10
|
-
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
11
|
+
import { assertLocalRunnerRequirements, resolveLocalRunner } from "../lib/localRunner.js";
|
|
11
12
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
12
13
|
import { workspaces } from "../lib/workspaces.js";
|
|
13
14
|
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
@@ -75,8 +76,16 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
75
76
|
if (!definition) {
|
|
76
77
|
throw new Error(`Unknown model: ${model}`);
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const host = await detectHostCapabilities(signal);
|
|
80
|
+
const runner = resolveLocalRunner(config.local.runner, host);
|
|
81
|
+
assertLocalRunnerRequirements(host, runner);
|
|
82
|
+
if (runner === "safehouse") {
|
|
83
|
+
await ensureClearance({ logger: log });
|
|
84
|
+
}
|
|
85
|
+
if (runner === "sdx" && definition.sandbox === undefined) {
|
|
86
|
+
throw new Error(`Local groundcrew runs with the sdx runner require a sandbox config on model '${model}'. ` +
|
|
87
|
+
"Add `sandbox: { agent: '<sbx-agent-name>' }` to the model in your config.ts.");
|
|
88
|
+
}
|
|
80
89
|
const spec = { repository, ticket };
|
|
81
90
|
let created;
|
|
82
91
|
try {
|
|
@@ -113,11 +122,21 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
113
122
|
const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
|
|
114
123
|
promptDir = stagedPrompt.directory;
|
|
115
124
|
const secretsFile = stageBuildSecrets(promptDir);
|
|
125
|
+
const sandboxName = runner === "sdx" ? sandboxNameFor({ repository, model }) : undefined;
|
|
126
|
+
if (runner === "sdx" && sandboxName !== undefined && definition.sandbox !== undefined) {
|
|
127
|
+
await ensureSandbox({
|
|
128
|
+
sandboxName,
|
|
129
|
+
sandbox: definition.sandbox,
|
|
130
|
+
mountPath: resolve(config.workspace.projectDir),
|
|
131
|
+
}, signal);
|
|
132
|
+
}
|
|
116
133
|
const launchCommand = buildLaunchCommand({
|
|
117
134
|
definition,
|
|
118
135
|
promptFile: stagedPrompt.file,
|
|
119
136
|
worktreeDir: launchDir,
|
|
120
137
|
secretsFile,
|
|
138
|
+
runner,
|
|
139
|
+
sandboxName,
|
|
121
140
|
});
|
|
122
141
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
123
142
|
log("Opening workspace...");
|