@clipboard-health/groundcrew 4.8.0 → 4.10.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 CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- Dispatch your ticket backlog to AI coding agents. One git worktree per ticket, sandboxed by default.
9
+ Dispatch your ticket backlog to local, interactive AI coding agents. One git worktree per ticket, sandboxed by default.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -17,16 +17,22 @@
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">
20
+ <a href="./static/demo.tape"><img alt="Groundcrew dispatching tickets into tmux panes with coding agents running in parallel" src="./static/demo.gif" width="800"></a>
21
+ </p>
22
+
23
+ <p align="center">
24
+ VHS source: <a href="./static/demo.tape">static/demo.tape</a>.
21
25
  </p>
22
26
 
23
27
  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)_.
24
28
 
25
29
  ## Why
26
30
 
31
+ - **Local.** Agents run on your machine with your tools, shell, and credentials. That makes them more steerable than remote agents, and easy to nudge when they drift.
32
+ - **Interactive.** Each ticket launches the real `claude` or `codex` CLI in its own terminal pane, not a wrapper that approximates it. Watch any session live and take over when you need to.
27
33
  - **One worktree per ticket.** Agents work in parallel without stepping on each other.
34
+ - **Sandboxed by default.** Safehouse or Docker Sandboxes isolate each agent on the host; `none` is an explicit escape hatch.
28
35
  - **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.
30
36
  - **Multi-agent routing.** Ships `claude` and `codex` presets; bring your own CLI in config.
31
37
 
32
38
  ## Prerequisites
@@ -83,8 +89,7 @@ crew init [--global | --local] [--force] [--dry-run] # create a crew.config.
83
89
  [--runner <auto|safehouse|sdx|none>] [--model <claude|codex>]
84
90
  crew doctor # check setup
85
91
  crew status [<TICKET>] # inspect current state or one ticket
86
- crew run # one-shot orchestration
87
- crew run --watch # poll forever
92
+ crew run [--watch] # one-shot or --watch forever
88
93
  crew start <TICKET> # provision + launch one ticket now
89
94
  crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
90
95
  crew resume <TICKET> # reopen a paused ticket
@@ -106,15 +111,18 @@ export default {
106
111
  projectDir: "~/dev",
107
112
  knownRepositories: ["OWNER/REPO"],
108
113
  },
109
- local: {
110
- runner: "auto",
111
- },
112
114
  models: {
113
115
  default: "claude",
114
116
  definitions: {
115
117
  claude: {},
116
118
  },
117
119
  },
120
+ defaults: {
121
+ hooks: {
122
+ // No-op placeholder; replace with your repo's setup, e.g. "npm ci".
123
+ prepareWorktree: "true",
124
+ },
125
+ },
118
126
  } satisfies Config;
119
127
  ```
120
128
 
@@ -125,7 +133,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
125
133
  - [Configuration](./docs/configuration.md): discovery order, repo layout, full config table, prompt customization.
126
134
  - [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
127
135
  - [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
128
- - [Setup hooks](./docs/setup-hooks.md): `.groundcrew/setup.sh --deps-only` for per-repo dependency setup.
136
+ - [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
129
137
  - [Ticket sources](./docs/ticket-sources.md): custom shell/Jira/local-plan adapters.
130
138
  - [Troubleshooting](./docs/troubleshooting.md): common operational pitfalls and fixes.
131
139
 
@@ -143,6 +151,12 @@ node --run crew:op -- run --watch
143
151
 
144
152
  Both forms discover config through cosmiconfig. Source edits in `src/**` are picked up on the next invocation. Requires Node >= 24.
145
153
 
154
+ Regenerate the README demo with VHS:
155
+
156
+ ```bash
157
+ ./static/render-demo.sh
158
+ ```
159
+
146
160
  ## License
147
161
 
148
162
  [MIT](./LICENSE)
@@ -64,8 +64,9 @@ raw.githubusercontent.com
64
64
  release-assets.githubusercontent.com
65
65
  results-receiver.actions.githubusercontent.com
66
66
 
67
- # npm registry + package website
67
+ # npm/Conda registries + package websites
68
68
  api.npmjs.org
69
+ conda.anaconda.org
69
70
  registry.npmjs.org
70
71
  www.npmjs.com
71
72
 
@@ -83,6 +84,9 @@ formulae.brew.sh
83
84
  hub.docker.com
84
85
  index.docker.io
85
86
  json.schemastore.org
87
+ mise-versions.jdx.dev
86
88
  nx.dev
89
+ playwright.azureedge.net
90
+ registry.terraform.io
87
91
  sourcegraph.com
88
92
  vitest.dev
@@ -35,6 +35,15 @@ export default {
35
35
  // },
36
36
  },
37
37
  },
38
+ // Repo-preparation hook: runs after each worktree is created and before the
39
+ // agent launches. The default below is a no-op placeholder. Replace it with
40
+ // your repo's setup, e.g. "npm ci" or "uv sync --dev --frozen". A repo-local
41
+ // `.groundcrew/config.json` hooks.prepareWorktree overrides this per repo.
42
+ defaults: {
43
+ hooks: {
44
+ prepareWorktree: "true",
45
+ },
46
+ },
38
47
  // Everything below is optional — defaults shown for reference. Uncomment
39
48
  // and edit to override.
40
49
  //
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuKH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA8E/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8KH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAgF/C"}
@@ -5,7 +5,7 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { createBoard } from "../lib/board.js";
7
7
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
8
- import { loadConfig, } from "../lib/config.js";
8
+ import { loadConfigWithSource, } from "../lib/config.js";
9
9
  import { detectHostCapabilities, which } from "../lib/host.js";
10
10
  import { resolveLocalRunner } from "../lib/localRunner.js";
11
11
  import { gatedModels } from "../lib/usage.js";
@@ -15,6 +15,11 @@ import { resolveWorkspaceKind } from "../lib/workspaces.js";
15
15
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
16
16
  const MAX_TOKENS_PER_CMD = 2;
17
17
  const BUILT_IN_MODEL_NAMES = ["claude", "codex"];
18
+ const CONFIG_SOURCE_LABELS = {
19
+ env: "GROUNDCREW_CONFIG",
20
+ project: "project",
21
+ xdg: "global XDG",
22
+ };
18
23
  async function checkCmd(cmd, required, hint) {
19
24
  const path = await which(cmd);
20
25
  const resolvedHint = path ?? hint;
@@ -148,8 +153,10 @@ export async function doctor() {
148
153
  writeOutput("=================");
149
154
  let config;
150
155
  try {
151
- config = await loadConfig();
152
- writeOutput("[ok] config loaded");
156
+ const { config: loadedConfig, source } = await loadConfigWithSource();
157
+ config = loadedConfig;
158
+ const sourceLabel = CONFIG_SOURCE_LABELS[source.kind];
159
+ writeOutput(`[ok] config loaded — ${source.filepath} (${sourceLabel})`);
153
160
  }
154
161
  catch (error) {
155
162
  writeOutput(`[--] config: ${errorMessage(error)}`);
@@ -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,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"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAkBnE,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,CAqHf;AAyID,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
@@ -4,6 +4,7 @@ import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
4
  import { createBoard } from "../lib/board.js";
5
5
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
6
6
  import { buildLaunchCommand } from "../lib/launchCommand.js";
7
+ import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
7
8
  import { recordRunState } from "../lib/runState.js";
8
9
  import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
9
10
  import { naturalIdFromCanonical } from "../lib/ticketSource.js";
@@ -73,12 +74,17 @@ export async function setupWorkspace(config, options, runOptions = {}) {
73
74
  workspaceContinuationInstruction: renderWorkspaceContinuationInstruction(accessHint),
74
75
  });
75
76
  promptDir = stagedPrompt.directory;
76
- const secretsFile = stageBuildSecrets(promptDir);
77
+ const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
78
+ worktreeDir: launchDir,
79
+ defaultHooks: config.defaults.hooks,
80
+ });
81
+ const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
77
82
  const launchCommand = buildLaunchCommand({
78
83
  definition,
79
84
  promptFile: stagedPrompt.file,
80
85
  worktreeDir: launchDir,
81
86
  secretsFile,
87
+ prepareWorktreeCommand,
82
88
  runner,
83
89
  sandboxName,
84
90
  });
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { orchestrate, type OrchestratorOptions } from "./commands/orchestrator.t
6
6
  export { resumeWorkspace, type ResumeWorkspaceOptions } from "./commands/resumeWorkspace.ts";
7
7
  export { setupWorkspace, type SetupWorkspaceOptions } from "./commands/setupWorkspace.ts";
8
8
  export { status, type StatusOptions } from "./commands/status.ts";
9
- export type { Config, ModelDefinition, ResolvedConfig, SourceConfig } from "./lib/config.ts";
9
+ export type { Config, HookCommands, ModelDefinition, ResolvedConfig, SourceConfig, } from "./lib/config.ts";
10
10
  export { loadConfig } from "./lib/config.ts";
11
11
  export { readRunState, recordRunState, removeRunState, runStateDirectory, runStatePath, updateRunState, type RunLifecycleState, type RunState, } from "./lib/runState.ts";
12
12
  export { fetchBlockersForTicket, fetchInProgressIssueCount, fetchRawLinearIssue, fetchResolvedIssue, isIssueInProgress, isIssueTodo, isTerminalStateType, isTerminalStatusForBlocker, isTerminalStatusForIssue, type RawLinearIssue, } from "./lib/adapters/linear/fetch.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAClE,YAAY,EACV,MAAM,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,cAAc,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,aAAa,EACb,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,KAAK,OAAO,IAAI,gBAAgB,EAChC,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,eAAe,EACpB,KAAK,eAAe,IAAI,wBAAwB,EAChD,KAAK,KAAK,IAAI,cAAc,EAC5B,iBAAiB,IAAI,0BAA0B,EAC/C,KAAK,UAAU,IAAI,mBAAmB,EACtC,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Build-time secrets we shuttle into setup phases only. The launched agent
3
- * process must not inherit these values.
2
+ * Build-time secrets we shuttle into prepareWorktree phases only. The launched
3
+ * agent process must not inherit these values.
4
4
  */
5
5
  export declare const BUILD_SECRET_NAMES: readonly ["NPM_TOKEN", "BUF_TOKEN"];
6
6
  //# sourceMappingURL=buildSecrets.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Build-time secrets we shuttle into setup phases only. The launched agent
3
- * process must not inherit these values.
2
+ * Build-time secrets we shuttle into prepareWorktree phases only. The launched
3
+ * agent process must not inherit these values.
4
4
  */
5
5
  export const BUILD_SECRET_NAMES = ["NPM_TOKEN", "BUF_TOKEN"];
@@ -8,6 +8,9 @@ export { BUILD_SECRET_NAMES } from "./buildSecrets.ts";
8
8
  * adapter's `schema.ts` and runs at `buildSources` time, not here.
9
9
  */
10
10
  export type SourceConfig = LinearAdapterConfig | ShellAdapterConfig;
11
+ export interface HookCommands {
12
+ prepareWorktree?: string;
13
+ }
11
14
  /**
12
15
  * Reserved model name. A ticket labeled `agent-any` resolves at runtime
13
16
  * to the configured model with the most available session capacity, so
@@ -47,12 +50,6 @@ export declare const LOCAL_RUNNER_SETTINGS: readonly LocalRunnerSetting[];
47
50
  export interface SandboxDefinition {
48
51
  /** sbx agent name (e.g. "claude", "codex"). */
49
52
  agent: string;
50
- /**
51
- * Setup command run **inside** the sandbox before the agent exec.
52
- * Defaults to the shared `.groundcrew/setup.sh --deps-only` convention
53
- * (see `launchCommand.ts`) when omitted.
54
- */
55
- setupCommand?: string;
56
53
  }
57
54
  export interface ModelDefinition {
58
55
  /**
@@ -69,7 +66,7 @@ export interface ModelDefinition {
69
66
  * exec'd and **outside** Safehouse/sdx. Use to mint short-lived credentials
70
67
  * (e.g. `export SESSION_TOKEN=...`) that the wrapped `cmd` inherits via
71
68
  * the process environment. `{{worktree}}` is replaced before launch.
72
- * Failures abort launch (unlike deps setup, which logs and continues).
69
+ * Failures abort launch (unlike prepareWorktree, which logs and continues).
73
70
  * Not supported for `local.runner` `sdx` in v1.
74
71
  */
75
72
  preLaunch?: string;
@@ -148,6 +145,9 @@ export interface Config {
148
145
  projectDir: string;
149
146
  knownRepositories: string[];
150
147
  };
148
+ defaults?: {
149
+ hooks?: HookCommands;
150
+ };
151
151
  orchestrator?: {
152
152
  maximumInProgress?: number;
153
153
  pollIntervalMilliseconds?: number;
@@ -209,6 +209,9 @@ export interface ResolvedConfig {
209
209
  projectDir: string;
210
210
  knownRepositories: string[];
211
211
  };
212
+ defaults: {
213
+ hooks: HookCommands;
214
+ };
212
215
  orchestrator: {
213
216
  maximumInProgress: number;
214
217
  pollIntervalMilliseconds: number;
@@ -238,6 +241,15 @@ export interface ResolvedConfig {
238
241
  file: string;
239
242
  };
240
243
  }
244
+ export type ConfigSourceKind = "env" | "project" | "xdg";
245
+ export interface ConfigSource {
246
+ kind: ConfigSourceKind;
247
+ filepath: string;
248
+ }
249
+ export interface LoadedConfig {
250
+ config: Readonly<ResolvedConfig>;
251
+ source: Readonly<ConfigSource>;
252
+ }
241
253
  /**
242
254
  * Single source of truth for "is preLaunchEnv asking us to forward anything?"
243
255
  *
@@ -255,5 +267,6 @@ export declare function hasPreLaunchEnv(definition: Pick<ModelDefinition, "preLa
255
267
  * not enabled from an arbitrary unknown label like `agent-typo`.
256
268
  */
257
269
  export declare function isBuiltInModelNotEnabled(config: Pick<ResolvedConfig, "models">, name: string): boolean;
270
+ export declare function loadConfigWithSource(): Promise<Readonly<LoadedConfig>>;
258
271
  export declare function loadConfig(): Promise<Readonly<ResolvedConfig>>;
259
272
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -152,6 +152,31 @@ function normalizeOptionalString(value, configKey) {
152
152
  }
153
153
  return value.trim();
154
154
  }
155
+ function normalizeHookCommands(value, configKey) {
156
+ if (value === undefined) {
157
+ return {};
158
+ }
159
+ if (!isPlainObject(value)) {
160
+ fail(`${configKey} must be an object`);
161
+ }
162
+ const hooks = {};
163
+ const prepareWorktree = normalizeOptionalString(value["prepareWorktree"], `${configKey}.prepareWorktree`);
164
+ if (prepareWorktree !== undefined) {
165
+ hooks.prepareWorktree = prepareWorktree;
166
+ }
167
+ return hooks;
168
+ }
169
+ function normalizeDefaults(value) {
170
+ if (value === undefined) {
171
+ return { hooks: {} };
172
+ }
173
+ if (!isPlainObject(value)) {
174
+ fail("defaults must be an object");
175
+ }
176
+ return {
177
+ hooks: normalizeHookCommands(value["hooks"], "defaults.hooks"),
178
+ };
179
+ }
155
180
  const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
156
181
  function validatePreLaunchEnv(modelName, value) {
157
182
  const configPath = `models.definitions.${modelName}.preLaunchEnv`;
@@ -163,7 +188,7 @@ function validatePreLaunchEnv(modelName, value) {
163
188
  fail(`${configPath}[${index}] must be a POSIX env var name matching ${ENV_VAR_NAME_PATTERN.source} (got ${JSON.stringify(entry)})`);
164
189
  }
165
190
  // Build secrets are sourced into the host launch shell, forwarded only to
166
- // the Safehouse *setup* wrap, and `unset` on the host before the agent
191
+ // the Safehouse *prepareWorktree* wrap, and `unset` on the host before the agent
167
192
  // wrap is exec'd. Listing one here would silently never reach the agent —
168
193
  // fail loudly so the operator picks a different name (or removes the
169
194
  // entry) instead of debugging a missing env var at runtime.
@@ -220,22 +245,20 @@ function normalizeSandbox(value, configKey) {
220
245
  if (Object.hasOwn(value, "kits")) {
221
246
  failRemovedConfigKey(`${configKey}.kits`, "Groundcrew no longer creates sdx sandboxes or applies sandbox kits.");
222
247
  }
223
- const { agent, setupCommand } = value;
248
+ if (Object.hasOwn(value, "setupCommand")) {
249
+ fail(`${configKey}.setupCommand is no longer supported: use repo-local \`.groundcrew/config.json\` \`hooks.prepareWorktree\`, or \`defaults.hooks.prepareWorktree\` in crew.config.ts when you need a fallback for repos without their own hook.`);
250
+ }
251
+ const { agent } = value;
224
252
  requireString(agent, `${configKey}.agent`);
225
253
  const trimmedAgent = agent.trim();
226
254
  if (trimmedAgent.length === 0) {
227
255
  fail(`${configKey}.agent must be a non-empty string (got ${JSON.stringify(agent)})`);
228
256
  }
229
- const sandbox = { agent: trimmedAgent };
230
- const normalizedSetup = normalizeOptionalString(setupCommand, `${configKey}.setupCommand`);
231
- if (normalizedSetup !== undefined) {
232
- sandbox.setupCommand = normalizedSetup;
233
- }
234
- return sandbox;
257
+ return { agent: trimmedAgent };
235
258
  }
236
259
  function failRemovedConfigKey(configKey, reason) {
237
260
  fail(`${configKey} is no longer supported: ${reason} ` +
238
- "Provision and manage the sandbox yourself with `sbx` (for example `sbx create --name groundcrew-<agent> <agent> <projectDir>`), then keep only `models.definitions.<model>.sandbox.agent` plus optional `setupCommand` in crew.config.ts.");
261
+ "Provision and manage the sandbox yourself with `sbx` (for example `sbx create --name groundcrew-<agent> <agent> <projectDir>`), then keep only `models.definitions.<model>.sandbox.agent` in crew.config.ts.");
239
262
  }
240
263
  function failIfLegacyModelKeys(name, override) {
241
264
  if (!isPlainObject(override)) {
@@ -423,6 +446,7 @@ function applyDefaults(user) {
423
446
  projectDir: expandHome(user.workspace.projectDir),
424
447
  knownRepositories: user.workspace.knownRepositories,
425
448
  },
449
+ defaults: normalizeDefaults(user.defaults),
426
450
  orchestrator: { ...DEFAULT_ORCHESTRATOR, ...user.orchestrator },
427
451
  models: {
428
452
  default: user.models?.default ?? "claude",
@@ -582,26 +606,28 @@ async function discoverUserConfig() {
582
606
  if (!existsSync(overridePath)) {
583
607
  fail(`GROUNDCREW_CONFIG=${overridePath} not found`);
584
608
  }
585
- return await loadAt(overridePath);
609
+ const result = await loadAt(overridePath);
610
+ return { result, source: { kind: "env", filepath: result.filepath } };
586
611
  }
587
612
  const project = await explorer.search(process.cwd());
588
613
  if (project !== null && project.isEmpty !== true) {
589
- return project;
614
+ return { result: project, source: { kind: "project", filepath: project.filepath } };
590
615
  }
591
616
  const xdgPath = findXdgConfigFile();
592
617
  if (xdgPath !== undefined) {
593
- return await loadAt(xdgPath);
618
+ const result = await loadAt(xdgPath);
619
+ return { result, source: { kind: "xdg", filepath: result.filepath } };
594
620
  }
595
621
  // Throw directly so oxlint's `consistent-return` rule sees a
596
622
  // terminating statement; it doesn't track `fail()`'s `never` return.
597
623
  throw new Error(`groundcrew config: no crew config found. Create crew.config.ts in your project root, or ${xdgConfigPath("groundcrew", "crew.config.ts")}, or set GROUNDCREW_CONFIG.`);
598
624
  }
599
625
  let cached;
600
- export async function loadConfig() {
626
+ export async function loadConfigWithSource() {
601
627
  if (cached) {
602
628
  return cached;
603
629
  }
604
- const result = await discoverUserConfig();
630
+ const { result, source } = await discoverUserConfig();
605
631
  const { filepath, isEmpty } = result;
606
632
  const userConfig = result.config;
607
633
  if (isEmpty === true || !isPlainObject(userConfig)) {
@@ -612,6 +638,13 @@ export async function loadConfig() {
612
638
  const resolved = applyDefaults(userConfig);
613
639
  validate(resolved);
614
640
  setLogFile(resolved.logging.file);
615
- cached = Object.freeze(resolved);
641
+ cached = Object.freeze({
642
+ config: Object.freeze(resolved),
643
+ source: Object.freeze(source),
644
+ });
616
645
  return cached;
617
646
  }
647
+ export async function loadConfig() {
648
+ const loadedConfig = await loadConfigWithSource();
649
+ return loadedConfig.config;
650
+ }
@@ -11,11 +11,6 @@ export { shellSingleQuote } from "./shell.ts";
11
11
  * exercise the catch branch.
12
12
  */
13
13
  export declare function resolveSafehouseClearancePath(baseUrl?: string): string;
14
- /**
15
- * Per-repo setup hook: if `.groundcrew/setup.sh` exists, run it with
16
- * `--deps-only`; otherwise no-op.
17
- */
18
- export declare const SETUP_COMMAND = "if [ -f .groundcrew/setup.sh ]; then bash .groundcrew/setup.sh --deps-only; fi";
19
14
  interface LaunchCommandArguments {
20
15
  definition: ModelDefinition;
21
16
  promptFile: string;
@@ -23,11 +18,17 @@ interface LaunchCommandArguments {
23
18
  /**
24
19
  * Optional path to a `KEY='value'` env file containing build-time
25
20
  * secrets (see `BUILD_SECRET_NAMES`). Sourced on the host shell before
26
- * setup; for the sdx runner the names are propagated into the sandbox
21
+ * prepareWorktree; for the sdx runner the names are propagated into the sandbox
27
22
  * via `sbx exec -e KEY`. Always unset before exec'ing the agent so the
28
23
  * agent process never inherits them.
29
24
  */
30
25
  secretsFile?: string | undefined;
26
+ /**
27
+ * Optional repo-preparation hook resolved by the caller from the freshly
28
+ * created worktree's `.groundcrew/config.json`, falling back to
29
+ * `defaults.hooks.prepareWorktree` from crew.config.ts.
30
+ */
31
+ prepareWorktreeCommand?: string | undefined;
31
32
  /**
32
33
  * Concrete local isolation backend chosen for this launch. Resolved
33
34
  * from `config.local.runner` via `resolveLocalRunner` before this
@@ -1 +1 @@
1
- {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;AAyKnF,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA0B7E"}
1
+ {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AA2KD,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA0B7E"}
@@ -25,11 +25,6 @@ export function resolveSafehouseClearancePath(baseUrl = import.meta.url) {
25
25
  return path.resolve(path.dirname(clearancePackageJson), "safehouse", "safehouse-clearance");
26
26
  }
27
27
  const SAFEHOUSE_CLEARANCE_WRAPPER_PATH = resolveSafehouseClearancePath();
28
- /**
29
- * Per-repo setup hook: if `.groundcrew/setup.sh` exists, run it with
30
- * `--deps-only`; otherwise no-op.
31
- */
32
- export const SETUP_COMMAND = "if [ -f .groundcrew/setup.sh ]; then bash .groundcrew/setup.sh --deps-only; fi";
33
28
  function renderAgentCommand(arguments_) {
34
29
  return arguments_.agentCmd
35
30
  .replaceAll("{{worktree}}", shellSingleQuote(arguments_.worktreeDir))
@@ -38,17 +33,17 @@ function renderAgentCommand(arguments_) {
38
33
  function renderPreLaunch(preLaunch, worktreeDir) {
39
34
  return preLaunch.replaceAll("{{worktree}}", shellSingleQuote(worktreeDir));
40
35
  }
41
- function setupWithStatusReporting(setupCommand) {
36
+ function prepareWorktreeWithStatusReporting(prepareWorktreeCommand) {
42
37
  return [
43
- setupCommand,
44
- "setup_status=$?",
45
- 'if [ "$setup_status" -ne 0 ]; then echo "groundcrew setup command exited with status $setup_status; continuing to agent." >&2; fi',
38
+ `(${prepareWorktreeCommand})`,
39
+ "prepare_status=$?",
40
+ 'if [ "$prepare_status" -ne 0 ]; then echo "groundcrew prepareWorktree hook exited with status $prepare_status; continuing to agent." >&2; fi',
46
41
  ].join("; ");
47
42
  }
48
43
  /**
49
44
  * Source a `KEY='value'` file with auto-export so build-time secrets land
50
- * in the shell env before setup runs. The `-f` guard keeps it a no-op if
51
- * the file disappeared between staging and launch.
45
+ * in the shell env before prepareWorktree runs. The `-f` guard keeps it a
46
+ * no-op if the file disappeared between staging and launch.
52
47
  */
53
48
  function sourceSecretsLine(secretsFile) {
54
49
  return `if [ -f ${shellSingleQuote(secretsFile)} ]; then set -a && . ${shellSingleQuote(secretsFile)} && set +a; fi`;
@@ -196,8 +191,9 @@ export function buildLaunchCommand(arguments_) {
196
191
  /**
197
192
  * The Safehouse wrap applies only when `runner === "safehouse"` and `cmd` does
198
193
  * not already invoke `safehouse` itself. A `safehouse …` cmd owns its own
199
- * sandbox flags, and we can't splice setup into a command we don't control, so
200
- * those (and the `none` runner) fall through to the unwrapped host path.
194
+ * sandbox flags, and we can't splice prepareWorktree into a command we don't
195
+ * control, so those (and the `none` runner) fall through to the unwrapped host
196
+ * path.
201
197
  */
202
198
  function shouldWrapWithSafehouse(arguments_) {
203
199
  if (arguments_.runner !== "safehouse") {
@@ -207,8 +203,9 @@ function shouldWrapWithSafehouse(arguments_) {
207
203
  }
208
204
  /**
209
205
  * Unsandboxed host launch (`runner === "none"`, or a `safehouse …` cmd that
210
- * brings its own wrap). Setup, secret sourcing, and the agent all run on the
211
- * host shell because there is no groundcrew-managed sandbox to run them inside.
206
+ * brings its own wrap). prepareWorktree, secret sourcing, and the agent all run
207
+ * on the host shell because there is no groundcrew-managed sandbox to run them
208
+ * inside.
212
209
  */
213
210
  function buildUnwrappedHostLaunchCommand(arguments_) {
214
211
  const promptDir = path.dirname(arguments_.promptFile);
@@ -220,8 +217,10 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
220
217
  const lines = [
221
218
  ...hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir }),
222
219
  ...hostSourceSecrets(arguments_.secretsFile),
223
- setupWithStatusReporting(SETUP_COMMAND),
224
220
  ];
221
+ if (arguments_.prepareWorktreeCommand !== undefined) {
222
+ lines.push(prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand));
223
+ }
225
224
  if (arguments_.secretsFile !== undefined) {
226
225
  lines.push(unsetSecretsLine());
227
226
  }
@@ -237,9 +236,11 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
237
236
  /**
238
237
  * Safehouse launch. Two Safehouse wraps, by design:
239
238
  *
240
- * 1. **Setup wrap**: plain `safehouse-clearance ... sh -c '<setup>'`. Runs
241
- * `.groundcrew/setup.sh --deps-only` filesystem-isolated and
242
- * egress-restricted, **without** inheriting agent-profile grants.
239
+ * 1. **prepareWorktree wrap**: plain
240
+ * `safehouse-clearance ... sh -c '<prepareWorktree>'`. Runs the repo
241
+ * preparation hook filesystem-isolated and egress-restricted,
242
+ * **without** inheriting agent-profile grants. Omitted entirely when no
243
+ * hook command is configured.
243
244
  * 2. **Agent wrap**: `safehouse-clearance "$shim" -c '<exec agent>' sh "$_p"`
244
245
  * where `$shim` is a `mktemp`-d symlink to `/bin/sh` named after the
245
246
  * agent (e.g. `claude`). Safehouse selects the matching agent profile
@@ -253,14 +254,14 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
253
254
  * from which `stageBuildSecrets` reads them) nor file-sourced values — and keeps
254
255
  * stale same-named ambient credentials from being forwarded. `secrets.env` is
255
256
  * then sourced into the host launch shell so Safehouse can forward build secrets
256
- * into the **setup wrap** via `--env-pass=` (Safehouse's `--env=FILE` mode strips
257
- * them otherwise). After setup returns, `BUILD_SECRET_NAMES` are `unset` again
257
+ * into the **prepareWorktree wrap** via `--env-pass=` (Safehouse's `--env=FILE` mode strips
258
+ * them otherwise). After prepareWorktree returns, `BUILD_SECRET_NAMES` are `unset` again
258
259
  * on the host so they cannot reach the agent wrap.
259
260
  *
260
261
  * `--env-pass` composition is split per wrap (deliberate, post PR #128):
261
- * - Setup wrap forwards build secrets only.
262
+ * - prepareWorktree wrap forwards build secrets only.
262
263
  * - Agent wrap forwards `preLaunchEnv` names only. preLaunch credentials never
263
- * reach the profile-neutral setup phase.
264
+ * reach the profile-neutral prepare phase.
264
265
  */
265
266
  function buildSafehouseLaunchCommand(arguments_) {
266
267
  const promptDir = path.dirname(arguments_.promptFile);
@@ -270,15 +271,17 @@ function buildSafehouseLaunchCommand(arguments_) {
270
271
  worktreeDir: arguments_.worktreeDir,
271
272
  sandboxName: "",
272
273
  });
273
- const setupCommand = setupWithStatusReporting(SETUP_COMMAND);
274
+ const prepareWorktreeCommand = arguments_.prepareWorktreeCommand === undefined
275
+ ? undefined
276
+ : prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand);
274
277
  const agentCommand = `exec ${agentCmd} "$@"`;
275
- // Split --env-pass per wrap: the setup wrap only needs build secrets (so
278
+ // Split --env-pass per wrap: the prepareWorktree wrap only needs build secrets (so
276
279
  // `npm install` etc. can authenticate); the agent wrap only needs the
277
280
  // user's preLaunchEnv (build secrets are `unset` on the host between the
278
281
  // two wraps, so forwarding them here would silently no-op). Keeps preLaunch
279
- // credentials out of the profile-neutral setup phase — see PR #128.
282
+ // credentials out of the profile-neutral prepare phase — see PR #128.
280
283
  // Trailing space keeps each flag separated from the next argv token.
281
- const setupEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
284
+ const prepareWorktreeEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
282
285
  const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
283
286
  const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
284
287
  const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
@@ -298,7 +301,10 @@ function buildSafehouseLaunchCommand(arguments_) {
298
301
  lines.push(unsetEnvironmentLine([...BUILD_SECRET_NAMES, ...preLaunchEnvNames]));
299
302
  lines.push(renderPreLaunch(arguments_.definition.preLaunch, arguments_.worktreeDir));
300
303
  }
301
- lines.push(...hostSourceSecrets(arguments_.secretsFile), `_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${setupEnvPassFlag}sh -c ${shellSingleQuote(setupCommand)}`);
304
+ lines.push(...hostSourceSecrets(arguments_.secretsFile), `_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`);
305
+ if (prepareWorktreeCommand !== undefined) {
306
+ lines.push(`${safehouseWrapper} ${prepareWorktreeEnvPassFlag}sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
307
+ }
302
308
  if (arguments_.secretsFile !== undefined) {
303
309
  lines.push(unsetSecretsLine());
304
310
  }
@@ -321,8 +327,10 @@ function buildSdxLaunchCommand(arguments_) {
321
327
  worktreeDir: arguments_.worktreeDir,
322
328
  sandboxName: arguments_.sandboxName,
323
329
  });
324
- const setupCommand = arguments_.definition.sandbox.setupCommand ?? SETUP_COMMAND;
325
- const innerParts = [setupWithStatusReporting(setupCommand)];
330
+ const innerParts = [];
331
+ if (arguments_.prepareWorktreeCommand !== undefined) {
332
+ innerParts.push(prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand));
333
+ }
326
334
  if (arguments_.secretsFile !== undefined) {
327
335
  innerParts.push(unsetSecretsLine());
328
336
  }