@clipboard-health/groundcrew 3.1.2 → 3.1.4

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.
Files changed (59) hide show
  1. package/README.md +47 -17
  2. package/crew.config.example.ts +25 -10
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +12 -0
  5. package/dist/commands/cleaner.d.ts.map +1 -1
  6. package/dist/commands/cleaner.js +6 -4
  7. package/dist/commands/cleanupWorkspace.d.ts.map +1 -1
  8. package/dist/commands/cleanupWorkspace.js +2 -0
  9. package/dist/commands/dispatcher.d.ts.map +1 -1
  10. package/dist/commands/dispatcher.js +6 -6
  11. package/dist/commands/eligibility.d.ts.map +1 -1
  12. package/dist/commands/eligibility.js +2 -2
  13. package/dist/commands/interruptWorkspace.d.ts +8 -0
  14. package/dist/commands/interruptWorkspace.d.ts.map +1 -0
  15. package/dist/commands/interruptWorkspace.js +108 -0
  16. package/dist/commands/orchestrator.d.ts +4 -2
  17. package/dist/commands/orchestrator.d.ts.map +1 -1
  18. package/dist/commands/orchestrator.js +6 -105
  19. package/dist/commands/resumeWorkspace.d.ts +7 -0
  20. package/dist/commands/resumeWorkspace.d.ts.map +1 -0
  21. package/dist/commands/resumeWorkspace.js +163 -0
  22. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  23. package/dist/commands/setupWorkspace.js +78 -79
  24. package/dist/commands/ticketDoctor.d.ts +18 -3
  25. package/dist/commands/ticketDoctor.d.ts.map +1 -1
  26. package/dist/commands/ticketDoctor.js +105 -11
  27. package/dist/index.d.ts +6 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -2
  30. package/dist/lib/agentLaunch.d.ts +29 -0
  31. package/dist/lib/agentLaunch.d.ts.map +1 -0
  32. package/dist/lib/agentLaunch.js +53 -0
  33. package/dist/lib/boardSource.d.ts +41 -5
  34. package/dist/lib/boardSource.d.ts.map +1 -1
  35. package/dist/lib/boardSource.js +211 -70
  36. package/dist/lib/config.d.ts +59 -25
  37. package/dist/lib/config.d.ts.map +1 -1
  38. package/dist/lib/config.js +130 -22
  39. package/dist/lib/linearIssueStatus.d.ts +3 -1
  40. package/dist/lib/linearIssueStatus.d.ts.map +1 -1
  41. package/dist/lib/linearIssueStatus.js +0 -0
  42. package/dist/lib/runState.d.ts +46 -0
  43. package/dist/lib/runState.d.ts.map +1 -0
  44. package/dist/lib/runState.js +137 -0
  45. package/dist/lib/runStateCleanup.d.ts +4 -0
  46. package/dist/lib/runStateCleanup.d.ts.map +1 -0
  47. package/dist/lib/runStateCleanup.js +12 -0
  48. package/dist/lib/stagedLaunch.d.ts +32 -0
  49. package/dist/lib/stagedLaunch.d.ts.map +1 -0
  50. package/dist/lib/stagedLaunch.js +58 -0
  51. package/dist/lib/util.d.ts +0 -1
  52. package/dist/lib/util.d.ts.map +1 -1
  53. package/dist/lib/util.js +0 -4
  54. package/dist/lib/workspaces.d.ts +19 -1
  55. package/dist/lib/workspaces.d.ts.map +1 -1
  56. package/dist/lib/workspaces.js +29 -9
  57. package/dist/lib/worktrees.d.ts.map +1 -1
  58. package/dist/lib/worktrees.js +12 -4
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -71,7 +71,7 @@ Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitiv
71
71
 
72
72
  Or drop `crew.config.ts` at the root of any repo you run `crew` from — `crew` discovers it via cosmiconfig project-walk. Any of `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, or `.config/crewrc{,.json}` work.
73
73
 
74
- Set `linear.projectSlug` (paste the trailing slug of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Defaults cover everything else.
74
+ Set `linear.projects[].projectSlug` (paste the trailing slug of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Defaults cover everything else. To watch multiple projects from one `crew` instance, add more entries to `linear.projects`; they all share the same `orchestrator.maximumInProgress` budget.
75
75
 
76
76
  Then clone each repo before the first `crew run` — groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
77
77
 
@@ -163,11 +163,11 @@ Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines
163
163
 
164
164
  Three keys are required; everything else has a default.
165
165
 
166
- | Key | What |
167
- | ----------------------------- | -------------------------------------------------------------------------- |
168
- | `linear.projectSlug` | Trailing slug of the Linear project URL (e.g. `ai-strategy-5152195762f3`). |
169
- | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
170
- | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
166
+ | Key | What |
167
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
168
+ | `linear.projects[].projectSlug` | Trailing slug of each Linear project URL to watch (e.g. `ai-strategy-5152195762f3`). One or more entries. |
169
+ | `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
170
+ | `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
171
171
 
172
172
  `crew` resolves config as: `GROUNDCREW_CONFIG` if set → project-walk from cwd (cosmiconfig: `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, `.config/crewrc{,.json}`) → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts` (also accepts legacy `config.ts` for one release). The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username` — not configurable.
173
173
 
@@ -197,16 +197,17 @@ This keeps package defaults portable while letting your private config reference
197
197
 
198
198
  | Key | Default | What it does |
199
199
  | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
- | `linear.projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `crew.config.ts` self-documenting and the lookup survives project renames. |
201
- | `linear.statuses.todo` | `"Todo"` | Status name picked up for new work. |
202
- | `linear.statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward `maximumInProgress`. |
203
- | `linear.statuses.done` | `"Done"` | Status that triggers worktree cleanup. |
204
- | `linear.statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup, board remaining counts, and blocker checks. The `done` status is always included. |
200
+ | `linear.projects` | **required** | Non-empty array of Linear projects to watch. One `crew` instance dispatches across every entry under a shared `maximumInProgress` budget. |
201
+ | `linear.projects[].projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `crew.config.ts` self-documenting and the lookup survives project renames. |
202
+ | `linear.projects[].statuses.todo` | `"Todo"` | Status name picked up for new work in this project. Per-project so multi-team setups with divergent state names can coexist. |
203
+ | `linear.projects[].statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward the shared `maximumInProgress`. |
204
+ | `linear.projects[].statuses.done` | `"Done"` | Status that triggers worktree cleanup for this project. |
205
+ | `linear.projects[].statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup and blocker checks. The project's `done` status is always included. |
205
206
  | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
206
207
  | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
207
208
  | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
208
209
  | `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
209
- | `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.statuses.inProgress` at once. |
210
+ | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once, shared across every project in `linear.projects`. |
210
211
  | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
211
212
  | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
212
213
  | `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`. |
@@ -316,14 +317,16 @@ crew run # one-shot dispatch
316
317
  crew run --watch # poll forever
317
318
  crew run --ticket <TICKET> # provision one ticket and exit
318
319
  crew setup repos [--dry-run] [<repo>...]
320
+ crew interrupt <TICKET> [--reason <text>] # stop the live workspace, keep the worktree
321
+ crew resume <TICKET> # reopen an existing ticket worktree
319
322
  crew cleanup <TICKET> # tear down every worktree carrying this ticket
320
323
  ```
321
324
 
322
- `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`.
325
+ `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 (recorded run state, host worktree, workspace pane, local branch, remote branch, open PR). Verdict precedence starts with PR outcomes (`pr-open` > `pr-merged`). Recorded failed launches report before ordinary local recovery, interrupted runs report concrete recoverable git work first when it exists and otherwise report `interrupted`, and ordinary post-dispatch cases report `in-flight` before `recoverable`. If none of those apply, doctor falls through to `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`.
323
326
 
324
327
  ### `crew doctor --ticket <ticket>`
325
328
 
326
- 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.
329
+ 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 recorded run state, host worktree, workspace pane, local branch, remote branch, and PR; prints a single verdict with a copy-pasteable recovery step when one applies.
327
330
 
328
331
  Flags:
329
332
 
@@ -344,6 +347,13 @@ Resolution
344
347
  Eligibility
345
348
  (skipped — post-dispatch — pre-dispatch checks are irrelevant)
346
349
 
350
+ Run state
351
+ [ok] Local run state (running)
352
+ [ok] Recorded model (claude)
353
+ [ok] Recorded worktree (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
354
+ [ok] Recorded branch (paul-hrd-442)
355
+ [ok] Resume count (0)
356
+
347
357
  Worktree
348
358
  [ok] Host worktree exists (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
349
359
  [--] Working tree clean (0 modified, 1 untracked)
@@ -374,11 +384,31 @@ The verdict on the last line maps to a recovery action:
374
384
  | `pr-merged` | Done. |
375
385
  | `in-flight` | The ticket is still being worked on; the verdict line names the workspace pane to attach to. |
376
386
  | `recoverable` | Run the printed `nextStep` exactly. |
387
+ | `interrupted` | Resume the preserved worktree with `crew resume <ticket>` or inspect it by hand. |
388
+ | `failed-launch` | Fix the launch failure, then run `crew resume <ticket>` or `crew cleanup <ticket>`. |
377
389
  | `would-dispatch` | Pre-dispatch checks pass; the orchestrator will pick the ticket up on its next tick. |
378
390
  | `ineligible` | A resolution or eligibility check failed; the reason after the colon names the failing check. |
379
391
  | `unresolvable` | The Linear ticket couldn't be fetched; the reason after the colon names the error. |
380
392
  | `lost` | No trace exists. Re-dispatch via `crew run --ticket <ticket>`. |
381
393
 
394
+ ### `crew interrupt <ticket>`
395
+
396
+ Stop a live workspace pane while preserving the ticket worktree and branch. This is the manual pause button for cases where you need terminal capacity back, want to stop an agent that is going in the wrong direction, or need to inspect the diff before letting another agent continue.
397
+
398
+ ```bash
399
+ crew interrupt HRD-442 --reason "wrong implementation direction"
400
+ crew doctor --ticket HRD-442
401
+ crew resume HRD-442
402
+ ```
403
+
404
+ The command closes the cmux/tmux workspace when it exists, records local run state under the groundcrew state directory, 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.
405
+
406
+ ### `crew resume <ticket>`
407
+
408
+ Reopen 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>`.
409
+
410
+ The resume prompt tells the agent to inspect current 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.
411
+
382
412
  ## Troubleshooting
383
413
 
384
414
  First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
@@ -414,7 +444,7 @@ When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm i
414
444
  <details>
415
445
  <summary>Status names matter</summary>
416
446
 
417
- If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"`.
447
+ If your team uses `Started` instead of `In Progress`, set `linear.projects[].statuses.inProgress = "Started"` on that project's entry. Status overrides are per-project so divergent team workflows coexist.
418
448
 
419
449
  </details>
420
450
 
@@ -433,9 +463,9 @@ Groundcrew sets a ticket to `inProgress` when it provisions a workspace and neve
433
463
  </details>
434
464
 
435
465
  <details>
436
- <summary>Project must be on a single Linear team in practice</summary>
466
+ <summary>Cross-team projects need a consistent `inProgress` name per project</summary>
437
467
 
438
- 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`.
468
+ Cross-team projects work — the orchestrator caches the in-progress state ID per `(team, statusName)` pair — but every team in a given project must use the same status name for that project's `statuses.inProgress`. If you watch two projects that share a Linear team but configure different `inProgress` names, each project's lookup is independent.
439
469
 
440
470
  </details>
441
471
 
@@ -3,16 +3,30 @@ import type { Config } from "./src/lib/config.js";
3
3
 
4
4
  export default {
5
5
  linear: {
6
- // Project URL slug to scope polling. Copy the trailing segment of
7
- // your Linear project URL
8
- // https://linear.app/<workspace>/project/<projectSlug>
9
- // — verbatim, for example "ai-strategy-5152195762f3". The 12-char hex
10
- // tail is the canonical ID groundcrew uses, so the orchestrator stays
11
- // resilient across project renames and across same-name projects in
12
- // different teams. The leading name segment keeps the file
13
- // self-documenting at a glance.
14
- projectSlug: "your-project-name-0123456789ab",
15
- // statuses: { todo: "Todo", inProgress: "In Progress", done: "Done", terminal: ["Done"] },
6
+ // One or more Linear projects to watch. A single `crew` process
7
+ // dispatches across all configured projects under a shared
8
+ // `orchestrator.maximumInProgress` budget.
9
+ //
10
+ // Each entry's `projectSlug` is the trailing segment of your Linear
11
+ // project URL copy it verbatim, e.g. "ai-strategy-5152195762f3"
12
+ // from "https://linear.app/<workspace>/project/ai-strategy-5152195762f3".
13
+ // The 12-char hex tail is the canonical ID groundcrew uses, so the
14
+ // orchestrator stays resilient across project renames and across
15
+ // same-name projects in different teams. The leading name segment
16
+ // keeps the file self-documenting at a glance.
17
+ //
18
+ // `statuses` is per-project so multi-team setups with divergent
19
+ // workflow state names (e.g. "Todo" vs "To Do", "Shipped" vs
20
+ // "Done") can coexist. Each field falls back to its default when
21
+ // omitted: { todo: "Todo", inProgress: "In Progress",
22
+ // done: "Done", terminal: ["Done"] }.
23
+ projects: [
24
+ { projectSlug: "your-project-name-0123456789ab" },
25
+ // {
26
+ // projectSlug: "platform-aaaaaaaaaaaa",
27
+ // statuses: { inProgress: "Doing", done: "Released", terminal: ["Released", "Won't Do"] },
28
+ // },
29
+ ],
16
30
  },
17
31
  workspace: {
18
32
  // Parent directory under which groundcrew clones repositories and
@@ -29,6 +43,7 @@ export default {
29
43
  // git: { remote: "origin", defaultBranch: "main" },
30
44
  //
31
45
  // orchestrator: {
46
+ // // Shared across all watched projects in linear.projects.
32
47
  // maximumInProgress: 4,
33
48
  // pollIntervalMilliseconds: 120_000,
34
49
  // sessionLimitPercentage: 85,
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA6JA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { cleanupWorkspaceCli } from "./commands/cleanupWorkspace.js";
3
3
  import { doctor } from "./commands/doctor.js";
4
+ import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
4
5
  import { orchestrate } from "./commands/orchestrator.js";
6
+ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
5
7
  import { setupReposCli } from "./commands/setupRepos.js";
6
8
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
7
9
  import { errorMessage, readTicketArgument, writeError, writeOutput } from "./lib/util.js";
@@ -90,6 +92,16 @@ const SUBCOMMANDS = {
90
92
  usage: "[--force] <ticket>",
91
93
  invoke: cleanupWorkspaceCli,
92
94
  },
95
+ interrupt: {
96
+ summary: "Stop a live ticket workspace while preserving its worktree",
97
+ usage: "<ticket> [--reason <text>]",
98
+ invoke: interruptWorkspaceCli,
99
+ },
100
+ resume: {
101
+ summary: "Reopen an existing ticket worktree with a continuation prompt",
102
+ usage: "<ticket>",
103
+ invoke: resumeWorkspaceCli,
104
+ },
93
105
  setup: {
94
106
  summary: "Project-level setup commands (currently: repos)",
95
107
  usage: "repos [--dry-run] [<repo>...]",
@@ -1 +1 @@
1
- {"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../src/commands/cleaner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,qBAAqB,CAAC;AAGpE,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAqDxD"}
1
+ {"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../src/commands/cleaner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,EAA4B,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,qBAAqB,CAAC;AAGpE,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAsDxD"}
@@ -3,7 +3,8 @@
3
3
  * tickets that have reached a terminal status. One per `orchestrate()`
4
4
  * invocation; stateless across iterations. Mirrors `Dispatcher`.
5
5
  */
6
- import { isTerminalStatus } from "../lib/boardSource.js";
6
+ import { isTerminalStatusForIssue } from "../lib/boardSource.js";
7
+ import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
7
8
  import { log, logEvent } from "../lib/util.js";
8
9
  import { worktrees } from "../lib/worktrees.js";
9
10
  import { logTeardown, recordTeardownEvents } from "./teardownReporter.js";
@@ -11,10 +12,10 @@ export function createCleaner(deps) {
11
12
  const { config } = deps;
12
13
  async function runOnce(arguments_) {
13
14
  const { state, worktreeEntries, dryRun, signal } = arguments_;
14
- // Only act on tickets in this project — if the dir name happens to look
15
- // like a Linear ticket from another project, leave it alone.
15
+ // Only act on tickets in configured projects — if the dir name happens to
16
+ // look like a Linear ticket from another project, leave it alone.
16
17
  const terminalTickets = new Set(state.issues
17
- .filter((issue) => isTerminalStatus(issue.status, config))
18
+ .filter((issue) => isTerminalStatusForIssue(issue, config))
18
19
  .map((issue) => issue.id));
19
20
  if (terminalTickets.size === 0) {
20
21
  return;
@@ -41,6 +42,7 @@ export function createCleaner(deps) {
41
42
  const result = signal === undefined
42
43
  ? await worktrees.teardown(config, stale)
43
44
  : await worktrees.teardown(config, stale, { signal });
45
+ recordCleanedUpRuns(config, result.removed);
44
46
  logTeardown(result);
45
47
  recordTeardownEvents(result);
46
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAKnE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE"}
1
+ {"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE"}
@@ -1,4 +1,5 @@
1
1
  import { loadConfig } from "../lib/config.js";
2
+ import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
2
3
  import { log } from "../lib/util.js";
3
4
  import { worktrees } from "../lib/worktrees.js";
4
5
  import { logTeardown } from "./teardownReporter.js";
@@ -29,6 +30,7 @@ export async function cleanupWorkspace(config, options) {
29
30
  return;
30
31
  }
31
32
  const result = await worktrees.teardown(config, entries, { force });
33
+ recordCleanedUpRuns(config, result.removed);
32
34
  logTeardown(result);
33
35
  if (result.failures.length > 0) {
34
36
  throw result.failures[0]?.error;
@@ -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;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"}
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,EACL,KAAK,UAAU,EAIhB,MAAM,uBAAuB,CAAC;AAC/B,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"}
@@ -6,7 +6,7 @@
6
6
  * Pure verdict logic lives in `eligibility.ts`; this module is responsible
7
7
  * for telemetry, Linear writes, and side-effecting setupWorkspace calls.
8
8
  */
9
- import { isGroundcrewIssue } from "../lib/boardSource.js";
9
+ import { isGroundcrewIssue, projectFor, } 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";
@@ -87,17 +87,17 @@ export function createDispatcher(deps) {
87
87
  async function runOnce(arguments_) {
88
88
  const { state, worktreeEntries, usage, dryRun, signal } = arguments_;
89
89
  issueStatusUpdater.resetMissingInProgressCache();
90
- const activeCount = state.issues.filter((issue) => issue.status === config.linear.statuses.inProgress).length;
90
+ const activeCount = state.issues.filter((issue) => issue.status === projectFor(issue, config).statuses.inProgress).length;
91
91
  const slots = config.orchestrator.maximumInProgress - activeCount;
92
92
  // Narrow Todo to tickets that opted in via an `agent-*` label.
93
93
  // Unlabeled tickets are not groundcrew's concern even when in Todo.
94
- const todo = state.issues.filter((issue) => issue.status === config.linear.statuses.todo && isGroundcrewIssue(issue));
94
+ const todo = state.issues.filter((issue) => issue.status === projectFor(issue, config).statuses.todo && isGroundcrewIssue(issue));
95
95
  if (slots <= 0) {
96
96
  log(`At capacity (${activeCount}/${config.orchestrator.maximumInProgress}), no new work to start`);
97
97
  return;
98
98
  }
99
99
  if (todo.length === 0) {
100
- log(`No ${config.linear.statuses.todo} tickets to pick up`);
100
+ log(`No Todo tickets to pick up`);
101
101
  return;
102
102
  }
103
103
  // Run the blocker pre-pass first so an all-blocked board short-circuits
@@ -107,7 +107,7 @@ export function createDispatcher(deps) {
107
107
  logSkip(skip);
108
108
  }
109
109
  if (unblocked.length === 0) {
110
- log(`No eligible ${config.linear.statuses.todo} tickets after blocker filtering`);
110
+ log(`No eligible Todo tickets after blocker filtering`);
111
111
  return;
112
112
  }
113
113
  // usage() is an HTTP call; workspaces.probe shells tmux/cmux. Kick off
@@ -146,7 +146,7 @@ export function createDispatcher(deps) {
146
146
  logSkip(skip);
147
147
  }
148
148
  if (starts.length === 0) {
149
- log(`No eligible ${config.linear.statuses.todo} tickets after eligibility filtering`);
149
+ log(`No eligible Todo tickets after eligibility filtering`);
150
150
  return;
151
151
  }
152
152
  log(`${slots} slot(s) available, starting ${starts.length} ticket(s): ${starts.map(({ issue }) => `${issue.id}(${issue.model})`).join(", ")}`);
@@ -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;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"}
1
+ {"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AAC/B,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"}
@@ -6,7 +6,7 @@
6
6
  * The Dispatcher consumes the verdict list to drive logging and side
7
7
  * effects.
8
8
  */
9
- import { isTerminalStatus } from "../lib/boardSource.js";
9
+ import { isTerminalStatusForBlocker, } from "../lib/boardSource.js";
10
10
  import { AGENT_ANY_MODEL } from "../lib/config.js";
11
11
  const PERCENT_FRACTION_DIVISOR = 100;
12
12
  const DAYS_PER_WEEK = 7;
@@ -26,7 +26,7 @@ function blockerVerdictFor(issue, config) {
26
26
  blockers,
27
27
  };
28
28
  }
29
- const unresolved = issue.blockers.filter((blocker) => blocker.status === undefined || !isTerminalStatus(blocker.status, config));
29
+ const unresolved = issue.blockers.filter((blocker) => !isTerminalStatusForBlocker(blocker, config));
30
30
  if (unresolved.length === 0) {
31
31
  return undefined;
32
32
  }
@@ -0,0 +1,8 @@
1
+ import { type ResolvedConfig } from "../lib/config.ts";
2
+ export interface InterruptWorkspaceOptions {
3
+ ticket: string;
4
+ reason?: string;
5
+ }
6
+ export declare function interruptWorkspace(config: ResolvedConfig, options: InterruptWorkspaceOptions): Promise<void>;
7
+ export declare function interruptWorkspaceCli(argv: string[]): Promise<void>;
8
+ //# sourceMappingURL=interruptWorkspace.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,108 @@
1
+ import { loadConfig } from "../lib/config.js";
2
+ import { readRunState, recordRunState } from "../lib/runState.js";
3
+ import { errorMessage, log } from "../lib/util.js";
4
+ import { workspaces } from "../lib/workspaces.js";
5
+ import { worktrees } from "../lib/worktrees.js";
6
+ function parseArguments(argv) {
7
+ let reason;
8
+ const positionals = [];
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const argument = argv[index];
11
+ /* v8 ignore next @preserve -- loop bounds ensure argv[index] exists; guard satisfies noUncheckedIndexedAccess */
12
+ if (argument === undefined) {
13
+ continue;
14
+ }
15
+ if (argument === "--reason") {
16
+ const value = argv[index + 1];
17
+ if (value === undefined || value.length === 0 || value.startsWith("-")) {
18
+ throw new Error("crew interrupt --reason: reason text is required");
19
+ }
20
+ reason = value;
21
+ index += 1;
22
+ continue;
23
+ }
24
+ if (argument.startsWith("-")) {
25
+ throw new Error(`Unknown option: ${argument}\nUsage: crew interrupt <ticket> [--reason <text>]`);
26
+ }
27
+ positionals.push(argument);
28
+ }
29
+ const [ticket, ...extras] = positionals;
30
+ if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
31
+ throw new Error("Usage: crew interrupt <ticket> [--reason <text>]");
32
+ }
33
+ return { ticket: ticket.toLowerCase(), ...(reason === undefined ? {} : { reason }) };
34
+ }
35
+ function sourceFromState(state) {
36
+ return {
37
+ ticket: state.ticket,
38
+ repository: state.repository,
39
+ model: state.model,
40
+ worktreeDir: state.worktreeDir,
41
+ branchName: state.branchName,
42
+ workspaceName: state.workspaceName,
43
+ resumeCount: state.resumeCount,
44
+ };
45
+ }
46
+ function sourceFromWorktree(config, ticket, entry) {
47
+ return {
48
+ ticket,
49
+ repository: entry.repository,
50
+ model: config.models.default,
51
+ worktreeDir: entry.dir,
52
+ branchName: entry.branchName,
53
+ workspaceName: ticket,
54
+ resumeCount: 0,
55
+ };
56
+ }
57
+ function resolveInterruptSource(arguments_) {
58
+ if (arguments_.state !== undefined) {
59
+ return sourceFromState(arguments_.state);
60
+ }
61
+ if (arguments_.entry !== undefined) {
62
+ return sourceFromWorktree(arguments_.config, arguments_.ticket, arguments_.entry);
63
+ }
64
+ throw new Error(`No run state or worktree found for ${arguments_.ticket}; nothing to interrupt.`);
65
+ }
66
+ function interruptDetail(result) {
67
+ if (result.kind === "missing") {
68
+ return "workspace missing";
69
+ }
70
+ return undefined;
71
+ }
72
+ function failOnUnavailable(result) {
73
+ if (result.kind !== "unavailable") {
74
+ return;
75
+ }
76
+ const detail = result.error === undefined ? "workspace adapter unavailable" : errorMessage(result.error);
77
+ throw new Error(`Could not interrupt workspace: ${detail}`);
78
+ }
79
+ export async function interruptWorkspace(config, options) {
80
+ const ticket = options.ticket.toLowerCase();
81
+ const state = readRunState(config, ticket);
82
+ const [entry] = worktrees.findByTicket(config, ticket);
83
+ const source = resolveInterruptSource({ config, ticket, state, entry });
84
+ const result = await workspaces.interrupt(config, source.workspaceName);
85
+ failOnUnavailable(result);
86
+ const detail = interruptDetail(result);
87
+ recordRunState({
88
+ config,
89
+ state: {
90
+ ticket,
91
+ repository: source.repository,
92
+ model: source.model,
93
+ worktreeDir: source.worktreeDir,
94
+ branchName: source.branchName,
95
+ workspaceName: source.workspaceName,
96
+ state: "interrupted",
97
+ resumeCount: source.resumeCount,
98
+ ...(options.reason === undefined ? {} : { reason: options.reason }),
99
+ ...(detail === undefined ? {} : { detail }),
100
+ },
101
+ });
102
+ log(`Interrupted ${ticket}; worktree preserved at ${source.worktreeDir}`);
103
+ log(`Next: crew doctor --ticket ${ticket}`);
104
+ }
105
+ export async function interruptWorkspaceCli(argv) {
106
+ const config = await loadConfig();
107
+ await interruptWorkspace(config, parseArguments(argv));
108
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
- * groundcrew orchestrator — polls a Linear project and spins up
3
- * workspace + git-worktree pairs for ready tickets.
2
+ * groundcrew orchestrator — polls Linear projects and spins up workspace +
3
+ * git-worktree pairs for ready tickets. Each tick fetches the board, runs
4
+ * the cleaner, and runs the dispatcher; logging from those modules is the
5
+ * orchestrator's user-facing output.
4
6
  */
5
7
  export interface OrchestratorOptions {
6
8
  watch: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4LH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC7E"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7E"}