@clipboard-health/groundcrew 4.1.0 → 4.2.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.
Files changed (81) hide show
  1. package/README.md +25 -9
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +16 -42
  4. package/dist/commands/cleaner.d.ts +1 -1
  5. package/dist/commands/cleaner.d.ts.map +1 -1
  6. package/dist/commands/cleaner.js +4 -2
  7. package/dist/commands/dispatcher.d.ts +6 -6
  8. package/dist/commands/dispatcher.d.ts.map +1 -1
  9. package/dist/commands/dispatcher.js +43 -27
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +18 -22
  12. package/dist/commands/eligibility.d.ts +1 -1
  13. package/dist/commands/eligibility.d.ts.map +1 -1
  14. package/dist/commands/eligibility.js +7 -6
  15. package/dist/commands/orchestrator.d.ts.map +1 -1
  16. package/dist/commands/orchestrator.js +18 -14
  17. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  18. package/dist/commands/resumeWorkspace.js +3 -2
  19. package/dist/commands/setupWorkspace.d.ts +2 -4
  20. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  21. package/dist/commands/setupWorkspace.js +27 -27
  22. package/dist/commands/status.d.ts.map +1 -1
  23. package/dist/commands/status.js +6 -3
  24. package/dist/commands/upgrade.d.ts +0 -11
  25. package/dist/commands/upgrade.d.ts.map +1 -1
  26. package/dist/commands/upgrade.js +14 -100
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/lib/adapters/linear/client.d.ts +22 -0
  31. package/dist/lib/adapters/linear/client.d.ts.map +1 -0
  32. package/dist/lib/adapters/linear/client.js +36 -0
  33. package/dist/lib/adapters/linear/factory.d.ts +24 -14
  34. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  35. package/dist/lib/adapters/linear/factory.js +113 -46
  36. package/dist/lib/{boardSource.d.ts → adapters/linear/fetch.d.ts} +19 -71
  37. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -0
  38. package/dist/lib/{boardSource.js → adapters/linear/fetch.js} +21 -133
  39. package/dist/lib/adapters/linear/index.d.ts +1 -0
  40. package/dist/lib/adapters/linear/index.d.ts.map +1 -1
  41. package/dist/lib/adapters/linear/parsing.d.ts +44 -0
  42. package/dist/lib/adapters/linear/parsing.d.ts.map +1 -0
  43. package/dist/lib/adapters/linear/parsing.js +144 -0
  44. package/dist/lib/{linearIssueStatus.d.ts → adapters/linear/writeback.d.ts} +1 -2
  45. package/dist/lib/adapters/linear/writeback.d.ts.map +1 -0
  46. package/dist/lib/{linearIssueStatus.js → adapters/linear/writeback.js} +16 -17
  47. package/dist/lib/adapters/shell/factory.d.ts +1 -1
  48. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  49. package/dist/lib/adapters/shell/factory.js +8 -4
  50. package/dist/lib/adapters/shell/invoke.d.ts +4 -7
  51. package/dist/lib/adapters/shell/invoke.d.ts.map +1 -1
  52. package/dist/lib/adapters/shell/invoke.js +46 -75
  53. package/dist/lib/adapters/shell/schema.d.ts +10 -0
  54. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  55. package/dist/lib/adapters/shell/schema.js +9 -5
  56. package/dist/lib/board.d.ts.map +1 -1
  57. package/dist/lib/board.js +43 -4
  58. package/dist/lib/buildSources.d.ts +11 -0
  59. package/dist/lib/buildSources.d.ts.map +1 -1
  60. package/dist/lib/buildSources.js +41 -0
  61. package/dist/lib/repositoryValidation.d.ts +13 -0
  62. package/dist/lib/repositoryValidation.d.ts.map +1 -0
  63. package/dist/lib/repositoryValidation.js +20 -0
  64. package/dist/lib/testing/canonicalFixtures.d.ts +19 -0
  65. package/dist/lib/testing/canonicalFixtures.d.ts.map +1 -0
  66. package/dist/lib/testing/canonicalFixtures.js +62 -0
  67. package/dist/lib/ticketSource.d.ts +71 -1
  68. package/dist/lib/ticketSource.d.ts.map +1 -1
  69. package/dist/lib/ticketSource.js +31 -0
  70. package/dist/lib/util.d.ts +0 -20
  71. package/dist/lib/util.d.ts.map +1 -1
  72. package/dist/lib/util.js +0 -35
  73. package/package.json +1 -1
  74. package/dist/commands/setupRepos.d.ts +0 -44
  75. package/dist/commands/setupRepos.d.ts.map +0 -1
  76. package/dist/commands/setupRepos.js +0 -212
  77. package/dist/lib/boardSource.d.ts.map +0 -1
  78. package/dist/lib/linearIssueStatus.d.ts.map +0 -1
  79. package/dist/lib/upgrade.d.ts +0 -66
  80. package/dist/lib/upgrade.d.ts.map +0 -1
  81. package/dist/lib/upgrade.js +0 -178
package/README.md CHANGED
@@ -63,7 +63,9 @@ npm install -g @clipboard-health/groundcrew
63
63
  crew init && $EDITOR crew.config.ts
64
64
 
65
65
  # 4. Clone the repos referenced in your config
66
- crew setup repos
66
+ PROJECT_DIR="$HOME/dev/c"
67
+ mkdir -p "$PROJECT_DIR/OWNER"
68
+ git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
67
69
 
68
70
  # 5. Export your Linear API key
69
71
  export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
@@ -86,14 +88,35 @@ crew status [<TICKET>] # inspect current state
86
88
  crew run # one-shot orchestration
87
89
  crew run --watch # poll forever
88
90
  crew start <TICKET> # provision + launch one ticket now
89
- crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
90
91
  crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
91
92
  crew resume <TICKET> # reopen a paused ticket
92
93
  crew cleanup <TICKET> # tear down every worktree for a ticket
94
+ crew upgrade [<version>] # reinstall crew globally through npm
93
95
  ```
94
96
 
95
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>`.
96
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:
104
+
105
+ ```bash
106
+ PROJECT_DIR="$HOME/dev/c"
107
+ mkdir -p "$PROJECT_DIR/OWNER"
108
+ git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
109
+ ```
110
+
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.
119
+
97
120
  ## Configuration
98
121
 
99
122
  Two keys are required; everything else has a default.
@@ -508,13 +531,6 @@ The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` al
508
531
 
509
532
  </details>
510
533
 
511
- <details>
512
- <summary><code>crew setup repos</code> only auto-clones <code>owner/repo</code> entries</summary>
513
-
514
- Bare-name entries in `workspace.knownRepositories` (e.g. `"api"` rather than `"clipboardhealth/api"`) are skipped with a hint to clone manually — the command refuses to guess the owner. After a partial setup, the exit code is non-zero so CI gates notice; rerun is idempotent once you clone the bare ones into `<projectDir>/<name>` yourself.
515
-
516
- </details>
517
-
518
534
  ## Development
519
535
 
520
536
  Clone the repo and the `crew` / `crew:op` scripts execute straight from TypeScript source — no build step needed.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuQA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmPA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCvD"}
package/dist/cli.js CHANGED
@@ -5,20 +5,21 @@ import { initConfigCli } from "./commands/init.js";
5
5
  import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
6
6
  import { orchestrate } from "./commands/orchestrator.js";
7
7
  import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
- import { setupReposCli } from "./commands/setupRepos.js";
9
8
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
10
9
  import { statusCli } from "./commands/status.js";
11
10
  import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
12
- import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
13
- import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
14
- const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
15
- const NUDGE_FETCH_TIMEOUT_MS = 1000;
11
+ import { errorMessage, parseDryRunPositionals, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
16
12
  const REMOVED_SANDBOX_COMMAND_MESSAGE = [
17
13
  "`crew sandbox` is no longer supported.",
18
14
  "Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
19
15
  "Use the manual `sbx` workflow in README.md#docker-sandboxes-sdx-setup, then keep `models.definitions.<model>.sandbox.agent` in crew.config.ts so launches can address the existing sandbox.",
20
16
  ].join("\n");
21
17
  const requireFromCli = createRequire(import.meta.url);
18
+ const SETUP_REPOS_REMOVED_MESSAGE = [
19
+ "crew setup repos was removed.",
20
+ "Clone repositories manually with git clone into workspace.projectDir.",
21
+ "See README.md#manual-repository-bootstrap for the replacement workflow.",
22
+ ].join(" ");
22
23
  /**
23
24
  * Prints a deprecation warning to stderr naming the canonical command and that
24
25
  * the old form is removed in the next major, then lets the caller proceed.
@@ -27,13 +28,12 @@ function warnDeprecated(forms) {
27
28
  writeError(`crew ${forms.oldForm} is deprecated and will be removed in the next major version; use crew ${forms.newForm} instead.`);
28
29
  }
29
30
  function setupUsage() {
30
- return "Usage: crew setup repos [--dry-run] [<repo>...]";
31
+ return `Usage: crew setup repos\n\n${SETUP_REPOS_REMOVED_MESSAGE}`;
31
32
  }
32
33
  async function setupCli(argv) {
33
- const [verb, ...rest] = argv;
34
+ const [verb] = argv;
34
35
  if (verb === "repos") {
35
- await setupReposCli(rest);
36
- return;
36
+ throw new Error(SETUP_REPOS_REMOVED_MESSAGE);
37
37
  }
38
38
  throw new Error(setupUsage());
39
39
  }
@@ -80,27 +80,10 @@ async function startCli(argv) {
80
80
  async function upgradeCliInvoke(argv) {
81
81
  const metadata = packageMetadata();
82
82
  await upgradeCli(argv, async () => await createDefaultUpgradeCliOptions({
83
- currentVersion: metadata.version,
84
83
  packageName: metadata.name,
85
84
  cliMetaUrl: import.meta.url,
86
85
  }));
87
86
  }
88
- async function maybeRunUpgradeNudge(metadata) {
89
- const message = await computeUpgradeNudge({
90
- currentVersion: metadata.version,
91
- packageName: metadata.name,
92
- cachePath: defaultUpgradeCheckCachePath(),
93
- ttlMs: NUDGE_TTL_MS,
94
- fetchTimeoutMs: NUDGE_FETCH_TIMEOUT_MS,
95
- registry: readEnvironmentVariable("npm_config_registry"),
96
- noUpgradeCheck: readEnvironmentVariable("GROUNDCREW_NO_UPGRADE_CHECK") === "1",
97
- now: Date.now,
98
- fetcher: fetchLatestVersion,
99
- });
100
- if (message !== undefined) {
101
- writeError(message);
102
- }
103
- }
104
87
  function doctorTicketAlias(argv) {
105
88
  if (argv[0] !== "--ticket") {
106
89
  return undefined;
@@ -175,28 +158,27 @@ const SUBCOMMANDS = {
175
158
  invoke: resumeWorkspaceCli,
176
159
  },
177
160
  setup: {
178
- summary: "Project-level setup commands (currently: repos)",
179
- usage: "repos [--dry-run] [<repo>...]",
161
+ summary: "Removed repository bootstrap command",
162
+ usage: "repos",
163
+ hidden: true,
180
164
  invoke: setupCli,
181
165
  },
182
166
  upgrade: {
183
167
  summary: "Install the latest version of crew (or pin to a specific version)",
184
- usage: "[<version>] [--check]",
168
+ usage: "[<version>]",
185
169
  invoke: upgradeCliInvoke,
186
170
  },
187
171
  };
188
172
  function printHelp() {
189
- const width = Math.max(...Object.keys(SUBCOMMANDS).map((key) => key.length));
173
+ const visibleCommands = Object.entries(SUBCOMMANDS).filter(([, command]) => command.hidden !== true && command.deprecated !== true);
174
+ const width = Math.max(...visibleCommands.map(([key]) => key.length));
190
175
  writeOutput("Usage: crew <command> [...args]\n");
191
176
  writeOutput("Options:");
192
177
  writeOutput(" -h, --help Show help");
193
178
  writeOutput(" -v, --version Print version");
194
179
  writeOutput("");
195
180
  writeOutput("Commands:");
196
- for (const [name, command] of Object.entries(SUBCOMMANDS)) {
197
- if (command.deprecated === true) {
198
- continue;
199
- }
181
+ for (const [name, command] of visibleCommands) {
200
182
  writeOutput(` ${name.padEnd(width)} ${command.summary}`);
201
183
  writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
202
184
  }
@@ -235,14 +217,6 @@ export async function run(argv) {
235
217
  process.exitCode = 1;
236
218
  return;
237
219
  }
238
- if (subcommand !== "upgrade") {
239
- try {
240
- await maybeRunUpgradeNudge(packageMetadata());
241
- }
242
- catch {
243
- // Passive nudge is never load-bearing; never block the user's command.
244
- }
245
- }
246
220
  try {
247
221
  await command.invoke(rest);
248
222
  }
@@ -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 } 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_: {
@@ -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,EAIhB,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,CA8MjE"}
@@ -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,24 +87,24 @@ 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 activeCount = state.issues.filter((issue) => issue.status === "in-progress").length;
103
104
  const slots = config.orchestrator.maximumInProgress - activeCount;
104
105
  // Narrow Todo to tickets that opted in via an `agent-*` label.
105
106
  // Unlabeled tickets are not groundcrew's concern even when in Todo.
106
- const todo = state.issues.filter((issue) => isIssueTodo(issue) && isGroundcrewIssue(issue));
107
+ const todo = state.issues.filter((issue) => issue.status === "todo" && isGroundcrewIssue(issue));
107
108
  if (slots <= 0) {
108
109
  log(`At capacity (${activeCount}/${config.orchestrator.maximumInProgress}), no new work to start${idleSuffix}`);
109
110
  return;
@@ -122,6 +123,20 @@ export function createDispatcher(deps) {
122
123
  log(`No eligible Todo tickets after blocker filtering${idleSuffix}`);
123
124
  return;
124
125
  }
126
+ // Validate repositories BEFORE the expensive probes so a tick whose only
127
+ // candidates have unknown repos short-circuits without paying for the
128
+ // usage() HTTP call or the workspaces.probe shell-out. Doing this filter
129
+ // here also keeps an unknown-repo ticket at the head of the queue from
130
+ // consuming a slot in classifyEligibility and starving later valid
131
+ // tickets. Each unknown repo still emits a WARN via dispatchableRepository.
132
+ const dispatchableUnblocked = unblocked.filter((issue) => {
133
+ const repository = dispatchableRepository(issue, config.workspace.knownRepositories, log);
134
+ return repository !== undefined;
135
+ });
136
+ if (dispatchableUnblocked.length === 0) {
137
+ log(`No eligible Todo tickets after repository validation${idleSuffix}`);
138
+ return;
139
+ }
125
140
  // usage() is an HTTP call; workspaces.probe shells tmux/cmux. Kick off
126
141
  // usage first so the workspace probe can overlap with the in-flight request.
127
142
  const usagePromise = usage(signal);
@@ -144,7 +159,7 @@ export function createDispatcher(deps) {
144
159
  const exhausted = buildExhaustedSet(fetchedUsage);
145
160
  const verdicts = classifyEligibility({
146
161
  config,
147
- unblocked,
162
+ unblocked: dispatchableUnblocked,
148
163
  worktreeEntries,
149
164
  workspaceProbe,
150
165
  usage: fetchedUsage,
@@ -161,12 +176,13 @@ export function createDispatcher(deps) {
161
176
  log(`No eligible Todo tickets after eligibility filtering${idleSuffix}`);
162
177
  return;
163
178
  }
164
- log(`${slots} slot(s) available, starting ${starts.length} ticket(s): ${starts.map(({ issue }) => `${issue.id}(${issue.model})`).join(", ")}`);
179
+ const dispatchable = starts;
180
+ log(`${slots} slot(s) available, starting ${dispatchable.length} ticket(s): ${dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}(${issue.model})`).join(", ")}`);
165
181
  logEvent("dispatch", {
166
182
  outcome: "starting",
167
- tickets: starts.map(({ issue }) => `${issue.id}:${issue.model}`),
183
+ tickets: dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}:${issue.model}`),
168
184
  });
169
- for (const start of starts) {
185
+ for (const start of dispatchable) {
170
186
  // oxlint-disable-next-line no-await-in-loop -- one workspace at a time avoids racing on git
171
187
  await startEligibleIssue(start, dryRun, signal);
172
188
  }
@@ -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"}