@clipboard-health/groundcrew 4.45.5 → 4.45.7
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/dist/lib/cmuxAdapter.d.ts.map +1 -1
- package/dist/lib/cmuxAdapter.js +127 -32
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/cmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"cmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/cmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,OAAO,EAKb,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,WAAW,EAAE,OA+EzB,CAAC"}
|
package/dist/lib/cmuxAdapter.js
CHANGED
|
@@ -4,21 +4,28 @@
|
|
|
4
4
|
* per-workspace status pill, which `open` applies best-effort.
|
|
5
5
|
*/
|
|
6
6
|
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
7
|
-
import { debug, errorMessage, log } from "./util.js";
|
|
7
|
+
import { debug, errorMessage, log, logEvent } from "./util.js";
|
|
8
8
|
export const cmuxAdapter = {
|
|
9
9
|
async open(spec, signal) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
let output;
|
|
11
|
+
try {
|
|
12
|
+
output = await runWorkspaceCommand("cmux", [
|
|
13
|
+
"--json",
|
|
14
|
+
"new-workspace",
|
|
15
|
+
"--name",
|
|
16
|
+
spec.name,
|
|
17
|
+
"--cwd",
|
|
18
|
+
spec.cwd,
|
|
19
|
+
"--command",
|
|
20
|
+
spec.command,
|
|
21
|
+
"--description",
|
|
22
|
+
cmuxDescriptionFor(spec.name),
|
|
23
|
+
], signal);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
await closeWorkspaceLeakedByFailedOpen(error, spec.name, signal);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
22
29
|
const workspaceId = extractCmuxOpenId(output);
|
|
23
30
|
if (workspaceId === undefined) {
|
|
24
31
|
log(`cmux new-workspace returned unrecognized output for ${spec.name}; if a workspace was created, run \`cmux close-workspace\` manually.`);
|
|
@@ -51,28 +58,21 @@ export const cmuxAdapter = {
|
|
|
51
58
|
debug(`cmux close-workspace skipped for ${name}: list-workspaces failed, no usable id`);
|
|
52
59
|
return { kind: "unavailable" };
|
|
53
60
|
}
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
61
|
+
const matches = raw.filter((ws) => cmuxTaskId(ws) === name);
|
|
62
|
+
if (matches.length === 0) {
|
|
56
63
|
return { kind: "missing" };
|
|
57
64
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (remaining === undefined) {
|
|
68
|
-
return { kind: "unavailable", error };
|
|
69
|
-
}
|
|
70
|
-
const isStillPresent = remaining.some((ws) => cmuxTaskId(ws) === name);
|
|
71
|
-
if (!isStillPresent) {
|
|
72
|
-
return { kind: "closed" };
|
|
73
|
-
}
|
|
74
|
-
throw error;
|
|
65
|
+
if (matches.length > 1) {
|
|
66
|
+
// A single task id maps to many cmux workspaces only after a leaked launch
|
|
67
|
+
// or session-restore duplication. Surface the leak so it's observable
|
|
68
|
+
// rather than silently reconciled away.
|
|
69
|
+
logEvent("cmux_close", {
|
|
70
|
+
outcome: "duplicate_markers",
|
|
71
|
+
task: name,
|
|
72
|
+
duplicates: matches.length,
|
|
73
|
+
});
|
|
75
74
|
}
|
|
75
|
+
return await closeAllCmuxMatches(matches, signal);
|
|
76
76
|
},
|
|
77
77
|
accessHint(_name) {
|
|
78
78
|
// cmux is a TUI; users surface workspaces by launching the cmux app,
|
|
@@ -177,9 +177,104 @@ async function applyCmuxStatus(workspaceId, status, signal) {
|
|
|
177
177
|
arguments_.push("--workspace", workspaceId);
|
|
178
178
|
await runWorkspaceCommand("cmux", arguments_, signal);
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Closes every workspace sharing the requested marker. Closes run sequentially:
|
|
182
|
+
* a failure path re-lists cmux to confirm the workspace is gone, and firing N
|
|
183
|
+
* mutations plus N confirmation lists concurrently would race on that shared
|
|
184
|
+
* state. Each match is attempted even when an earlier one fails, so a single
|
|
185
|
+
* stuck duplicate can't orphan the rest; a confirmed-still-present failure is
|
|
186
|
+
* collected and rethrown only after the loop (preserving single-match
|
|
187
|
+
* semantics). A failure that cannot be confirmed yields `unavailable`; `closed`
|
|
188
|
+
* wins only when every match is gone.
|
|
189
|
+
*/
|
|
190
|
+
async function closeAllCmuxMatches(matches, signal) {
|
|
191
|
+
let unavailable;
|
|
192
|
+
const errors = [];
|
|
193
|
+
for (const match of matches) {
|
|
194
|
+
try {
|
|
195
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential by design; failure re-lists shared cmux state
|
|
196
|
+
const result = await closeCmuxMatch(match, signal);
|
|
197
|
+
if (result.kind === "unavailable") {
|
|
198
|
+
unavailable = result;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (isSignalAborted(signal)) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
errors.push(error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (errors.length > 0) {
|
|
209
|
+
throw errors.length === 1 ? errors[0] : new AggregateError(errors);
|
|
210
|
+
}
|
|
211
|
+
return unavailable ?? { kind: "closed" };
|
|
212
|
+
}
|
|
213
|
+
async function closeCmuxMatch(match, signal) {
|
|
214
|
+
try {
|
|
215
|
+
await closeCmuxWorkspace(match.id, signal);
|
|
216
|
+
return { kind: "closed" };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (isSignalAborted(signal)) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
const remaining = await listCmuxRaw(signal);
|
|
223
|
+
if (remaining === undefined) {
|
|
224
|
+
return { kind: "unavailable", error };
|
|
225
|
+
}
|
|
226
|
+
const isStillPresent = remaining.some((ws) => ws.id === match.id);
|
|
227
|
+
if (!isStillPresent) {
|
|
228
|
+
return { kind: "closed" };
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
180
233
|
async function closeCmuxWorkspace(workspaceId, signal) {
|
|
181
234
|
await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
|
|
182
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* cmux occasionally exits non-zero from `new-workspace` while still having
|
|
238
|
+
* created the workspace (a known flake where it also lands in the wrong `--cwd`).
|
|
239
|
+
* The id rides along in the failed command's captured output, so recover it and
|
|
240
|
+
* close that exact workspace by id — a failed launch must not strand an orphan
|
|
241
|
+
* tagged with the task's `groundcrew:<taskId>` marker. Closing by the recovered
|
|
242
|
+
* id needs no `list-workspaces`, so it survives a concurrent list failure that
|
|
243
|
+
* would defeat re-enumeration. Re-enumeration close is unsafe here anyway; we
|
|
244
|
+
* hold the precise id cmux returned, so there is no same-named-sibling risk.
|
|
245
|
+
*/
|
|
246
|
+
async function closeWorkspaceLeakedByFailedOpen(error, name, signal) {
|
|
247
|
+
if (isSignalAborted(signal)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const workspaceId = extractCmuxOpenIdFromFailure(error);
|
|
251
|
+
if (workspaceId === undefined) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await closeCmuxWorkspace(workspaceId, signal);
|
|
256
|
+
debug(`cmux new-workspace for ${name} exited non-zero but had created ${workspaceId}; closed the leaked workspace.`);
|
|
257
|
+
}
|
|
258
|
+
catch (closeError) {
|
|
259
|
+
log(`cmux new-workspace for ${name} exited non-zero and left workspace ${workspaceId}; automatic close failed (${errorMessage(closeError)}). Run \`cmux close-workspace --workspace ${workspaceId}\` by hand.`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Recover the created workspace id from a failed `new-workspace`. Parse only
|
|
264
|
+
* the captured stdout slice of the command error's message (see
|
|
265
|
+
* `normalizeCommandError`), never the whole message — matching against stderr
|
|
266
|
+
* or the echoed command line risks grabbing an unrelated `workspace:N` and
|
|
267
|
+
* closing the wrong workspace. The slice is the same shape the success path
|
|
268
|
+
* sees, so `extractCmuxOpenId` handles both the `--json` id object and a bare
|
|
269
|
+
* `workspace:N` ref.
|
|
270
|
+
*/
|
|
271
|
+
function extractCmuxOpenIdFromFailure(error) {
|
|
272
|
+
const stdout = cmuxStdoutFromFailureMessage(errorMessage(error));
|
|
273
|
+
return stdout === undefined ? undefined : extractCmuxOpenId(stdout);
|
|
274
|
+
}
|
|
275
|
+
function cmuxStdoutFromFailureMessage(message) {
|
|
276
|
+
return /\nStdout:\n([\s\S]*?)(?:\nCause: |$)/.exec(message)?.[1];
|
|
277
|
+
}
|
|
183
278
|
function isCmuxSetStatusUnsupported(error) {
|
|
184
279
|
return errorMessage(error).includes('unknown command "set-status"');
|
|
185
280
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.45.
|
|
3
|
+
"version": "4.45.7",
|
|
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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"cspell": "10.0.1",
|
|
89
89
|
"dependency-cruiser": "17.4.3",
|
|
90
90
|
"husky": "9.1.7",
|
|
91
|
-
"jscpd": "5.0.
|
|
91
|
+
"jscpd": "5.0.11",
|
|
92
92
|
"knip": "6.16.1",
|
|
93
93
|
"lint-staged": "17.0.7",
|
|
94
94
|
"markdownlint-cli2": "0.22.1",
|