@clipboard-health/groundcrew 4.10.0 → 4.10.2
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/cleanupWorkspace.d.ts.map +1 -1
- package/dist/commands/cleanupWorkspace.js +17 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +74 -22
- package/docs/commands.md +1 -1
- package/package.json +3 -3
- package/static/demo-fixture.sh +18 -4
- package/static/demo.gif +0 -0
- package/static/demo.tape +7 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQnE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { loadConfig } from "../lib/config.js";
|
|
2
|
+
import { readRunState, removeRunState } from "../lib/runState.js";
|
|
2
3
|
import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
|
|
3
4
|
import { log } from "../lib/util.js";
|
|
5
|
+
import { workspaces } from "../lib/workspaces.js";
|
|
4
6
|
import { worktrees } from "../lib/worktrees.js";
|
|
5
7
|
import { logTeardown } from "./teardownReporter.js";
|
|
6
8
|
function parseArguments(argv) {
|
|
@@ -26,7 +28,21 @@ export async function cleanupWorkspace(config, options) {
|
|
|
26
28
|
const { ticket, force = false } = options;
|
|
27
29
|
const entries = worktrees.findByTicket(config, ticket);
|
|
28
30
|
if (entries.length === 0) {
|
|
29
|
-
|
|
31
|
+
if (readRunState(config, ticket) === undefined) {
|
|
32
|
+
log(`No worktree found for ${ticket}; nothing to clean up.`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const workspaceProbe = await workspaces.probe(config);
|
|
36
|
+
if (workspaceProbe.kind === "unavailable") {
|
|
37
|
+
log(`No worktree found for ${ticket}; workspace probe unavailable, leaving run-state intact.`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (workspaceProbe.names.has(ticket)) {
|
|
41
|
+
log(`No worktree found for ${ticket}; workspace still present; leaving run-state intact.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
removeRunState(config, ticket);
|
|
45
|
+
log(`No worktree found for ${ticket}; cleared stale run-state.`);
|
|
30
46
|
return;
|
|
31
47
|
}
|
|
32
48
|
const result = await worktrees.teardown(config, entries, { force });
|
|
@@ -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;
|
|
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;AAonBD,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"}
|
package/dist/commands/status.js
CHANGED
|
@@ -76,11 +76,14 @@ function ticketWorkspaceText(probe, ticket) {
|
|
|
76
76
|
function isWorkspaceExited(probe, ticket) {
|
|
77
77
|
return probe.kind === "ok" && probe.exitedNames?.has(ticket) === true;
|
|
78
78
|
}
|
|
79
|
-
function formatRunState(state) {
|
|
79
|
+
function formatRunState(state, flags = []) {
|
|
80
80
|
if (state === undefined) {
|
|
81
81
|
return "(none)";
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// Only the leading lifecycle token gains the reconciliation flags; the
|
|
84
|
+
// `;`-separated detail (model/updated/resumes/reason) is preserved verbatim.
|
|
85
|
+
const lifecycle = flags.length === 0 ? state.state : `${state.state} (${flags.join(", ")})`;
|
|
86
|
+
const summary = `${lifecycle}; model=${state.model}; updated=${state.updatedAt}; resumes=${state.resumeCount}`;
|
|
84
87
|
const detail = state.reason ?? state.detail;
|
|
85
88
|
return detail === undefined ? summary : `${summary}; ${detail}`;
|
|
86
89
|
}
|
|
@@ -174,7 +177,7 @@ async function writeTicketStatus(config, rawTicket) {
|
|
|
174
177
|
const accessHint = await exitedWorkspaceAccessHint(config, workspaceProbe, ticket);
|
|
175
178
|
writeOutput(formatTicketLine(ticket, runState, sourceStatus));
|
|
176
179
|
writeTicketTitle(runState, sourceStatus);
|
|
177
|
-
writeOutput(`run: ${formatRunState(runState)}`);
|
|
180
|
+
writeOutput(`run: ${formatRunState(runState, runProbeFlags(runState, workspaceProbe, ticket))}`);
|
|
178
181
|
writeOutput(`workspace: ${ticketWorkspaceText(workspaceProbe, ticket)}`);
|
|
179
182
|
if (accessHint !== undefined) {
|
|
180
183
|
writeOutput(`attach: ${accessHint.command}`);
|
|
@@ -220,32 +223,43 @@ function formatDuration(ms) {
|
|
|
220
223
|
const hours = Math.floor((ms - days * MS_PER_DAY) / MS_PER_HOUR);
|
|
221
224
|
return hours === 0 ? `${days}d` : `${days}d ${hours}h`;
|
|
222
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Probe-reconciliation flags shared by the inventory row and the per-ticket
|
|
228
|
+
* `run:` line. Flags the two interesting disagreements between the recorded
|
|
229
|
+
* RunState lifecycle and the live workspace probe: a running/resumed dispatch
|
|
230
|
+
* with a missing or exited session, and an idle row with a stray live or
|
|
231
|
+
* exited session. `probe.kind === "unavailable"` is "we don't know" and yields
|
|
232
|
+
* no flags. Excludes the duration token, which only the inventory row appends.
|
|
233
|
+
*/
|
|
234
|
+
function runProbeFlags(runState, probe, ticket) {
|
|
235
|
+
if (probe.kind !== "ok") {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
const lifecycle = runState?.state ?? "idle";
|
|
239
|
+
const sessionPresent = probe.names.has(ticket);
|
|
240
|
+
const sessionExited = isWorkspaceExited(probe, ticket);
|
|
241
|
+
const flags = [];
|
|
242
|
+
if (lifecycle === "idle" && sessionPresent) {
|
|
243
|
+
flags.push(sessionExited ? "stray exited session" : "stray session");
|
|
244
|
+
}
|
|
245
|
+
if ((lifecycle === "running" || lifecycle === "resumed") && sessionExited) {
|
|
246
|
+
flags.push("session exited");
|
|
247
|
+
}
|
|
248
|
+
else if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
|
|
249
|
+
flags.push("session dead");
|
|
250
|
+
}
|
|
251
|
+
return flags;
|
|
252
|
+
}
|
|
223
253
|
/**
|
|
224
254
|
* Combined human-readable state for the inventory row. Surfaces RunState
|
|
225
255
|
* lifecycle and flags the two interesting disagreements with the workspace
|
|
226
|
-
* probe
|
|
227
|
-
*
|
|
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.
|
|
256
|
+
* probe via `runProbeFlags`. When the row is actively running, appends the
|
|
257
|
+
* elapsed wall-clock time since dispatch.
|
|
231
258
|
*/
|
|
232
259
|
function inventoryStateText(runState, probe, ticket, now) {
|
|
233
260
|
const lifecycle = runState?.state ?? "idle";
|
|
261
|
+
const flags = runProbeFlags(runState, probe, ticket);
|
|
234
262
|
const duration = runStateDurationMs(runState, now);
|
|
235
|
-
const flags = [];
|
|
236
|
-
if (probe.kind === "ok") {
|
|
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");
|
|
244
|
-
}
|
|
245
|
-
else if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
|
|
246
|
-
flags.push("session dead");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
263
|
if (duration !== undefined) {
|
|
250
264
|
flags.push(formatDuration(duration));
|
|
251
265
|
}
|
|
@@ -459,6 +473,43 @@ function writeQueueSections(boardResult) {
|
|
|
459
473
|
function inProgressCount(issues) {
|
|
460
474
|
return issues.filter((issue) => issue.status === "in-progress").length;
|
|
461
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* In-progress board issues that have no local worktree. These count toward the
|
|
478
|
+
* `slots: N/M used` total but are absent from the Worktrees section (their
|
|
479
|
+
* worktree was removed, or lives outside this config's projectDir /
|
|
480
|
+
* knownRepositories), so without this they'd be counted yet invisible. Worktree
|
|
481
|
+
* tickets are lowercased, so the natural id is lowercased before matching.
|
|
482
|
+
*/
|
|
483
|
+
function inProgressWithoutWorktree(issues, worktreeTickets) {
|
|
484
|
+
return issues
|
|
485
|
+
.filter((issue) => issue.status === "in-progress")
|
|
486
|
+
.filter((issue) => !worktreeTickets.has(naturalIdFromCanonical(issue.id).toLowerCase()))
|
|
487
|
+
.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
488
|
+
}
|
|
489
|
+
function writeInProgressIssue(issue) {
|
|
490
|
+
const naturalId = naturalIdFromCanonical(issue.id);
|
|
491
|
+
writeOutput(issue.url === undefined ? naturalId : `${naturalId} ${issue.url}`);
|
|
492
|
+
writeOutput(inventoryField("title", issue.title));
|
|
493
|
+
if (issue.repository !== undefined) {
|
|
494
|
+
writeOutput(inventoryField("repo", issue.repository));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function writeInProgressWithoutWorktree(boardResult, worktreeTickets) {
|
|
498
|
+
if (boardResult.kind !== "ok") {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const issues = inProgressWithoutWorktree(boardResult.issues, worktreeTickets);
|
|
502
|
+
if (issues.length === 0) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
writeSection("In progress (no local worktree)");
|
|
506
|
+
for (const [index, issue] of issues.entries()) {
|
|
507
|
+
if (index > 0) {
|
|
508
|
+
writeOutput();
|
|
509
|
+
}
|
|
510
|
+
writeInProgressIssue(issue);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
462
513
|
async function writeInventoryStatus(config) {
|
|
463
514
|
// Banner ("groundcrew status\n=================") dropped: the command
|
|
464
515
|
// you just ran already tells you what report you're looking at, and the
|
|
@@ -469,6 +520,7 @@ async function writeInventoryStatus(config) {
|
|
|
469
520
|
const worktreeTickets = new Set(worktrees.list(config).map((entry) => entry.ticket));
|
|
470
521
|
writeStraySessions(probe, worktreeTickets);
|
|
471
522
|
const boardResult = await boardResultPromise;
|
|
523
|
+
writeInProgressWithoutWorktree(boardResult, worktreeTickets);
|
|
472
524
|
if (boardResult.kind === "ok") {
|
|
473
525
|
const used = inProgressCount(boardResult.issues);
|
|
474
526
|
writeOutput();
|
package/docs/commands.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`crew status <TICKET>` prints a read-only snapshot for one ticket: cached title and URL when present, recorded run state, live workspace presence, matching worktrees, git dirtiness, PR links for matching branches, recent log lines when present, and the ticket status from the configured ticket source.
|
|
6
6
|
|
|
7
|
-
`crew status` with no ticket prints the current inventory: known worktrees with cached ticket metadata, workspace/run-state agreement, attach hints, worktree paths, PR links, and stray sessions reported by the configured backend. Local diagnostics are printed before ticket-source fetches complete. When the source fetch succeeds, status also prints slot usage
|
|
7
|
+
`crew status` with no ticket prints the current inventory: known worktrees with cached ticket metadata, workspace/run-state agreement, attach hints, worktree paths, PR links, and stray sessions reported by the configured backend. Local diagnostics are printed before ticket-source fetches complete. When the source fetch succeeds, status also prints any in-progress source tickets with no local worktree, slot usage, and Queue/Blocked sections for eligible Todo tickets. Worktree-less in-progress rows include the ticket title, URL when the source provides one, and repository when the source resolves one. If the source fetch fails, Queue shows `unavailable: <reason>` and the slots line is omitted.
|
|
8
8
|
|
|
9
9
|
Status is informational only. Use `crew cleanup <TICKET>` to tear down stale worktrees and `crew resume <TICKET>` to reopen preserved work.
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.2",
|
|
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",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"verify": "node scripts/verifyAll.mts"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@clipboard-health/clearance": "1.1.
|
|
71
|
+
"@clipboard-health/clearance": "1.1.13",
|
|
72
72
|
"@linear/sdk": "86.0.0",
|
|
73
73
|
"cosmiconfig": "9.0.1",
|
|
74
74
|
"tslib": "2.8.1",
|
|
75
75
|
"zod": "4.4.3"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@clipboard-health/ai-rules": "2.21.
|
|
78
|
+
"@clipboard-health/ai-rules": "2.21.1",
|
|
79
79
|
"@clipboard-health/oxlint-config": "1.9.4",
|
|
80
80
|
"@nx/js": "22.7.3",
|
|
81
81
|
"@tsconfig/node24": "24.0.4",
|
package/static/demo-fixture.sh
CHANGED
|
@@ -16,12 +16,20 @@ tmux rename-window 'crew run --watch'
|
|
|
16
16
|
tmux set-option -g status off
|
|
17
17
|
tmux set-option -g pane-border-status top
|
|
18
18
|
tmux set-option -g pane-border-format ' #{?#{m:codex*,#{pane_title}},#[fg=#fbbf24],#{?#{m:claude*,#{pane_title}},#[fg=#60a5fa],#[fg=#77d94e]}}#[bold]#{pane_title}#[default] '
|
|
19
|
-
tmux
|
|
20
|
-
|
|
19
|
+
# Use one divider color for active and inactive panes. tmux highlights only the
|
|
20
|
+
# active-adjacent segment of a shared divider, so a distinct active color makes a
|
|
21
|
+
# single divider render two-tone (and the dim half reads as "missing"). A uniform
|
|
22
|
+
# color keeps every divider a clean, continuous line. Accent lives in pane titles.
|
|
23
|
+
tmux set-option -g pane-border-style 'fg=#52525b'
|
|
24
|
+
tmux set-option -g pane-active-border-style 'fg=#52525b'
|
|
21
25
|
tmux set-option -g remain-on-exit on
|
|
22
26
|
|
|
23
27
|
printf '\033]2;groundcrew\033\\'
|
|
24
28
|
|
|
29
|
+
# This agent script prints 12 lines into a split pane. The pane heights are set
|
|
30
|
+
# by `Set Height` in demo.tape (see the note there) so all 12 fit without
|
|
31
|
+
# scrolling — a scrolling pane makes its top border drift. If you add or remove
|
|
32
|
+
# output lines here, keep that height in sync.
|
|
25
33
|
demo_agent_script="$(mktemp "${TMPDIR:-/tmp}/groundcrew-vhs-agent.XXXXXX")"
|
|
26
34
|
trap 'rm -f "${demo_agent_script}"' EXIT
|
|
27
35
|
cat >"${demo_agent_script}" <<'SH'
|
|
@@ -60,7 +68,10 @@ printf '%sEditing in isolated branch...%s\n' "${dim}" "${reset}"
|
|
|
60
68
|
sleep 0.5
|
|
61
69
|
printf '%sRunning verification...%s\n' "${dim}" "${reset}"
|
|
62
70
|
sleep 0.5
|
|
63
|
-
|
|
71
|
+
# No trailing newline: keeps the final line flush against the pane bottom so the
|
|
72
|
+
# pane never has to scroll a phantom empty line (which VHS renders as the top
|
|
73
|
+
# border drifting down).
|
|
74
|
+
printf '%s%s✓ Ready for review.%s' "${green}" "${bold}" "${reset}"
|
|
64
75
|
|
|
65
76
|
while :; do
|
|
66
77
|
sleep 60
|
|
@@ -107,5 +118,8 @@ crew() {
|
|
|
107
118
|
sleep 0.7
|
|
108
119
|
|
|
109
120
|
demo_log "${c_dim}Queue clear · next poll in 60s${c_reset}"
|
|
110
|
-
|
|
121
|
+
tmux refresh-client
|
|
122
|
+
# Hold the fully-settled layout on screen. Must outlast the tape's trailing
|
|
123
|
+
# Sleep so the orchestrator pane never drops back to a shell prompt on camera.
|
|
124
|
+
sleep 20
|
|
111
125
|
}
|
package/static/demo.gif
CHANGED
|
Binary file
|
package/static/demo.tape
CHANGED
|
@@ -8,7 +8,11 @@ Require ttyd
|
|
|
8
8
|
|
|
9
9
|
Set Shell "bash"
|
|
10
10
|
Set Width 1320
|
|
11
|
-
|
|
11
|
+
# 690px ≈ 28 terminal rows. The right column splits into two stacked agent panes
|
|
12
|
+
# of ~13 content rows each, sized to hold the demo-fixture.sh agent script's
|
|
13
|
+
# 12-line output without scrolling (a scrolling pane makes its top border drift).
|
|
14
|
+
# Grow this if that output grows past ~13 lines.
|
|
15
|
+
Set Height 690
|
|
12
16
|
Set FontSize 16
|
|
13
17
|
Set FontFamily "Menlo"
|
|
14
18
|
Set Padding 16
|
|
@@ -35,7 +39,8 @@ Sleep 1.0
|
|
|
35
39
|
Type "crew run --watch"
|
|
36
40
|
Sleep 0.4
|
|
37
41
|
Enter
|
|
38
|
-
|
|
42
|
+
# ~8s of action, then hold the settled layout long enough to read all three panes.
|
|
43
|
+
Sleep 15
|
|
39
44
|
|
|
40
45
|
Hide
|
|
41
46
|
Type "tmux -f /dev/null -L groundcrew-demo kill-session -t groundcrew"
|