@clipboard-health/groundcrew 4.2.0 → 4.2.2

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 (70) hide show
  1. package/README.md +15 -25
  2. package/dist/commands/cleaner.d.ts +1 -1
  3. package/dist/commands/cleaner.d.ts.map +1 -1
  4. package/dist/commands/cleaner.js +4 -2
  5. package/dist/commands/dispatcher.d.ts +7 -6
  6. package/dist/commands/dispatcher.d.ts.map +1 -1
  7. package/dist/commands/dispatcher.js +56 -28
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/doctor.js +18 -22
  10. package/dist/commands/eligibility.d.ts +1 -1
  11. package/dist/commands/eligibility.d.ts.map +1 -1
  12. package/dist/commands/eligibility.js +7 -6
  13. package/dist/commands/orchestrator.d.ts.map +1 -1
  14. package/dist/commands/orchestrator.js +18 -14
  15. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  16. package/dist/commands/resumeWorkspace.js +3 -2
  17. package/dist/commands/setupWorkspace.d.ts +2 -4
  18. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  19. package/dist/commands/setupWorkspace.js +27 -27
  20. package/dist/commands/status.d.ts.map +1 -1
  21. package/dist/commands/status.js +6 -3
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/lib/adapters/linear/client.d.ts +22 -0
  26. package/dist/lib/adapters/linear/client.d.ts.map +1 -0
  27. package/dist/lib/adapters/linear/client.js +36 -0
  28. package/dist/lib/adapters/linear/factory.d.ts +24 -14
  29. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  30. package/dist/lib/adapters/linear/factory.js +113 -46
  31. package/dist/lib/{boardSource.d.ts → adapters/linear/fetch.d.ts} +22 -74
  32. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -0
  33. package/dist/lib/{boardSource.js → adapters/linear/fetch.js} +28 -136
  34. package/dist/lib/adapters/linear/index.d.ts +1 -0
  35. package/dist/lib/adapters/linear/index.d.ts.map +1 -1
  36. package/dist/lib/adapters/linear/parsing.d.ts +44 -0
  37. package/dist/lib/adapters/linear/parsing.d.ts.map +1 -0
  38. package/dist/lib/adapters/linear/parsing.js +144 -0
  39. package/dist/lib/{linearIssueStatus.d.ts → adapters/linear/writeback.d.ts} +1 -2
  40. package/dist/lib/adapters/linear/writeback.d.ts.map +1 -0
  41. package/dist/lib/{linearIssueStatus.js → adapters/linear/writeback.js} +16 -17
  42. package/dist/lib/adapters/shell/factory.d.ts +1 -1
  43. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  44. package/dist/lib/adapters/shell/factory.js +8 -4
  45. package/dist/lib/adapters/shell/invoke.d.ts +4 -7
  46. package/dist/lib/adapters/shell/invoke.d.ts.map +1 -1
  47. package/dist/lib/adapters/shell/invoke.js +46 -75
  48. package/dist/lib/adapters/shell/schema.d.ts +10 -0
  49. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  50. package/dist/lib/adapters/shell/schema.js +9 -5
  51. package/dist/lib/board.d.ts.map +1 -1
  52. package/dist/lib/board.js +43 -4
  53. package/dist/lib/buildSources.d.ts +11 -0
  54. package/dist/lib/buildSources.d.ts.map +1 -1
  55. package/dist/lib/buildSources.js +41 -0
  56. package/dist/lib/repositoryValidation.d.ts +13 -0
  57. package/dist/lib/repositoryValidation.d.ts.map +1 -0
  58. package/dist/lib/repositoryValidation.js +20 -0
  59. package/dist/lib/testing/canonicalFixtures.d.ts +19 -0
  60. package/dist/lib/testing/canonicalFixtures.d.ts.map +1 -0
  61. package/dist/lib/testing/canonicalFixtures.js +62 -0
  62. package/dist/lib/ticketSource.d.ts +73 -3
  63. package/dist/lib/ticketSource.d.ts.map +1 -1
  64. package/dist/lib/ticketSource.js +31 -0
  65. package/dist/lib/util.d.ts +0 -20
  66. package/dist/lib/util.d.ts.map +1 -1
  67. package/dist/lib/util.js +0 -35
  68. package/package.json +1 -1
  69. package/dist/lib/boardSource.d.ts.map +0 -1
  70. package/dist/lib/linearIssueStatus.d.ts.map +0 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- Dispatch your Linear backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
9
+ Dispatch your ticket backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -46,9 +46,9 @@ In Progress (state.type=started) — Add retry logic to the sync job
46
46
 
47
47
  ## Why
48
48
 
49
- - **Linear-native.** Polls issues assigned to the API key's viewer with `agent-*` labels, honors blockers.
49
+ - **Pluggable ticket sources.** Ships with a built-in Linear adapter (polls your API key viewer's `agent-*`-labeled issues, honors blockers); bring shell, Jira, or any source via `crew.config.ts`.
50
50
  - **One worktree per ticket.** Agents work in parallel without stepping on each other.
51
- - **Local-first sandboxing.** Safehouse on macOS, Docker Sandboxes on Linux, or an explicit `none` escape hatch.
51
+ - **Local-first sandboxing.** Safehouse on macOS, Docker Sandboxes on Linux/WSL, or an explicit `none` escape hatch.
52
52
  - **Multi-agent.** Ships with `claude` and `codex`; bring your own CLI via `crew.config.ts`.
53
53
 
54
54
  ## Quickstart
@@ -94,28 +94,18 @@ crew cleanup <TICKET> # tear down every workt
94
94
  crew upgrade [<version>] # reinstall crew globally through npm
95
95
  ```
96
96
 
97
- 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>`.
97
+ ## Manual repository bootstrap
98
98
 
99
- ## Manual Repository Bootstrap
100
-
101
- Groundcrew no longer clones repositories for you. For each `workspace.knownRepositories` entry,
102
- clone the repository into `workspace.projectDir` using the same relative path that appears in the
103
- config. For an `OWNER/REPO` entry:
99
+ Groundcrew never clones repositories for you. Clone each `workspace.knownRepositories` entry into `workspace.projectDir` using the same relative path the config uses. For an `OWNER/REPO` entry:
104
100
 
105
101
  ```bash
106
102
  PROJECT_DIR="$HOME/dev/c"
107
103
  mkdir -p "$PROJECT_DIR/OWNER"
108
104
  git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
105
+ # HTTPS works the same: git clone https://github.com/OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
109
106
  ```
110
107
 
111
- HTTPS works the same way if you do not use SSH:
112
-
113
- ```bash
114
- git clone https://github.com/OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
115
- ```
116
-
117
- Bare-name entries do not include an owner, so choose the correct remote URL yourself and clone it to
118
- `$PROJECT_DIR/<name>`. `crew setup repos` now exits non-zero and points back to this section.
108
+ Bare-name entries have no owner, so pick the remote URL yourself and clone to `$PROJECT_DIR/<name>`.
119
109
 
120
110
  ## Configuration
121
111
 
@@ -144,7 +134,7 @@ Status classification uses Linear's workflow `state.type` (`unstarted`, `started
144
134
  <details>
145
135
  <summary>Config discovery</summary>
146
136
 
147
- Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any of `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` (legacy `config.ts` accepted for one release). The "Loaded config from …" line at startup tells you which won.
137
+ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any of `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`. The "Loaded config from …" line at startup tells you which won.
148
138
 
149
139
  </details>
150
140
 
@@ -153,7 +143,7 @@ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any
153
143
 
154
144
  | Key | Default | What it does |
155
145
  | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
- | `sources` | `[]` | Additional pluggable ticket sources. Extra sources are verified at startup; the built-in Linear adapter remains the dispatch read path until the canonical consumer refactor. Built-in kinds: `shell`, `linear`. |
146
+ | `sources` | `[]` | Additional pluggable ticket sources, dispatched alongside the built-in Linear adapter. Built-in kinds: `shell`, `linear`. |
157
147
  | `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
158
148
  | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
159
149
  | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
@@ -235,7 +225,7 @@ Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKE
235
225
 
236
226
  ## Doctor
237
227
 
238
- `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.
228
+ `crew doctor` checks host prerequisites only: config validity, ticket-source reachability (every configured source's `verify()`, including the built-in Linear adapter), required binaries on PATH, workspace backend availability, workspace.projectDir, local runner capability, and enabled model commands.
239
229
 
240
230
  <details>
241
231
  <summary>Sample ticket status output</summary>
@@ -269,7 +259,7 @@ In Progress (state.type=started) — Multi-event extractor: year inference can p
269
259
 
270
260
  ### `crew start <TICKET>`
271
261
 
272
- 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>`.)
262
+ Launches one ticket immediately, bypassing orchestrator eligibility. Use it to dispatch a specific ticket on demand — including unlabeled tickets that `crew run` ignores.
273
263
 
274
264
  ```bash
275
265
  crew start HRD-442
@@ -278,7 +268,7 @@ crew start HRD-442 --dry-run
278
268
 
279
269
  ### `crew stop <TICKET>`
280
270
 
281
- 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>`.)
271
+ 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.
282
272
 
283
273
  ```bash
284
274
  crew stop HRD-442 --reason "wrong implementation direction"
@@ -368,7 +358,7 @@ To scaffold `.groundcrew/setup.sh` with a coding agent (Claude Code, Cursor, etc
368
358
 
369
359
  ## Pluggable ticket sources
370
360
 
371
- `sources` declares extra ticket-system adapters. The current release verifies configured extra sources during `crew run` startup; the dispatch loop still reads Linear directly through the built-in Linear adapter until the canonical consumer refactor lands. This lets you validate shell/Jira/local-plan integrations without changing existing Linear behavior.
361
+ `sources` declares extra ticket-system adapters. They're verified at `crew run` startup and dispatched alongside the built-in Linear adapter, so a shell, Jira, or local-plan integration feeds the same orchestration loop as Linear.
372
362
 
373
363
  The built-in `shell` adapter runs command templates and reads JSON from stdout:
374
364
 
@@ -543,11 +533,11 @@ node --run crew -- doctor
543
533
  node --run crew:op -- run --watch
544
534
  ```
545
535
 
546
- Both forms discover config via cosmiconfig — project-walk from cwd for `crew.config.ts` and friends, then `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts` (legacy `config.ts` is still accepted for one release). Set `GROUNDCREW_CONFIG` to point elsewhere. The `crew:op` wrapper additionally reads `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env` (1Password env-file with `op://` references resolved at launch).
536
+ Both forms discover config via cosmiconfig — project-walk from cwd for `crew.config.ts` and friends, then `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts`. Set `GROUNDCREW_CONFIG` to point elsewhere. The `crew:op` wrapper additionally reads `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/op.env` (1Password env-file with `op://` references resolved at launch).
547
537
 
548
538
  Logs land in `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log` by default (override via `logging.file`).
549
539
 
550
- Source edits in `src/**` are picked up on the next invocation. Requires Node ≥ 24.3 (native `.ts` type stripping).
540
+ Source edits in `src/**` are picked up on the next invocation. Requires Node ≥ 24 (native `.ts` type stripping).
551
541
 
552
542
  ## License
553
543
 
@@ -3,8 +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 { type BoardState } from "../lib/boardSource.ts";
7
6
  import type { ResolvedConfig } from "../lib/config.ts";
7
+ import { type BoardState } from "../lib/ticketSource.ts";
8
8
  import { type WorktreeEntry } from "../lib/worktrees.ts";
9
9
  interface CleanerDeps {
10
10
  config: ResolvedConfig;
@@ -1 +1 @@
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,CAkDxD"}
1
+ {"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../src/commands/cleaner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEjF,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,CAoDxD"}
@@ -3,8 +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 { isTerminalStatusForIssue } from "../lib/boardSource.js";
7
6
  import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
7
+ import { naturalIdFromCanonical } from "../lib/ticketSource.js";
8
8
  import { log, logEvent } from "../lib/util.js";
9
9
  import { worktrees } from "../lib/worktrees.js";
10
10
  import { logTeardown, recordTeardownEvents } from "./teardownReporter.js";
@@ -12,7 +12,9 @@ export function createCleaner(deps) {
12
12
  const { config } = deps;
13
13
  async function runOnce(arguments_) {
14
14
  const { state, worktreeEntries, dryRun, signal } = arguments_;
15
- const terminalTickets = new Set(state.issues.filter((issue) => isTerminalStatusForIssue(issue)).map((issue) => issue.id));
15
+ const terminalTickets = new Set(state.issues
16
+ .filter((issue) => issue.status === "done")
17
+ .map((issue) => naturalIdFromCanonical(issue.id)));
16
18
  if (terminalTickets.size === 0) {
17
19
  return;
18
20
  }
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * Per-iteration decider that picks Todo tickets to start and acts on the
3
- * picks. One per `orchestrate()` invocation; reuses its team-state cache
4
- * across iterations within an invocation.
3
+ * picks. Stateless across iterations. The Board adapter owns its own writeback
4
+ * caches (e.g., Linear's team-state cache lives in `src/lib/adapters/linear/writeback.ts`).
5
5
  *
6
6
  * Pure verdict logic lives in `eligibility.ts`; this module is responsible
7
- * for telemetry, Linear writes, and side-effecting setupWorkspace calls.
7
+ * for telemetry, writeback via Board, and side-effecting setupWorkspace calls.
8
8
  */
9
- import type { LinearClient } from "@linear/sdk";
10
- import { type BoardState } from "../lib/boardSource.ts";
9
+ import type { Board } from "../lib/board.ts";
11
10
  import type { ResolvedConfig } from "../lib/config.ts";
11
+ import { type BoardState, type Issue } from "../lib/ticketSource.ts";
12
12
  import type { UsageByModel } from "../lib/usage.ts";
13
13
  import type { WorktreeEntry } from "../lib/worktrees.ts";
14
14
  interface DispatcherDeps {
15
15
  config: ResolvedConfig;
16
- client: LinearClient;
16
+ board: Board;
17
17
  }
18
18
  export interface Dispatcher {
19
19
  runOnce(arguments_: {
@@ -32,5 +32,6 @@ export interface Dispatcher {
32
32
  }): Promise<void>;
33
33
  }
34
34
  export declare function createDispatcher(deps: DispatcherDeps): Dispatcher;
35
+ export declare function formatActiveSlotList(active: readonly Issue[]): string;
35
36
  export {};
36
37
  //# sourceMappingURL=dispatcher.d.ts.map
@@ -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,EACL,KAAK,UAAU,EAKhB,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;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAyLjE"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,wBAAwB,CAAC;AAChC,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,KAAK,EAAE,KAAK,CAAC;CACd;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;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAiNjE;AAUD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Per-iteration decider that picks Todo tickets to start and acts on the
3
- * picks. One per `orchestrate()` invocation; reuses its team-state cache
4
- * across iterations within an invocation.
3
+ * picks. Stateless across iterations. The Board adapter owns its own writeback
4
+ * caches (e.g., Linear's team-state cache lives in `src/lib/adapters/linear/writeback.ts`).
5
5
  *
6
6
  * Pure verdict logic lives in `eligibility.ts`; this module is responsible
7
- * for telemetry, Linear writes, and side-effecting setupWorkspace calls.
7
+ * for telemetry, writeback via Board, and side-effecting setupWorkspace calls.
8
8
  */
9
- import { isGroundcrewIssue, isIssueInProgress, isIssueTodo, } from "../lib/boardSource.js";
10
- import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
9
+ import { dispatchableRepository } from "../lib/repositoryValidation.js";
10
+ import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/ticketSource.js";
11
11
  import { errorMessage, log, logEvent } from "../lib/util.js";
12
12
  import { workspaces } from "../lib/workspaces.js";
13
13
  import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, } from "./eligibility.js";
@@ -17,14 +17,13 @@ function logSkip(verdict) {
17
17
  logEvent("dispatch", {
18
18
  outcome: "skipped",
19
19
  reason: verdict.eventReason,
20
- ticket: verdict.issue.id,
20
+ ticket: naturalIdFromCanonical(verdict.issue.id),
21
21
  blockers: verdict.blockers,
22
22
  model: verdict.model,
23
23
  });
24
24
  }
25
25
  export function createDispatcher(deps) {
26
- const { config, client } = deps;
27
- const issueStatusUpdater = createLinearIssueStatusUpdater({ client });
26
+ const { config, board } = deps;
28
27
  function buildExhaustedSet(usage) {
29
28
  const exhausted = new Set();
30
29
  for (const exhaustion of classifyUsageExhaustion(config, usage)) {
@@ -35,17 +34,18 @@ export function createDispatcher(deps) {
35
34
  }
36
35
  async function startEligibleIssue(start, dryRun, signal) {
37
36
  const { issue, recovery } = start;
37
+ const ticketId = naturalIdFromCanonical(issue.id);
38
38
  if (start.resolvedFromAny) {
39
- log(`Resolved agent-any for ${issue.id} → ${issue.model}`);
39
+ log(`Resolved agent-any for ${ticketId} → ${issue.model}`);
40
40
  }
41
41
  if (dryRun) {
42
42
  log(
43
43
  /* v8 ignore next @preserve -- classifyTodo forces recovery=false in dry-run, so the resume branch can't fire here */
44
- `[dry-run] Would ${recovery ? "resume" : "start"} ${issue.id} in ${issue.repository} (${issue.model})`);
44
+ `[dry-run] Would ${recovery ? "resume" : "start"} ${ticketId} in ${issue.repository} (${issue.model})`);
45
45
  logEvent("dispatch", {
46
46
  outcome: "skipped",
47
47
  reason: "dry_run",
48
- ticket: issue.id,
48
+ ticket: ticketId,
49
49
  model: issue.model,
50
50
  repository: issue.repository,
51
51
  });
@@ -53,31 +53,32 @@ export function createDispatcher(deps) {
53
53
  }
54
54
  try {
55
55
  if (recovery) {
56
- log(`Worktree and workspace already exist for ${issue.id}; resuming with markInProgress`);
56
+ log(`Worktree and workspace already exist for ${ticketId}; resuming with markInProgress`);
57
57
  }
58
58
  else {
59
59
  const setupOptions = {
60
60
  repository: issue.repository,
61
- ticket: issue.id,
61
+ ticket: ticketId,
62
62
  model: issue.model,
63
+ details: { title: issue.title, description: issue.description },
63
64
  };
64
65
  await (signal === undefined
65
66
  ? setupWorkspace(config, setupOptions)
66
67
  : setupWorkspace(config, setupOptions, { signal }));
67
68
  }
68
- await issueStatusUpdater.markInProgress(issue);
69
+ await board.markInProgress(issue);
69
70
  logEvent("dispatch", {
70
71
  outcome: recovery ? "resumed" : "started",
71
- ticket: issue.id,
72
+ ticket: ticketId,
72
73
  model: issue.model,
73
74
  repository: issue.repository,
74
75
  });
75
76
  }
76
77
  catch (error) {
77
- log(`Failed to start ${issue.id}: ${errorMessage(error)}`);
78
+ log(`Failed to start ${ticketId}: ${errorMessage(error)}`);
78
79
  logEvent("dispatch", {
79
80
  outcome: "failed",
80
- ticket: issue.id,
81
+ ticket: ticketId,
81
82
  model: issue.model,
82
83
  repository: issue.repository,
83
84
  error: errorMessage(error),
@@ -86,26 +87,29 @@ export function createDispatcher(deps) {
86
87
  }
87
88
  async function runOnce(arguments_) {
88
89
  const { state, worktreeEntries, usage, dryRun, signal, idleSuffix = "" } = arguments_;
89
- issueStatusUpdater.resetMissingInProgressCache();
90
- // Surface parent tickets that fetchBoard silently dropped. Without this
90
+ // Surface parent tickets that fetch silently dropped. Without this
91
91
  // an operator sees "No Todo tickets to pick up" with no signal that an
92
92
  // expected Todo+labelled ticket was skipped because it has sub-issues.
93
93
  for (const skip of state.parentSkips) {
94
- log(`Skipping ${skip.id}: parent ticket with ${skip.childCount} sub-issue(s) — groundcrew works sub-issues, not parents`);
94
+ const ticket = naturalIdFromCanonical(skip.id);
95
+ log(`Skipping ${ticket}: parent ticket with ${skip.childCount} sub-issue(s) — groundcrew works sub-issues, not parents`);
95
96
  logEvent("dispatch", {
96
97
  outcome: "skipped",
97
98
  reason: "parent_with_children",
98
- ticket: skip.id,
99
+ ticket,
99
100
  children: skip.childCount,
100
101
  });
101
102
  }
102
- const activeCount = state.issues.filter((issue) => isIssueInProgress(issue)).length;
103
+ const active = state.issues
104
+ .filter((issue) => issue.status === "in-progress")
105
+ .toSorted((a, b) => a.id.localeCompare(b.id));
106
+ const activeCount = active.length;
103
107
  const slots = config.orchestrator.maximumInProgress - activeCount;
104
108
  // Narrow Todo to tickets that opted in via an `agent-*` label.
105
109
  // Unlabeled tickets are not groundcrew's concern even when in Todo.
106
- const todo = state.issues.filter((issue) => isIssueTodo(issue) && isGroundcrewIssue(issue));
110
+ const todo = state.issues.filter((issue) => issue.status === "todo" && isGroundcrewIssue(issue));
107
111
  if (slots <= 0) {
108
- log(`At capacity (${activeCount}/${config.orchestrator.maximumInProgress}), no new work to start${idleSuffix}`);
112
+ log(`At capacity (${activeCount}/${config.orchestrator.maximumInProgress})${formatActiveSlotList(active)}, no new work to start${idleSuffix}`);
109
113
  return;
110
114
  }
111
115
  if (todo.length === 0) {
@@ -122,6 +126,20 @@ export function createDispatcher(deps) {
122
126
  log(`No eligible Todo tickets after blocker filtering${idleSuffix}`);
123
127
  return;
124
128
  }
129
+ // Validate repositories BEFORE the expensive probes so a tick whose only
130
+ // candidates have unknown repos short-circuits without paying for the
131
+ // usage() HTTP call or the workspaces.probe shell-out. Doing this filter
132
+ // here also keeps an unknown-repo ticket at the head of the queue from
133
+ // consuming a slot in classifyEligibility and starving later valid
134
+ // tickets. Each unknown repo still emits a WARN via dispatchableRepository.
135
+ const dispatchableUnblocked = unblocked.filter((issue) => {
136
+ const repository = dispatchableRepository(issue, config.workspace.knownRepositories, log);
137
+ return repository !== undefined;
138
+ });
139
+ if (dispatchableUnblocked.length === 0) {
140
+ log(`No eligible Todo tickets after repository validation${idleSuffix}`);
141
+ return;
142
+ }
125
143
  // usage() is an HTTP call; workspaces.probe shells tmux/cmux. Kick off
126
144
  // usage first so the workspace probe can overlap with the in-flight request.
127
145
  const usagePromise = usage(signal);
@@ -144,7 +162,7 @@ export function createDispatcher(deps) {
144
162
  const exhausted = buildExhaustedSet(fetchedUsage);
145
163
  const verdicts = classifyEligibility({
146
164
  config,
147
- unblocked,
165
+ unblocked: dispatchableUnblocked,
148
166
  worktreeEntries,
149
167
  workspaceProbe,
150
168
  usage: fetchedUsage,
@@ -161,12 +179,13 @@ export function createDispatcher(deps) {
161
179
  log(`No eligible Todo tickets after eligibility filtering${idleSuffix}`);
162
180
  return;
163
181
  }
164
- log(`${slots} slot(s) available, starting ${starts.length} ticket(s): ${starts.map(({ issue }) => `${issue.id}(${issue.model})`).join(", ")}`);
182
+ const dispatchable = starts;
183
+ log(`Slots ${activeCount}/${config.orchestrator.maximumInProgress} used${formatActiveSlotList(active)}, starting ${dispatchable.length} ticket(s): ${dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}(${issue.model})`).join(", ")}`);
165
184
  logEvent("dispatch", {
166
185
  outcome: "starting",
167
- tickets: starts.map(({ issue }) => `${issue.id}:${issue.model}`),
186
+ tickets: dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}:${issue.model}`),
168
187
  });
169
- for (const start of starts) {
188
+ for (const start of dispatchable) {
170
189
  // oxlint-disable-next-line no-await-in-loop -- one workspace at a time avoids racing on git
171
190
  await startEligibleIssue(start, dryRun, signal);
172
191
  }
@@ -180,3 +199,12 @@ function formatUsageExhaustion(exhaustion) {
180
199
  }
181
200
  return `${exhaustion.model} weekly at ${exhaustion.usedPercentage.toFixed(1)}% (> ${exhaustion.allowedPercentage.toFixed(1)}% paced budget), resets in ${exhaustion.resetMinutes}m — skipping its tickets`;
182
201
  }
202
+ export function formatActiveSlotList(active) {
203
+ if (active.length === 0) {
204
+ return "";
205
+ }
206
+ const entries = active
207
+ .map((issue) => `${naturalIdFromCanonical(issue.id)}(${issue.model ?? "?"})`)
208
+ .join(", ");
209
+ return ` [${entries}]`;
210
+ }
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgJH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA8E/C"}
@@ -3,12 +3,13 @@
3
3
  * Returns true if every required check passes; false otherwise.
4
4
  */
5
5
  import { existsSync, statSync } from "node:fs";
6
+ import { createBoard } from "../lib/board.js";
7
+ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
6
8
  import { loadConfig, } from "../lib/config.js";
7
- import { createBoardSource } from "../lib/boardSource.js";
8
9
  import { detectHostCapabilities, which } from "../lib/host.js";
9
10
  import { resolveLocalRunner } from "../lib/localRunner.js";
10
11
  import { gatedModels } from "../lib/usage.js";
11
- import { errorMessage, getLinearClient, resolveLinearApiKey, writeOutput } from "../lib/util.js";
12
+ import { errorMessage, writeOutput } from "../lib/util.js";
12
13
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
13
14
  // Tokenization stops after this many non-flag tokens. Two is enough to
14
15
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
@@ -26,32 +27,27 @@ async function checkCmd(cmd, required, hint) {
26
27
  }
27
28
  return result;
28
29
  }
29
- async function checkLinearReachability(config) {
30
- const resolved = resolveLinearApiKey();
31
- if (resolved === undefined) {
32
- return {
33
- name: "linear reachability",
34
- ok: false,
35
- required: true,
36
- hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
37
- };
38
- }
30
+ /**
31
+ * Source-agnostic reachability check: build every configured ticket source
32
+ * and run the Board's `verify()` fan-out. Replaces the old Linear-only
33
+ * "api key + reachability" probe so a misconfigured shell (or future Jira)
34
+ * source surfaces here too. A missing Linear API key still fails verify with
35
+ * its own user-facing message, so the prior behavior is preserved.
36
+ */
37
+ async function checkSourceProbe(config) {
39
38
  try {
40
- await createBoardSource({ config, client: getLinearClient() }).verify();
39
+ const sources = await buildSources(sourcesFromConfig(config), { globalConfig: config });
40
+ const board = createBoard(sources);
41
+ await board.verify();
41
42
  return {
42
- name: "linear reachability",
43
+ name: "source probe",
43
44
  ok: true,
44
45
  required: true,
45
- hint: `set via $${resolved.source}`,
46
+ hint: `${sources.length} source(s) verified`,
46
47
  };
47
48
  }
48
49
  catch (error) {
49
- return {
50
- name: "linear reachability",
51
- ok: false,
52
- required: true,
53
- hint: errorMessage(error),
54
- };
50
+ return { name: "source probe", ok: false, required: true, hint: errorMessage(error) };
55
51
  }
56
52
  }
57
53
  function checkDir(path, label) {
@@ -161,7 +157,7 @@ export async function doctor() {
161
157
  const workspaceOutcome = resolveWorkspaceOutcome(config, host);
162
158
  reportWorkspaceKind(config, workspaceOutcome);
163
159
  const checks = [
164
- await checkLinearReachability(config),
160
+ await checkSourceProbe(config),
165
161
  await checkCmd("git", true, "https://git-scm.com/"),
166
162
  ...(await workspaceChecks(workspaceOutcome)),
167
163
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
@@ -6,8 +6,8 @@
6
6
  * The Dispatcher consumes the verdict list to drive logging and side
7
7
  * effects.
8
8
  */
9
- import { type GroundcrewIssue } from "../lib/boardSource.ts";
10
9
  import { type ResolvedConfig } from "../lib/config.ts";
10
+ import { type GroundcrewIssue } from "../lib/ticketSource.ts";
11
11
  import type { UsageByModel } from "../lib/usage.ts";
12
12
  import type { WorkspaceProbe } from "../lib/workspaces.ts";
13
13
  import type { WorktreeEntry } from "../lib/worktrees.ts";
@@ -1 +1 @@
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;AAgCD;;;;;;;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,CAAC,IAAI,EAAE,SAAS,eAAe,EAAE,GAAG,qBAAqB,CAYxF;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,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAwC,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACpG,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;AAgCD;;;;;;;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;AA6CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,eAAe,EAAE,GAAG,qBAAqB,CAYxF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,EAAE,CAgE5E"}
@@ -6,14 +6,14 @@
6
6
  * The Dispatcher consumes the verdict list to drive logging and side
7
7
  * effects.
8
8
  */
9
- import { isTerminalStatusForBlocker, } from "../lib/boardSource.js";
10
9
  import { AGENT_ANY_MODEL } from "../lib/config.js";
10
+ import { naturalIdFromCanonical } from "../lib/ticketSource.js";
11
11
  const PERCENT_FRACTION_DIVISOR = 100;
12
12
  const DAYS_PER_WEEK = 7;
13
13
  const MINUTES_PER_DAY = 24 * 60;
14
14
  const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
15
15
  function blockerSummary(blocker) {
16
- return `${blocker.id}:${blocker.status ?? "missing"}`;
16
+ return `${blocker.id}:${blocker.status}`;
17
17
  }
18
18
  function blockerVerdictFor(issue) {
19
19
  if (issue.hasMoreBlockers) {
@@ -26,7 +26,7 @@ function blockerVerdictFor(issue) {
26
26
  blockers,
27
27
  };
28
28
  }
29
- const unresolved = issue.blockers.filter((blocker) => !isTerminalStatusForBlocker(blocker));
29
+ const unresolved = issue.blockers.filter((blocker) => blocker.status !== "done");
30
30
  if (unresolved.length === 0) {
31
31
  return undefined;
32
32
  }
@@ -110,7 +110,8 @@ function classifyRecovery(arguments_) {
110
110
  if (dryRun) {
111
111
  return { kind: "go", recovery: false };
112
112
  }
113
- const exists = worktreeEntries.some((entry) => entry.repository === issue.repository && entry.ticket === issue.id);
113
+ const naturalId = naturalIdFromCanonical(issue.id);
114
+ const exists = worktreeEntries.some((entry) => entry.repository === issue.repository && entry.ticket === naturalId);
114
115
  if (!exists) {
115
116
  return { kind: "go", recovery: false };
116
117
  }
@@ -122,11 +123,11 @@ function classifyRecovery(arguments_) {
122
123
  eventReason: "workspace_list_unavailable",
123
124
  };
124
125
  }
125
- if (!workspaceProbe.names.has(issue.id)) {
126
+ if (!workspaceProbe.names.has(naturalId)) {
126
127
  return {
127
128
  kind: "skip",
128
129
  issue,
129
- message: `Skipping ${issue.id}: worktree exists but no live workspace. Run \`crew cleanup ${issue.id}\` to allow re-provisioning.`,
130
+ message: `Skipping ${issue.id}: worktree exists but no live workspace. Run \`crew cleanup ${naturalId}\` to allow re-provisioning.`,
130
131
  eventReason: "workspace_missing",
131
132
  };
132
133
  }
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAqDH,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,CA4C7E"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyDH,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,CA0C7E"}
@@ -4,12 +4,12 @@
4
4
  * the cleaner, and runs the dispatcher; logging from those modules is the
5
5
  * orchestrator's user-facing output.
6
6
  */
7
- import { createBoardSource } from "../lib/boardSource.js";
8
7
  import { createBoard } from "../lib/board.js";
9
- import { buildSources } from "../lib/buildSources.js";
8
+ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
10
9
  import { loadConfig } from "../lib/config.js";
10
+ import { RepositoryResolutionError } from "../lib/ticketSource.js";
11
11
  import { getUsageByModel } from "../lib/usage.js";
12
- import { errorMessage, getLinearClient, log, sleep } from "../lib/util.js";
12
+ import { errorMessage, log, sleep } from "../lib/util.js";
13
13
  import { worktrees } from "../lib/worktrees.js";
14
14
  import { createCleaner } from "./cleaner.js";
15
15
  import { createDispatcher } from "./dispatcher.js";
@@ -24,6 +24,10 @@ async function withRetry(function_, signal, maxRetries = RETRY_MAX_ATTEMPTS, bas
24
24
  return await function_();
25
25
  }
26
26
  catch (error) {
27
+ /* v8 ignore next 2 @preserve -- fetch() warns-and-skips since PR#88; guard is a defensive no-op in practice */
28
+ if (error instanceof RepositoryResolutionError) {
29
+ throw error;
30
+ }
27
31
  if (attempt === maxRetries) {
28
32
  throw error;
29
33
  }
@@ -61,25 +65,21 @@ async function fetchUsageOrEmpty(config, signal) {
61
65
  }
62
66
  export async function orchestrate(options) {
63
67
  const config = await loadConfig();
64
- const client = getLinearClient();
65
- const boardSource = createBoardSource({ config, client });
66
- await boardSource.verify();
67
- // Verify any pluggable sources declared in config.sources (shell adapters,
68
- // future built-in adapters) at startup. The Linear path still runs through
69
- // boardSource.fetch in the main loop; shell-source dispatch is a follow-up.
70
- // An empty config.sources resolves to an empty Board and verify() is a no-op.
71
- const extraSources = await buildSources(config.sources, { globalConfig: config });
72
- const board = createBoard(extraSources);
68
+ // Build all sources (Linear implicit + any user-declared shell adapters).
69
+ // sourcesFromConfig synthesizes the implicit linear source from the config;
70
+ // this ensures the Linear adapter's markInProgress is reachable via board.
71
+ const allSources = await buildSources(sourcesFromConfig(config), { globalConfig: config });
72
+ const board = createBoard(allSources);
73
73
  await board.verify();
74
74
  const cleaner = createCleaner({ config });
75
- const dispatcher = createDispatcher({ config, client });
75
+ const dispatcher = createDispatcher({ config, board });
76
76
  // Folded into the dispatcher's idle log lines in watch mode so each idle
77
77
  // tick prints one combined line instead of "<reason>" + "Next poll in Xs".
78
78
  const idleSuffix = options.watch
79
79
  ? `; next poll in ${config.orchestrator.pollIntervalMilliseconds / MS_PER_SECOND}s`
80
80
  : undefined;
81
81
  const tick = async (signal) => {
82
- const state = await withRetry(async () => await boardSource.fetch(), signal);
82
+ const state = await withRetry(async () => await board.fetch(), signal);
83
83
  const worktreeEntries = worktrees.list(config);
84
84
  const tickArguments = {
85
85
  state,
@@ -147,6 +147,10 @@ async function runWatchLoop(tick, config) {
147
147
  if (error instanceof WatchLoopShutdownError) {
148
148
  break;
149
149
  }
150
+ /* v8 ignore next 2 @preserve -- fetch() warns-and-skips since PR#88; guard is a defensive no-op in practice */
151
+ if (error instanceof RepositoryResolutionError) {
152
+ throw error;
153
+ }
150
154
  const message = errorMessage(error);
151
155
  if (message.includes("Signal: SIGINT")) {
152
156
  if (!shutdown.signal.aborted) {
@@ -1 +1 @@
1
- {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;CAChB;AA6HD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
1
+ {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;CAChB;AA6HD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}