@clipboard-health/groundcrew 4.45.4 → 4.45.6
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/commands/cleaner.d.ts +1 -1
- package/dist/commands/cleaner.d.ts.map +1 -1
- package/dist/commands/cleaner.js +2 -9
- package/dist/commands/reviewer.d.ts +5 -2
- package/dist/commands/reviewer.d.ts.map +1 -1
- package/dist/commands/reviewer.js +24 -4
- package/dist/commands/teardownReporter.d.ts +8 -1
- package/dist/commands/teardownReporter.d.ts.map +1 -1
- package/dist/commands/teardownReporter.js +16 -0
- package/dist/lib/cmuxAdapter.d.ts.map +1 -1
- package/dist/lib/cmuxAdapter.js +61 -12
- package/dist/lib/usage.d.ts.map +1 -1
- package/dist/lib/usage.js +60 -7
- package/package.json +2 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ResolvedConfig } from "../lib/config.ts";
|
|
7
7
|
import { type BoardState } from "../lib/taskSource.ts";
|
|
8
|
-
import {
|
|
8
|
+
import type { WorktreeEntry } from "../lib/worktrees.ts";
|
|
9
9
|
interface CleanerDeps {
|
|
10
10
|
config: ResolvedConfig;
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AACvD,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,CAAC,UAAU,EAAE;QACpB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CA8CxD"}
|
package/dist/commands/cleaner.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
* tasks that have reached a terminal status. One per `orchestrate()`
|
|
4
4
|
* invocation; stateless across iterations. Mirrors `Dispatcher`.
|
|
5
5
|
*/
|
|
6
|
-
import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
|
|
7
6
|
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
8
7
|
import { log, logEvent } from "../lib/util.js";
|
|
9
|
-
import {
|
|
10
|
-
import { logTeardown, recordTeardownEvents } from "./teardownReporter.js";
|
|
8
|
+
import { reapWorktrees } from "./teardownReporter.js";
|
|
11
9
|
export function createCleaner(deps) {
|
|
12
10
|
const { config } = deps;
|
|
13
11
|
async function runOnce(arguments_) {
|
|
@@ -37,12 +35,7 @@ export function createCleaner(deps) {
|
|
|
37
35
|
return;
|
|
38
36
|
}
|
|
39
37
|
log(`Cleaning up ${stale.length} terminal worktree(s)`);
|
|
40
|
-
|
|
41
|
-
? await worktrees.teardown(config, stale)
|
|
42
|
-
: await worktrees.teardown(config, stale, { signal });
|
|
43
|
-
recordCleanedUpRuns(config, result.removed);
|
|
44
|
-
logTeardown(result);
|
|
45
|
-
recordTeardownEvents(result);
|
|
38
|
+
await reapWorktrees(config, stale, signal);
|
|
46
39
|
}
|
|
47
40
|
return { runOnce };
|
|
48
41
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* dispatch slot (slot math counts only in-progress) while leaving the
|
|
8
8
|
* worktree intact for review, since the cleaner only tears down `done`.
|
|
9
9
|
* - A **merged** PR (on an in-progress or in-review task) → `markDone`:
|
|
10
|
-
* the work has landed, so the task is terminal
|
|
11
|
-
*
|
|
10
|
+
* the work has landed, so the task is terminal. On a successful `markDone`
|
|
11
|
+
* the reviewer tears the worktree down immediately (same step). The cleaner
|
|
12
|
+
* remains the level-triggered backstop for sources where `markDone` is
|
|
13
|
+
* unsupported or that keep reporting `done`. `merged` never routes to
|
|
14
|
+
* `in-review`.
|
|
12
15
|
*
|
|
13
16
|
* Sources that don't implement `markDone` (e.g. Linear) return `unsupported`;
|
|
14
17
|
* the reviewer logs the skip and does nothing — there is no in-review
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,EAAE,KAAK,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,sEAAsE;AACtE,UAAU,eAAe;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AA+CD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAiJ3D"}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* dispatch slot (slot math counts only in-progress) while leaving the
|
|
8
8
|
* worktree intact for review, since the cleaner only tears down `done`.
|
|
9
9
|
* - A **merged** PR (on an in-progress or in-review task) → `markDone`:
|
|
10
|
-
* the work has landed, so the task is terminal
|
|
11
|
-
*
|
|
10
|
+
* the work has landed, so the task is terminal. On a successful `markDone`
|
|
11
|
+
* the reviewer tears the worktree down immediately (same step). The cleaner
|
|
12
|
+
* remains the level-triggered backstop for sources where `markDone` is
|
|
13
|
+
* unsupported or that keep reporting `done`. `merged` never routes to
|
|
14
|
+
* `in-review`.
|
|
12
15
|
*
|
|
13
16
|
* Sources that don't implement `markDone` (e.g. Linear) return `unsupported`;
|
|
14
17
|
* the reviewer logs the skip and does nothing — there is no in-review
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
import { naturalIdFromCanonical, } from "../lib/taskSource.js";
|
|
25
28
|
import { debug, errorMessage, log, logEvent } from "../lib/util.js";
|
|
26
29
|
import { effectiveBranchName } from "../lib/worktreeRunState.js";
|
|
30
|
+
import { reapWorktrees } from "./teardownReporter.js";
|
|
27
31
|
// Maps a worktree's PRs to the transition its task should make. A merged PR
|
|
28
32
|
// means the work landed → done; an open PR on an in-progress task means it's
|
|
29
33
|
// up for review. `merged` wins over `open`. An open PR on an already in-review
|
|
@@ -118,7 +122,15 @@ export function createReviewer(deps) {
|
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
120
124
|
// oxlint-disable-next-line no-await-in-loop -- single write-back then return; never iterates past the first actionable worktree.
|
|
121
|
-
await advance({
|
|
125
|
+
await advance({
|
|
126
|
+
issue,
|
|
127
|
+
task,
|
|
128
|
+
pullRequest,
|
|
129
|
+
transition,
|
|
130
|
+
entry,
|
|
131
|
+
reviewerConfig,
|
|
132
|
+
signal,
|
|
133
|
+
});
|
|
122
134
|
return;
|
|
123
135
|
}
|
|
124
136
|
}
|
|
@@ -126,7 +138,7 @@ export function createReviewer(deps) {
|
|
|
126
138
|
// task keeps its status and is retried next tick, exactly like a failed
|
|
127
139
|
// lookup. We never let one task's writeback abort the others' reviews.
|
|
128
140
|
async function advance(arguments_) {
|
|
129
|
-
const { issue, task, pullRequest, transition } = arguments_;
|
|
141
|
+
const { issue, task, pullRequest, transition, entry, reviewerConfig, signal } = arguments_;
|
|
130
142
|
try {
|
|
131
143
|
const result = transition === "done" ? await board.markDone(issue) : await board.markInReview(issue);
|
|
132
144
|
if (result.outcome === "unsupported") {
|
|
@@ -147,6 +159,14 @@ export function createReviewer(deps) {
|
|
|
147
159
|
state: pullRequest.state,
|
|
148
160
|
to: transition,
|
|
149
161
|
});
|
|
162
|
+
if (transition === "done" && reviewerConfig !== undefined) {
|
|
163
|
+
try {
|
|
164
|
+
await reapWorktrees(reviewerConfig, [entry], signal);
|
|
165
|
+
}
|
|
166
|
+
catch (teardownError) {
|
|
167
|
+
log(`Teardown failed for ${task}: ${errorMessage(teardownError)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
150
170
|
}
|
|
151
171
|
catch (error) {
|
|
152
172
|
log(`Failed to advance ${task} to ${transition}: ${errorMessage(error)}`);
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolvedConfig } from "../lib/config.ts";
|
|
2
|
+
import { type TeardownResult, type WorktreeEntry } from "../lib/worktrees.ts";
|
|
2
3
|
export declare function logTeardown(result: TeardownResult): void;
|
|
4
|
+
/**
|
|
5
|
+
* Shared helper: runs `worktrees.teardown` then records run-state cleanup,
|
|
6
|
+
* logs the result, and emits telemetry events. Used by both the cleaner and
|
|
7
|
+
* the reviewer fast-path so the sequence is never duplicated.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reapWorktrees(config: ResolvedConfig, entries: readonly WorktreeEntry[], signal?: AbortSignal): Promise<TeardownResult>;
|
|
3
10
|
export declare function recordTeardownEvents(result: TeardownResult): void;
|
|
4
11
|
//# sourceMappingURL=teardownReporter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"teardownReporter.d.ts","sourceRoot":"","sources":["../../src/commands/teardownReporter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"teardownReporter.d.ts","sourceRoot":"","sources":["../../src/commands/teardownReporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAa,MAAM,qBAAqB,CAAC;AAEzF,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAmBxD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CASzB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAwCjE"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
|
|
1
2
|
import { debug, errorMessage, log, logEvent, okMark } from "../lib/util.js";
|
|
3
|
+
import { worktrees } from "../lib/worktrees.js";
|
|
2
4
|
export function logTeardown(result) {
|
|
3
5
|
if (result.workspaceProbe.kind === "unavailable" && result.workspaceProbe.error !== undefined) {
|
|
4
6
|
log(`workspace list failed: ${errorMessage(result.workspaceProbe.error)}`);
|
|
@@ -20,6 +22,20 @@ export function logTeardown(result) {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Shared helper: runs `worktrees.teardown` then records run-state cleanup,
|
|
27
|
+
* logs the result, and emits telemetry events. Used by both the cleaner and
|
|
28
|
+
* the reviewer fast-path so the sequence is never duplicated.
|
|
29
|
+
*/
|
|
30
|
+
export async function reapWorktrees(config, entries, signal) {
|
|
31
|
+
const result = signal === undefined
|
|
32
|
+
? await worktrees.teardown(config, entries)
|
|
33
|
+
: await worktrees.teardown(config, entries, { signal });
|
|
34
|
+
recordCleanedUpRuns(config, result.removed);
|
|
35
|
+
logTeardown(result);
|
|
36
|
+
recordTeardownEvents(result);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
23
39
|
export function recordTeardownEvents(result) {
|
|
24
40
|
if (result.workspaceProbe.kind === "unavailable") {
|
|
25
41
|
logEvent("cleanup", {
|
|
@@ -1 +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,
|
|
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,OAqFzB,CAAC"}
|
package/dist/lib/cmuxAdapter.js
CHANGED
|
@@ -7,18 +7,25 @@ import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
|
7
7
|
import { debug, errorMessage, log } 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.`);
|
|
@@ -180,6 +187,48 @@ async function applyCmuxStatus(workspaceId, status, signal) {
|
|
|
180
187
|
async function closeCmuxWorkspace(workspaceId, signal) {
|
|
181
188
|
await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
|
|
182
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* cmux occasionally exits non-zero from `new-workspace` while still having
|
|
192
|
+
* created the workspace (a known flake where it also lands in the wrong `--cwd`).
|
|
193
|
+
* The id rides along in the failed command's captured output, so recover it and
|
|
194
|
+
* close that exact workspace by id — a failed launch must not strand an orphan
|
|
195
|
+
* tagged with the task's `groundcrew:<taskId>` marker. Closing by the recovered
|
|
196
|
+
* id needs no `list-workspaces`, so it survives a concurrent list failure that
|
|
197
|
+
* would defeat re-enumeration. Re-enumeration close is unsafe here anyway; we
|
|
198
|
+
* hold the precise id cmux returned, so there is no same-named-sibling risk.
|
|
199
|
+
*/
|
|
200
|
+
async function closeWorkspaceLeakedByFailedOpen(error, name, signal) {
|
|
201
|
+
if (isSignalAborted(signal)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const workspaceId = extractCmuxOpenIdFromFailure(error);
|
|
205
|
+
if (workspaceId === undefined) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
await closeCmuxWorkspace(workspaceId, signal);
|
|
210
|
+
debug(`cmux new-workspace for ${name} exited non-zero but had created ${workspaceId}; closed the leaked workspace.`);
|
|
211
|
+
}
|
|
212
|
+
catch (closeError) {
|
|
213
|
+
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.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Recover the created workspace id from a failed `new-workspace`. Parse only
|
|
218
|
+
* the captured stdout slice of the command error's message (see
|
|
219
|
+
* `normalizeCommandError`), never the whole message — matching against stderr
|
|
220
|
+
* or the echoed command line risks grabbing an unrelated `workspace:N` and
|
|
221
|
+
* closing the wrong workspace. The slice is the same shape the success path
|
|
222
|
+
* sees, so `extractCmuxOpenId` handles both the `--json` id object and a bare
|
|
223
|
+
* `workspace:N` ref.
|
|
224
|
+
*/
|
|
225
|
+
function extractCmuxOpenIdFromFailure(error) {
|
|
226
|
+
const stdout = cmuxStdoutFromFailureMessage(errorMessage(error));
|
|
227
|
+
return stdout === undefined ? undefined : extractCmuxOpenId(stdout);
|
|
228
|
+
}
|
|
229
|
+
function cmuxStdoutFromFailureMessage(message) {
|
|
230
|
+
return /\nStdout:\n([\s\S]*?)(?:\nCause: |$)/.exec(message)?.[1];
|
|
231
|
+
}
|
|
183
232
|
function isCmuxSetStatusUnsupported(error) {
|
|
184
233
|
return errorMessage(error).includes('unknown command "set-status"');
|
|
185
234
|
}
|
package/dist/lib/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/lib/usage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,aAAa,CAAC;AA+BnE,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAE3D;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,eAK7B,CAAC;
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/lib/usage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,aAAa,CAAC;AA+BnE,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAE3D;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,eAK7B,CAAC;AA8JF,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAI5D;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,CAAC,CA+BvB"}
|
package/dist/lib/usage.js
CHANGED
|
@@ -52,12 +52,30 @@ async function codexbarUsage(definition, signal) {
|
|
|
52
52
|
"--format",
|
|
53
53
|
"json",
|
|
54
54
|
];
|
|
55
|
-
const
|
|
55
|
+
const options = signal === undefined
|
|
56
56
|
? { timeoutMs: CODEXBAR_TIMEOUT_MS }
|
|
57
|
-
: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
: { signal, timeoutMs: CODEXBAR_TIMEOUT_MS };
|
|
58
|
+
let out;
|
|
59
|
+
try {
|
|
60
|
+
out = await runCommandAsync("codexbar", arguments_, options);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (signal?.aborted === true) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
// codexbar exits non-zero (e.g. status 3 for "no rate limit events yet")
|
|
67
|
+
// for handled provider conditions while still writing its JSON payload to
|
|
68
|
+
// stdout. runCommandAsync rejects on the exit code, so recover that payload
|
|
69
|
+
// and parse it like a success — its per-entry `error` field, not the exit
|
|
70
|
+
// code, distinguishes "available" from a genuine failure below. A missing
|
|
71
|
+
// or unparseable payload is a real failure: rethrow so the caller fails
|
|
72
|
+
// closed.
|
|
73
|
+
const recovered = recoverStdout(error);
|
|
74
|
+
if (recovered === undefined) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
out = recovered;
|
|
78
|
+
}
|
|
61
79
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; codexbar's --format json output matches CodexbarEntry[]
|
|
62
80
|
const parsed = JSON.parse(out);
|
|
63
81
|
// codexbar can return multiple entries when a provider has several
|
|
@@ -77,8 +95,16 @@ async function codexbarUsage(definition, signal) {
|
|
|
77
95
|
throw new Error(`codexbar returned no matching entry for provider=${provider}, source=${source}`);
|
|
78
96
|
}
|
|
79
97
|
if (!match.usage) {
|
|
80
|
-
// codexbar
|
|
81
|
-
//
|
|
98
|
+
// codexbar reports a valid session that simply has no rate-limit events
|
|
99
|
+
// recorded yet — a fresh, low-traffic, or unlimited-quota account that has
|
|
100
|
+
// consumed nothing the windows can measure. That is the *least* exhausted
|
|
101
|
+
// state, not an unreadable one: return empty windows so it normalizes to
|
|
102
|
+
// `session: null` (available) instead of fail-closing to exhausted.
|
|
103
|
+
if (isNoRateLimitEvents(match.error)) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
// codexbar can otherwise return `{error: ...}` instead of `{usage: ...}`
|
|
107
|
+
// when the underlying provider failed (e.g. codex app-server crashed). The
|
|
82
108
|
// outer catch in getUsageByAgent turns this into a fail-closed
|
|
83
109
|
// exhausted entry; surface codexbar's error message so the operator
|
|
84
110
|
// can fix the underlying CLI.
|
|
@@ -87,6 +113,33 @@ async function codexbarUsage(definition, signal) {
|
|
|
87
113
|
}
|
|
88
114
|
return match.usage;
|
|
89
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* codexbar signals an authenticated account with zero recorded rate-limit
|
|
118
|
+
* events via this provider error (e.g. "Found sessions, but no rate limit
|
|
119
|
+
* events yet."). It means no quota consumed — available — not a probe failure.
|
|
120
|
+
*/
|
|
121
|
+
function isNoRateLimitEvents(error) {
|
|
122
|
+
return /no rate limit events/i.test(error?.message ?? "");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* runCommandAsync rejects on a non-zero exit, attaching the captured stdout
|
|
126
|
+
* Buffer to the thrown error's `cause`. codexbar uses non-zero exits for handled
|
|
127
|
+
* provider conditions while still emitting its JSON payload, so pull that stdout
|
|
128
|
+
* back out to parse like a success. Returns undefined when no stdout was
|
|
129
|
+
* captured (a real failure with nothing to parse).
|
|
130
|
+
*/
|
|
131
|
+
function recoverStdout(error) {
|
|
132
|
+
/* v8 ignore next 3 @preserve -- runCommandAsync always rejects with an Error */
|
|
133
|
+
if (!(error instanceof Error)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
// A plain command failure with no captured output carries no cause/stdout.
|
|
137
|
+
if (!(error.cause instanceof Error) || !("stdout" in error.cause)) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const { stdout } = error.cause;
|
|
141
|
+
return Buffer.isBuffer(stdout) ? stdout.toString("utf8") : undefined;
|
|
142
|
+
}
|
|
90
143
|
function toFraction(value) {
|
|
91
144
|
if (value === undefined || value === null) {
|
|
92
145
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.45.
|
|
3
|
+
"version": "4.45.6",
|
|
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",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@tsconfig/node24": "24.0.4",
|
|
84
84
|
"@tsconfig/strictest": "2.0.8",
|
|
85
85
|
"@types/node": "25.9.4",
|
|
86
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
86
|
+
"@typescript/native-preview": "7.0.0-dev.20260621.1",
|
|
87
87
|
"@vitest/coverage-v8": "4.1.9",
|
|
88
88
|
"cspell": "10.0.1",
|
|
89
89
|
"dependency-cruiser": "17.4.3",
|