@clipboard-health/groundcrew 2.3.6 → 3.0.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 CHANGED
@@ -105,6 +105,25 @@ Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitiv
105
105
  crew run --watch # poll forever
106
106
  ```
107
107
 
108
+ ## Secrets
109
+
110
+ Groundcrew forwards a small allowlist of build-time secrets from your shell into the setup phase (so `npm install` can authenticate against private registries) and then strips them before the agent runs. The agent process never inherits these values in its environment.
111
+
112
+ **Recognized names.** Defined in [`BUILD_SECRET_NAMES`](./src/lib/buildSecrets.ts):
113
+
114
+ - `NPM_TOKEN`
115
+ - `BUF_TOKEN`
116
+
117
+ Set them in the shell you run `crew` from. Anything not in this list is ignored by the secret-shuttling path.
118
+
119
+ **Flow.** For each ticket:
120
+
121
+ 1. If any recognized var is set and non-empty, groundcrew writes `secrets.env` (mode `0600`) into the ticket's temp prompt dir as `KEY='value'` lines — see `stageBuildSecrets` in [`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts).
122
+ 2. The launch script sources `secrets.env` with `set -a` so the values are exported into the setup phase only (and under `sdx`, forwarded into the sandbox via `-e KEY` flags).
123
+ 3. After setup completes, the script `unset`s every name in `BUILD_SECRET_NAMES` and then `rm -rf`s the entire prompt dir (including `secrets.env`) before `exec`'ing the agent. See `sourceSecretsLine` / `unsetSecretsLine` and the `rm -rf` / `exec` lines in [`src/lib/launchCommand.ts`](./src/lib/launchCommand.ts). The rollback path on setup failure ([`src/commands/setupWorkspace.ts`](./src/commands/setupWorkspace.ts)) wipes the prompt dir too.
124
+
125
+ Net effect: by the time the agent process exists, the values are gone from the environment and the file is gone from disk.
126
+
108
127
  ## Runners
109
128
 
110
129
  `local.runner` picks the local isolation backend. `auto` resolves per platform.
@@ -216,42 +235,129 @@ Rules:
216
235
 
217
236
  </details>
218
237
 
238
+ ## Per-repo setup hook
239
+
240
+ When groundcrew launches a worktree, if `.groundcrew/setup.sh` exists in the repo root it's invoked as `bash .groundcrew/setup.sh --deps-only` before the agent starts; otherwise nothing runs. The same convention applies inside the sdx sandbox (overridable per-model via `models.definitions.<name>.sandbox.setupCommand`). No implicit `npm install`, `uv sync`, or anything else — groundcrew is language-agnostic, so opt in by adding the script.
241
+
242
+ ### The `--deps-only` contract
243
+
244
+ The flag tells the script "you're being called by an automated system before an agent launches — skip anything interactive or one-time-only." The same script handles both modes; branch on `$1`. The name is historical and Node-flavored, but the semantic is language-neutral:
245
+
246
+ - **With `--deps-only`**: do the cheap recurring work this worktree needs (lockfile install, generate types, etc.). No prompts, no global installs, no `nvm` / `pyenv` bootstrap that the host should already have.
247
+ - **Without the flag**: full interactive bootstrap. Use this path when an engineer runs the script by hand for first-time onboarding, or when wiring it into another tool's SessionStart hook.
248
+
249
+ Setup failures are advisory — groundcrew logs the non-zero exit and still launches the agent so a flaky network or stale lockfile doesn't block the session.
250
+
251
+ ### Examples
252
+
253
+ **Python (uv):**
254
+
255
+ ```bash
256
+ #!/usr/bin/env bash
257
+ set -euo pipefail
258
+ if [ "${1:-}" = "--deps-only" ]; then
259
+ uv sync --dev
260
+ else
261
+ uv sync --dev
262
+ # ... extra one-time bootstrap (e.g., pre-commit install, db seed) ...
263
+ fi
264
+ ```
265
+
266
+ **Node (npm):**
267
+
268
+ ```bash
269
+ #!/usr/bin/env bash
270
+ set -euo pipefail
271
+ if [ "${1:-}" = "--deps-only" ]; then
272
+ npm clean-install
273
+ else
274
+ npm clean-install
275
+ # ... extra one-time bootstrap (e.g., husky install, codegen, link local packages) ...
276
+ fi
277
+ ```
278
+
279
+ **Docs-only or polyglot repo with no install step:**
280
+
281
+ Omit the script. With nothing at `.groundcrew/setup.sh`, groundcrew skips the hook silently — fine for documentation repos, polyglot monorepos where setup happens per-package, or anywhere the per-worktree work is genuinely zero.
282
+
283
+ For a more comprehensive real-world example (nvm bootstrap, hash-based skip-on-no-changes caching, portable SHA-256 detection), see [this repo's own `.groundcrew/setup.sh`](./.groundcrew/setup.sh). It's also symlinked at `.claude/setup.sh` so the same script doubles as a Claude Code SessionStart hook for this repo — that symlink is local convenience, not part of groundcrew's contract.
284
+
285
+ ### Generating it with an agent
286
+
287
+ To have a coding agent (Claude Code, Cursor, etc.) scaffold `.groundcrew/setup.sh` for a repo you're onboarding, see [docs/setup-hook-agent-prompt.md](./docs/setup-hook-agent-prompt.md) — it encodes the contract above as a copy-pasteable prompt.
288
+
219
289
  ## Commands
220
290
 
221
291
  ```bash
222
- crew doctor # full setup check
223
- crew doctor --ticket <TICKET> # diagnose one ticket
224
- crew run # one-shot dispatch
225
- crew run --watch # poll forever
226
- crew run --ticket <TICKET> # provision one ticket and exit
292
+ crew doctor # full setup check
293
+ crew doctor --ticket <TICKET> [--no-linear] [--no-fetch] # full ticket lifecycle (dispatch + recovery)
294
+ crew run # one-shot dispatch
295
+ crew run --watch # poll forever
296
+ crew run --ticket <TICKET> # provision one ticket and exit
227
297
  crew setup repos [--dry-run] [<repo>...]
228
- crew cleanup <TICKET> # tear down every worktree carrying this ticket
298
+ crew cleanup <TICKET> # tear down every worktree carrying this ticket
229
299
  ```
230
300
 
231
- `--watch` and `--ticket` are mutually exclusive. To inspect codexbar session windows directly, run `codexbar usage`.
301
+ `crew doctor --ticket <TICKET>` covers the full per-ticket lifecycle: pre-dispatch eligibility (Todo status, `agent-*` label, model resolution, repository mention, local clone, blockers, model session usage, in-progress capacity) **and** post-dispatch local-state recovery (host worktree, workspace pane, local branch, remote branch, open PR). Verdict precedence runs from post-dispatch outcomes down: `pr-open` > `pr-merged` > `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. To inspect codexbar session windows directly, run `codexbar usage`.
232
302
 
233
303
  ### `crew doctor --ticket <ticket>`
234
304
 
235
- Diagnose why a ticket would or wouldn't be dispatched on the next tick. Runs the same resolution and eligibility chain as the dispatcher. Exits 0 if the ticket would dispatch, 1 otherwise. The hero above shows a passing run; here's a failing one:
305
+ Diagnose where a ticket is in its lifecycle and what to do next. Runs the same resolution and eligibility chain as the dispatcher, plus probes the host worktree, workspace pane, local branch, remote branch, and PR; prints a single verdict with a copy-pasteable recovery step when one applies.
306
+
307
+ Flags:
308
+
309
+ - `--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.
310
+ - `--no-fetch` — skip the upfront `git fetch origin <branch>` before checking remote presence.
311
+
312
+ 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. The hero above shows a passing pre-dispatch run; here's the same command on a ticket that's already past dispatch:
236
313
 
237
314
  ```text
238
- groundcrew doctor --ticket HRD-447 (Refactor auth middleware)
239
- ─────────────────────────────────────────────────────────────
315
+ groundcrew doctor --ticket HRD-442 (Multi-event extractor: year inference can produce date_start > date_end)
316
+ ────────────────────────────────────────────────────────────────────────────────────────────────────────────
240
317
 
241
318
  Resolution
242
- [ok] Ticket exists in Linear ("Refactor auth middleware")
243
- [--] Status is Todo (current: In Progress)
244
- [ok] Has agent-* label (agent-claude)
245
- [ok] Model resolves from agent-* label (model "claude")
246
- [ok] Description mentions known repo (owner/repo)
247
- [ok] Resolved repo is cloned locally (/dev/workspaces/owner/repo)
319
+ [ok] Ticket exists in Linear ("Multi-event extractor: year inference can produce date_start > date_end")
320
+ [ok] Status is Todo
321
+ (skipped post-dispatch pre-dispatch checks are irrelevant)
248
322
 
249
323
  Eligibility
250
- (skipped — resolution checks failed)
324
+ (skipped — post-dispatch — pre-dispatch checks are irrelevant)
325
+
326
+ Worktree
327
+ [ok] Host worktree exists (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
328
+ [--] Working tree clean (0 modified, 1 untracked)
329
+ [ok] Branch checked out (paul-hrd-442)
330
+
331
+ Workspace
332
+ [ok] Workspace pane open (hrd-442 — attach: `tmux attach -t groundcrew:hrd-442`)
333
+
334
+ Local branch
335
+ [ok] Local branch exists (paul-hrd-442, 2 ahead / 0 behind origin/main)
251
336
 
252
- ineligible: status is In Progress (need Todo)
337
+ Remote branch
338
+ [ok] Branch present on origin
339
+
340
+ Pull request
341
+ [ok] Open PR for this branch (#224 https://github.com/herds-social/herds/pull/224)
342
+
343
+ → pr-open: https://github.com/herds-social/herds/pull/224 (#224)
253
344
  ```
254
345
 
346
+ #### Recovering a stranded ticket
347
+
348
+ The verdict on the last line maps to a recovery action:
349
+
350
+ | Verdict | What to do |
351
+ | ---------------- | --------------------------------------------------------------------------------------------- |
352
+ | `pr-open` | Nothing — the PR is the source of truth. |
353
+ | `pr-merged` | Done. |
354
+ | `in-flight` | The ticket is still being worked on; the verdict line names the workspace pane to attach to. |
355
+ | `recoverable` | Run the printed `nextStep` exactly. |
356
+ | `would-dispatch` | Pre-dispatch checks pass; the orchestrator will pick the ticket up on its next tick. |
357
+ | `ineligible` | A resolution or eligibility check failed; the reason after the colon names the failing check. |
358
+ | `unresolvable` | The Linear ticket couldn't be fetched; the reason after the colon names the error. |
359
+ | `lost` | No trace exists. Re-dispatch via `crew run --ticket <ticket>`. |
360
+
255
361
  ## Troubleshooting
256
362
 
257
363
  First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
@@ -55,6 +55,7 @@ productionresultssa17.blob.core.windows.net
55
55
  productionresultssa18.blob.core.windows.net
56
56
  productionresultssa19.blob.core.windows.net
57
57
  raw.githubusercontent.com
58
+ release-assets.githubusercontent.com
58
59
  results-receiver.actions.githubusercontent.com
59
60
 
60
61
  # npm registry
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAiJA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { doctor } from "./commands/doctor.js";
4
4
  import { orchestrate } from "./commands/orchestrator.js";
5
5
  import { setupReposCli } from "./commands/setupRepos.js";
6
6
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
7
- import { errorMessage, writeError, writeOutput } from "./lib/util.js";
7
+ import { errorMessage, readTicketArgument, writeError, writeOutput } from "./lib/util.js";
8
8
  const requireFromCli = createRequire(import.meta.url);
9
9
  function setupUsage() {
10
10
  return "Usage: crew setup repos [--dry-run] [<repo>...]";
@@ -32,11 +32,7 @@ async function runCli(argv) {
32
32
  continue;
33
33
  }
34
34
  if (argument === "--ticket") {
35
- const value = argv[index + 1];
36
- if (value === undefined || value.length === 0 || value.startsWith("-")) {
37
- throw new Error("crew run --ticket: ticket id is required");
38
- }
39
- ticket = value;
35
+ ticket = readTicketArgument(argv, index, "run");
40
36
  index += 1;
41
37
  continue;
42
38
  }
@@ -53,20 +49,29 @@ async function runCli(argv) {
53
49
  }
54
50
  async function doctorCli(argv) {
55
51
  let ticket;
52
+ const remainingArgs = [];
56
53
  for (let index = 0; index < argv.length; index += 1) {
57
54
  const argument = argv[index];
58
55
  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;
56
+ ticket = readTicketArgument(argv, index, "doctor");
64
57
  index += 1;
65
58
  continue;
66
59
  }
60
+ if (argument === "--no-linear" || argument === "--no-fetch") {
61
+ remainingArgs.push(argument);
62
+ continue;
63
+ }
67
64
  throw new Error(`crew doctor: unknown argument: ${argument}`);
68
65
  }
69
- const ok = ticket === undefined ? await doctor() : await doctor({ ticket });
66
+ if (ticket === undefined) {
67
+ if (remainingArgs.length > 0) {
68
+ throw new Error(`crew doctor: ${remainingArgs[0]} requires --ticket (host doctor mode has no flags)`);
69
+ }
70
+ const ok = await doctor();
71
+ process.exitCode = ok ? process.exitCode : 1;
72
+ return;
73
+ }
74
+ const ok = await doctor({ ticket, ticketArgv: remainingArgs });
70
75
  process.exitCode = ok ? process.exitCode : 1;
71
76
  }
72
77
  const SUBCOMMANDS = {
@@ -76,8 +81,8 @@ const SUBCOMMANDS = {
76
81
  invoke: runCli,
77
82
  },
78
83
  doctor: {
79
- summary: "Verify prereqs, or diagnose one ticket with --ticket",
80
- usage: "[--ticket <ticket>]",
84
+ summary: "Verify prereqs, or diagnose one ticket with --ticket (full lifecycle: dispatch eligibility + local-state recovery)",
85
+ usage: "[--ticket <ticket> [--no-linear] [--no-fetch]]",
81
86
  invoke: doctorCli,
82
87
  },
83
88
  cleanup: {
@@ -4,6 +4,8 @@
4
4
  */
5
5
  export interface DoctorOptions {
6
6
  ticket?: string;
7
+ /** Extra flags after `--ticket <id>`; currently `--no-linear` and `--no-fetch`. */
8
+ ticketArgv?: string[];
7
9
  }
8
10
  export declare function doctor(options?: DoctorOptions): Promise<boolean>;
9
11
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
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"}
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;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAsHD,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E"}
@@ -8,7 +8,7 @@ import { detectHostCapabilities, which } from "../lib/host.js";
8
8
  import { resolveLocalRunner } from "../lib/localRunner.js";
9
9
  import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
10
10
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
11
- import { runTicketDoctor } from "./ticketDoctor.js";
11
+ import { parseTicketDoctorFlags, runTicketDoctor } from "./ticketDoctor.js";
12
12
  // Tokenization stops after this many non-flag tokens. Two is enough to
13
13
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
14
14
  const MAX_TOKENS_PER_CMD = 2;
@@ -124,13 +124,18 @@ function format(check) {
124
124
  }
125
125
  export async function doctor(options = {}) {
126
126
  if (options.ticket !== undefined) {
127
- return await doctorTicket(options.ticket);
127
+ return await doctorTicket(options.ticket, options.ticketArgv ?? []);
128
128
  }
129
129
  return await doctorHost();
130
130
  }
131
- async function doctorTicket(ticket) {
131
+ async function doctorTicket(ticket, ticketArgv) {
132
132
  try {
133
- return await runTicketDoctor(ticket);
133
+ const flags = parseTicketDoctorFlags(ticketArgv);
134
+ return await runTicketDoctor({
135
+ ticket,
136
+ doLinear: flags.doLinear,
137
+ doFetch: flags.doFetch,
138
+ });
134
139
  }
135
140
  catch (error) {
136
141
  const displayTicket = ticket.toUpperCase();
@@ -0,0 +1,22 @@
1
+ export interface TicketCheck {
2
+ name: string;
3
+ status: "ok" | "fail" | "skipped";
4
+ detail?: string;
5
+ failureSummary?: string;
6
+ }
7
+ export interface Section {
8
+ name: string;
9
+ checks: TicketCheck[];
10
+ /** When present and `checks` is empty, the section renders as `(skipped — <skipReason>)`. */
11
+ skipReason?: string;
12
+ }
13
+ interface RenderInput {
14
+ command: string;
15
+ argument: string;
16
+ title?: string;
17
+ sections: Section[];
18
+ verdict: string;
19
+ }
20
+ export declare function renderTicketCheckResult(input: RenderInput): string[];
21
+ export {};
22
+ //# sourceMappingURL=ticketCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticketCheck.d.ts","sourceRoot":"","sources":["../../src/commands/ticketCheck.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,CAMpE"}
@@ -0,0 +1,23 @@
1
+ const STATUS_TAG = {
2
+ ok: "[ok]",
3
+ fail: "[--]",
4
+ skipped: "[? ]",
5
+ };
6
+ function formatCheck(check) {
7
+ const tag = STATUS_TAG[check.status];
8
+ const detail = check.detail === undefined ? "" : ` (${check.detail})`;
9
+ return ` ${tag} ${check.name}${detail}`;
10
+ }
11
+ function sectionLines(section) {
12
+ if (section.checks.length === 0 && section.skipReason !== undefined) {
13
+ return [section.name, ` (skipped — ${section.skipReason})`];
14
+ }
15
+ return [section.name, ...section.checks.map(formatCheck)];
16
+ }
17
+ export function renderTicketCheckResult(input) {
18
+ const titlePart = input.title === undefined ? "" : ` (${input.title})`;
19
+ const header = `groundcrew ${input.command} ${input.argument}${titlePart}`;
20
+ const bar = "─".repeat(header.length);
21
+ const body = input.sections.flatMap((section) => ["", ...sectionLines(section)]);
22
+ return [header, bar, ...body, "", input.verdict];
23
+ }
@@ -1,7 +1,25 @@
1
1
  import { type Blocker, type RawLinearIssue } from "../lib/boardSource.ts";
2
2
  import { type ResolvedConfig } from "../lib/config.ts";
3
3
  import { type UsageByModel } from "../lib/usage.ts";
4
+ import { type WorkspaceAccessHint, type WorkspaceProbe } from "../lib/workspaces.ts";
5
+ import { type WorktreeDirtiness, type WorktreeEntry } from "../lib/worktrees.ts";
6
+ import { type TicketCheck } from "./ticketCheck.ts";
4
7
  export type TicketDoctorVerdict = {
8
+ kind: "pr-open";
9
+ number: number;
10
+ url: string;
11
+ } | {
12
+ kind: "pr-merged";
13
+ number: number;
14
+ url: string;
15
+ } | {
16
+ kind: "in-flight";
17
+ reason: string;
18
+ } | {
19
+ kind: "recoverable";
20
+ reason: string;
21
+ nextStep: string;
22
+ } | {
5
23
  kind: "would-dispatch";
6
24
  } | {
7
25
  kind: "ineligible";
@@ -9,40 +27,170 @@ export type TicketDoctorVerdict = {
9
27
  } | {
10
28
  kind: "unresolvable";
11
29
  reason: string;
30
+ } | {
31
+ kind: "lost";
32
+ reason: string;
12
33
  };
13
- export interface TicketCheck {
14
- name: string;
15
- status: "ok" | "fail" | "skipped";
16
- detail?: string;
17
- failureSummary?: string;
18
- }
19
- export interface TicketDoctorResult {
20
- ticket: string;
21
- title?: string;
22
- resolution: TicketCheck[];
23
- eligibility: TicketCheck[];
24
- verdict: TicketDoctorVerdict;
34
+ export type LinearStatusProbe = {
35
+ kind: "terminal";
36
+ stateName: string;
37
+ } | {
38
+ kind: "non-terminal";
39
+ stateName: string;
40
+ } | {
41
+ kind: "skipped";
42
+ } | {
43
+ kind: "unresolvable";
44
+ reason: string;
45
+ };
46
+ export type WorktreeProbe = {
47
+ kind: "present-clean";
48
+ } | {
49
+ kind: "present-dirty";
50
+ modified: number;
51
+ untracked: number;
52
+ } | {
53
+ kind: "present-unknown-dirtiness";
54
+ reason: string;
55
+ } | {
56
+ kind: "absent";
57
+ };
58
+ export type LocalBranchProbe = {
59
+ kind: "present";
60
+ ahead: number;
61
+ behind: number;
62
+ defaultBranch?: string;
63
+ } | {
64
+ kind: "absent";
65
+ } | {
66
+ kind: "unknown";
67
+ reason: string;
68
+ };
69
+ export type RemoteBranchProbe = {
70
+ kind: "present";
71
+ } | {
72
+ kind: "absent";
73
+ } | {
74
+ kind: "unknown";
75
+ reason: string;
76
+ };
77
+ export type PullRequestProbe = {
78
+ kind: "open";
79
+ number: number;
80
+ url: string;
81
+ } | {
82
+ kind: "merged";
83
+ number: number;
84
+ url: string;
85
+ } | {
86
+ kind: "absent";
87
+ } | {
88
+ kind: "gh-missing";
89
+ } | {
90
+ kind: "unknown";
91
+ reason: string;
92
+ };
93
+ export interface DecideVerdictInput {
94
+ linear: LinearStatusProbe;
95
+ worktree: WorktreeProbe;
96
+ localBranch: LocalBranchProbe;
97
+ remoteBranch: RemoteBranchProbe;
98
+ pullRequest: PullRequestProbe;
99
+ branch: string;
100
+ worktreeDir: string | undefined;
101
+ workspaceName: string | undefined;
25
102
  }
103
+ /**
104
+ * Returns a post-dispatch verdict if the probe bundle matches one of the
105
+ * "ticket has moved past dispatch" cases. Returns `undefined` otherwise,
106
+ * signalling that the caller should fall through to the pre-dispatch path.
107
+ *
108
+ * Precedence: PR > in-flight > recoverable. Inside `recoverable`, dirty
109
+ * worktree beats clean-with-un-pushed-local beats remote-only beats stranded
110
+ * local.
111
+ */
112
+ export declare function decidePostDispatchVerdict(input: DecideVerdictInput): TicketDoctorVerdict | undefined;
26
113
  export interface TicketDoctorDependencies {
27
114
  config: ResolvedConfig;
28
115
  ticket: string;
29
116
  /**
30
- * Injected to keep `ticketDoctor` pure and easy to unit-test. Production
31
- * callers pass a closure that delegates to `fetchRawLinearIssue` with a
32
- * real `LinearClient`; tests pass a `vi.fn()` returning a fixture.
117
+ * Injected to keep `ticketDoctor` pure and easy to unit-test. `undefined`
118
+ * means the caller passed `--no-linear` the Linear-backed pre-dispatch
119
+ * checks (status, label, repo, eligibility) are all skipped.
33
120
  */
34
- fetchRawIssue: (input: {
121
+ fetchRawIssue: ((input: {
35
122
  ticket: string;
36
- }) => Promise<RawLinearIssue>;
123
+ }) => Promise<RawLinearIssue>) | undefined;
37
124
  fetchBlockersFor: (input: {
38
125
  ticket: string;
39
126
  uuid: string;
40
127
  }) => Promise<readonly Blocker[]>;
41
128
  fetchUsage: () => Promise<UsageByModel>;
42
129
  countInProgress: () => Promise<number>;
130
+ findWorktree: (ticket: string) => WorktreeEntry | undefined;
131
+ probeWorkspaces: () => Promise<WorkspaceProbe>;
132
+ workspaceAccessHint: (name: string) => Promise<WorkspaceAccessHint | undefined>;
133
+ probeWorkingTree: (input: {
134
+ worktreeDir: string;
135
+ }) => Promise<WorktreeDirtiness>;
136
+ probeLocalBranch: (input: {
137
+ repoDir: string;
138
+ branch: string;
139
+ defaultBranch: string;
140
+ }) => Promise<LocalBranchProbe>;
141
+ probeRemoteBranch: (input: {
142
+ repoDir: string;
143
+ branch: string;
144
+ doFetch: boolean;
145
+ }) => Promise<RemoteBranchProbe>;
146
+ probePullRequest: (input: {
147
+ repoDir: string;
148
+ branch: string;
149
+ }) => Promise<PullRequestProbe>;
150
+ doFetch: boolean;
43
151
  }
44
- export declare function renderTicketDoctorResult(result: TicketDoctorResult): string[];
152
+ export interface TicketDoctorResult {
153
+ ticket: string;
154
+ title?: string;
155
+ resolution: TicketCheck[];
156
+ eligibility: TicketCheck[];
157
+ worktree: TicketCheck[];
158
+ workspace: TicketCheck[];
159
+ localBranch: TicketCheck[];
160
+ remoteBranch: TicketCheck[];
161
+ pullRequest: TicketCheck[];
162
+ skipReasons: {
163
+ resolution: string;
164
+ eligibility: string;
165
+ worktree: string;
166
+ workspace: string;
167
+ localBranch: string;
168
+ remoteBranch: string;
169
+ pullRequest: string;
170
+ };
171
+ verdict: TicketDoctorVerdict;
172
+ }
173
+ /**
174
+ * Pure-with-async orchestrator that gathers all sections plus the verdict.
175
+ * All I/O happens via injected probes — the function itself does no
176
+ * filesystem, network, or stdout work.
177
+ */
45
178
  export declare function ticketDoctor(dependencies: TicketDoctorDependencies): Promise<TicketDoctorResult>;
46
- export declare function ticketDoctorCli(argv: string[]): Promise<void>;
47
- export declare function runTicketDoctor(ticket: string): Promise<boolean>;
179
+ interface TicketDoctorArguments {
180
+ ticket: string;
181
+ doLinear: boolean;
182
+ doFetch: boolean;
183
+ }
184
+ /**
185
+ * Parses optional `--no-linear` and `--no-fetch` flags that follow
186
+ * `crew doctor --ticket <id>`. The ticket id is consumed by `cli.ts` before
187
+ * this point.
188
+ */
189
+ export declare function parseTicketDoctorFlags(argv: string[]): {
190
+ doLinear: boolean;
191
+ doFetch: boolean;
192
+ };
193
+ export declare function renderTicketDoctorResult(result: TicketDoctorResult): string[];
194
+ export declare function runTicketDoctor(parsed: TicketDoctorArguments): Promise<boolean>;
195
+ export {};
48
196
  //# sourceMappingURL=ticketDoctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ticketDoctor.d.ts","sourceRoot":"","sources":["../../src/commands/ticketDoctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,OAAO,EAEZ,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAA+B,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AASrE,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACtE,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IAC3F,UAAU,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;IACxC,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACxC;AA+SD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAmB7E;AAED,wBAAsB,YAAY,CAChC,YAAY,EAAE,wBAAwB,GACrC,OAAO,CAAC,kBAAkB,CAAC,CA0F7B;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAenE;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBtE"}
1
+ {"version":3,"file":"ticketDoctor.d.ts","sourceRoot":"","sources":["../../src/commands/ticketDoctor.ts"],"names":[],"mappings":"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"}