@clipboard-health/groundcrew 4.6.0 → 4.7.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 +32 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -2
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +17 -8
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +2 -1
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +26 -11
- package/dist/commands/teardownReporter.js +4 -4
- package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
- package/dist/lib/adapters/linear/fetch.js +4 -4
- package/dist/lib/adapters/linear/writeback.js +2 -2
- package/dist/lib/adapters/shell/invoke.js +2 -2
- package/dist/lib/agentLaunch.d.ts +1 -0
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +24 -16
- package/dist/lib/cmuxAdapter.js +5 -5
- package/dist/lib/config.js +2 -2
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +2 -2
- package/dist/lib/runStateCleanup.js +2 -2
- package/dist/lib/tmuxAdapter.js +2 -2
- package/dist/lib/usage.d.ts.map +1 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/util.d.ts +13 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +54 -5
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +30 -20
- package/docs/commands.md +12 -12
- package/package.json +1 -1
- package/static/demo.gif +0 -0
package/README.md
CHANGED
|
@@ -16,48 +16,64 @@
|
|
|
16
16
|
<a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@clipboard-health/groundcrew?style=flat-square&label=license&color=18181b&labelColor=18181b"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img alt="Groundcrew picking up tickets and running coding agents in parallel" src="./static/demo.gif" width="800">
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
Groundcrew watches assigned tickets, creates isolated worktrees, launches agent CLIs in dedicated terminals, and leaves each ticket's work on its own PR-ready branch. For the backstory, read _[Tickets to pull requests while you sleep](https://www.clipboardworks.com/resources/blog/tickets-to-pull-requests-while-you-sleep)_.
|
|
20
24
|
|
|
21
25
|
## Why
|
|
22
26
|
|
|
23
27
|
- **One worktree per ticket.** Agents work in parallel without stepping on each other.
|
|
24
|
-
- **
|
|
25
|
-
- **Local-first isolation.** Safehouse
|
|
28
|
+
- **Pluggable ticket sources.** Linear by default; Jira and local files via [ticket sources](./docs/ticket-sources.md).
|
|
29
|
+
- **Local-first isolation.** Safehouse, Docker Sandboxes, or an explicit `none` escape hatch.
|
|
26
30
|
- **Multi-agent routing.** Ships with `claude` and `codex`; bring your own CLI in config.
|
|
27
31
|
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
`crew doctor` checks all of these, so you can install as you go.
|
|
35
|
+
|
|
36
|
+
- **Node >= 24:** [nvm](https://github.com/nvm-sh/nvm): `nvm install 24`.
|
|
37
|
+
- **git:** e.g., `brew install git`, `apt install git`.
|
|
38
|
+
- **A terminal multiplexer:** [tmux](https://github.com/tmux/tmux/wiki/Installing) (cross-platform) or [cmux](https://cmux.com/) (macOS).
|
|
39
|
+
- **An agent CLI:** [Claude Code](https://code.claude.com/docs/en/quickstart) and/or [Codex](https://developers.openai.com/codex/quickstart?setup=cli).
|
|
40
|
+
- **A sandbox runner:** [Docker Sandboxes](https://docs.docker.com/sandboxes/) (cross-platform) or [Safehouse](https://agent-safehouse.dev/) on macOS. Skip only with `--runner none`.
|
|
41
|
+
|
|
28
42
|
## Quickstart
|
|
29
43
|
|
|
30
44
|
```bash
|
|
31
|
-
# 1. Install
|
|
32
|
-
|
|
33
|
-
# 2. Install groundcrew.
|
|
45
|
+
# 1. Install groundcrew.
|
|
34
46
|
npm install -g @clipboard-health/groundcrew
|
|
35
47
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
crew init --global --project-dir ~/dev --repo OWNER/REPO --
|
|
48
|
+
# 2. Scaffold a global config. Agents are sandboxed by default
|
|
49
|
+
# (Safehouse/Docker Sandboxes); add --runner none to run unsandboxed on the host.
|
|
50
|
+
crew init --global --project-dir ~/dev --repo OWNER/REPO --model claude
|
|
39
51
|
|
|
40
|
-
#
|
|
52
|
+
# 3. Run the clone commands printed by `crew init`.
|
|
41
53
|
|
|
42
|
-
#
|
|
54
|
+
# 4. Using Linear? Export your API key. (Jira and other trackers: see Ticket Pickup.)
|
|
43
55
|
export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
|
|
44
56
|
|
|
45
|
-
#
|
|
57
|
+
# 5. Verify setup, then dispatch.
|
|
46
58
|
crew doctor
|
|
47
59
|
crew run --watch
|
|
48
60
|
```
|
|
49
61
|
|
|
50
|
-
`crew init --global` writes config to `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/`. Pass `--repo` more than once for multiple repos.
|
|
62
|
+
`crew init --global` writes config to `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/`. Pass `--repo` more than once for multiple repos. If you only have one CLI installed, pass `--model claude` (or `--model codex`) so Groundcrew disables the other model and `doctor` won't flag it as missing.
|
|
51
63
|
|
|
52
64
|
## Ticket Pickup
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
**Not on Linear?** Use Jira or local files via [ticket sources](./docs/ticket-sources.md).
|
|
67
|
+
|
|
68
|
+
Linear works out of the box: assign tickets to yourself and add an `agent-*` label.
|
|
55
69
|
|
|
56
70
|
- `agent-claude`, `agent-codex`, or `agent-<name>` routes to that model.
|
|
57
71
|
- `agent-any` routes to the enabled model with the most available capacity.
|
|
58
72
|
- Tickets without an `agent-*` label are ignored by `crew run`; dispatch one manually with `crew start <TICKET>`.
|
|
59
73
|
|
|
60
|
-
Groundcrew scans `workspace.knownRepositories` to infer which repo a ticket belongs to.
|
|
74
|
+
Groundcrew scans `workspace.knownRepositories` to infer which repo a ticket belongs to.
|
|
75
|
+
|
|
76
|
+
A ticket blocked by non-terminal blockers is skipped until those blockers are done.
|
|
61
77
|
|
|
62
78
|
## Commands
|
|
63
79
|
|
|
@@ -91,7 +107,7 @@ export default {
|
|
|
91
107
|
knownRepositories: ["OWNER/REPO"],
|
|
92
108
|
},
|
|
93
109
|
local: {
|
|
94
|
-
runner: "
|
|
110
|
+
runner: "auto",
|
|
95
111
|
},
|
|
96
112
|
models: {
|
|
97
113
|
default: "claude",
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyQA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCvD"}
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
|
|
|
8
8
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
9
9
|
import { statusCli } from "./commands/status.js";
|
|
10
10
|
import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
|
|
11
|
-
import { errorMessage, parseDryRunPositionals, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
|
|
11
|
+
import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, setVerbose, writeError, writeOutput, } from "./lib/util.js";
|
|
12
12
|
const REMOVED_SANDBOX_COMMAND_MESSAGE = [
|
|
13
13
|
"`crew sandbox` is no longer supported.",
|
|
14
14
|
"Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
|
|
@@ -176,6 +176,7 @@ function printHelp() {
|
|
|
176
176
|
writeOutput("Options:");
|
|
177
177
|
writeOutput(" -h, --help Show help");
|
|
178
178
|
writeOutput(" -v, --version Print version");
|
|
179
|
+
writeOutput(" --verbose Show diagnostic output (or set GROUNDCREW_VERBOSE)");
|
|
179
180
|
writeOutput("");
|
|
180
181
|
writeOutput("Commands:");
|
|
181
182
|
for (const [name, command] of visibleCommands) {
|
|
@@ -192,8 +193,27 @@ function packageMetadata() {
|
|
|
192
193
|
function packageVersion() {
|
|
193
194
|
return packageMetadata().version;
|
|
194
195
|
}
|
|
196
|
+
const VERBOSE_FLAG = "--verbose";
|
|
197
|
+
function environmentVerbose() {
|
|
198
|
+
const raw = readEnvironmentVariable("GROUNDCREW_VERBOSE");
|
|
199
|
+
return raw !== undefined && raw !== "" && raw !== "0" && raw.toLowerCase() !== "false";
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Pulls the global `--verbose` flag out of argv before subcommand dispatch so
|
|
203
|
+
* every command supports it and the strict per-command parsers never see it.
|
|
204
|
+
* GROUNDCREW_VERBOSE enables it without the flag.
|
|
205
|
+
*/
|
|
206
|
+
function extractVerbose(argv) {
|
|
207
|
+
const commandArgv = argv.filter((argument) => argument !== VERBOSE_FLAG);
|
|
208
|
+
const verbose = commandArgv.length !== argv.length || environmentVerbose();
|
|
209
|
+
return { verbose, commandArgv };
|
|
210
|
+
}
|
|
195
211
|
export async function run(argv) {
|
|
196
|
-
const
|
|
212
|
+
const { verbose, commandArgv } = extractVerbose(argv);
|
|
213
|
+
if (verbose) {
|
|
214
|
+
setVerbose(true);
|
|
215
|
+
}
|
|
216
|
+
const [subcommand, ...rest] = commandArgv;
|
|
197
217
|
if (subcommand === undefined || subcommand === "-h" || subcommand === "--help") {
|
|
198
218
|
printHelp();
|
|
199
219
|
if (subcommand === undefined) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAaD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAwNjE;AAsBD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { dispatchableRepository } from "../lib/repositoryValidation.js";
|
|
10
10
|
import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/ticketSource.js";
|
|
11
|
-
import { errorMessage, log, logEvent } from "../lib/util.js";
|
|
11
|
+
import { errorMessage, failMark, log, logEvent } from "../lib/util.js";
|
|
12
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
13
13
|
import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, } from "./eligibility.js";
|
|
14
14
|
import { setupWorkspace } from "./setupWorkspace.js";
|
|
@@ -79,7 +79,7 @@ export function createDispatcher(deps) {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
|
-
log(
|
|
82
|
+
log(`${failMark()} Failed to start ${ticketId}: ${errorMessage(error)}`);
|
|
83
83
|
logEvent("dispatch", {
|
|
84
84
|
outcome: "failed",
|
|
85
85
|
ticket: ticketId,
|
|
@@ -145,18 +145,21 @@ export function createDispatcher(deps) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
// usage() is an HTTP call; workspaces.probe shells tmux/cmux. Kick off
|
|
148
|
-
// usage first so
|
|
148
|
+
// usage first so any necessary workspace probe can overlap with the
|
|
149
|
+
// in-flight request.
|
|
149
150
|
const usagePromise = usage(signal);
|
|
150
151
|
// Snapshot live workspace names once per iteration so eligibility can
|
|
151
152
|
// distinguish "worktree exists AND its agent is still running" (resume)
|
|
152
153
|
// from "worktree exists but the workspace is gone" (ambiguous — don't
|
|
153
|
-
// auto-recover).
|
|
154
|
-
//
|
|
154
|
+
// auto-recover). Skip the shell-out entirely for fresh-start-only ticks:
|
|
155
|
+
// if none of the candidates has a matching worktree, classifyRecovery()
|
|
156
|
+
// will never read the probe.
|
|
155
157
|
let workspaceProbe;
|
|
156
158
|
try {
|
|
157
|
-
workspaceProbe =
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
workspaceProbe =
|
|
160
|
+
dryRun || !hasRecoverableCandidate(dispatchableUnblocked, worktreeEntries)
|
|
161
|
+
? { kind: "ok", names: new Set() }
|
|
162
|
+
: await workspaces.probe(config, signal);
|
|
160
163
|
}
|
|
161
164
|
catch (error) {
|
|
162
165
|
usagePromise.catch(() => "ignored");
|
|
@@ -196,6 +199,12 @@ export function createDispatcher(deps) {
|
|
|
196
199
|
}
|
|
197
200
|
return { runOnce };
|
|
198
201
|
}
|
|
202
|
+
function hasRecoverableCandidate(issues, worktreeEntries) {
|
|
203
|
+
return issues.some((issue) => {
|
|
204
|
+
const naturalId = naturalIdFromCanonical(issue.id);
|
|
205
|
+
return worktreeEntries.some((entry) => entry.repository === issue.repository && entry.ticket === naturalId);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
199
208
|
function formatUsageExhaustion(exhaustion) {
|
|
200
209
|
if (exhaustion.kind === "session") {
|
|
201
210
|
const mins = exhaustion.resetMinutes ?? "?";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAEA,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,
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAEA,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"}
|
|
@@ -106,12 +106,13 @@ export async function resumeWorkspace(config, options) {
|
|
|
106
106
|
if (definition === undefined) {
|
|
107
107
|
throw new Error(`Unknown model: ${context.model}`);
|
|
108
108
|
}
|
|
109
|
-
const { runner, sandboxName } = await prepareAgentLaunch({
|
|
109
|
+
const { runner, sandboxName, ensureReady } = await prepareAgentLaunch({
|
|
110
110
|
config,
|
|
111
111
|
model: context.model,
|
|
112
112
|
definition,
|
|
113
113
|
purpose: "resumes",
|
|
114
114
|
});
|
|
115
|
+
await ensureReady();
|
|
115
116
|
const stagedPrompt = stagePromptText({
|
|
116
117
|
prefix: "groundcrew-resume",
|
|
117
118
|
ticket,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA+Gf;AAyID,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
|
|
@@ -7,7 +7,7 @@ import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
|
7
7
|
import { recordRunState } from "../lib/runState.js";
|
|
8
8
|
import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
9
9
|
import { naturalIdFromCanonical } from "../lib/ticketSource.js";
|
|
10
|
-
import { errorMessage, log } from "../lib/util.js";
|
|
10
|
+
import { debug, errorMessage, log, okMark } from "../lib/util.js";
|
|
11
11
|
import { workspaces } from "../lib/workspaces.js";
|
|
12
12
|
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
13
13
|
function stagePrompt(input) {
|
|
@@ -31,7 +31,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
31
31
|
if (!definition) {
|
|
32
32
|
throw new Error(`Unknown model: ${model}`);
|
|
33
33
|
}
|
|
34
|
-
const { runner, sandboxName } = await prepareAgentLaunch({
|
|
34
|
+
const { runner, sandboxName, ensureReady } = await prepareAgentLaunch({
|
|
35
35
|
config,
|
|
36
36
|
model,
|
|
37
37
|
definition,
|
|
@@ -40,11 +40,10 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
40
40
|
});
|
|
41
41
|
const spec = { repository, ticket };
|
|
42
42
|
let created;
|
|
43
|
+
const createdPromise = signal === undefined ? worktrees.create(config, spec) : worktrees.create(config, spec, signal);
|
|
44
|
+
const readinessPromise = startLaunchReadiness(ensureReady);
|
|
43
45
|
try {
|
|
44
|
-
created =
|
|
45
|
-
signal === undefined
|
|
46
|
-
? await worktrees.create(config, spec)
|
|
47
|
-
: await worktrees.create(config, spec, signal);
|
|
46
|
+
created = await createdPromise;
|
|
48
47
|
}
|
|
49
48
|
catch (error) {
|
|
50
49
|
if (isWorktreeAlreadyExistsError(error)) {
|
|
@@ -63,6 +62,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
63
62
|
// the ticket strands forever.
|
|
64
63
|
let promptDir;
|
|
65
64
|
try {
|
|
65
|
+
await assertLaunchReady(readinessPromise);
|
|
66
66
|
const ticketDetails = options.details;
|
|
67
67
|
const accessHint = await workspaces.accessHint(config, ticket, signal);
|
|
68
68
|
const stagedPrompt = stagePrompt({
|
|
@@ -83,7 +83,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
83
83
|
sandboxName,
|
|
84
84
|
});
|
|
85
85
|
const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
|
|
86
|
-
|
|
86
|
+
debug("Opening workspace...");
|
|
87
87
|
await openAgentWorkspace({
|
|
88
88
|
config,
|
|
89
89
|
name: ticket,
|
|
@@ -105,9 +105,9 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
105
105
|
title: ticketDetails.title,
|
|
106
106
|
...(ticketDetails.url === undefined ? {} : { url: ticketDetails.url }),
|
|
107
107
|
});
|
|
108
|
-
log(
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
log(`${okMark()} "${ticket}" launched (${model}) worktree ${worktreeName}`);
|
|
109
|
+
debug(` Worktree: ${launchDir}`);
|
|
110
|
+
debug(` Branch: ${branchName}`);
|
|
111
111
|
if (accessHint !== undefined) {
|
|
112
112
|
logAccessHint(accessHint);
|
|
113
113
|
}
|
|
@@ -130,6 +130,21 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
130
130
|
throw error;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
async function startLaunchReadiness(ensureReady) {
|
|
134
|
+
try {
|
|
135
|
+
await ensureReady();
|
|
136
|
+
return { kind: "ready" };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return { kind: "failed", error };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function assertLaunchReady(readinessPromise) {
|
|
143
|
+
const readiness = await readinessPromise;
|
|
144
|
+
if (readiness.kind === "failed") {
|
|
145
|
+
throw readiness.error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
133
148
|
/**
|
|
134
149
|
* Probe the workspace backend and, if a workspace for `ticket` is still
|
|
135
150
|
* live, log the access hint. Used on the pre-launch error path (e.g. the
|
|
@@ -151,7 +166,7 @@ async function logAccessHintForExistingWorkspace(arguments_) {
|
|
|
151
166
|
logAccessHint(accessHint);
|
|
152
167
|
}
|
|
153
168
|
function logAccessHint(accessHint) {
|
|
154
|
-
|
|
169
|
+
debug(` Attach: ${accessHint.command}`);
|
|
155
170
|
}
|
|
156
171
|
function renderWorkspaceContinuationInstruction(accessHint) {
|
|
157
172
|
if (accessHint === undefined) {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { errorMessage, log, logEvent } from "../lib/util.js";
|
|
1
|
+
import { debug, errorMessage, log, logEvent, okMark } from "../lib/util.js";
|
|
2
2
|
export function logTeardown(result) {
|
|
3
3
|
if (result.workspaceProbe.kind === "unavailable" && result.workspaceProbe.error !== undefined) {
|
|
4
4
|
log(`workspace list failed: ${errorMessage(result.workspaceProbe.error)}`);
|
|
5
5
|
}
|
|
6
6
|
for (const ticket of result.closed) {
|
|
7
|
-
|
|
7
|
+
debug(`Closed workspace ${ticket}`);
|
|
8
8
|
}
|
|
9
9
|
for (const entry of result.removed) {
|
|
10
|
-
log(
|
|
11
|
-
|
|
10
|
+
log(`${okMark()} Cleanup complete for ${entry.ticket} (${entry.kind})`);
|
|
11
|
+
debug(` Worktree: ${entry.dir} (removed)`);
|
|
12
12
|
}
|
|
13
13
|
for (const failure of result.failures) {
|
|
14
14
|
const message = errorMessage(failure.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Done -> Shipped, etc.) Just Work without per-team config.
|
|
10
10
|
*/
|
|
11
11
|
import { RepositoryResolutionError } from "../../ticketSource.js";
|
|
12
|
-
import { log } from "../../util.js";
|
|
12
|
+
import { log, styleWarning } from "../../util.js";
|
|
13
13
|
import { AGENT_LABEL_PREFIX, resolveModelFor, resolveRepositoryFor, } from "./parsing.js";
|
|
14
14
|
export const ISSUES_PAGE_SIZE = 250;
|
|
15
15
|
// `state.type` values surfaced by `fetch()`. `backlog` / `triage` are dropped
|
|
@@ -33,13 +33,13 @@ export function createBoardSource(deps) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
async function verifyViewer(client) {
|
|
36
|
-
const response = await client.client.rawRequest(`query VerifyViewer { viewer { id name
|
|
36
|
+
const response = await client.client.rawRequest(`query VerifyViewer { viewer { id name } }`);
|
|
37
37
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- shape is fixed by our GraphQL query above
|
|
38
38
|
const { viewer } = response.data;
|
|
39
39
|
if (viewer === null) {
|
|
40
40
|
throw new Error("Linear API did not return a viewer for this API key. Confirm LINEAR_API_KEY is set and points to a personal API key, not a workspace key.");
|
|
41
41
|
}
|
|
42
|
-
log(`Resolved Linear viewer: ${viewer.name}
|
|
42
|
+
log(`Resolved Linear viewer: ${viewer.name}`);
|
|
43
43
|
}
|
|
44
44
|
export function isIssueInProgress(issue) {
|
|
45
45
|
return issue.stateType === "started";
|
|
@@ -158,7 +158,7 @@ function resolveAgentMetadata(arguments_) {
|
|
|
158
158
|
}
|
|
159
159
|
else {
|
|
160
160
|
model = undefined;
|
|
161
|
-
log(`WARNING: ${ticket} has an ${AGENT_LABEL_PREFIX}* label but no known repository in its description; skipping dispatch. Add one of workspace.knownRepositories to the description, or remove the ${AGENT_LABEL_PREFIX}* label: ${config.workspace.knownRepositories.join(", ")}`);
|
|
161
|
+
log(styleWarning(`WARNING: ${ticket} has an ${AGENT_LABEL_PREFIX}* label but no known repository in its description; skipping dispatch. Add one of workspace.knownRepositories to the description, or remove the ${AGENT_LABEL_PREFIX}* label: ${config.workspace.knownRepositories.join(", ")}`));
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
return { repository, model };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { debug } from "../../util.js";
|
|
2
2
|
export function createLinearIssueStatusUpdater(arguments_) {
|
|
3
3
|
const { client } = arguments_;
|
|
4
4
|
// Positive cache only. Keyed by teamId because the in-progress-state
|
|
@@ -45,7 +45,7 @@ export function createLinearIssueStatusUpdater(arguments_) {
|
|
|
45
45
|
throw new Error(`Could not find a workflow state with type "started" for ${issue.id} (team ${issue.teamId.length > 0 ? issue.teamId : "?"}). Confirm the team's Linear workflow has an in-progress column.`);
|
|
46
46
|
}
|
|
47
47
|
await client.updateIssue(issue.uuid, { stateId });
|
|
48
|
-
|
|
48
|
+
debug(`Marked ${issue.id} as in progress`);
|
|
49
49
|
}
|
|
50
50
|
return { markInProgress };
|
|
51
51
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* interpret); any other nonzero exit throws.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import {
|
|
15
|
+
import { debug } from "../../util.js";
|
|
16
16
|
/**
|
|
17
17
|
* Hard cap on captured stdout/stderr per stream. Misbehaving scripts that
|
|
18
18
|
* `yes | head -c <huge>` would otherwise exhaust memory. 10 MB is enough for
|
|
@@ -96,7 +96,7 @@ export async function invokeShellCommand(args) {
|
|
|
96
96
|
clearTimeout(timer);
|
|
97
97
|
const stderrText = stderr.toString("utf8");
|
|
98
98
|
if (stderrText.length > 0) {
|
|
99
|
-
|
|
99
|
+
debug(`[shell:${args.sourceName}] ${command}\n${stderrText.trimEnd()}`);
|
|
100
100
|
}
|
|
101
101
|
/* v8 ignore next @preserve -- `code` is null only when the process was killed by signal; the timeout path SIGKILLs but settles via the timer rather than 'close' */
|
|
102
102
|
const exitCode = code ?? 1;
|
|
@@ -2,6 +2,7 @@ import { type LocalRunner, type ModelDefinition, type ResolvedConfig } from "./c
|
|
|
2
2
|
interface PreparedAgentLaunch {
|
|
3
3
|
runner: LocalRunner;
|
|
4
4
|
sandboxName: string | undefined;
|
|
5
|
+
ensureReady: () => Promise<void>;
|
|
5
6
|
}
|
|
6
7
|
export declare function prepareAgentLaunch(input: {
|
|
7
8
|
config: ResolvedConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;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,CA+C/B;AAqBD,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"}
|
package/dist/lib/agentLaunch.js
CHANGED
|
@@ -3,26 +3,17 @@ import { hasPreLaunchEnv, } from "./config.js";
|
|
|
3
3
|
import { detectHostCapabilities } from "./host.js";
|
|
4
4
|
import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
|
|
5
5
|
import { sandboxNameFor } from "./sandboxName.js";
|
|
6
|
-
import {
|
|
6
|
+
import { debug, sleep } from "./util.js";
|
|
7
7
|
import { workspaces } from "./workspaces.js";
|
|
8
8
|
export async function prepareAgentLaunch(input) {
|
|
9
9
|
const host = await detectHostCapabilities(input.signal);
|
|
10
10
|
const runner = resolveLocalRunner(input.config.local.runner, host);
|
|
11
11
|
assertLocalRunnerRequirements(host, runner);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
: {
|
|
18
|
-
sleep: async (ms) => {
|
|
19
|
-
await sleep(ms, input.signal);
|
|
20
|
-
input.signal?.throwIfAborted();
|
|
21
|
-
},
|
|
22
|
-
}),
|
|
23
|
-
});
|
|
24
|
-
input.signal?.throwIfAborted();
|
|
25
|
-
}
|
|
12
|
+
const ensureReady = runner === "safehouse"
|
|
13
|
+
? async () => {
|
|
14
|
+
await ensureSafehouseClearance(input.signal);
|
|
15
|
+
}
|
|
16
|
+
: alreadyReady;
|
|
26
17
|
if (runner === "sdx" && input.definition.sandbox === undefined) {
|
|
27
18
|
throw new Error(`Local groundcrew ${input.purpose} with the sdx runner require a sandbox config on model '${input.model}'.`);
|
|
28
19
|
}
|
|
@@ -46,7 +37,24 @@ export async function prepareAgentLaunch(input) {
|
|
|
46
37
|
const sandboxName = runner === "sdx" && input.definition.sandbox !== undefined
|
|
47
38
|
? sandboxNameFor({ agent: input.definition.sandbox.agent })
|
|
48
39
|
: undefined;
|
|
49
|
-
return { runner, sandboxName };
|
|
40
|
+
return { runner, sandboxName, ensureReady };
|
|
41
|
+
}
|
|
42
|
+
async function alreadyReady() {
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
async function ensureSafehouseClearance(signal) {
|
|
46
|
+
await ensureClearance({
|
|
47
|
+
logger: debug,
|
|
48
|
+
...(signal === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: {
|
|
51
|
+
sleep: async (ms) => {
|
|
52
|
+
await sleep(ms, signal);
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
signal?.throwIfAborted();
|
|
50
58
|
}
|
|
51
59
|
export async function openAgentWorkspace(input) {
|
|
52
60
|
const spec = {
|
package/dist/lib/cmuxAdapter.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* per-workspace status pill, which `open` applies best-effort.
|
|
5
5
|
*/
|
|
6
6
|
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
7
|
-
import { errorMessage, log } from "./util.js";
|
|
7
|
+
import { debug, errorMessage, log } from "./util.js";
|
|
8
8
|
export const cmuxAdapter = {
|
|
9
9
|
async open(spec, signal) {
|
|
10
10
|
const output = await runWorkspaceCommand("cmux", [
|
|
@@ -31,7 +31,7 @@ export const cmuxAdapter = {
|
|
|
31
31
|
// so swallow that specific gap silently; surface anything else so a real
|
|
32
32
|
// regression doesn't hide behind the same swallow.
|
|
33
33
|
if (!isCmuxSetStatusUnsupported(error)) {
|
|
34
|
-
|
|
34
|
+
debug(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -46,7 +46,7 @@ export const cmuxAdapter = {
|
|
|
46
46
|
// cmux v2 `workspace.close` rejects titles, so forwarding `name`
|
|
47
47
|
// would always fail. The list failure has already been logged by
|
|
48
48
|
// `listCmuxRaw`; bail rather than guarantee a downstream error.
|
|
49
|
-
|
|
49
|
+
debug(`cmux close-workspace skipped for ${name}: list-workspaces failed, no usable id`);
|
|
50
50
|
return { kind: "unavailable" };
|
|
51
51
|
}
|
|
52
52
|
const match = raw.find((ws) => ws.title === name);
|
|
@@ -90,7 +90,7 @@ function parseCmuxList(output) {
|
|
|
90
90
|
}
|
|
91
91
|
const id = pickCmuxId(ws);
|
|
92
92
|
if (id === undefined) {
|
|
93
|
-
|
|
93
|
+
debug(`cmux list-workspaces returned workspace "${ws.title}" without a usable id or ref; skipping`);
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
items.push({ title: ws.title, id });
|
|
@@ -121,7 +121,7 @@ async function listCmuxRaw(signal) {
|
|
|
121
121
|
if (isSignalAborted(signal)) {
|
|
122
122
|
throw error;
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
debug(`cmux list-workspaces failed: ${errorMessage(error)}`);
|
|
125
125
|
return undefined;
|
|
126
126
|
}
|
|
127
127
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { cosmiconfig } from "cosmiconfig";
|
|
7
|
-
import { log, readEnvironmentVariable, setLogFile } from "./util.js";
|
|
7
|
+
import { debug, log, readEnvironmentVariable, setLogFile } from "./util.js";
|
|
8
8
|
import { xdgConfigPath, xdgStatePath } from "./xdg.js";
|
|
9
9
|
import { BUILD_SECRET_NAMES } from "./buildSecrets.js";
|
|
10
10
|
export { BUILD_SECRET_NAMES } from "./buildSecrets.js";
|
|
@@ -601,7 +601,7 @@ export async function loadConfig() {
|
|
|
601
601
|
if (isEmpty === true || !isPlainObject(userConfig)) {
|
|
602
602
|
fail(`${filepath} must export a config object (e.g. \`export default { ... } satisfies Config\`)`);
|
|
603
603
|
}
|
|
604
|
-
|
|
604
|
+
debug(`Loaded config from ${filepath}`);
|
|
605
605
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- runtime fields are validated by applyDefaults/validate
|
|
606
606
|
const resolved = applyDefaults(userConfig);
|
|
607
607
|
validate(resolved);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localRunner.d.ts","sourceRoot":"","sources":["../../src/lib/localRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,gBAAgB,GACrB,WAAW,CAQb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"localRunner.d.ts","sourceRoot":"","sources":["../../src/lib/localRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,gBAAgB,GACrB,WAAW,CAQb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CA+B/F"}
|
package/dist/lib/localRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log } from "./util.js";
|
|
1
|
+
import { log, styleWarning } from "./util.js";
|
|
2
2
|
/**
|
|
3
3
|
* Resolve `local.runner` from config + host capabilities into a concrete
|
|
4
4
|
* backend. `auto` defaults to safehouse on macOS and sdx on Linux — both
|
|
@@ -47,5 +47,5 @@ export function assertLocalRunnerRequirements(host, runner) {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
// runner === "none"
|
|
50
|
-
log("WARNING: local.runner='none' — agent process will run on the host without sandboxing. Only use this when you understand the implications.");
|
|
50
|
+
log(styleWarning("WARNING: local.runner='none' — agent process will run on the host without sandboxing. Only use this when you understand the implications."));
|
|
51
51
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { removeRunState } from "./runState.js";
|
|
2
|
-
import {
|
|
2
|
+
import { debug, errorMessage } from "./util.js";
|
|
3
3
|
export function recordCleanedUpRuns(config, entries) {
|
|
4
4
|
for (const entry of entries) {
|
|
5
5
|
try {
|
|
6
6
|
removeRunState(config, entry.ticket);
|
|
7
7
|
}
|
|
8
8
|
catch (error) {
|
|
9
|
-
|
|
9
|
+
debug(`Run state cleanup failed for ${entry.ticket}: ${errorMessage(error)}`);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
package/dist/lib/tmuxAdapter.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Linux/WSL path where cmux is unavailable.
|
|
6
6
|
*/
|
|
7
7
|
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
8
|
-
import {
|
|
8
|
+
import { debug, errorMessage, readEnvironmentVariable } from "./util.js";
|
|
9
9
|
const TMUX_SESSION = "groundcrew";
|
|
10
10
|
// `tmux new-session -d -s …` always creates one initial window. Without
|
|
11
11
|
// `-n`, that window is named after the running shell (e.g. "0" / "zsh") and
|
|
@@ -49,7 +49,7 @@ export const tmuxAdapter = {
|
|
|
49
49
|
return [];
|
|
50
50
|
}
|
|
51
51
|
if (probe.status === "failed") {
|
|
52
|
-
|
|
52
|
+
debug(`tmux list-windows failed: ${probe.reason}`);
|
|
53
53
|
// oxlint-disable-next-line unicorn/no-useless-undefined -- undefined marks the workspace backend as unavailable.
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
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;AA2GF,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,
|
|
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;AA2GF,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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `codexbar` itself is the user-facing inspection tool.
|
|
7
7
|
*/
|
|
8
8
|
import { runCommandAsync } from "./commandRunner.js";
|
|
9
|
-
import {
|
|
9
|
+
import { debug, errorMessage } from "./util.js";
|
|
10
10
|
/**
|
|
11
11
|
* Synthetic snapshot used when codexbar can't be read for a model. Both
|
|
12
12
|
* window fractions are pinned to Infinity so the dispatcher's
|
|
@@ -138,10 +138,12 @@ export async function getUsageByModel(config, signal) {
|
|
|
138
138
|
}
|
|
139
139
|
// Per-model failure: fail closed. A silent skip would let the
|
|
140
140
|
// dispatcher spawn agents on a model whose quota we can't see —
|
|
141
|
-
// the exact bug a usage gate is supposed to prevent.
|
|
142
|
-
// failure
|
|
143
|
-
//
|
|
144
|
-
|
|
141
|
+
// the exact bug a usage gate is supposed to prevent. Record the
|
|
142
|
+
// failure (debug-tier — always in the log file, console under
|
|
143
|
+
// --verbose) so operators can fix the underlying CLI, and return a
|
|
144
|
+
// fully-exhausted snapshot so the dispatcher gates the model. The
|
|
145
|
+
// gate itself surfaces a visible skip line via formatUsageExhaustion.
|
|
146
|
+
debug(`Usage check failed for ${model} (treating as exhausted): ${errorMessage(error)}`);
|
|
145
147
|
out[model] = EXHAUSTED_USAGE;
|
|
146
148
|
}
|
|
147
149
|
}
|
package/dist/lib/util.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
2
2
|
export declare function writeOutput(message?: string): void;
|
|
3
3
|
export declare function writeError(message: string): void;
|
|
4
|
+
export declare function setVerbose(value: boolean): void;
|
|
5
|
+
export declare function isVerbose(): boolean;
|
|
6
|
+
export declare function okMark(): string;
|
|
7
|
+
export declare function failMark(): string;
|
|
8
|
+
export declare function styleWarning(text: string): string;
|
|
9
|
+
export declare function styleDim(text: string): string;
|
|
4
10
|
export declare function setLogFile(path: string | undefined): void;
|
|
5
11
|
export declare function withLogOutputSuppressed<T>(operation: () => Promise<T>): Promise<T>;
|
|
12
|
+
/** Important tier: always on the console (dimmed timestamp) and the log file. */
|
|
6
13
|
export declare function log(message: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Diagnostic tier: always tee'd to the log file, but echoed to the console only
|
|
16
|
+
* under --verbose. Use for mechanics (git porcelain brackets, adapter probe
|
|
17
|
+
* failures, "loaded config") that an operator rarely needs while watching.
|
|
18
|
+
*/
|
|
19
|
+
export declare function debug(message: string): void;
|
|
7
20
|
type LogEventFieldValue = boolean | number | string | readonly string[] | undefined;
|
|
8
21
|
export declare function logEvent(event: string, fields: Record<string, LogEventFieldValue>): void;
|
|
9
22
|
export declare function readEnvironmentVariable(name: string): string | undefined;
|
package/dist/lib/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAIA,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAQD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAWD,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAED,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOxF;AAuBD,iFAAiF;AACjF,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAOzC;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAS3C;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAiBxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAcvF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
|
package/dist/lib/util.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
import { styleText } from "node:util";
|
|
3
4
|
export async function sleep(ms, signal) {
|
|
4
5
|
if (signal?.aborted === true) {
|
|
5
6
|
return;
|
|
@@ -30,6 +31,32 @@ export function writeError(message) {
|
|
|
30
31
|
// oxlint-disable-next-line no-console -- Centralized CLI stderr writer.
|
|
31
32
|
console.error(message);
|
|
32
33
|
}
|
|
34
|
+
// Gates whether the diagnostic tier — debug() and logEvent() — is echoed to the
|
|
35
|
+
// console. Both tiers always tee to the log file regardless of this flag, so the
|
|
36
|
+
// full stream is never lost. The CLI arms this from `--verbose` /
|
|
37
|
+
// GROUNDCREW_VERBOSE before dispatching a command.
|
|
38
|
+
let verboseConsole = false;
|
|
39
|
+
export function setVerbose(value) {
|
|
40
|
+
verboseConsole = value;
|
|
41
|
+
}
|
|
42
|
+
export function isVerbose() {
|
|
43
|
+
return verboseConsole;
|
|
44
|
+
}
|
|
45
|
+
function paint(format, text) {
|
|
46
|
+
return styleText(format, text, { stream: process.stdout });
|
|
47
|
+
}
|
|
48
|
+
export function okMark() {
|
|
49
|
+
return paint("green", "✓");
|
|
50
|
+
}
|
|
51
|
+
export function failMark() {
|
|
52
|
+
return paint("red", "✗");
|
|
53
|
+
}
|
|
54
|
+
export function styleWarning(text) {
|
|
55
|
+
return paint("yellow", text);
|
|
56
|
+
}
|
|
57
|
+
export function styleDim(text) {
|
|
58
|
+
return paint("dim", text);
|
|
59
|
+
}
|
|
33
60
|
// Module-scoped sink for tee-ing log()/logEvent() to disk. Unset by default
|
|
34
61
|
// so tests don't write to the host filesystem; the CLI arms it after
|
|
35
62
|
// loadConfig() resolves `logging.file`.
|
|
@@ -63,14 +90,33 @@ function appendLogLine(line) {
|
|
|
63
90
|
writeError(`groundcrew: disabling file logging — could not write to ${broken}`);
|
|
64
91
|
}
|
|
65
92
|
}
|
|
93
|
+
function timestamped(message) {
|
|
94
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
95
|
+
return { plain: `[${timestamp}] ${message}`, timestamp };
|
|
96
|
+
}
|
|
97
|
+
/** Important tier: always on the console (dimmed timestamp) and the log file. */
|
|
66
98
|
export function log(message) {
|
|
67
99
|
if (suppressedLogDepth > 0) {
|
|
68
100
|
return;
|
|
69
101
|
}
|
|
70
|
-
const timestamp =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
const { plain, timestamp } = timestamped(message);
|
|
103
|
+
writeOutput(`${styleDim(`[${timestamp}]`)} ${message}`);
|
|
104
|
+
appendLogLine(plain);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Diagnostic tier: always tee'd to the log file, but echoed to the console only
|
|
108
|
+
* under --verbose. Use for mechanics (git porcelain brackets, adapter probe
|
|
109
|
+
* failures, "loaded config") that an operator rarely needs while watching.
|
|
110
|
+
*/
|
|
111
|
+
export function debug(message) {
|
|
112
|
+
if (suppressedLogDepth > 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { plain } = timestamped(message);
|
|
116
|
+
if (verboseConsole) {
|
|
117
|
+
writeOutput(styleDim(plain));
|
|
118
|
+
}
|
|
119
|
+
appendLogLine(plain);
|
|
74
120
|
}
|
|
75
121
|
function formatLogEventFieldValue(value) {
|
|
76
122
|
const raw = Array.isArray(value) ? value.join(",") : String(value);
|
|
@@ -91,7 +137,10 @@ export function logEvent(event, fields) {
|
|
|
91
137
|
parts.push(`${key}=${formatLogEventFieldValue(value)}`);
|
|
92
138
|
}
|
|
93
139
|
const line = parts.join(" ");
|
|
94
|
-
|
|
140
|
+
// Structured telemetry is diagnostic: file always, console only under --verbose.
|
|
141
|
+
if (verboseConsole) {
|
|
142
|
+
writeOutput(styleDim(line));
|
|
143
|
+
}
|
|
95
144
|
appendLogLine(line);
|
|
96
145
|
}
|
|
97
146
|
export function readEnvironmentVariable(name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AAmQD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAyBD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -12,9 +12,8 @@ import { userInfo } from "node:os";
|
|
|
12
12
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
13
13
|
import { runCommandAsync } from "./commandRunner.js";
|
|
14
14
|
import { resolveDefaultBranch } from "./defaultBranch.js";
|
|
15
|
-
import { errorMessage,
|
|
15
|
+
import { debug, errorMessage, isVerbose } from "./util.js";
|
|
16
16
|
import { workspaces } from "./workspaces.js";
|
|
17
|
-
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
18
17
|
const WORKTREE_LIST_PREFIX = "worktree ";
|
|
19
18
|
export class WorktreeAlreadyExistsError extends Error {
|
|
20
19
|
dir;
|
|
@@ -72,23 +71,33 @@ function basePaths(config, repository, ticket) {
|
|
|
72
71
|
function signalProperty(signal) {
|
|
73
72
|
return signal === undefined ? {} : { signal };
|
|
74
73
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Runs a long-running git command (fetch, worktree add/remove/prune) with no
|
|
76
|
+
* timeout. Under --verbose the git porcelain streams live to the terminal;
|
|
77
|
+
* otherwise it is captured and discarded on success — the bracketing debug()
|
|
78
|
+
* lines record what ran, and a failure still carries git's stderr via the
|
|
79
|
+
* thrown error (see normalizeCommandError in commandRunner.ts).
|
|
80
|
+
*/
|
|
81
|
+
async function runLongGitCommand(arguments_, signal) {
|
|
82
|
+
const signalOption = signal === undefined ? {} : { signal };
|
|
83
|
+
if (isVerbose()) {
|
|
84
|
+
await runCommandAsync("git", arguments_, { stdio: "inherit", timeoutMs: 0, ...signalOption });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await runCommandAsync("git", arguments_, { stdio: "captured", timeoutMs: 0, ...signalOption });
|
|
79
88
|
}
|
|
80
89
|
async function deleteBranchBestEffort(arguments_) {
|
|
81
90
|
try {
|
|
82
91
|
await (arguments_.signal === undefined
|
|
83
92
|
? runCommandAsync(arguments_.cmd, arguments_.cmdArgs)
|
|
84
93
|
: runCommandAsync(arguments_.cmd, arguments_.cmdArgs, { signal: arguments_.signal }));
|
|
85
|
-
|
|
94
|
+
debug(`Deleted branch ${arguments_.branchName}`);
|
|
86
95
|
}
|
|
87
96
|
catch (error) {
|
|
88
97
|
if (arguments_.signal?.aborted === true) {
|
|
89
98
|
throw error;
|
|
90
99
|
}
|
|
91
|
-
|
|
100
|
+
debug(`Branch ${arguments_.branchName} cleanup skipped: ${errorMessage(error)}`);
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
async function createWorktree(config, spec, signal) {
|
|
@@ -100,10 +109,10 @@ async function createWorktree(config, spec, signal) {
|
|
|
100
109
|
...signalProperty(signal),
|
|
101
110
|
});
|
|
102
111
|
const baseRef = `${config.git.remote}/${defaultBranch}`;
|
|
103
|
-
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
await
|
|
112
|
+
debug(`Fetching ${baseRef} in ${spec.repository}...`);
|
|
113
|
+
await runLongGitCommand(["-C", base.repoDir, "fetch", config.git.remote, defaultBranch], signal);
|
|
114
|
+
debug(`Creating worktree ${spec.repository}-${spec.ticket} (branch ${base.branchName} from ${baseRef})...`);
|
|
115
|
+
await runLongGitCommand(["-C", base.repoDir, "worktree", "add", "-b", base.branchName, base.hostWorktreeDir, baseRef], signal);
|
|
107
116
|
return {
|
|
108
117
|
repository: spec.repository,
|
|
109
118
|
ticket: spec.ticket,
|
|
@@ -172,19 +181,20 @@ async function removeWorktree(config, entry, options) {
|
|
|
172
181
|
const projectDir = resolve(config.workspace.projectDir);
|
|
173
182
|
const repoDir = resolve(projectDir, entry.repository);
|
|
174
183
|
if (existsSync(entry.dir)) {
|
|
175
|
-
|
|
184
|
+
debug(`Removing worktree ${entry.dir}${options.force ? " (--force)" : ""}...`);
|
|
176
185
|
const removeArguments = ["-C", repoDir, "worktree", "remove"];
|
|
177
186
|
if (options.force) {
|
|
178
187
|
removeArguments.push("--force");
|
|
179
188
|
}
|
|
180
189
|
removeArguments.push(entry.dir);
|
|
181
190
|
try {
|
|
182
|
-
await
|
|
191
|
+
await runLongGitCommand(removeArguments, options.signal);
|
|
183
192
|
}
|
|
184
193
|
catch (error) {
|
|
185
|
-
// git's `fatal: ...`
|
|
186
|
-
// captured error
|
|
187
|
-
// ourselves so the failure message names the
|
|
194
|
+
// Under --verbose git's `fatal: ...` streams to the terminal rather than
|
|
195
|
+
// the captured error, so the failure may surface as just "Exit status:
|
|
196
|
+
// 128". Probe the worktree ourselves so the failure message names the
|
|
197
|
+
// condition either way — dirty
|
|
188
198
|
// (modified/untracked files, fixable with `crew cleanup --force`) or
|
|
189
199
|
// orphan (directory exists on disk but is not registered with the
|
|
190
200
|
// parent repo, fixable with `crew cleanup --force` when the path still
|
|
@@ -230,8 +240,8 @@ async function removeWorktree(config, entry, options) {
|
|
|
230
240
|
}
|
|
231
241
|
}
|
|
232
242
|
else {
|
|
233
|
-
|
|
234
|
-
await
|
|
243
|
+
debug(`Worktree directory ${entry.dir} not found, pruning stale refs...`);
|
|
244
|
+
await runLongGitCommand(["-C", repoDir, "worktree", "prune"], options.signal);
|
|
235
245
|
}
|
|
236
246
|
await deleteBranchBestEffort({
|
|
237
247
|
cmd: "git",
|
|
@@ -318,7 +328,7 @@ function removeOrphanWorktreeDirectory(config, entry) {
|
|
|
318
328
|
if (targetDir !== expectedDir || !isInsideDirectory(projectDir, targetDir)) {
|
|
319
329
|
throw new Error(`Refusing to force-delete ${entry.dir}: expected groundcrew worktree path ${expectedDir}.`);
|
|
320
330
|
}
|
|
321
|
-
|
|
331
|
+
debug(`Removing orphaned worktree directory ${entry.dir} (--force)...`);
|
|
322
332
|
rmSync(targetDir, { recursive: true, force: true });
|
|
323
333
|
}
|
|
324
334
|
function list(config) {
|
package/docs/commands.md
CHANGED
|
@@ -12,24 +12,24 @@ Status is informational only. Use `crew cleanup <TICKET>` to tear down stale wor
|
|
|
12
12
|
<summary>Sample ticket status output</summary>
|
|
13
13
|
|
|
14
14
|
```text
|
|
15
|
-
crew status
|
|
15
|
+
crew status ENG-123
|
|
16
16
|
===================
|
|
17
|
-
ticket:
|
|
17
|
+
ticket: eng-123 in-progress https://linear.app/example/issue/ENG-123
|
|
18
18
|
title: Multi-event extractor: year inference can produce date_start > date_end
|
|
19
19
|
run: running; model=claude; updated=2026-05-26T00:01:00.000Z; resumes=0
|
|
20
20
|
workspace: live
|
|
21
21
|
|
|
22
22
|
Worktrees
|
|
23
23
|
---------
|
|
24
|
-
-
|
|
25
|
-
branch:
|
|
26
|
-
dir: /dev/workspaces/
|
|
24
|
+
- acme/widgets host
|
|
25
|
+
branch: dev-eng-123
|
|
26
|
+
dir: /dev/workspaces/acme/widgets-eng-123
|
|
27
27
|
git: dirty (0 modified, 1 untracked)
|
|
28
|
-
pr: https://github.com/
|
|
28
|
+
pr: https://github.com/acme/widgets/pull/224 (open)
|
|
29
29
|
|
|
30
30
|
Recent logs
|
|
31
31
|
-----------
|
|
32
|
-
[10:15:30] Workspace "
|
|
32
|
+
[10:15:30] Workspace "eng-123" launched
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
</details>
|
|
@@ -45,8 +45,8 @@ Doctor's command introspection is intentionally shallow. It reports the resolved
|
|
|
45
45
|
`crew start <TICKET>` launches one ticket immediately, bypassing orchestrator eligibility. Use it to dispatch a specific ticket on demand, including unlabeled tickets that `crew run` ignores.
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
crew start
|
|
49
|
-
crew start
|
|
48
|
+
crew start ENG-123
|
|
49
|
+
crew start ENG-123 --dry-run
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Stop
|
|
@@ -54,9 +54,9 @@ crew start HRD-442 --dry-run
|
|
|
54
54
|
`crew stop <TICKET>` stops a live workspace pane while preserving the ticket worktree and branch. Use it when you need terminal capacity back, want to stop an agent going in the wrong direction, or need to inspect the diff before letting another agent continue.
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
crew stop
|
|
58
|
-
crew status
|
|
59
|
-
crew resume
|
|
57
|
+
crew stop ENG-123 --reason "wrong implementation direction"
|
|
58
|
+
crew status ENG-123
|
|
59
|
+
crew resume ENG-123
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
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.
|
package/package.json
CHANGED
package/static/demo.gif
ADDED
|
Binary file
|