@clipboard-health/groundcrew 4.0.2 → 4.1.0

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 (62) hide show
  1. package/README.md +32 -13
  2. package/crew.config.example.ts +5 -18
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +64 -10
  5. package/dist/commands/interruptWorkspace.d.ts.map +1 -1
  6. package/dist/commands/interruptWorkspace.js +3 -3
  7. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  8. package/dist/commands/resumeWorkspace.js +1 -2
  9. package/dist/commands/setupRepos.d.ts.map +1 -1
  10. package/dist/commands/setupRepos.js +2 -13
  11. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  12. package/dist/commands/setupWorkspace.js +1 -7
  13. package/dist/lib/agentLaunch.d.ts +0 -6
  14. package/dist/lib/agentLaunch.d.ts.map +1 -1
  15. package/dist/lib/agentLaunch.js +1 -12
  16. package/dist/lib/cmuxAdapter.d.ts +8 -0
  17. package/dist/lib/cmuxAdapter.d.ts.map +1 -0
  18. package/dist/lib/cmuxAdapter.js +163 -0
  19. package/dist/lib/config.d.ts +2 -76
  20. package/dist/lib/config.d.ts.map +1 -1
  21. package/dist/lib/config.js +29 -102
  22. package/dist/lib/launchCommand.d.ts +3 -3
  23. package/dist/lib/sandboxName.d.ts +9 -0
  24. package/dist/lib/sandboxName.d.ts.map +1 -0
  25. package/dist/lib/sandboxName.js +12 -0
  26. package/dist/lib/tmuxAdapter.d.ts +9 -0
  27. package/dist/lib/tmuxAdapter.d.ts.map +1 -0
  28. package/dist/lib/tmuxAdapter.js +156 -0
  29. package/dist/lib/util.d.ts +11 -0
  30. package/dist/lib/util.d.ts.map +1 -1
  31. package/dist/lib/util.js +21 -0
  32. package/dist/lib/workspaceAdapter.d.ts +79 -0
  33. package/dist/lib/workspaceAdapter.d.ts.map +1 -0
  34. package/dist/lib/workspaceAdapter.js +17 -0
  35. package/dist/lib/workspaces.d.ts +7 -55
  36. package/dist/lib/workspaces.d.ts.map +1 -1
  37. package/dist/lib/workspaces.js +8 -404
  38. package/package.json +1 -2
  39. package/dist/commands/sandbox/auth.d.ts +0 -3
  40. package/dist/commands/sandbox/auth.d.ts.map +0 -1
  41. package/dist/commands/sandbox/auth.js +0 -227
  42. package/dist/commands/sandbox/index.d.ts +0 -2
  43. package/dist/commands/sandbox/index.d.ts.map +0 -1
  44. package/dist/commands/sandbox/index.js +0 -47
  45. package/dist/commands/sandbox/inspect.d.ts +0 -2
  46. package/dist/commands/sandbox/inspect.d.ts.map +0 -1
  47. package/dist/commands/sandbox/inspect.js +0 -18
  48. package/dist/commands/sandbox/lifecycle.d.ts +0 -7
  49. package/dist/commands/sandbox/lifecycle.d.ts.map +0 -1
  50. package/dist/commands/sandbox/lifecycle.js +0 -68
  51. package/dist/commands/sandbox/model.d.ts +0 -10
  52. package/dist/commands/sandbox/model.d.ts.map +0 -1
  53. package/dist/commands/sandbox/model.js +0 -37
  54. package/dist/commands/sandbox/picker.d.ts +0 -20
  55. package/dist/commands/sandbox/picker.d.ts.map +0 -1
  56. package/dist/commands/sandbox/picker.js +0 -23
  57. package/dist/lib/dockerSandbox.d.ts +0 -43
  58. package/dist/lib/dockerSandbox.d.ts.map +0 -1
  59. package/dist/lib/dockerSandbox.js +0 -69
  60. package/dist/lib/sandboxGitDefaults.d.ts +0 -10
  61. package/dist/lib/sandboxGitDefaults.d.ts.map +0 -1
  62. package/dist/lib/sandboxGitDefaults.js +0 -31
package/README.md CHANGED
@@ -83,15 +83,17 @@ In Linear, assign tickets to yourself and add an `agent-*` label (`agent-claude`
83
83
  crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
84
84
  crew doctor # check setup
85
85
  crew status [<TICKET>] # inspect current state or one ticket
86
- crew run # one-shot dispatch
86
+ crew run # one-shot orchestration
87
87
  crew run --watch # poll forever
88
- crew run --ticket <TICKET> # dispatch one ticket
88
+ crew start <TICKET> # provision + launch one ticket now
89
89
  crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
90
- crew interrupt <TICKET> [--reason <text>] # stop workspace, keep worktree
90
+ crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
91
91
  crew resume <TICKET> # reopen a paused ticket
92
92
  crew cleanup <TICKET> # tear down every worktree for a ticket
93
93
  ```
94
94
 
95
+ Deprecated aliases still work but print a warning and will be removed in the next major version: `crew interrupt` → `crew stop`, `crew run --ticket <TICKET>` → `crew start <TICKET>`, `crew doctor --ticket <TICKET>` → `crew status <TICKET>`.
96
+
95
97
  ## Configuration
96
98
 
97
99
  Two keys are required; everything else has a default.
@@ -109,7 +111,7 @@ The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username`
109
111
  - `agent-claude`, `agent-codex`, `agent-<name>` → that model.
110
112
  - `agent-any` → the model with the most available session capacity.
111
113
  - Unknown `agent-<name>` → falls back to `models.default` with a warning.
112
- - No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew run --ticket <TICKET>` (also falls back to `models.default`).
114
+ - No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew start <TICKET>` (also falls back to `models.default`).
113
115
  - Todo tickets blocked by non-terminal blockers are skipped until their blockers reach a terminal status.
114
116
 
115
117
  Status classification uses Linear's workflow `state.type` (`unstarted`, `started`, `completed`, `canceled`, `duplicate`), so renamed status columns work without configuration. Parent issues with children are ignored — sub-issues are the work items.
@@ -136,12 +138,12 @@ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any
136
138
  | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
137
139
  | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
138
140
  | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
139
- | `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
141
+ | `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew start <TICKET>` for unlabeled tickets. `crew run` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
140
142
  | `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
141
143
  | `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
142
144
  | `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
143
145
  | `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Falls back to default when unset, with gating enabled for known models. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. Set to `{ disabled: true }` to disable usage gating. |
144
- | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name), `template`, `kits`, `setupCommand` (override for the inside-sandbox setup script). |
146
+ | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name) and `setupCommand` (override for the inside-sandbox setup script). Groundcrew assumes the `groundcrew-<agent>` sandbox already exists. |
145
147
  | `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. |
146
148
  | `prompts.initial` | unattended template | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override this from `crew.config.ts` for team-specific statuses, tools, plugins, or review loops. |
147
149
  | `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. |
@@ -186,9 +188,17 @@ Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines
186
188
  <details>
187
189
  <summary>Docker Sandboxes (sdx) setup</summary>
188
190
 
189
- Each model that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew names sandboxes `groundcrew-<agent>` (e.g. `groundcrew-claude`) and reuses one sandbox per agent across repos and tickets. First-time agent auth happens inside the sandbox the first time it launches. To bootstrap manually instead, run `sbx create --name groundcrew-<agent> <agent> <projectDir>` once.
191
+ Each model that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew addresses the sandbox as `groundcrew-<agent>` (e.g. `groundcrew-claude`) and reuses one existing sandbox per agent across repos and tickets.
192
+
193
+ First-time setup is manual:
194
+
195
+ ```bash
196
+ sbx create --name groundcrew-claude claude <projectDir>
197
+ sbx exec -it groundcrew-claude claude auth login
198
+ sbx exec -it groundcrew-claude gh auth login
199
+ ```
190
200
 
191
- Groundcrew auto-creates sandboxes when missing but never deletes them they persist across tickets and `crew cleanup`. Auth state lives inside the sandbox, so deleting it forces a re-login. Manage with `sbx ls` / `sbx rm`.
201
+ Replace `claude` with the sbx agent for the model and `<projectDir>` with `workspace.projectDir` from `crew.config.ts`. Manage lifecycle and auth with `sbx` directly (`sbx ls`, `sbx exec`, `sbx rm`). Groundcrew does not create, authenticate, regenerate, list, or remove sandboxes.
192
202
 
193
203
  </details>
194
204
 
@@ -234,21 +244,30 @@ In Progress (state.type=started) — Multi-event extractor: year inference can p
234
244
 
235
245
  </details>
236
246
 
237
- ### `crew interrupt <TICKET>`
247
+ ### `crew start <TICKET>`
248
+
249
+ 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>`.)
250
+
251
+ ```bash
252
+ crew start HRD-442
253
+ crew start HRD-442 --dry-run
254
+ ```
255
+
256
+ ### `crew stop <TICKET>`
238
257
 
239
- 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.
258
+ 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>`.)
240
259
 
241
260
  ```bash
242
- crew interrupt HRD-442 --reason "wrong implementation direction"
261
+ crew stop HRD-442 --reason "wrong implementation direction"
243
262
  crew status HRD-442
244
263
  crew resume HRD-442
245
264
  ```
246
265
 
247
- The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, interrupt records that fact so status can show the preserved branch.
266
+ The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, stop records that fact so status can show the preserved branch.
248
267
 
249
268
  ### `crew resume <TICKET>`
250
269
 
251
- Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew run --ticket <ticket>`.
270
+ Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew start <ticket>`.
252
271
 
253
272
  The resume prompt tells the agent to inspect git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
254
273
 
@@ -73,24 +73,11 @@ export default {
73
73
  // // macOS when you need an agent to use Docker safely.
74
74
  // local: { runner: "auto" },
75
75
  //
76
- // // Additional auth recipes for `crew sandbox auth <model> <tool>`. The
77
- // // shipped recipes (claude/codex/cursor agents + github tool) are merged
78
- // // with whatever you declare here; your recipe wins on key collision.
79
- // // Describe each tool's in-sandbox login + status commands and a regex
80
- // // that matches its logged-in output. Omit `kind` for cross-cutting
81
- // // tools that should appear in every sandbox's picker; set
82
- // // `kind: "agent"` to scope a recipe to a single sbx agent.
83
- // sandbox: {
84
- // authRecipes: {
85
- // gcloud: {
86
- // displayName: "gcloud",
87
- // binary: "gcloud",
88
- // loginArgs: ["auth", "login", "--no-launch-browser"],
89
- // statusArgs: ["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"],
90
- // authenticatedPattern: /@/,
91
- // },
92
- // },
93
- // },
76
+ // // Groundcrew does not create or authenticate sdx sandboxes. For an sdx
77
+ // // model, create the matching sandbox yourself before first launch:
78
+ // // sbx create --name groundcrew-claude claude ~/dev/groundcrew
79
+ // // sbx exec -it groundcrew-claude claude auth login
80
+ // // sbx exec -it groundcrew-claude gh auth login
94
81
  //
95
82
  // prompts: {
96
83
  // // Keep personal workflow instructions next to this config, for example
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA6MA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuQA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CvD"}
package/dist/cli.js CHANGED
@@ -5,16 +5,27 @@ 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 { sandboxCli } from "./commands/sandbox/index.js";
9
8
  import { setupReposCli } from "./commands/setupRepos.js";
10
9
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
11
10
  import { statusCli } from "./commands/status.js";
12
11
  import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
13
12
  import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
14
- import { errorMessage, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
13
+ import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
15
14
  const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
16
15
  const NUDGE_FETCH_TIMEOUT_MS = 1000;
16
+ const REMOVED_SANDBOX_COMMAND_MESSAGE = [
17
+ "`crew sandbox` is no longer supported.",
18
+ "Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
19
+ "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
+ ].join("\n");
17
21
  const requireFromCli = createRequire(import.meta.url);
22
+ /**
23
+ * Prints a deprecation warning to stderr naming the canonical command and that
24
+ * the old form is removed in the next major, then lets the caller proceed.
25
+ */
26
+ function warnDeprecated(forms) {
27
+ writeError(`crew ${forms.oldForm} is deprecated and will be removed in the next major version; use crew ${forms.newForm} instead.`);
28
+ }
18
29
  function setupUsage() {
19
30
  return "Usage: crew setup repos [--dry-run] [<repo>...]";
20
31
  }
@@ -54,6 +65,16 @@ async function runCli(argv) {
54
65
  await orchestrate({ watch, dryRun });
55
66
  return;
56
67
  }
68
+ warnDeprecated({ oldForm: "run --ticket", newForm: "start" });
69
+ await setupWorkspaceCli(ticket, { dryRun });
70
+ }
71
+ const START_USAGE = "crew start <ticket> [--dry-run]";
72
+ async function startCli(argv) {
73
+ const { dryRun, positionals } = parseDryRunPositionals(argv, START_USAGE);
74
+ const [ticket, ...extras] = positionals;
75
+ if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
76
+ throw new Error(`Usage: ${START_USAGE}`);
77
+ }
57
78
  await setupWorkspaceCli(ticket, { dryRun });
58
79
  }
59
80
  async function upgradeCliInvoke(argv) {
@@ -80,7 +101,23 @@ async function maybeRunUpgradeNudge(metadata) {
80
101
  writeError(message);
81
102
  }
82
103
  }
104
+ function doctorTicketAlias(argv) {
105
+ if (argv[0] !== "--ticket") {
106
+ return undefined;
107
+ }
108
+ const ticket = readTicketArgument(argv, 0, "doctor");
109
+ if (argv.length > 2) {
110
+ throw new Error("Usage: crew status [<ticket>]");
111
+ }
112
+ return ticket;
113
+ }
83
114
  async function doctorCli(argv) {
115
+ const aliasTicket = doctorTicketAlias(argv);
116
+ if (aliasTicket !== undefined) {
117
+ warnDeprecated({ oldForm: "doctor --ticket", newForm: "status" });
118
+ await statusCli([aliasTicket]);
119
+ return;
120
+ }
84
121
  if (argv.length > 0) {
85
122
  throw new Error("Usage: crew doctor");
86
123
  }
@@ -94,10 +131,15 @@ const SUBCOMMANDS = {
94
131
  invoke: initConfigCli,
95
132
  },
96
133
  run: {
97
- summary: "Run the orchestrator (one-shot by default), or provision one ticket with --ticket",
98
- usage: "[--watch] [--dry-run] [--ticket <ticket>]",
134
+ summary: "Run the orchestrator: poll sources and start eligible tickets (one-shot by default)",
135
+ usage: "[--watch] [--dry-run]",
99
136
  invoke: runCli,
100
137
  },
138
+ start: {
139
+ summary: "Launch one ticket immediately, bypassing eligibility",
140
+ usage: "<ticket> [--dry-run]",
141
+ invoke: startCli,
142
+ },
101
143
  doctor: {
102
144
  summary: "Verify host prerequisites (PATH tools, config validity, Linear reachability)",
103
145
  usage: "",
@@ -113,21 +155,25 @@ const SUBCOMMANDS = {
113
155
  usage: "[--force] <ticket>",
114
156
  invoke: cleanupWorkspaceCli,
115
157
  },
116
- interrupt: {
158
+ stop: {
117
159
  summary: "Stop a live ticket workspace while preserving its worktree",
118
160
  usage: "<ticket> [--reason <text>]",
119
161
  invoke: interruptWorkspaceCli,
120
162
  },
163
+ interrupt: {
164
+ summary: "Deprecated alias for `crew stop`",
165
+ usage: "<ticket> [--reason <text>]",
166
+ deprecated: true,
167
+ invoke: async (argv) => {
168
+ warnDeprecated({ oldForm: "interrupt", newForm: "stop" });
169
+ await interruptWorkspaceCli(argv);
170
+ },
171
+ },
121
172
  resume: {
122
173
  summary: "Reopen an existing ticket worktree with a continuation prompt",
123
174
  usage: "<ticket>",
124
175
  invoke: resumeWorkspaceCli,
125
176
  },
126
- sandbox: {
127
- summary: "Manage Docker Sandboxes (sbx) for configured models",
128
- usage: "<list|ensure|regenerate|auth|rm> [...args]",
129
- invoke: sandboxCli,
130
- },
131
177
  setup: {
132
178
  summary: "Project-level setup commands (currently: repos)",
133
179
  usage: "repos [--dry-run] [<repo>...]",
@@ -148,6 +194,9 @@ function printHelp() {
148
194
  writeOutput("");
149
195
  writeOutput("Commands:");
150
196
  for (const [name, command] of Object.entries(SUBCOMMANDS)) {
197
+ if (command.deprecated === true) {
198
+ continue;
199
+ }
151
200
  writeOutput(` ${name.padEnd(width)} ${command.summary}`);
152
201
  writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
153
202
  }
@@ -174,6 +223,11 @@ export async function run(argv) {
174
223
  writeOutput(packageVersion());
175
224
  return;
176
225
  }
226
+ if (subcommand === "sandbox") {
227
+ writeError(REMOVED_SANDBOX_COMMAND_MESSAGE);
228
+ process.exitCode = 1;
229
+ return;
230
+ }
177
231
  const command = SUBCOMMANDS[subcommand];
178
232
  if (!command) {
179
233
  writeError(`Unknown command: ${subcommand}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
1
+ {"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAqGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
@@ -15,20 +15,20 @@ function parseArguments(argv) {
15
15
  if (argument === "--reason") {
16
16
  const value = argv[index + 1];
17
17
  if (value === undefined || value.length === 0 || value.startsWith("-")) {
18
- throw new Error("crew interrupt --reason: reason text is required");
18
+ throw new Error("crew stop --reason: reason text is required");
19
19
  }
20
20
  reason = value;
21
21
  index += 1;
22
22
  continue;
23
23
  }
24
24
  if (argument.startsWith("-")) {
25
- throw new Error(`Unknown option: ${argument}\nUsage: crew interrupt <ticket> [--reason <text>]`);
25
+ throw new Error(`Unknown option: ${argument}\nUsage: crew stop <ticket> [--reason <text>]`);
26
26
  }
27
27
  positionals.push(argument);
28
28
  }
29
29
  const [ticket, ...extras] = positionals;
30
30
  if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
31
- throw new Error("Usage: crew interrupt <ticket> [--reason <text>]");
31
+ throw new Error("Usage: crew stop <ticket> [--reason <text>]");
32
32
  }
33
33
  return { ticket: ticket.toLowerCase(), ...(reason === undefined ? {} : { reason }) };
34
34
  }
@@ -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,CA6Df;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":"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,6 +1,6 @@
1
1
  import { fetchResolvedIssue } from "../lib/boardSource.js";
2
2
  import { loadConfig } from "../lib/config.js";
3
- import { ensureAgentSandbox, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
3
+ import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
4
  import { buildLaunchCommand } from "../lib/launchCommand.js";
5
5
  import { readRunState, recordRunState } from "../lib/runState.js";
6
6
  import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
@@ -117,7 +117,6 @@ export async function resumeWorkspace(config, options) {
117
117
  text: renderResumePrompt(context),
118
118
  });
119
119
  const secretsFile = stageBuildSecrets(stagedPrompt.directory);
120
- await ensureAgentSandbox({ config, definition, sandboxName });
121
120
  const launchCommand = buildLaunchCommand({
122
121
  definition,
123
122
  promptFile: stagedPrompt.file,
@@ -1 +1 @@
1
- {"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAwBD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
1
+ {"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAcD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
@@ -10,7 +10,7 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
10
10
  import { runCommandAsync } from "../lib/commandRunner.js";
11
11
  import { loadConfig } from "../lib/config.js";
12
12
  import { which } from "../lib/host.js";
13
- import { errorMessage, log, writeOutput } from "../lib/util.js";
13
+ import { errorMessage, log, parseDryRunPositionals, writeOutput } from "../lib/util.js";
14
14
  function emptyResult() {
15
15
  return {
16
16
  existing: [],
@@ -190,18 +190,7 @@ export async function setupRepos(config, options) {
190
190
  return result;
191
191
  }
192
192
  function parseArguments(argv) {
193
- let dryRun = false;
194
- const positionals = [];
195
- for (const argument of argv) {
196
- if (argument === "--dry-run") {
197
- dryRun = true;
198
- continue;
199
- }
200
- if (argument.startsWith("-")) {
201
- throw new Error(`Unknown option: ${argument}\nUsage: crew setup repos [--dry-run] [<repo>...]`);
202
- }
203
- positionals.push(argument);
204
- }
193
+ const { dryRun, positionals } = parseDryRunPositionals(argv, "crew setup repos [--dry-run] [<repo>...]");
205
194
  const options = { dryRun };
206
195
  if (positionals.length > 0) {
207
196
  options.only = positionals;
@@ -1 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAqBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA6Gf;AAwHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAqBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAsGf;AAwHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -1,7 +1,7 @@
1
1
  import { rmSync } from "node:fs";
2
2
  import { fetchResolvedIssue } from "../lib/boardSource.js";
3
3
  import { loadConfig } from "../lib/config.js";
4
- import { ensureAgentSandbox, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
+ import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
5
5
  import { buildLaunchCommand } from "../lib/launchCommand.js";
6
6
  import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
7
7
  import { recordRunState } from "../lib/runState.js";
@@ -80,12 +80,6 @@ export async function setupWorkspace(config, options, runOptions = {}) {
80
80
  const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
81
81
  promptDir = stagedPrompt.directory;
82
82
  const secretsFile = stageBuildSecrets(promptDir);
83
- await ensureAgentSandbox({
84
- config,
85
- definition,
86
- sandboxName,
87
- ...(signal === undefined ? {} : { signal }),
88
- });
89
83
  const launchCommand = buildLaunchCommand({
90
84
  definition,
91
85
  promptFile: stagedPrompt.file,
@@ -10,12 +10,6 @@ export declare function prepareAgentLaunch(input: {
10
10
  purpose: "runs" | "resumes";
11
11
  signal?: AbortSignal;
12
12
  }): Promise<PreparedAgentLaunch>;
13
- export declare function ensureAgentSandbox(input: {
14
- config: ResolvedConfig;
15
- definition: ModelDefinition;
16
- sandboxName: string | undefined;
17
- signal?: AbortSignal;
18
- }): Promise<void>;
19
13
  export declare function openAgentWorkspace(input: {
20
14
  config: ResolvedConfig;
21
15
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
@@ -1,8 +1,7 @@
1
- import { resolve } from "node:path";
2
1
  import { ensureClearance } from "@clipboard-health/clearance";
3
- import { ensureSandbox, sandboxNameFor } from "./dockerSandbox.js";
4
2
  import { detectHostCapabilities } from "./host.js";
5
3
  import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
4
+ import { sandboxNameFor } from "./sandboxName.js";
6
5
  import { log, sleep } from "./util.js";
7
6
  import { workspaces } from "./workspaces.js";
8
7
  export async function prepareAgentLaunch(input) {
@@ -31,16 +30,6 @@ export async function prepareAgentLaunch(input) {
31
30
  : undefined;
32
31
  return { runner, sandboxName };
33
32
  }
34
- export async function ensureAgentSandbox(input) {
35
- if (input.sandboxName !== undefined && input.definition.sandbox !== undefined) {
36
- await ensureSandbox({
37
- sandboxName: input.sandboxName,
38
- sandbox: input.definition.sandbox,
39
- mountPath: resolve(input.config.workspace.projectDir),
40
- gitDefaults: input.config.sandbox.gitDefaults,
41
- }, input.signal);
42
- }
43
- }
44
33
  export async function openAgentWorkspace(input) {
45
34
  const spec = {
46
35
  name: input.name,
@@ -0,0 +1,8 @@
1
+ /**
2
+ * cmux Workspace backend. cmux is the macOS TUI; workspaces surface in its
3
+ * own app, so `accessHint` has nothing concise to emit. cmux can paint a
4
+ * per-workspace status pill, which `open` applies best-effort.
5
+ */
6
+ import { type Adapter } from "./workspaceAdapter.ts";
7
+ export declare const cmuxAdapter: Adapter;
8
+ //# sourceMappingURL=cmuxAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/cmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,WAAW,EAAE,OA6EzB,CAAC"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * cmux Workspace backend. cmux is the macOS TUI; workspaces surface in its
3
+ * own app, so `accessHint` has nothing concise to emit. cmux can paint a
4
+ * per-workspace status pill, which `open` applies best-effort.
5
+ */
6
+ import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
7
+ import { errorMessage, log } from "./util.js";
8
+ export const cmuxAdapter = {
9
+ async open(spec, signal) {
10
+ const output = await runWorkspaceCommand("cmux", [
11
+ "--json",
12
+ "new-workspace",
13
+ "--name",
14
+ spec.name,
15
+ "--cwd",
16
+ spec.cwd,
17
+ "--command",
18
+ spec.command,
19
+ ], signal);
20
+ const workspaceId = extractCmuxOpenId(output);
21
+ if (workspaceId === undefined) {
22
+ log(`cmux new-workspace returned unrecognized output for ${spec.name}; if a workspace was created, run \`cmux close-workspace\` manually.`);
23
+ throw new Error(`Unexpected cmux output: ${output}`);
24
+ }
25
+ if (spec.status !== undefined) {
26
+ try {
27
+ await applyCmuxStatus(workspaceId, spec.status, signal);
28
+ }
29
+ catch (error) {
30
+ // Status pills are best-effort. cmux v2+ dropped `set-status` entirely,
31
+ // so swallow that specific gap silently; surface anything else so a real
32
+ // regression doesn't hide behind the same swallow.
33
+ if (!isCmuxSetStatusUnsupported(error)) {
34
+ log(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
35
+ }
36
+ }
37
+ }
38
+ },
39
+ async list(signal) {
40
+ const raw = await listCmuxRaw(signal);
41
+ return raw?.map((ws) => ({ name: ws.title }));
42
+ },
43
+ async close(name, signal) {
44
+ const raw = await listCmuxRaw(signal);
45
+ if (raw === undefined) {
46
+ // cmux v2 `workspace.close` rejects titles, so forwarding `name`
47
+ // would always fail. The list failure has already been logged by
48
+ // `listCmuxRaw`; bail rather than guarantee a downstream error.
49
+ log(`cmux close-workspace skipped for ${name}: list-workspaces failed, no usable id`);
50
+ return { kind: "unavailable" };
51
+ }
52
+ const match = raw.find((ws) => ws.title === name);
53
+ if (match === undefined) {
54
+ return { kind: "missing" };
55
+ }
56
+ try {
57
+ await closeCmuxWorkspace(match.id, signal);
58
+ return { kind: "closed" };
59
+ }
60
+ catch (error) {
61
+ if (isSignalAborted(signal)) {
62
+ throw error;
63
+ }
64
+ const remaining = await listCmuxRaw(signal);
65
+ if (remaining === undefined) {
66
+ return { kind: "unavailable", error };
67
+ }
68
+ const isStillPresent = remaining.some((ws) => ws.title === name);
69
+ if (!isStillPresent) {
70
+ return { kind: "closed" };
71
+ }
72
+ throw error;
73
+ }
74
+ },
75
+ accessHint(_name) {
76
+ // cmux is a TUI; users surface workspaces by launching the cmux app,
77
+ // not a shell command. No useful hint to emit.
78
+ // oxlint-disable-next-line unicorn/no-useless-undefined -- explicit signal that the backend has no hint
79
+ return undefined;
80
+ },
81
+ };
82
+ function parseCmuxList(output) {
83
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json list-workspaces always emits this shape
84
+ const parsed = JSON.parse(output);
85
+ const items = [];
86
+ /* v8 ignore next @preserve -- cmux always emits a workspaces field; default keeps the loop safe */
87
+ for (const ws of parsed.workspaces ?? []) {
88
+ if (typeof ws.title !== "string" || ws.title.length === 0) {
89
+ continue;
90
+ }
91
+ const id = pickCmuxId(ws);
92
+ if (id === undefined) {
93
+ log(`cmux list-workspaces returned workspace "${ws.title}" without a usable id or ref; skipping`);
94
+ continue;
95
+ }
96
+ items.push({ title: ws.title, id });
97
+ }
98
+ return items;
99
+ }
100
+ /**
101
+ * The stable workspace handle cmux v2 expects in JSON-RPC params. Prefer
102
+ * the UUID; fall back to the legacy `workspace:N` short ref when older
103
+ * cmux builds don't surface it. Returns `undefined` when neither is
104
+ * available — cmux v2 `workspace.close` rejects titles, so we must never
105
+ * forward `title` as a workspace handle.
106
+ */
107
+ function pickCmuxId(ws) {
108
+ if (typeof ws.id === "string" && ws.id.length > 0) {
109
+ return ws.id;
110
+ }
111
+ if (typeof ws.ref === "string" && ws.ref.length > 0) {
112
+ return ws.ref;
113
+ }
114
+ return undefined;
115
+ }
116
+ async function listCmuxRaw(signal) {
117
+ try {
118
+ return parseCmuxList(await runWorkspaceCommand("cmux", ["--json", "list-workspaces"], signal));
119
+ }
120
+ catch (error) {
121
+ if (isSignalAborted(signal)) {
122
+ throw error;
123
+ }
124
+ log(`cmux list-workspaces failed: ${errorMessage(error)}`);
125
+ return undefined;
126
+ }
127
+ }
128
+ function extractCmuxOpenId(output) {
129
+ try {
130
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json prints a workspace_id/ref object
131
+ const parsed = JSON.parse(output);
132
+ const uuid = parsed.workspace_id ?? parsed.id ?? "";
133
+ if (uuid.length > 0) {
134
+ return uuid;
135
+ }
136
+ const ref = parsed.workspace_ref ?? parsed.ref ?? "";
137
+ if (ref.length > 0) {
138
+ return ref;
139
+ }
140
+ }
141
+ catch {
142
+ /* not JSON; fall through to regex */
143
+ }
144
+ const match = /workspace:\d+/.exec(output);
145
+ return match ? match[0] : undefined;
146
+ }
147
+ async function applyCmuxStatus(workspaceId, status, signal) {
148
+ const arguments_ = ["set-status", "model", status.text];
149
+ if (status.icon !== undefined) {
150
+ arguments_.push("--icon", status.icon);
151
+ }
152
+ if (status.color !== undefined) {
153
+ arguments_.push("--color", status.color);
154
+ }
155
+ arguments_.push("--workspace", workspaceId);
156
+ await runWorkspaceCommand("cmux", arguments_, signal);
157
+ }
158
+ async function closeCmuxWorkspace(workspaceId, signal) {
159
+ await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
160
+ }
161
+ function isCmuxSetStatusUnsupported(error) {
162
+ return errorMessage(error).includes('unknown command "set-status"');
163
+ }