@clipboard-health/groundcrew 4.4.0 → 4.5.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.
package/README.md CHANGED
@@ -502,7 +502,7 @@ If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew
502
502
  <details>
503
503
  <summary>Dead tmux windows vanish by default</summary>
504
504
 
505
- When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. Close it manually with `tmux kill-window -t groundcrew:<ticket>` after diagnosis. tmux backend only.
505
+ When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. `crew status` reports those kept windows as `exited` and keeps the tmux attach command visible so you can inspect scrollback before resuming or cleaning up. tmux backend only.
506
506
 
507
507
  </details>
508
508
 
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6gBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkjBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -68,8 +68,14 @@ function ticketWorkspaceText(probe, ticket) {
68
68
  if (probe.kind === "unavailable") {
69
69
  return workspaceProbeUnavailableLine(probe);
70
70
  }
71
+ if (isWorkspaceExited(probe, ticket)) {
72
+ return "exited";
73
+ }
71
74
  return probe.names.has(ticket) ? "live" : "not live";
72
75
  }
76
+ function isWorkspaceExited(probe, ticket) {
77
+ return probe.kind === "ok" && probe.exitedNames?.has(ticket) === true;
78
+ }
73
79
  function formatRunState(state) {
74
80
  if (state === undefined) {
75
81
  return "(none)";
@@ -116,6 +122,17 @@ function writeRecentLogs(config, ticket) {
116
122
  writeSection("Recent logs");
117
123
  writeOutput(logLines.join("\n"));
118
124
  }
125
+ async function exitedWorkspaceAccessHint(config, probe, ticket) {
126
+ if (!isWorkspaceExited(probe, ticket)) {
127
+ return undefined;
128
+ }
129
+ try {
130
+ return await withLogOutputSuppressed(async () => await workspaces.accessHint(config, ticket));
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ }
119
136
  function formatTicketLine(ticket, runState, sourceStatus) {
120
137
  const parts = [`ticket: ${ticket}`];
121
138
  if (sourceStatus.kind === "found") {
@@ -154,10 +171,14 @@ async function writeTicketStatus(config, rawTicket) {
154
171
  withLogOutputSuppressed(async () => await workspaces.probe(config)),
155
172
  readTicketSourceStatus(config, ticket),
156
173
  ]);
174
+ const accessHint = await exitedWorkspaceAccessHint(config, workspaceProbe, ticket);
157
175
  writeOutput(formatTicketLine(ticket, runState, sourceStatus));
158
176
  writeTicketTitle(runState, sourceStatus);
159
177
  writeOutput(`run: ${formatRunState(runState)}`);
160
178
  writeOutput(`workspace: ${ticketWorkspaceText(workspaceProbe, ticket)}`);
179
+ if (accessHint !== undefined) {
180
+ writeOutput(`attach: ${accessHint.command}`);
181
+ }
161
182
  await writeTicketWorktrees(config, ticket);
162
183
  writeRecentLogs(config, ticket);
163
184
  }
@@ -202,22 +223,26 @@ function formatDuration(ms) {
202
223
  /**
203
224
  * Combined human-readable state for the inventory row. Surfaces RunState
204
225
  * lifecycle and flags the two interesting disagreements with the workspace
205
- * probe `(session dead)` when we recorded a running dispatch but no
206
- * session is alive, and `(stray session)` when a session is alive without
207
- * any recorded dispatch. `probe.kind === "unavailable"` is treated as
208
- * "we don't know" and never produces a suffix. When the row is actively
209
- * running, appends the elapsed wall-clock time since dispatch.
226
+ * probe. A recorded running dispatch can have a missing or exited session;
227
+ * an idle row can have a stray live or exited session. `probe.kind ===
228
+ * "unavailable"` is treated as "we don't know" and never produces a suffix.
229
+ * When the row is actively running, appends the elapsed wall-clock time since
230
+ * dispatch.
210
231
  */
211
232
  function inventoryStateText(runState, probe, ticket, now) {
212
233
  const lifecycle = runState?.state ?? "idle";
213
234
  const duration = runStateDurationMs(runState, now);
214
235
  const flags = [];
215
236
  if (probe.kind === "ok") {
216
- const sessionLive = probe.names.has(ticket);
217
- if (lifecycle === "idle" && sessionLive) {
218
- flags.push("stray session");
237
+ const sessionPresent = probe.names.has(ticket);
238
+ const sessionExited = isWorkspaceExited(probe, ticket);
239
+ if (lifecycle === "idle" && sessionPresent) {
240
+ flags.push(sessionExited ? "stray exited session" : "stray session");
241
+ }
242
+ if ((lifecycle === "running" || lifecycle === "resumed") && sessionExited) {
243
+ flags.push("session exited");
219
244
  }
220
- if ((lifecycle === "running" || lifecycle === "resumed") && !sessionLive) {
245
+ else if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
221
246
  flags.push("session dead");
222
247
  }
223
248
  }
@@ -231,9 +256,11 @@ function inventoryStateText(runState, probe, ticket, now) {
231
256
  * probe disagree. Returned commands are safe defaults; the user is free to
232
257
  * ignore them and use `attach:` + `pr:` to investigate first.
233
258
  *
234
- * - Stray session (live session, no run-state record) `crew cleanup` to
235
- * tear down the orphaned worktree + close the session.
236
- * - Session dead (run-state says running/resumed, no live session)
259
+ * - Stray session (session present, no run-state record) -> `crew cleanup`
260
+ * to tear down the orphaned worktree and close the session.
261
+ * - Session exited (run-state says running/resumed, kept dead tmux window)
262
+ * -> attach first so the failed command remains available for inspection.
263
+ * - Session dead (run-state says running/resumed, no session present) ->
237
264
  * `crew resume` to bring the agent back; the worktree is preserved.
238
265
  *
239
266
  * No hint when the probe is unavailable (we genuinely don't know whether
@@ -244,11 +271,17 @@ function inventoryHint(runState, probe, ticket) {
244
271
  return undefined;
245
272
  }
246
273
  const lifecycle = runState?.state ?? "idle";
247
- const sessionLive = probe.names.has(ticket);
248
- if (lifecycle === "idle" && sessionLive) {
249
- return `run 'crew cleanup ${ticket}' to clear this stray session`;
274
+ const sessionPresent = probe.names.has(ticket);
275
+ const sessionExited = isWorkspaceExited(probe, ticket);
276
+ if (lifecycle === "idle" && sessionPresent) {
277
+ return sessionExited
278
+ ? `run 'crew cleanup ${ticket}' to clear this stray exited session`
279
+ : `run 'crew cleanup ${ticket}' to clear this stray session`;
280
+ }
281
+ if ((lifecycle === "running" || lifecycle === "resumed") && sessionExited) {
282
+ return `attach to inspect scrollback, then run 'crew resume ${ticket}'`;
250
283
  }
251
- if ((lifecycle === "running" || lifecycle === "resumed") && !sessionLive) {
284
+ if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
252
285
  return `run 'crew resume ${ticket}' to bring the session back`;
253
286
  }
254
287
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,OAgEzB,CAAC"}
1
+ {"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,OA+DzB,CAAC"}
@@ -17,8 +17,7 @@ export const tmuxAdapter = {
17
17
  async open(spec, signal) {
18
18
  await ensureTmuxSession(signal);
19
19
  const target = tmuxTarget(spec.name);
20
- const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
21
- const keepDeadWindows = keepDeadWindowsEnv !== undefined && keepDeadWindowsEnv.length > 0;
20
+ const keepDeadWindows = shouldKeepDeadWindows();
22
21
  await runWorkspaceCommand("tmux", [
23
22
  "new-window",
24
23
  "-d",
@@ -54,7 +53,7 @@ export const tmuxAdapter = {
54
53
  // oxlint-disable-next-line unicorn/no-useless-undefined -- undefined marks the workspace backend as unavailable.
55
54
  return undefined;
56
55
  }
57
- return parseTmuxWindows(probe.output);
56
+ return parseTmuxWindows(probe.output, { includeExited: shouldKeepDeadWindows() });
58
57
  },
59
58
  async close(name, signal) {
60
59
  try {
@@ -78,6 +77,10 @@ export const tmuxAdapter = {
78
77
  function tmuxTarget(name) {
79
78
  return `${TMUX_SESSION}:${name}`;
80
79
  }
80
+ function shouldKeepDeadWindows() {
81
+ const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
82
+ return keepDeadWindowsEnv === "1";
83
+ }
81
84
  function isTmuxNotFoundError(error) {
82
85
  // runCommand surfaces the child's stderr in error.message, so the "no
83
86
  // server" / "missing session" / "can't find window" signatures are visible
@@ -130,7 +133,7 @@ async function ensureTmuxSession(signal) {
130
133
  }
131
134
  }
132
135
  }
133
- function parseTmuxWindows(output) {
136
+ function parseTmuxWindows(output, options = {}) {
134
137
  const items = [];
135
138
  for (const line of output.split("\n")) {
136
139
  if (line.length === 0) {
@@ -147,10 +150,11 @@ function parseTmuxWindows(output) {
147
150
  // pane_dead != 0 means the command exited and the window is a zombie
148
151
  // (only happens when remain-on-exit is on; defense in depth in case a
149
152
  // user-globally-set value beats our per-window override).
150
- if (deadFlag !== undefined && deadFlag !== "0") {
153
+ const isExited = deadFlag !== undefined && deadFlag !== "0";
154
+ if (isExited && options.includeExited !== true) {
151
155
  continue;
152
156
  }
153
- items.push({ name });
157
+ items.push(isExited ? { name, state: "exited" } : { name });
154
158
  }
155
159
  return items;
156
160
  }
@@ -10,6 +10,8 @@ export type WorkspaceKind = "cmux" | "tmux";
10
10
  export interface Workspace {
11
11
  /** Ticket id; the join key callers use. */
12
12
  name: string;
13
+ /** Omitted means live, for backends that do not expose an exited state. */
14
+ state?: "exited";
13
15
  }
14
16
  export interface WorkspaceStatus {
15
17
  text: string;
@@ -37,6 +39,7 @@ export interface OpenSpec {
37
39
  export type WorkspaceProbe = {
38
40
  kind: "ok";
39
41
  names: Set<string>;
42
+ exitedNames?: Set<string>;
40
43
  } | {
41
44
  kind: "unavailable";
42
45
  error?: unknown;
@@ -60,7 +63,7 @@ export type WorkspaceCloseResult = {
60
63
  export interface Adapter {
61
64
  open(spec: OpenSpec, signal?: AbortSignal): Promise<void>;
62
65
  /**
63
- * Live workspaces only. Returns:
66
+ * Known workspaces. Returns:
64
67
  * - `Workspace[]` when the adapter probe succeeded (may be empty).
65
68
  * - `undefined` when the adapter binary failed in a way that doesn't
66
69
  * distinguish "no live workspaces" from "couldn't ask".
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,wBAAwB,GAChC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IAC7D,0DAA0D;IAC1D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzE;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC3D;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAE7D"}
1
+ {"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,wBAAwB,GAChC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IAC7D,0DAA0D;IAC1D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzE;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC3D;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAE7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,iBAAe,sBAAsB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACT,UAAU;CACX,CAAC"}
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,iBAAe,sBAAsB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACT,UAAU;CACX,CAAC"}
@@ -74,7 +74,9 @@ async function probeWorkspaces(config, signal) {
74
74
  if (raw === undefined) {
75
75
  return { kind: "unavailable" };
76
76
  }
77
- return { kind: "ok", names: new Set(raw.map((ws) => ws.name)) };
77
+ const names = new Set(raw.map((ws) => ws.name));
78
+ const exitedNames = new Set(raw.filter((ws) => ws.state === "exited").map((ws) => ws.name));
79
+ return exitedNames.size === 0 ? { kind: "ok", names } : { kind: "ok", names, exitedNames };
78
80
  }
79
81
  async function accessHintForWorkspace(config, name, signal) {
80
82
  const adapter = await adapterFor(config, signal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",