@clipboard-health/groundcrew 4.0.1 → 4.0.3

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 CHANGED
@@ -17,24 +17,31 @@
17
17
  </p>
18
18
 
19
19
  ```text
20
- $ crew doctor --ticket HRD-446
21
- groundcrew doctor --ticket HRD-446 (Add retry logic to the sync job)
22
- ────────────────────────────────────────────────────────────────────
23
-
24
- Resolution
25
- [ok] Ticket exists in Linear ("Add retry logic to the sync job")
26
- [ok] Status is Todo
27
- [ok] Has agent-* label (agent-claude)
28
- [ok] Model resolves from agent-* label (model "claude")
29
- [ok] Description mentions known repo (owner/repo)
30
- [ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
31
-
32
- Eligibility
33
- [ok] No active blockers
34
- [ok] Model "claude" usage under sessionLimitPercentage (12% (limit 85%))
35
- [ok] In-progress cap not hit (2/4 used)
36
-
37
- → would be dispatched on next tick
20
+ $ crew status HRD-446
21
+ groundcrew status HRD-446
22
+ ========================
23
+ ticket: hrd-446
24
+
25
+ Config snapshot
26
+ ---------------
27
+ projectDir: /dev/workspaces
28
+ repositories: owner/repo
29
+ git: remote=origin; defaultBranch=main
30
+ workspaceKind: auto
31
+
32
+ Worktree state
33
+ --------------
34
+ - owner/repo host
35
+ branch: rocky-hrd-446
36
+ git: dirty (2 modified, 1 untracked)
37
+
38
+ Workspace probe
39
+ ---------------
40
+ live: yes
41
+
42
+ Last Linear status
43
+ ------------------
44
+ In Progress (state.type=started) — Add retry logic to the sync job
38
45
  ```
39
46
 
40
47
  ## Why
@@ -75,16 +82,18 @@ In Linear, assign tickets to yourself and add an `agent-*` label (`agent-claude`
75
82
  ```bash
76
83
  crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
77
84
  crew doctor # check setup
78
- crew doctor --ticket <TICKET> # diagnose a specific ticket
79
- crew run # one-shot dispatch
85
+ crew status [<TICKET>] # inspect current state or one ticket
86
+ crew run # one-shot orchestration
80
87
  crew run --watch # poll forever
81
- crew run --ticket <TICKET> # dispatch one ticket
88
+ crew start <TICKET> # provision + launch one ticket now
82
89
  crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
83
- crew interrupt <TICKET> [--reason <text>] # stop workspace, keep worktree
90
+ crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
84
91
  crew resume <TICKET> # reopen a paused ticket
85
92
  crew cleanup <TICKET> # tear down every worktree for a ticket
86
93
  ```
87
94
 
95
+ Deprecated aliases still work but print a warning and will be removed in the next major version: `crew interrupt` → `crew stop`, `crew run --ticket <TICKET>` → `crew start <TICKET>`, `crew doctor --ticket <TICKET>` → `crew status <TICKET>`.
96
+
88
97
  ## Configuration
89
98
 
90
99
  Two keys are required; everything else has a default.
@@ -102,7 +111,7 @@ The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username`
102
111
  - `agent-claude`, `agent-codex`, `agent-<name>` → that model.
103
112
  - `agent-any` → the model with the most available session capacity.
104
113
  - Unknown `agent-<name>` → falls back to `models.default` with a warning.
105
- - No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew run --ticket <TICKET>` (also falls back to `models.default`).
114
+ - No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew start <TICKET>` (also falls back to `models.default`).
106
115
  - Todo tickets blocked by non-terminal blockers are skipped until their blockers reach a terminal status.
107
116
 
108
117
  Status classification uses Linear's workflow `state.type` (`unstarted`, `started`, `completed`, `canceled`, `duplicate`), so renamed status columns work without configuration. Parent issues with children are ignored — sub-issues are the work items.
@@ -129,7 +138,7 @@ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any
129
138
  | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
130
139
  | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
131
140
  | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
132
- | `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`. |
141
+ | `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew start <TICKET>` for unlabeled tickets. `crew run` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
133
142
  | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
134
143
  | `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. |
135
144
  | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
@@ -185,91 +194,72 @@ Groundcrew auto-creates sandboxes when missing but never deletes them — they p
185
194
 
186
195
  </details>
187
196
 
188
- ## Diagnosing tickets
197
+ ## Inspecting status
189
198
 
190
- `crew doctor --ticket <TICKET>` runs the full per-ticket lifecycle: pre-dispatch eligibility (Todo status, `agent-*` label, model resolution, repo mention, local clone, blockers, session usage, capacity) **and** post-dispatch local recovery (run state, host worktree, workspace pane, branches, PR). Prints a single verdict with a copy-pasteable next step.
199
+ `crew status <TICKET>` prints a read-only snapshot for one ticket: resolved config, matching worktrees, workspace probe result, recorded run state, recent log lines for that ticket, and the latest Linear status. It does not fetch, recover, tear down, resume, or mutate any local/remote state.
191
200
 
192
- Verdict precedence: PR outcomes (`pr-open` > `pr-merged`) recorded failed launches `interrupted` (concrete recoverable git work first) → `in-flight` `recoverable` `unresolvable` > `ineligible` > `would-dispatch` > `lost`. Exits 0 on `would-dispatch`, `pr-open`, or `pr-merged`; any other verdict exits 1. `--watch` and `--ticket` are mutually exclusive. Use `codexbar usage` to inspect session windows directly.
201
+ `crew status` with no ticket prints the current inventory: known worktrees with workspace/run-state presence plus live workspaces reported by the configured backend.
193
202
 
194
- Flags:
203
+ Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKET>` to reopen preserved work. Status is intentionally informational only.
195
204
 
196
- - `--no-linear` — skip the Linear GraphQL call. Resolution and Eligibility sections are skipped; verdicts that need only local state (`in-flight`, `recoverable`, `pr-open`, `pr-merged`, `lost`) still fire.
197
- - `--no-fetch` — skip the upfront `git fetch origin <branch>` before checking remote presence.
205
+ ## Doctor
198
206
 
199
- | Verdict | What to do |
200
- | ---------------- | --------------------------------------------------------------------------------------------- |
201
- | `pr-open` | Nothing — the PR is the source of truth. |
202
- | `pr-merged` | Done. |
203
- | `in-flight` | The ticket is still being worked on; the verdict line names the workspace pane to attach to. |
204
- | `recoverable` | Run the printed `nextStep` exactly. |
205
- | `interrupted` | Resume the preserved worktree with `crew resume <ticket>` or inspect it by hand. |
206
- | `failed-launch` | Fix the launch failure, then run `crew resume <ticket>` or `crew cleanup <ticket>`. |
207
- | `would-dispatch` | Pre-dispatch checks pass; the orchestrator will pick the ticket up on its next tick. |
208
- | `ineligible` | A resolution or eligibility check failed; the reason after the colon names the failing check. |
209
- | `unresolvable` | The Linear ticket couldn't be fetched; the reason after the colon names the error. |
210
- | `lost` | No trace exists. Re-dispatch via `crew run --ticket <ticket>`. |
207
+ `crew doctor` checks host prerequisites only: config validity, Linear reachability, required binaries on PATH, workspace backend availability, workspace.projectDir, local runner capability, and enabled model commands.
211
208
 
212
209
  <details>
213
- <summary>Sample output (post-dispatch)</summary>
214
-
215
- The Workspace section appends an attach hint to the pane name when the workspace backend exposes one (e.g. `tmux attach -t <session>:<pane>` or `cmux attach <name>`), so the verdict line is immediately actionable.
210
+ <summary>Sample ticket status output</summary>
216
211
 
217
212
  ```text
218
- groundcrew doctor --ticket HRD-442 (Multi-event extractor: year inference can produce date_start > date_end)
219
- ────────────────────────────────────────────────────────────────────────────────────────────────────────────
220
-
221
- Resolution
222
- [ok] Ticket exists in Linear ("Multi-event extractor: year inference can produce date_start > date_end")
223
- [ok] Status is Todo
224
- (skipped — post-dispatch — pre-dispatch checks are irrelevant)
225
-
226
- Eligibility
227
- (skipped — post-dispatch — pre-dispatch checks are irrelevant)
213
+ groundcrew status HRD-442
214
+ =========================
215
+ ticket: hrd-442
228
216
 
229
217
  Run state
230
- [ok] Local run state (running)
231
- [ok] Recorded model (claude)
232
- [ok] Recorded worktree (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
233
- [ok] Recorded branch (paul-hrd-442)
234
- [ok] Resume count (0)
218
+ ---------
219
+ running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
235
220
 
236
221
  Worktree
237
- [ok] Host worktree exists (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
238
- [--] Working tree clean (0 modified, 1 untracked)
239
- [ok] Branch checked out (paul-hrd-442)
222
+ --------
223
+ - herds-social host
224
+ branch: paul-hrd-442
225
+ dir: /Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442
226
+ git: dirty (0 modified, 1 untracked)
240
227
 
241
228
  Workspace
242
- [ok] Workspace pane open (hrd-442 — attach: `tmux attach -t groundcrew:hrd-442`)
229
+ ---------
230
+ live: yes
231
+
232
+ Last Linear status
233
+ ------------------
234
+ In Progress (state.type=started) — Multi-event extractor: year inference can produce date_start > date_end
235
+ ```
243
236
 
244
- Local branch
245
- [ok] Local branch exists (paul-hrd-442, 2 ahead / 0 behind origin/main)
237
+ </details>
246
238
 
247
- Remote branch
248
- [ok] Branch present on origin
239
+ ### `crew start <TICKET>`
249
240
 
250
- Pull request
251
- [ok] Open PR for this branch (#224 https://github.com/herds-social/herds/pull/224)
241
+ Provisions and launches one ticket immediately, bypassing orchestrator eligibility. Use it to dispatch a specific ticket on demand — including unlabeled tickets that `crew run` ignores. (Replaces the deprecated `crew run --ticket <TICKET>`.)
252
242
 
253
- → pr-open: https://github.com/herds-social/herds/pull/224 (#224)
243
+ ```bash
244
+ crew start HRD-442
245
+ crew start HRD-442 --dry-run
254
246
  ```
255
247
 
256
- </details>
257
-
258
- ### `crew interrupt <TICKET>`
248
+ ### `crew stop <TICKET>`
259
249
 
260
- Stops a live workspace pane while preserving the ticket worktree and branch. The manual pause button for cases where you need terminal capacity back, want to stop an agent that's going in the wrong direction, or need to inspect the diff before letting another agent continue.
250
+ Stops a live workspace pane while preserving the ticket worktree and branch. The manual pause button for cases where you need terminal capacity back, want to stop an agent that's going in the wrong direction, or need to inspect the diff before letting another agent continue. (Replaces the deprecated `crew interrupt <TICKET>`.)
261
251
 
262
252
  ```bash
263
- crew interrupt HRD-442 --reason "wrong implementation direction"
264
- crew doctor --ticket HRD-442
253
+ crew stop HRD-442 --reason "wrong implementation direction"
254
+ crew status HRD-442
265
255
  crew resume HRD-442
266
256
  ```
267
257
 
268
- The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, interrupt records that fact so doctor can point at the preserved branch instead of reporting a mystery ticket.
258
+ The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, stop records that fact so status can show the preserved branch.
269
259
 
270
260
  ### `crew resume <TICKET>`
271
261
 
272
- Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew run --ticket <ticket>`.
262
+ Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew start <ticket>`.
273
263
 
274
264
  The resume prompt tells the agent to inspect git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
275
265
 
@@ -452,7 +442,7 @@ op run --env-file .env.1password -- crew doctor
452
442
 
453
443
  ## Troubleshooting
454
444
 
455
- First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
445
+ First stop for "what exists locally right now": `crew status <ticket>` shows the ticket's worktrees, workspace presence, run state, logs, and latest Linear status. Use `crew doctor` when you need to verify host setup.
456
446
 
457
447
  <details>
458
448
  <summary>Safehouse-already-wrapped commands are not re-wrapped</summary>
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgOA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAwQA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
package/dist/cli.js CHANGED
@@ -8,12 +8,20 @@ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
8
  import { sandboxCli } from "./commands/sandbox/index.js";
9
9
  import { setupReposCli } from "./commands/setupRepos.js";
10
10
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
11
+ import { statusCli } from "./commands/status.js";
11
12
  import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
12
13
  import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
13
- import { errorMessage, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
14
+ import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
14
15
  const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
15
16
  const NUDGE_FETCH_TIMEOUT_MS = 1000;
16
17
  const requireFromCli = createRequire(import.meta.url);
18
+ /**
19
+ * Prints a deprecation warning to stderr naming the canonical command and that
20
+ * the old form is removed in the next major, then lets the caller proceed.
21
+ */
22
+ function warnDeprecated(forms) {
23
+ writeError(`crew ${forms.oldForm} is deprecated and will be removed in the next major version; use crew ${forms.newForm} instead.`);
24
+ }
17
25
  function setupUsage() {
18
26
  return "Usage: crew setup repos [--dry-run] [<repo>...]";
19
27
  }
@@ -53,6 +61,16 @@ async function runCli(argv) {
53
61
  await orchestrate({ watch, dryRun });
54
62
  return;
55
63
  }
64
+ warnDeprecated({ oldForm: "run --ticket", newForm: "start" });
65
+ await setupWorkspaceCli(ticket, { dryRun });
66
+ }
67
+ const START_USAGE = "crew start <ticket> [--dry-run]";
68
+ async function startCli(argv) {
69
+ const { dryRun, positionals } = parseDryRunPositionals(argv, START_USAGE);
70
+ const [ticket, ...extras] = positionals;
71
+ if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
72
+ throw new Error(`Usage: ${START_USAGE}`);
73
+ }
56
74
  await setupWorkspaceCli(ticket, { dryRun });
57
75
  }
58
76
  async function upgradeCliInvoke(argv) {
@@ -79,31 +97,27 @@ async function maybeRunUpgradeNudge(metadata) {
79
97
  writeError(message);
80
98
  }
81
99
  }
82
- async function doctorCli(argv) {
83
- let ticket;
84
- const remainingArgs = [];
85
- for (let index = 0; index < argv.length; index += 1) {
86
- const argument = argv[index];
87
- if (argument === "--ticket") {
88
- ticket = readTicketArgument(argv, index, "doctor");
89
- index += 1;
90
- continue;
91
- }
92
- if (argument === "--no-linear" || argument === "--no-fetch") {
93
- remainingArgs.push(argument);
94
- continue;
95
- }
96
- throw new Error(`crew doctor: unknown argument: ${argument}`);
100
+ function doctorTicketAlias(argv) {
101
+ if (argv[0] !== "--ticket") {
102
+ return undefined;
97
103
  }
98
- if (ticket === undefined) {
99
- if (remainingArgs.length > 0) {
100
- throw new Error(`crew doctor: ${remainingArgs[0]} requires --ticket (host doctor mode has no flags)`);
101
- }
102
- const ok = await doctor();
103
- process.exitCode = ok ? process.exitCode : 1;
104
+ const ticket = readTicketArgument(argv, 0, "doctor");
105
+ if (argv.length > 2) {
106
+ throw new Error("Usage: crew status [<ticket>]");
107
+ }
108
+ return ticket;
109
+ }
110
+ async function doctorCli(argv) {
111
+ const aliasTicket = doctorTicketAlias(argv);
112
+ if (aliasTicket !== undefined) {
113
+ warnDeprecated({ oldForm: "doctor --ticket", newForm: "status" });
114
+ await statusCli([aliasTicket]);
104
115
  return;
105
116
  }
106
- const ok = await doctor({ ticket, ticketArgv: remainingArgs });
117
+ if (argv.length > 0) {
118
+ throw new Error("Usage: crew doctor");
119
+ }
120
+ const ok = await doctor();
107
121
  process.exitCode = ok ? process.exitCode : 1;
108
122
  }
109
123
  const SUBCOMMANDS = {
@@ -113,25 +127,44 @@ const SUBCOMMANDS = {
113
127
  invoke: initConfigCli,
114
128
  },
115
129
  run: {
116
- summary: "Run the orchestrator (one-shot by default), or provision one ticket with --ticket",
117
- usage: "[--watch] [--dry-run] [--ticket <ticket>]",
130
+ summary: "Run the orchestrator: poll sources and start eligible tickets (one-shot by default)",
131
+ usage: "[--watch] [--dry-run]",
118
132
  invoke: runCli,
119
133
  },
134
+ start: {
135
+ summary: "Provision and launch one ticket immediately, bypassing eligibility",
136
+ usage: "<ticket> [--dry-run]",
137
+ invoke: startCli,
138
+ },
120
139
  doctor: {
121
- summary: "Verify prereqs, or diagnose one ticket with --ticket (full lifecycle: dispatch eligibility + local-state recovery)",
122
- usage: "[--ticket <ticket> [--no-linear] [--no-fetch]]",
140
+ summary: "Verify host prerequisites (PATH tools, config validity, Linear reachability)",
141
+ usage: "",
123
142
  invoke: doctorCli,
124
143
  },
144
+ status: {
145
+ summary: "Print read-only groundcrew state, or one ticket's local/Linear status",
146
+ usage: "[<ticket>]",
147
+ invoke: statusCli,
148
+ },
125
149
  cleanup: {
126
150
  summary: "Tear down a worktree",
127
151
  usage: "[--force] <ticket>",
128
152
  invoke: cleanupWorkspaceCli,
129
153
  },
130
- interrupt: {
154
+ stop: {
131
155
  summary: "Stop a live ticket workspace while preserving its worktree",
132
156
  usage: "<ticket> [--reason <text>]",
133
157
  invoke: interruptWorkspaceCli,
134
158
  },
159
+ interrupt: {
160
+ summary: "Deprecated alias for `crew stop`",
161
+ usage: "<ticket> [--reason <text>]",
162
+ deprecated: true,
163
+ invoke: async (argv) => {
164
+ warnDeprecated({ oldForm: "interrupt", newForm: "stop" });
165
+ await interruptWorkspaceCli(argv);
166
+ },
167
+ },
135
168
  resume: {
136
169
  summary: "Reopen an existing ticket worktree with a continuation prompt",
137
170
  usage: "<ticket>",
@@ -162,6 +195,9 @@ function printHelp() {
162
195
  writeOutput("");
163
196
  writeOutput("Commands:");
164
197
  for (const [name, command] of Object.entries(SUBCOMMANDS)) {
198
+ if (command.deprecated === true) {
199
+ continue;
200
+ }
165
201
  writeOutput(` ${name.padEnd(width)} ${command.summary}`);
166
202
  writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
167
203
  }
@@ -2,10 +2,5 @@
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 interface DoctorOptions {
6
- ticket?: string;
7
- /** Extra flags after `--ticket <id>`; currently `--no-linear` and `--no-fetch`. */
8
- ticketArgv?: string[];
9
- }
10
- export declare function doctor(options?: DoctorOptions): Promise<boolean>;
5
+ export declare function doctor(): Promise<boolean>;
11
6
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4BH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAgHD,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoJH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA8E/C"}
@@ -4,12 +4,12 @@
4
4
  */
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { loadConfig, } from "../lib/config.js";
7
+ import { createBoardSource } from "../lib/boardSource.js";
7
8
  import { detectHostCapabilities, which } from "../lib/host.js";
8
9
  import { resolveLocalRunner } from "../lib/localRunner.js";
9
10
  import { gatedModels } from "../lib/usage.js";
10
- import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
11
+ import { errorMessage, getLinearClient, resolveLinearApiKey, writeOutput } from "../lib/util.js";
11
12
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
12
- import { parseTicketDoctorFlags, runTicketDoctor } from "./ticketDoctor.js";
13
13
  // Tokenization stops after this many non-flag tokens. Two is enough to
14
14
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
15
15
  const MAX_TOKENS_PER_CMD = 2;
@@ -26,22 +26,33 @@ async function checkCmd(cmd, required, hint) {
26
26
  }
27
27
  return result;
28
28
  }
29
- function checkLinearApiKey() {
29
+ async function checkLinearReachability(config) {
30
30
  const resolved = resolveLinearApiKey();
31
- if (resolved !== undefined) {
31
+ if (resolved === undefined) {
32
32
  return {
33
- name: "linear api key",
33
+ name: "linear reachability",
34
+ ok: false,
35
+ required: true,
36
+ hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
37
+ };
38
+ }
39
+ try {
40
+ await createBoardSource({ config, client: getLinearClient() }).verify();
41
+ return {
42
+ name: "linear reachability",
34
43
  ok: true,
35
44
  required: true,
36
45
  hint: `set via $${resolved.source}`,
37
46
  };
38
47
  }
39
- return {
40
- name: "linear api key",
41
- ok: false,
42
- required: true,
43
- hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
44
- };
48
+ catch (error) {
49
+ return {
50
+ name: "linear reachability",
51
+ ok: false,
52
+ required: true,
53
+ hint: errorMessage(error),
54
+ };
55
+ }
45
56
  }
46
57
  function checkDir(path, label) {
47
58
  // statSync can throw on permission errors or path races; surface those
@@ -120,31 +131,7 @@ function format(check) {
120
131
  const hint = check.hint !== undefined && check.hint.length > 0 ? ` — ${check.hint}` : "";
121
132
  return `${tag}${check.name}${hint}`;
122
133
  }
123
- export async function doctor(options = {}) {
124
- if (options.ticket !== undefined) {
125
- return await doctorTicket(options.ticket, options.ticketArgv ?? []);
126
- }
127
- return await doctorHost();
128
- }
129
- async function doctorTicket(ticket, ticketArgv) {
130
- try {
131
- const flags = parseTicketDoctorFlags(ticketArgv);
132
- return await runTicketDoctor({
133
- ticket,
134
- doLinear: flags.doLinear,
135
- doFetch: flags.doFetch,
136
- });
137
- }
138
- catch (error) {
139
- const displayTicket = ticket.toUpperCase();
140
- const header = `groundcrew doctor --ticket ${displayTicket}`;
141
- writeOutput(header);
142
- writeOutput("=".repeat(header.length));
143
- writeOutput(`[--] config: ${errorMessage(error)}`);
144
- return false;
145
- }
146
- }
147
- async function doctorHost() {
134
+ export async function doctor() {
148
135
  writeOutput("groundcrew doctor");
149
136
  writeOutput("=================");
150
137
  let config;
@@ -174,7 +161,7 @@ async function doctorHost() {
174
161
  const workspaceOutcome = resolveWorkspaceOutcome(config, host);
175
162
  reportWorkspaceKind(config, workspaceOutcome);
176
163
  const checks = [
177
- checkLinearApiKey(),
164
+ await checkLinearReachability(config),
178
165
  await checkCmd("git", true, "https://git-scm.com/"),
179
166
  ...(await workspaceChecks(workspaceOutcome)),
180
167
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
@@ -1 +1 @@
1
- {"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
1
+ {"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAqGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
@@ -15,20 +15,20 @@ function parseArguments(argv) {
15
15
  if (argument === "--reason") {
16
16
  const value = argv[index + 1];
17
17
  if (value === undefined || value.length === 0 || value.startsWith("-")) {
18
- throw new Error("crew interrupt --reason: reason text is required");
18
+ throw new Error("crew stop --reason: reason text is required");
19
19
  }
20
20
  reason = value;
21
21
  index += 1;
22
22
  continue;
23
23
  }
24
24
  if (argument.startsWith("-")) {
25
- throw new Error(`Unknown option: ${argument}\nUsage: crew interrupt <ticket> [--reason <text>]`);
25
+ throw new Error(`Unknown option: ${argument}\nUsage: crew stop <ticket> [--reason <text>]`);
26
26
  }
27
27
  positionals.push(argument);
28
28
  }
29
29
  const [ticket, ...extras] = positionals;
30
30
  if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
31
- throw new Error("Usage: crew interrupt <ticket> [--reason <text>]");
31
+ throw new Error("Usage: crew stop <ticket> [--reason <text>]");
32
32
  }
33
33
  return { ticket: ticket.toLowerCase(), ...(reason === undefined ? {} : { reason }) };
34
34
  }
@@ -100,7 +100,7 @@ export async function interruptWorkspace(config, options) {
100
100
  },
101
101
  });
102
102
  log(`Interrupted ${ticket}; worktree preserved at ${source.worktreeDir}`);
103
- log(`Next: crew doctor --ticket ${ticket}`);
103
+ log(`Next: crew status ${ticket}`);
104
104
  }
105
105
  export async function interruptWorkspaceCli(argv) {
106
106
  const config = await loadConfig();
@@ -1 +1 @@
1
- {"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAwBD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
1
+ {"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAcD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
@@ -10,7 +10,7 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
10
10
  import { runCommandAsync } from "../lib/commandRunner.js";
11
11
  import { loadConfig } from "../lib/config.js";
12
12
  import { which } from "../lib/host.js";
13
- import { errorMessage, log, writeOutput } from "../lib/util.js";
13
+ import { errorMessage, log, parseDryRunPositionals, writeOutput } from "../lib/util.js";
14
14
  function emptyResult() {
15
15
  return {
16
16
  existing: [],
@@ -190,18 +190,7 @@ export async function setupRepos(config, options) {
190
190
  return result;
191
191
  }
192
192
  function parseArguments(argv) {
193
- let dryRun = false;
194
- const positionals = [];
195
- for (const argument of argv) {
196
- if (argument === "--dry-run") {
197
- dryRun = true;
198
- continue;
199
- }
200
- if (argument.startsWith("-")) {
201
- throw new Error(`Unknown option: ${argument}\nUsage: crew setup repos [--dry-run] [<repo>...]`);
202
- }
203
- positionals.push(argument);
204
- }
193
+ const { dryRun, positionals } = parseDryRunPositionals(argv, "crew setup repos [--dry-run] [<repo>...]");
205
194
  const options = { dryRun };
206
195
  if (positionals.length > 0) {
207
196
  options.only = positionals;
@@ -0,0 +1,7 @@
1
+ import { type ResolvedConfig } from "../lib/config.ts";
2
+ export interface StatusOptions {
3
+ ticket?: string;
4
+ }
5
+ export declare function status(config: ResolvedConfig, options?: StatusOptions): Promise<void>;
6
+ export declare function statusCli(argv: string[]): Promise<void>;
7
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAWnE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuLD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}