@clipboard-health/groundcrew 1.8.0 → 1.9.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 +32 -1
- package/clearance-allow-hosts +0 -2
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +18 -2
- package/dist/lib/config.d.ts +20 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +36 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Required fields are marked **required**; everything else has a default and can b
|
|
|
134
134
|
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Local macOS runs execute in the worktree through Safehouse/clearance; remote runs execute inside the remote worktree. `{{worktree}}` is replaced before launch and legacy `{{sandbox}}` expands to an empty string. |
|
|
135
135
|
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
136
136
|
| `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. |
|
|
137
|
+
| `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. |
|
|
137
138
|
| `prompts.initial` | (template) | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. |
|
|
138
139
|
| `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. |
|
|
139
140
|
| `remote.provider` | `"sprite"` | Remote runner provider used for tickets labeled `agent-remote`. Sprite is currently the only provider. |
|
|
@@ -146,6 +147,36 @@ Required fields are marked **required**; everything else has a default and can b
|
|
|
146
147
|
|
|
147
148
|
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` 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 setup <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.
|
|
148
149
|
|
|
150
|
+
### Disabling a shipped default
|
|
151
|
+
|
|
152
|
+
Groundcrew ships `claude` and `codex` as default model definitions, additively merged into every resolved config. If you only ever route work through one of them, set `disabled: true` on the other so doctor stops probing for the unused CLI:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// config.ts
|
|
156
|
+
export const config = {
|
|
157
|
+
// …
|
|
158
|
+
models: {
|
|
159
|
+
default: "claude",
|
|
160
|
+
definitions: {
|
|
161
|
+
codex: { disabled: true },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Effects:
|
|
168
|
+
|
|
169
|
+
- `crew doctor` does not probe the disabled model's CLI. `crew doctor || exit 1` becomes viable as a CI gate when you only have one agent installed.
|
|
170
|
+
- `agent-any` only resolves to enabled models.
|
|
171
|
+
- An `agent-<disabled>` label on a ticket (e.g. `agent-codex` after disabling codex) falls back to `models.default` with a warning in the log, so the ticket still runs and you can see the mismatch.
|
|
172
|
+
|
|
173
|
+
Rules:
|
|
174
|
+
|
|
175
|
+
- `disabled` only accepts shipped-default keys (`claude`, `codex`). A typo on the key fails loudly at config load instead of silently disabling nothing.
|
|
176
|
+
- `disabled` must be exactly the boolean `true`.
|
|
177
|
+
- It cannot be combined with `cmd`, `color`, or `usage` in the same entry — disable a model or override its fields, not both.
|
|
178
|
+
- `models.default` must point at an enabled model.
|
|
179
|
+
|
|
149
180
|
## Manual commands
|
|
150
181
|
|
|
151
182
|
```bash
|
|
@@ -177,7 +208,7 @@ crew cleanup <TICKET>
|
|
|
177
208
|
- **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`.
|
|
178
209
|
- **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.
|
|
179
210
|
- **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.
|
|
180
|
-
- **Doctor checks every
|
|
211
|
+
- **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.
|
|
181
212
|
- **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.
|
|
182
213
|
- **Agent CLI must accept a positional prompt.** The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` all support this.
|
|
183
214
|
|
package/clearance-allow-hosts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,
|
|
1
|
+
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAMrB,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,0FAA0F;IAC1F,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;AAgMD,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;CACzB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAyCzB"}
|
package/dist/lib/boardSource.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* snapshot. Owns the GraphQL queries and shape parsing so callers consume a
|
|
4
4
|
* typed `BoardState` instead of raw nodes.
|
|
5
5
|
*/
|
|
6
|
-
import { AGENT_ANY_MODEL } from "./config.js";
|
|
6
|
+
import { AGENT_ANY_MODEL, isShippedDefaultDisabled, } from "./config.js";
|
|
7
7
|
import { log } from "./util.js";
|
|
8
8
|
const AGENT_LABEL_PREFIX = "agent-";
|
|
9
9
|
const ISSUES_PAGE_SIZE = 250;
|
|
@@ -130,6 +130,7 @@ async function fetchBoard(client, config) {
|
|
|
130
130
|
.filter((node) => node.children.nodes.length === 0)
|
|
131
131
|
.map((node) => {
|
|
132
132
|
const parsedAgentLabels = parseAgentLabels(node.labels.nodes, config);
|
|
133
|
+
warnIfDisabledFallback(node.identifier, parsedAgentLabels, config);
|
|
133
134
|
const repository = parsedAgentLabels === undefined
|
|
134
135
|
? undefined
|
|
135
136
|
: parseRepository({
|
|
@@ -202,6 +203,7 @@ export async function fetchResolvedIssue(arguments_) {
|
|
|
202
203
|
// unlabeled ticket still resolves to `models.default` — different from
|
|
203
204
|
// the auto-pickup path, where unlabeled tickets are ignored.
|
|
204
205
|
const parsed = parseAgentLabels(issue.labels.nodes, config);
|
|
206
|
+
warnIfDisabledFallback(ticket, parsed, config);
|
|
205
207
|
const model = parsed === undefined || parsed.model === AGENT_ANY_MODEL ? config.models.default : parsed.model;
|
|
206
208
|
const runner = parsed?.runner ?? "local";
|
|
207
209
|
return { title: issue.title, description, repository, model, runner };
|
|
@@ -229,6 +231,7 @@ function parseAgentLabels(labels, config) {
|
|
|
229
231
|
return undefined;
|
|
230
232
|
}
|
|
231
233
|
const runner = agentLabels.some((label) => label.name === "agent-remote") ? "remote" : "local";
|
|
234
|
+
let disabledFallback;
|
|
232
235
|
for (const label of agentLabels) {
|
|
233
236
|
if (label.name === "agent-remote") {
|
|
234
237
|
continue;
|
|
@@ -243,8 +246,21 @@ function parseAgentLabels(labels, config) {
|
|
|
243
246
|
if (Object.hasOwn(config.models.definitions, name)) {
|
|
244
247
|
return { model: name, runner };
|
|
245
248
|
}
|
|
249
|
+
if (disabledFallback === undefined && isShippedDefaultDisabled(config, name)) {
|
|
250
|
+
disabledFallback = name;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const fallback = { model: config.models.default, runner };
|
|
254
|
+
if (disabledFallback !== undefined) {
|
|
255
|
+
fallback.disabledFallback = disabledFallback;
|
|
256
|
+
}
|
|
257
|
+
return fallback;
|
|
258
|
+
}
|
|
259
|
+
function warnIfDisabledFallback(ticket, parsed, config) {
|
|
260
|
+
if (parsed?.disabledFallback === undefined) {
|
|
261
|
+
return;
|
|
246
262
|
}
|
|
247
|
-
|
|
263
|
+
log(`${ticket.toLowerCase()}: agent-${parsed.disabledFallback} label refers to a disabled model; falling back to models.default (${config.models.default})`);
|
|
248
264
|
}
|
|
249
265
|
function blockersFromRelations(relations) {
|
|
250
266
|
return relations
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -49,7 +49,19 @@ export interface RemoteRunnerConfig {
|
|
|
49
49
|
worktreeRoot: string;
|
|
50
50
|
secretNames: string[];
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* User-facing model entry shape. Discriminated union so the type system
|
|
54
|
+
* mirrors the runtime contract: an entry is either a pure overlay
|
|
55
|
+
* (every concrete field optional, no `disabled` key) or a pure
|
|
56
|
+
* disable directive (`{ disabled: true }` and nothing else).
|
|
57
|
+
*/
|
|
58
|
+
type EnabledUserModelDefinition = Partial<ModelDefinition> & {
|
|
59
|
+
disabled?: never;
|
|
60
|
+
};
|
|
61
|
+
interface DisabledUserModelDefinition {
|
|
62
|
+
disabled: true;
|
|
63
|
+
}
|
|
64
|
+
type UserModelDefinition = EnabledUserModelDefinition | DisabledUserModelDefinition;
|
|
53
65
|
/**
|
|
54
66
|
* Setup command run inside sibling worktrees on the host. The host is
|
|
55
67
|
* assumed to already have the right Node and npm versions, so this skips
|
|
@@ -169,5 +181,12 @@ export interface ResolvedConfig {
|
|
|
169
181
|
file: string;
|
|
170
182
|
};
|
|
171
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* True when `name` is a shipped default the user removed via `disabled: true`.
|
|
186
|
+
* Derived from absence in `definitions` — that's the only path that removes a
|
|
187
|
+
* shipped default, codified in `failIfLegacyModelKeys` + `mergeDefinitions`.
|
|
188
|
+
* Consumers needing to distinguish disabled-by-user from unknown-label use this.
|
|
189
|
+
*/
|
|
190
|
+
export declare function isShippedDefaultDisabled(config: Pick<ResolvedConfig, "models">, name: string): boolean;
|
|
172
191
|
export declare function loadConfig(): Promise<Readonly<ResolvedConfig>>;
|
|
173
192
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AAEvE;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EAAiC,CAAC;AAE1F,eAAO,MAAM,4BAA4B,YAAI,QAAQ,CAAU,CAAC;AAEhE,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAErF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,wBAAwB,CAI5F;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,wBAAwB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,KAAK,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -256,6 +256,25 @@ function failIfLegacyModelKeys(name, override) {
|
|
|
256
256
|
if (Object.hasOwn(override, "sandbox")) {
|
|
257
257
|
fail(`models.definitions.${name}.sandbox is no longer supported: Docker Sandboxes are no longer supported`);
|
|
258
258
|
}
|
|
259
|
+
if (Object.hasOwn(override, "disabled")) {
|
|
260
|
+
if (override["disabled"] !== true) {
|
|
261
|
+
fail(`models.definitions.${name}.disabled must be exactly \`true\` when set (got ${JSON.stringify(override["disabled"])})`);
|
|
262
|
+
}
|
|
263
|
+
const conflicting = ["cmd", "color", "usage"].filter((key) => Object.hasOwn(override, key));
|
|
264
|
+
if (conflicting.length > 0) {
|
|
265
|
+
fail(`models.definitions.${name}: cannot combine \`disabled: true\` with other fields (${conflicting.join(", ")}). Either disable the model or override its fields, not both.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* True when `name` is a shipped default the user removed via `disabled: true`.
|
|
271
|
+
* Derived from absence in `definitions` — that's the only path that removes a
|
|
272
|
+
* shipped default, codified in `failIfLegacyModelKeys` + `mergeDefinitions`.
|
|
273
|
+
* Consumers needing to distinguish disabled-by-user from unknown-label use this.
|
|
274
|
+
*/
|
|
275
|
+
export function isShippedDefaultDisabled(config, name) {
|
|
276
|
+
return (Object.hasOwn(DEFAULT_MODEL_DEFINITIONS, name) &&
|
|
277
|
+
!Object.hasOwn(config.models.definitions, name));
|
|
259
278
|
}
|
|
260
279
|
function mergeDefinitions(user) {
|
|
261
280
|
if (user !== undefined && !isPlainObject(user)) {
|
|
@@ -267,6 +286,17 @@ function mergeDefinitions(user) {
|
|
|
267
286
|
]));
|
|
268
287
|
for (const [name, override] of Object.entries(user ?? {})) {
|
|
269
288
|
failIfLegacyModelKeys(name, override);
|
|
289
|
+
if (override.disabled === true) {
|
|
290
|
+
if (!Object.hasOwn(DEFAULT_MODEL_DEFINITIONS, name)) {
|
|
291
|
+
fail(`models.definitions.${name}: \`disabled: true\` is only valid for shipped defaults (${Object.keys(DEFAULT_MODEL_DEFINITIONS).join(", ")}). Remove the entry instead.`);
|
|
292
|
+
}
|
|
293
|
+
// Drop the key so downstream iterators (doctor, eligibility, usage) ignore
|
|
294
|
+
// the model automatically; `isShippedDefaultDisabled` lets the few consumers
|
|
295
|
+
// that need to distinguish disabled from unknown re-derive the set.
|
|
296
|
+
// oxlint-disable-next-line typescript/no-dynamic-delete -- `merged` is a fresh function-local clone of DEFAULT_MODEL_DEFINITIONS; no V8 dictionary-mode/pollution concerns
|
|
297
|
+
delete merged[name];
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
270
300
|
const base = merged[name] === undefined ? {} : cloneModelDefinition(merged[name]);
|
|
271
301
|
// Per-key spread so overriding `cmd` alone preserves the default
|
|
272
302
|
// `color` / `usage`. Brand-new entries must supply both required fields.
|
|
@@ -400,6 +430,12 @@ function validate(config) {
|
|
|
400
430
|
requireString(codexbar.provider, `${usagePath}.codexbar.provider`);
|
|
401
431
|
}
|
|
402
432
|
}
|
|
433
|
+
// Disabled-default check must run before the generic "not a key" check so
|
|
434
|
+
// the user gets the specific "is disabled" message instead of a stale-list
|
|
435
|
+
// message they can't act on without realizing they need to re-enable.
|
|
436
|
+
if (isShippedDefaultDisabled(config, config.models.default)) {
|
|
437
|
+
fail(`models.default ("${config.models.default}") is disabled. Either re-enable it or set models.default to an enabled model.`);
|
|
438
|
+
}
|
|
403
439
|
if (!(config.models.default in definitions)) {
|
|
404
440
|
fail(`models.default ("${config.models.default}") is not a key in models.definitions (have: ${Object.keys(definitions).join(", ")})`);
|
|
405
441
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle, remote runners, and usage tracking.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"verify": "node scripts/verifyAll.mts"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@clipboard-health/clearance": "1.0.
|
|
69
|
+
"@clipboard-health/clearance": "1.0.8",
|
|
70
70
|
"@linear/sdk": "84.0.0",
|
|
71
71
|
"tslib": "2.8.1"
|
|
72
72
|
},
|