@clipboard-health/groundcrew 4.41.0 → 4.43.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.
Files changed (55) hide show
  1. package/README.md +2 -0
  2. package/crew.config.example.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +6 -0
  5. package/dist/commands/openWorkspace.d.ts +24 -0
  6. package/dist/commands/openWorkspace.d.ts.map +1 -0
  7. package/dist/commands/openWorkspace.js +339 -0
  8. package/dist/commands/orchestrator.d.ts.map +1 -1
  9. package/dist/commands/orchestrator.js +1 -0
  10. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  11. package/dist/commands/resumeWorkspace.js +8 -14
  12. package/dist/commands/reviewer.d.ts +2 -0
  13. package/dist/commands/reviewer.d.ts.map +1 -1
  14. package/dist/commands/reviewer.js +9 -4
  15. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  16. package/dist/commands/setupWorkspace.js +2 -1
  17. package/dist/commands/status.d.ts.map +1 -1
  18. package/dist/commands/status.js +17 -6
  19. package/dist/commands/task.d.ts.map +1 -1
  20. package/dist/commands/task.js +10 -6
  21. package/dist/lib/adapters/shell/schema.d.ts +1 -0
  22. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  23. package/dist/lib/adapters/shell/schema.js +12 -0
  24. package/dist/lib/agentLaunch.d.ts +5 -1
  25. package/dist/lib/agentLaunch.d.ts.map +1 -1
  26. package/dist/lib/agentLaunch.js +10 -6
  27. package/dist/lib/config.d.ts +27 -10
  28. package/dist/lib/config.d.ts.map +1 -1
  29. package/dist/lib/config.js +26 -0
  30. package/dist/lib/launchCommand.d.ts +15 -1
  31. package/dist/lib/launchCommand.d.ts.map +1 -1
  32. package/dist/lib/launchCommand.js +23 -5
  33. package/dist/lib/pullRequests.d.ts +26 -0
  34. package/dist/lib/pullRequests.d.ts.map +1 -1
  35. package/dist/lib/pullRequests.js +51 -0
  36. package/dist/lib/runState.d.ts +6 -0
  37. package/dist/lib/runState.d.ts.map +1 -1
  38. package/dist/lib/runState.js +8 -3
  39. package/dist/lib/taskSourceFilesystem.d.ts +7 -0
  40. package/dist/lib/taskSourceFilesystem.d.ts.map +1 -1
  41. package/dist/lib/taskSourceFilesystem.js +29 -6
  42. package/dist/lib/workspaceLiveness.d.ts +8 -0
  43. package/dist/lib/workspaceLiveness.d.ts.map +1 -0
  44. package/dist/lib/workspaceLiveness.js +17 -0
  45. package/dist/lib/worktreeRunState.d.ts +20 -0
  46. package/dist/lib/worktreeRunState.d.ts.map +1 -0
  47. package/dist/lib/worktreeRunState.js +35 -0
  48. package/dist/lib/worktrees.d.ts +15 -1
  49. package/dist/lib/worktrees.d.ts.map +1 -1
  50. package/dist/lib/worktrees.js +97 -25
  51. package/docs/commands.md +18 -0
  52. package/docs/configuration.md +29 -27
  53. package/docs/runners.md +20 -0
  54. package/docs/task-sources.md +8 -0
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -106,6 +106,8 @@ crew run [--watch] # one-shot or --watch f
106
106
  crew start <TASK> # provision + launch one task now
107
107
  crew stop <TASK> [--reason <text>] # stop workspace, keep worktree
108
108
  crew resume <TASK> # reopen a paused task
109
+ crew open <pr> | --branch <name> [--repo <owner/repo>] # iterate on an existing PR or branch
110
+ [--prompt <text> | --prompt-file <path>] [--task <id>] [--dry-run]
109
111
  crew cleanup [--force] <TASK> # tear down every worktree for a task
110
112
  crew upgrade [<version>] # reinstall crew globally through npm
111
113
  ```
@@ -121,6 +121,9 @@ export default {
121
121
  // {
122
122
  // kind: "shell",
123
123
  // name: "jira",
124
+ // // Open local task-store directories for read/write inside the
125
+ // // safehouse/srt sandbox when this source owns the launched task.
126
+ // sandboxWritePaths: ["~/plans"],
124
127
  // commands: {
125
128
  // verify: "jira me",
126
129
  // fetch: "~/.config/groundcrew/jira-fetch.sh",
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAqRA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA4RA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCvD"}
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { cleanupWorkspaceCli } from "./commands/cleanupWorkspace.js";
3
3
  import { doctor } from "./commands/doctor.js";
4
4
  import { initConfigCli } from "./commands/init.js";
5
5
  import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
6
+ import { openWorkspaceCli } from "./commands/openWorkspace.js";
6
7
  import { orchestrate } from "./commands/orchestrator.js";
7
8
  import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
9
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
@@ -169,6 +170,11 @@ const SUBCOMMANDS = {
169
170
  usage: "<task>",
170
171
  invoke: resumeWorkspaceCli,
171
172
  },
173
+ open: {
174
+ summary: "Open an existing PR or branch in a new worktree and launch a session",
175
+ usage: "<pr> | --branch <name> [--repo <owner/repo>] [--agent <agent>] [--prompt <text> | --prompt-file <path>] [--task <id>] [--dry-run]",
176
+ invoke: openWorkspaceCli,
177
+ },
172
178
  setup: {
173
179
  summary: "Removed repository bootstrap command",
174
180
  usage: "repos",
@@ -0,0 +1,24 @@
1
+ import { type ResolvedConfig } from "../lib/config.ts";
2
+ interface PullRequestInput {
3
+ kind: "pr";
4
+ pr: string;
5
+ repositoryHint?: string;
6
+ }
7
+ interface BranchInput {
8
+ kind: "branch";
9
+ branch: string;
10
+ }
11
+ export interface OpenWorkspaceOptions {
12
+ input: PullRequestInput | BranchInput;
13
+ repository?: string;
14
+ agent?: string;
15
+ /** Resolved prompt text; when undefined the agent opens interactively. */
16
+ promptText?: string;
17
+ taskOverride?: string;
18
+ dryRun?: boolean;
19
+ }
20
+ export declare function openWorkspace(config: ResolvedConfig, options: OpenWorkspaceOptions): Promise<void>;
21
+ export declare function parseOpenWorkspaceArgs(argv: string[]): OpenWorkspaceOptions;
22
+ export declare function openWorkspaceCli(argv: string[]): Promise<void>;
23
+ export {};
24
+ //# sourceMappingURL=openWorkspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/openWorkspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AActF,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA6JD,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAwGf;AA4ID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAE3E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
@@ -0,0 +1,339 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
+ import { loadConfig, repositoryBaseDir } from "../lib/config.js";
5
+ import { resolvePullRequest } from "../lib/pullRequests.js";
6
+ import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
7
+ import { recordRunState, readRunState } from "../lib/runState.js";
8
+ import { stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
9
+ import { normalizePlainTaskId } from "../lib/taskId.js";
10
+ import { debug, errorMessage, log, okMark } from "../lib/util.js";
11
+ import { failIfWorkspaceAlreadyLive } from "../lib/workspaceLiveness.js";
12
+ import { resolveLaunchDir, worktrees } from "../lib/worktrees.js";
13
+ const OPEN_USAGE = "Usage: crew open <pr> | --branch <name> [--repo <owner/repo>] [--agent <agent>] [--prompt <text> | --prompt-file <path>] [--task <id>] [--dry-run]";
14
+ const PULL_REQUEST_URL_PATTERN = /^https?:\/\/github\.com\/(?<repository>[^/]+\/[^/]+)\/pull\/(?<pr>\d+)/;
15
+ function parsePullRequestReference(reference) {
16
+ const groups = PULL_REQUEST_URL_PATTERN.exec(reference)?.groups;
17
+ if (groups?.["repository"] !== undefined && groups["pr"] !== undefined) {
18
+ return { repository: groups["repository"], pr: groups["pr"] };
19
+ }
20
+ return { pr: reference };
21
+ }
22
+ function slugifyBranch(branch) {
23
+ return branch
24
+ .toLowerCase()
25
+ .replaceAll(/[^a-z0-9]+/g, "-")
26
+ .replaceAll(/^-+|-+$/g, "");
27
+ }
28
+ function repositoryCloneDir(config, repository) {
29
+ return path.resolve(repositoryBaseDir(config, repository), repository);
30
+ }
31
+ function assertKnownRepository(config, repository) {
32
+ if (!config.workspace.knownRepositories.includes(repository)) {
33
+ throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
34
+ }
35
+ }
36
+ function repositoryBasename(repository) {
37
+ const lastSlash = repository.lastIndexOf("/");
38
+ return lastSlash === -1 ? repository : repository.slice(lastSlash + 1);
39
+ }
40
+ function resolveRepositoryHint(config, repositoryHint) {
41
+ const normalizedHint = repositoryHint.toLowerCase();
42
+ const exactMatches = config.workspace.knownRepositories.filter((repository) => repository.toLowerCase() === normalizedHint);
43
+ const [exactMatch] = exactMatches;
44
+ if (exactMatches.length === 1 && exactMatch !== undefined) {
45
+ return exactMatch;
46
+ }
47
+ const basename = repositoryBasename(repositoryHint).toLowerCase();
48
+ const basenameMatches = config.workspace.knownRepositories.filter((repository) => repositoryBasename(repository).toLowerCase() === basename);
49
+ const [basenameMatch] = basenameMatches;
50
+ if (basenameMatches.length === 1 && basenameMatch !== undefined) {
51
+ return basenameMatch;
52
+ }
53
+ if (basenameMatches.length > 1) {
54
+ throw new Error(`Repository hint "${repositoryHint}" matches multiple configured repositories: ${basenameMatches.join(", ")}. Pass --repo <name> to choose one.`);
55
+ }
56
+ throw new Error(`Repository hint "${repositoryHint}" does not match workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}. Pass --repo <name> to choose one.`);
57
+ }
58
+ function resolveOpenRepository(config, options) {
59
+ if (options.repository !== undefined) {
60
+ assertKnownRepository(config, options.repository);
61
+ return options.repository;
62
+ }
63
+ if (options.input.kind === "pr" && options.input.repositoryHint !== undefined) {
64
+ return resolveRepositoryHint(config, options.input.repositoryHint);
65
+ }
66
+ throw new Error(`crew open: --repo <owner/repo> is required\n${OPEN_USAGE}`);
67
+ }
68
+ function failIfAlreadyTracked(config, task, repository) {
69
+ const hasWorktree = worktrees
70
+ .findByTask(config, task)
71
+ .some((entry) => entry.repository === repository);
72
+ if (hasWorktree || readRunState(config, task) !== undefined) {
73
+ throw new Error(`Task ${task} already has a worktree or run state. Use 'crew resume ${task}' to continue it, or 'crew status ${task}' to inspect it.`);
74
+ }
75
+ }
76
+ async function resolveTarget(config, options, repository) {
77
+ if (options.input.kind === "branch") {
78
+ const { branch } = options.input;
79
+ return {
80
+ branch,
81
+ title: branch,
82
+ task: normalizePlainTaskId(options.taskOverride ?? slugifyBranch(branch)),
83
+ };
84
+ }
85
+ const pullRequest = await resolvePullRequest({
86
+ repoDir: repositoryCloneDir(config, repository),
87
+ pr: options.input.pr,
88
+ });
89
+ if (pullRequest.isCrossRepository) {
90
+ throw new Error(`PR #${pullRequest.number} is from a fork (cross-repository); crew open cannot fetch fork branches. Check the branch out locally, then run crew open --branch <name> --repo ${repository}.`);
91
+ }
92
+ return {
93
+ branch: pullRequest.branch,
94
+ title: pullRequest.title,
95
+ task: normalizePlainTaskId(options.taskOverride ?? `pr-${pullRequest.number}`),
96
+ url: pullRequest.url,
97
+ };
98
+ }
99
+ async function rollback(arguments_) {
100
+ log(`Open failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.task}...`);
101
+ try {
102
+ await worktrees.teardown(arguments_.config, [arguments_.entry], { force: true });
103
+ }
104
+ catch (error) {
105
+ log(`Worktree teardown failed during rollback: ${errorMessage(error)}`);
106
+ }
107
+ for (const dir of [arguments_.promptDir, arguments_.srtSettingsDir]) {
108
+ if (dir !== undefined) {
109
+ try {
110
+ rmSync(dir, { recursive: true, force: true });
111
+ }
112
+ catch {
113
+ // already gone
114
+ }
115
+ }
116
+ }
117
+ }
118
+ export async function openWorkspace(config, options) {
119
+ const repository = resolveOpenRepository(config, options);
120
+ const repoDir = repositoryCloneDir(config, repository);
121
+ if (!existsSync(repoDir)) {
122
+ throw new Error(`Repository not found: ${repoDir}`);
123
+ }
124
+ const target = await resolveTarget(config, options, repository);
125
+ await failIfWorkspaceAlreadyLive(config, target.task, "opening");
126
+ failIfAlreadyTracked(config, target.task, repository);
127
+ const agent = options.agent ?? config.agents.default;
128
+ const definition = config.agents.definitions[agent];
129
+ if (definition === undefined) {
130
+ throw new Error(`Unknown agent: ${agent}`);
131
+ }
132
+ if (options.dryRun === true) {
133
+ log(`[dry-run] Would open ${target.task} on branch ${target.branch} in ${repository} (${agent})`);
134
+ return;
135
+ }
136
+ const { runner, networkEgress, sandboxName, workspaceKind, ensureReady } = await prepareAgentLaunch({
137
+ config,
138
+ agent,
139
+ definition,
140
+ purpose: "runs",
141
+ });
142
+ await ensureReady();
143
+ const created = await worktrees.open(config, {
144
+ repository,
145
+ task: target.task,
146
+ branch: target.branch,
147
+ });
148
+ const launchDir = resolveLaunchDir(config, repository, created.dir);
149
+ const omitPromptArgument = options.promptText === undefined;
150
+ const stagedPrompt = stagePromptText({
151
+ prefix: "groundcrew-open",
152
+ task: target.task,
153
+ text: options.promptText ?? "",
154
+ });
155
+ let srtSettingsDir;
156
+ try {
157
+ const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
158
+ worktreeDir: launchDir,
159
+ defaultHooks: config.defaults.hooks,
160
+ });
161
+ const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(stagedPrompt.directory);
162
+ let launchCommand;
163
+ ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
164
+ runner,
165
+ networkEgress,
166
+ task: target.task,
167
+ definition,
168
+ promptFile: stagedPrompt.file,
169
+ worktreeDir: created.dir,
170
+ workingDir: launchDir,
171
+ secretsFile,
172
+ prepareWorktreeCommand,
173
+ sandboxName,
174
+ workspaceKind,
175
+ omitPromptArgument,
176
+ }));
177
+ const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
178
+ await openAgentWorkspace({
179
+ config,
180
+ name: target.task,
181
+ cwd: launchDir,
182
+ command: launchCmd,
183
+ agent,
184
+ color: definition.color,
185
+ });
186
+ }
187
+ catch (error) {
188
+ await rollback({ config, entry: created, promptDir: stagedPrompt.directory, srtSettingsDir });
189
+ throw error;
190
+ }
191
+ recordRunState({
192
+ config,
193
+ state: {
194
+ task: target.task,
195
+ repository,
196
+ agent,
197
+ worktreeDir: created.dir,
198
+ branchName: target.branch,
199
+ workspaceName: target.task,
200
+ state: "running",
201
+ title: target.title,
202
+ adoptedBranch: true,
203
+ ...(target.url === undefined ? {} : { url: target.url }),
204
+ },
205
+ });
206
+ log(`${okMark()} "${target.task}" opened on branch ${target.branch} (${agent})`);
207
+ debug(` Worktree: ${launchDir}`);
208
+ if (omitPromptArgument) {
209
+ debug(" Launched interactively (no prompt).");
210
+ }
211
+ }
212
+ function readValue(argv, index, flag) {
213
+ const value = argv[index + 1];
214
+ if (value === undefined || value.startsWith("-")) {
215
+ throw new Error(`crew open: ${flag} requires a value\n${OPEN_USAGE}`);
216
+ }
217
+ return value;
218
+ }
219
+ function parseArguments(argv) {
220
+ const parsed = { dryRun: false };
221
+ for (let index = 0; index < argv.length; index += 1) {
222
+ const argument = argv[index];
223
+ /* v8 ignore next @preserve -- loop bound guarantees argv[index] is defined; the guard narrows the type */
224
+ if (argument === undefined) {
225
+ continue;
226
+ }
227
+ switch (argument) {
228
+ case "--repo": {
229
+ parsed.repository = readValue(argv, index, "--repo");
230
+ index += 1;
231
+ break;
232
+ }
233
+ case "--agent": {
234
+ parsed.agent = readValue(argv, index, "--agent");
235
+ index += 1;
236
+ break;
237
+ }
238
+ case "--prompt": {
239
+ parsed.promptText = readValue(argv, index, "--prompt");
240
+ index += 1;
241
+ break;
242
+ }
243
+ case "--prompt-file": {
244
+ parsed.promptFile = readValue(argv, index, "--prompt-file");
245
+ index += 1;
246
+ break;
247
+ }
248
+ case "--branch": {
249
+ parsed.branch = readValue(argv, index, "--branch");
250
+ index += 1;
251
+ break;
252
+ }
253
+ case "--task": {
254
+ parsed.task = readValue(argv, index, "--task");
255
+ index += 1;
256
+ break;
257
+ }
258
+ case "--dry-run": {
259
+ parsed.dryRun = true;
260
+ break;
261
+ }
262
+ default: {
263
+ if (argument.startsWith("-")) {
264
+ throw new Error(`crew open: unknown option: ${argument}\n${OPEN_USAGE}`);
265
+ }
266
+ if (parsed.positional !== undefined) {
267
+ throw new Error(`crew open: unexpected extra argument: ${argument}\n${OPEN_USAGE}`);
268
+ }
269
+ parsed.positional = argument;
270
+ }
271
+ }
272
+ }
273
+ return parsed;
274
+ }
275
+ function resolvePromptText(parsed) {
276
+ if (parsed.promptText !== undefined && parsed.promptFile !== undefined) {
277
+ throw new Error(`crew open: --prompt and --prompt-file are mutually exclusive\n${OPEN_USAGE}`);
278
+ }
279
+ if (parsed.promptText !== undefined) {
280
+ return parsed.promptText;
281
+ }
282
+ if (parsed.promptFile !== undefined) {
283
+ try {
284
+ return readFileSync(parsed.promptFile, "utf8");
285
+ }
286
+ catch (error) {
287
+ throw new Error(`crew open: could not read --prompt-file ${parsed.promptFile}`, {
288
+ cause: error,
289
+ });
290
+ }
291
+ }
292
+ return undefined;
293
+ }
294
+ function toOpenWorkspaceOptions(parsed) {
295
+ if (parsed.branch !== undefined && parsed.positional !== undefined) {
296
+ throw new Error(`crew open: pass either a PR or --branch, not both\n${OPEN_USAGE}`);
297
+ }
298
+ const promptText = resolvePromptText(parsed);
299
+ const common = {
300
+ ...(parsed.agent === undefined ? {} : { agent: parsed.agent }),
301
+ ...(promptText === undefined ? {} : { promptText }),
302
+ ...(parsed.task === undefined ? {} : { taskOverride: parsed.task }),
303
+ dryRun: parsed.dryRun,
304
+ };
305
+ if (parsed.branch !== undefined) {
306
+ if (parsed.repository === undefined) {
307
+ throw new Error(`crew open: --branch requires --repo <owner/repo>\n${OPEN_USAGE}`);
308
+ }
309
+ return {
310
+ input: { kind: "branch", branch: parsed.branch },
311
+ repository: parsed.repository,
312
+ ...common,
313
+ };
314
+ }
315
+ if (parsed.positional === undefined) {
316
+ throw new Error(`crew open: a PR (number or URL) or --branch is required\n${OPEN_USAGE}`);
317
+ }
318
+ const reference = parsePullRequestReference(parsed.positional);
319
+ if (parsed.repository === undefined && reference.repository === undefined) {
320
+ throw new Error(`crew open: --repo <owner/repo> is required when the PR is given by number\n${OPEN_USAGE}`);
321
+ }
322
+ return {
323
+ input: {
324
+ kind: "pr",
325
+ pr: reference.pr,
326
+ ...(reference.repository === undefined ? {} : { repositoryHint: reference.repository }),
327
+ },
328
+ ...(parsed.repository === undefined ? {} : { repository: parsed.repository }),
329
+ ...common,
330
+ };
331
+ }
332
+ export function parseOpenWorkspaceArgs(argv) {
333
+ return toOpenWorkspaceOptions(parseArguments(argv));
334
+ }
335
+ export async function openWorkspaceCli(argv) {
336
+ const options = parseOpenWorkspaceArgs(argv);
337
+ const config = await loadConfig();
338
+ await openWorkspace(config, options);
339
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoEH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgE7E"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoEH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiE7E"}
@@ -94,6 +94,7 @@ export async function orchestrate(options) {
94
94
  const cleaner = createCleaner({ config });
95
95
  const reviewer = createReviewer({
96
96
  board,
97
+ config,
97
98
  findPullRequests: findPullRequestsForBranch,
98
99
  });
99
100
  const dispatcher = createDispatcher({ config, board });
@@ -1 +1 @@
1
- {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAqJD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA0Ff;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
1
+ {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AA2ID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA4Ff;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
@@ -10,7 +10,7 @@ import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceL
10
10
  import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
11
11
  import { naturalIdFromCanonical, toCanonicalId } from "../lib/taskSource.js";
12
12
  import { errorMessage, log } from "../lib/util.js";
13
- import { workspaces } from "../lib/workspaces.js";
13
+ import { failIfWorkspaceAlreadyLive } from "../lib/workspaceLiveness.js";
14
14
  import { resolveLaunchDir, worktrees } from "../lib/worktrees.js";
15
15
  function parseArguments(argv) {
16
16
  const [task, ...extras] = argv;
@@ -60,7 +60,10 @@ async function contextFromState(config, task, state, worktree) {
60
60
  task,
61
61
  repository: state.repository,
62
62
  agent: state.agent,
63
- worktree,
63
+ // Prefer the branch recorded in run state: `crew open` worktrees check out
64
+ // an existing PR branch that diverges from the `<prefix>-<task>` name the
65
+ // worktree-dir scan derives, and run state is the source of truth for it.
66
+ worktree: { ...worktree, branchName: state.branchName },
64
67
  title: details?.title ?? task.toUpperCase(),
65
68
  description: details?.description ?? "",
66
69
  completionTaskId,
@@ -113,25 +116,15 @@ function renderResumePrompt(context) {
113
116
  "Run the repository's documented verification before stopping, then leave the branch ready or open a PR when possible.",
114
117
  ].join("\n");
115
118
  }
116
- async function failIfWorkspaceAlreadyLive(config, task) {
117
- const probe = await workspaces.probe(config);
118
- if (probe.kind === "unavailable") {
119
- const detail = probe.error === undefined ? "" : `: ${errorMessage(probe.error)}`;
120
- throw new Error(`Could not verify whether workspace for ${task} is already live${detail}. Retry or inspect the workspace backend manually before resuming.`);
121
- }
122
- if (probe.names.has(task)) {
123
- throw new Error(`Workspace for ${task} is already live; attach to it instead of resuming.`);
124
- }
125
- }
126
119
  export async function resumeWorkspace(config, options) {
127
120
  const task = options.task.toLowerCase();
128
- await failIfWorkspaceAlreadyLive(config, task);
121
+ await failIfWorkspaceAlreadyLive(config, task, "resuming");
129
122
  const context = await buildResumeContext(config, task);
130
123
  const definition = config.agents.definitions[context.agent];
131
124
  if (definition === undefined) {
132
125
  throw new Error(`Unknown agent: ${context.agent}`);
133
126
  }
134
- const { runner, sandboxName, workspaceKind, ensureReady } = await prepareAgentLaunch({
127
+ const { runner, networkEgress, sandboxName, workspaceKind, ensureReady } = await prepareAgentLaunch({
135
128
  config,
136
129
  agent: context.agent,
137
130
  definition,
@@ -162,6 +155,7 @@ export async function resumeWorkspace(config, options) {
162
155
  : undefined;
163
156
  ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
164
157
  runner,
158
+ networkEgress,
165
159
  task,
166
160
  definition,
167
161
  promptFile: stagedPrompt.file,
@@ -22,6 +22,7 @@
22
22
  * `Cleaner`.
23
23
  */
24
24
  import type { Board } from "../lib/board.ts";
25
+ import type { ResolvedConfig } from "../lib/config.ts";
25
26
  import type { PullRequestSummary } from "../lib/pullRequests.ts";
26
27
  import { type BoardState } from "../lib/taskSource.ts";
27
28
  import type { WorktreeEntry } from "../lib/worktrees.ts";
@@ -39,6 +40,7 @@ export type FindPullRequests = (arguments_: {
39
40
  interface ReviewerDeps {
40
41
  board: Board;
41
42
  findPullRequests: FindPullRequests;
43
+ config?: ResolvedConfig;
42
44
  }
43
45
  /** Per-tick inputs, mirroring the other orchestrator steps' shape. */
44
46
  interface ReviewArguments {
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,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;AAEzD;;;;;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;CACpC;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,CAwH3D"}
1
+ {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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;AAGzD;;;;;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,CA8H3D"}
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { naturalIdFromCanonical, } from "../lib/taskSource.js";
25
25
  import { debug, errorMessage, log, logEvent } from "../lib/util.js";
26
+ import { effectiveBranchName } from "../lib/worktreeRunState.js";
26
27
  // Maps a worktree's PRs to the transition its task should make. A merged PR
27
28
  // means the work landed → done; an open PR on an in-progress task means it's
28
29
  // up for review. `merged` wins over `open`. An open PR on an already in-review
@@ -52,7 +53,7 @@ function matchingWorktreeEntries(arguments_) {
52
53
  return worktreeEntries.filter((entry) => entry.task === task && entry.repository === issue.repository);
53
54
  }
54
55
  export function createReviewer(deps) {
55
- const { board, findPullRequests } = deps;
56
+ const { board, config, findPullRequests } = deps;
56
57
  async function runOnce(arguments_) {
57
58
  const { state, worktreeEntries, dryRun, signal } = arguments_;
58
59
  const candidates = state.issues.filter((issue) => issue.status === "in-progress" || issue.status === "in-review");
@@ -65,6 +66,7 @@ export function createReviewer(deps) {
65
66
  issue,
66
67
  worktreeEntries,
67
68
  dryRun,
69
+ ...(config === undefined ? {} : { config }),
68
70
  ...(signal === undefined ? {} : { signal }),
69
71
  });
70
72
  }
@@ -74,10 +76,13 @@ export function createReviewer(deps) {
74
76
  // Unsupported writebacks are skipped without claiming success and may retry
75
77
  // on later ticks.
76
78
  async function advanceIfReviewable(arguments_) {
77
- const { issue, worktreeEntries, dryRun, signal } = arguments_;
79
+ const { issue, worktreeEntries, dryRun, config: reviewerConfig, signal } = arguments_;
78
80
  const task = naturalIdFromCanonical(issue.id);
79
81
  const entries = matchingWorktreeEntries({ issue, worktreeEntries, task });
80
82
  for (const entry of entries) {
83
+ const branchName = reviewerConfig === undefined
84
+ ? entry.branchName
85
+ : effectiveBranchName({ config: reviewerConfig, entry });
81
86
  // The injected lookup is contracted never to reject (failures resolve to
82
87
  // []), but we still guard it so one bad lookup can never abort the tick
83
88
  // and starve the other candidates. A failure means "can't tell yet" →
@@ -87,12 +92,12 @@ export function createReviewer(deps) {
87
92
  // oxlint-disable-next-line no-await-in-loop -- a task almost always has one worktree; sequential lookups are fine.
88
93
  pullRequests = await findPullRequests({
89
94
  cwd: entry.dir,
90
- branchName: entry.branchName,
95
+ branchName,
91
96
  ...(signal === undefined ? {} : { signal }),
92
97
  });
93
98
  }
94
99
  catch (error) {
95
- debug(`PR lookup failed for ${task} (${entry.branchName}): ${errorMessage(error)}`);
100
+ debug(`PR lookup failed for ${task} (${branchName}): ${errorMessage(error)}`);
96
101
  continue;
97
102
  }
98
103
  const transition = intendedTransition(pullRequests, issue.status);
@@ -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;AAyBnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;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,CAmJf;AAgJD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAyBnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;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,CAqJf;AAgJD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -34,7 +34,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
34
34
  if (!definition) {
35
35
  throw new Error(`Unknown agent: ${agent}`);
36
36
  }
37
- const { runner, sandboxName, workspaceKind, ensureReady } = await prepareAgentLaunch({
37
+ const { runner, networkEgress, sandboxName, workspaceKind, ensureReady } = await prepareAgentLaunch({
38
38
  config,
39
39
  agent,
40
40
  definition,
@@ -99,6 +99,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
99
99
  : undefined;
100
100
  const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
101
101
  runner,
102
+ networkEgress,
102
103
  task,
103
104
  definition,
104
105
  promptFile: stagedPrompt.file,
@@ -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;AAcnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAkqBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA8qBD,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"}
@@ -8,6 +8,7 @@ import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/taskSource.js
8
8
  import { errorMessage, withLogOutputSuppressed, writeOutput } from "../lib/util.js";
9
9
  import { workspaces } from "../lib/workspaces.js";
10
10
  import { worktrees } from "../lib/worktrees.js";
11
+ import { effectiveBranchNameFromRunState } from "../lib/worktreeRunState.js";
11
12
  const RECENT_LOG_LINE_COUNT = 10;
12
13
  function escapeRegExp(value) {
13
14
  return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
@@ -40,7 +41,9 @@ async function writeTaskWorktrees(config, task) {
40
41
  writeOutput("(none)");
41
42
  return;
42
43
  }
44
+ const runState = readRunState(config, task);
43
45
  for (const entry of entries) {
46
+ const branchName = effectiveBranchNameFromRunState({ entry, runState });
44
47
  // oxlint-disable-next-line no-await-in-loop -- status output is easier to read in worktree order.
45
48
  const dirtiness = await worktrees.probeWorkingTree({
46
49
  worktreeDir: entry.dir,
@@ -48,10 +51,10 @@ async function writeTaskWorktrees(config, task) {
48
51
  // oxlint-disable-next-line no-await-in-loop -- one gh lookup per worktree is acceptable; multi-worktree-per-task is rare.
49
52
  const prs = await findPullRequestsForBranch({
50
53
  cwd: entry.dir,
51
- branchName: entry.branchName,
54
+ branchName,
52
55
  });
53
56
  writeOutput(`- ${entry.repository} ${entry.kind}`);
54
- writeOutput(` branch: ${entry.branchName}`);
57
+ writeOutput(` branch: ${branchName}`);
55
58
  writeOutput(` dir: ${entry.dir}`);
56
59
  writeOutput(` git: ${formatDirtiness(dirtiness)}`);
57
60
  if (prs.length > 0) {
@@ -325,14 +328,22 @@ async function writeInventoryWorktrees(config, probe, statusByTask) {
325
328
  writeOutput("(none)");
326
329
  return;
327
330
  }
328
- const accessHints = await collectAccessHints(config, entries);
329
- const pullRequests = await collectPullRequests(entries);
330
331
  const runStates = new Map();
331
- const now = new Date();
332
- for (const [index, entry] of entries.entries()) {
332
+ for (const entry of entries) {
333
333
  if (!runStates.has(entry.task)) {
334
334
  runStates.set(entry.task, readRunState(config, entry.task));
335
335
  }
336
+ }
337
+ const accessHints = await collectAccessHints(config, entries);
338
+ const pullRequests = await collectPullRequests(entries.map((entry) => ({
339
+ dir: entry.dir,
340
+ branchName: effectiveBranchNameFromRunState({
341
+ entry,
342
+ runState: runStates.get(entry.task),
343
+ }),
344
+ })));
345
+ const now = new Date();
346
+ for (const [index, entry] of entries.entries()) {
336
347
  const runState = runStates.get(entry.task);
337
348
  const accessHint = accessHints.get(entry.task);
338
349
  // `collectPullRequests` guarantees an entry for every worktree dir seen