@clipboard-health/groundcrew 4.8.0 → 4.9.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
@@ -125,7 +125,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
125
125
  - [Configuration](./docs/configuration.md): discovery order, repo layout, full config table, prompt customization.
126
126
  - [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
127
127
  - [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.
128
+ - [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
129
129
  - [Ticket sources](./docs/ticket-sources.md): custom shell/Jira/local-plan adapters.
130
130
  - [Troubleshooting](./docs/troubleshooting.md): common operational pitfalls and fixes.
131
131
 
@@ -59,6 +59,14 @@ export default {
59
59
  //
60
60
  // git: { remote: "origin", defaultBranch: "main" },
61
61
  //
62
+ // // Fallback repo-preparation hook for repos that do not define
63
+ // // `.groundcrew/config.json` hooks.prepareWorktree. Repo-local config wins.
64
+ // defaults: {
65
+ // hooks: {
66
+ // prepareWorktree: "test ! -f package-lock.json || npm ci",
67
+ // },
68
+ // },
69
+ //
62
70
  // orchestrator: {
63
71
  // maximumInProgress: 4,
64
72
  // pollIntervalMilliseconds: 120_000,
@@ -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;
@@ -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",
@@ -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
  }
@@ -0,0 +1,8 @@
1
+ import type { HookCommands } from "./config.ts";
2
+ interface ResolvePrepareWorktreeCommandArguments {
3
+ worktreeDir: string;
4
+ defaultHooks: HookCommands;
5
+ }
6
+ export declare function resolvePrepareWorktreeCommand(arguments_: ResolvePrepareWorktreeCommandArguments): string | undefined;
7
+ export {};
8
+ //# sourceMappingURL=repositoryHooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repositoryHooks.d.ts","sourceRoot":"","sources":["../../src/lib/repositoryHooks.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,UAAU,sCAAsC;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,sCAAsC,GACjD,MAAM,GAAG,SAAS,CAGpB"}
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const REPOSITORY_CONFIG_RELATIVE_PATH = ".groundcrew/config.json";
4
+ export function resolvePrepareWorktreeCommand(arguments_) {
5
+ const repositoryConfig = readRepositoryConfig(arguments_.worktreeDir);
6
+ return repositoryConfig?.hooks.prepareWorktree ?? arguments_.defaultHooks.prepareWorktree;
7
+ }
8
+ function readRepositoryConfig(worktreeDir) {
9
+ const configPath = path.join(worktreeDir, REPOSITORY_CONFIG_RELATIVE_PATH);
10
+ let contents;
11
+ try {
12
+ contents = readFileSync(configPath, "utf8");
13
+ }
14
+ catch (error) {
15
+ if (isFileNotFoundError(error)) {
16
+ return undefined;
17
+ }
18
+ throw new Error(`Could not read ${REPOSITORY_CONFIG_RELATIVE_PATH}.`, { cause: error });
19
+ }
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(contents);
23
+ }
24
+ catch (error) {
25
+ throw new Error(`${REPOSITORY_CONFIG_RELATIVE_PATH}: expected valid JSON.`, { cause: error });
26
+ }
27
+ return normalizeRepositoryConfig(parsed);
28
+ }
29
+ function normalizeRepositoryConfig(value) {
30
+ if (!isPlainObject(value)) {
31
+ fail("must be a JSON object");
32
+ }
33
+ if (value["version"] !== 1) {
34
+ fail("version must be 1");
35
+ }
36
+ return {
37
+ hooks: normalizeHookCommands(value["hooks"]),
38
+ };
39
+ }
40
+ function normalizeHookCommands(value) {
41
+ if (value === undefined) {
42
+ return {};
43
+ }
44
+ if (!isPlainObject(value)) {
45
+ fail("hooks must be an object");
46
+ }
47
+ const hooks = {};
48
+ const prepareWorktree = normalizeOptionalHookCommand(value["prepareWorktree"], "hooks.prepareWorktree");
49
+ if (prepareWorktree !== undefined) {
50
+ hooks.prepareWorktree = prepareWorktree;
51
+ }
52
+ return hooks;
53
+ }
54
+ function normalizeOptionalHookCommand(value, configKey) {
55
+ if (value === undefined) {
56
+ return undefined;
57
+ }
58
+ if (typeof value !== "string" || value.trim().length === 0) {
59
+ fail(`${configKey} must be a non-empty string`);
60
+ }
61
+ return value.trim();
62
+ }
63
+ function fail(message) {
64
+ throw new Error(`${REPOSITORY_CONFIG_RELATIVE_PATH}: ${message}`);
65
+ }
66
+ function isFileNotFoundError(error) {
67
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
68
+ }
69
+ function isPlainObject(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
@@ -7,7 +7,7 @@ Groundcrew launches agent processes _inside_ an isolation backend (safehouse on
7
7
  ## Considered Options
8
8
 
9
9
  - **Keep the sdx lifecycle commands** — rejected: they reimplement `sbx run`/`sbx exec` setup flows, and every concept they expose (`authRecipes`, `template`, `kits`, `gitDefaults`) is a sandbox concern, not an orchestration concern.
10
- - **Generalize the launch wrap to a user-supplied template string** — rejected for now: the build-time-secrets and `.groundcrew/setup.sh` plumbing inside the sdx wrap is awkward to express in a user template. Kept a small `safehouse | sdx | none` WRAP enum in core instead.
10
+ - **Generalize the launch wrap to a user-supplied template string** — rejected for now: the build-time-secrets and `prepareWorktree` plumbing inside the sdx wrap is awkward to express in a user template. Kept a small `safehouse | sdx | none` WRAP enum in core instead.
11
11
 
12
12
  ## Consequences
13
13
 
@@ -105,6 +105,25 @@ export default {
105
105
 
106
106
  This keeps package defaults portable while letting your private config reference team-specific statuses, tools, plugins, or review loops.
107
107
 
108
+ ## Default Hooks
109
+
110
+ Repo-local `.groundcrew/config.json` is the preferred place for
111
+ `hooks.prepareWorktree`. To provide a fallback for repos that do not define one,
112
+ set `defaults.hooks.prepareWorktree`:
113
+
114
+ ```ts
115
+ export default {
116
+ defaults: {
117
+ hooks: {
118
+ prepareWorktree: "test ! -f package-lock.json || npm ci",
119
+ },
120
+ },
121
+ };
122
+ ```
123
+
124
+ See [Prepare Worktree Hooks](./setup-hooks.md) for the repo-local config shape
125
+ and hook contract.
126
+
108
127
  ## Full Reference
109
128
 
110
129
  | Key | Default | What it does |
@@ -114,6 +133,7 @@ This keeps package defaults portable while letting your private config reference
114
133
  | `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
115
134
  | `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
116
135
  | `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
136
+ | `defaults.hooks.prepareWorktree` | optional | Fallback repo-preparation command used only when the worktree does not define `.groundcrew/config.json` `hooks.prepareWorktree`. The hook runs after worktree creation and before the agent starts. Repo-local config wins. |
117
137
  | `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
118
138
  | `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
119
139
  | `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
@@ -122,7 +142,7 @@ This keeps package defaults portable while letting your private config reference
122
142
  | `models.definitions.<name>.cmd` | preset for built-ins | Shell command launched for the model. Required for custom models. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
123
143
  | `models.definitions.<name>.color` | preset for built-ins | Color for the workspace status pill (cmux only; tmux silently drops it). Required for custom models. |
124
144
  | `models.definitions.<name>.usage` | preset for built-ins | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. Set to `{ disabled: true }` to disable usage gating while keeping the model enabled. |
125
- | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name) and `setupCommand` (override for the inside-sandbox setup script). Groundcrew assumes the `groundcrew-<agent>` sandbox already exists. |
145
+ | `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Field: `agent` (required sbx agent name). Groundcrew assumes the `groundcrew-<agent>` sandbox already exists. |
126
146
  | `models.definitions.<name>.preLaunch` | optional | Host-only shell snippet run before the agent exec and outside Safehouse/sdx. Exports survive into the launch shell; under the default `safehouse` runner they are only forwarded to the agent when listed via `preLaunchEnv` or when `cmd` includes its own `safehouse --env-pass=NAMES`. `{{worktree}}` is substituted. A non-zero exit aborts launch. Not supported when `local.runner` resolves to `sdx` in v1. |
127
147
  | `models.definitions.<name>.preLaunchEnv` | optional | Companion to `preLaunch`: list of env var names to append to groundcrew's `safehouse-clearance` `--env-pass=` flag, so `preLaunch` exports reach the agent without overriding `cmd` and losing the project's egress allowlist. Each entry must match `[A-Za-z_][A-Za-z0-9_]*`. Under `runner: "none"` exports already inherit and `preLaunchEnv` is a no-op. An empty array is a uniform no-op in every runner; a non-empty list is rejected when `cmd` already starts with `safehouse` or when `runner` resolves to `sdx`. |
128
148
  | `prompts.initial` | unattended template | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override this from `crew.config.ts` for team-specific statuses, tools, plugins, or review loops. |
@@ -18,7 +18,7 @@ op run --env-file .env.1password -- crew doctor
18
18
 
19
19
  ## Build-Time Secrets
20
20
 
21
- Groundcrew forwards a small allowlist of build-time secrets from your shell into the setup phase so package installs can authenticate against private registries. The agent process never inherits these values.
21
+ Groundcrew forwards a small allowlist of build-time secrets from your shell into the `prepareWorktree` phase so package installs can authenticate against private registries. The agent process never inherits these values.
22
22
 
23
23
  Recognized names, defined in [`BUILD_SECRET_NAMES`](../src/lib/buildSecrets.ts):
24
24
 
@@ -32,9 +32,9 @@ Set them in the shell you run `crew` from. Anything not in this list is ignored.
32
32
 
33
33
  For each ticket:
34
34
 
35
- 1. If any recognized var is set and non-empty, groundcrew writes `secrets.env` with mode `0600` into the ticket's temp prompt dir as `KEY='value'` lines.
36
- 2. The launch script sources `secrets.env` with `set -a` so the values are exported into the setup phase only. Under `sdx`, they are forwarded into the sandbox via `-e KEY` flags.
37
- 3. After setup completes, the script removes every name in `BUILD_SECRET_NAMES` from the environment and removes the entire prompt dir before executing the agent.
35
+ 1. If a `prepareWorktree` hook is configured and any recognized var is set and non-empty, groundcrew writes `secrets.env` with mode `0600` into the ticket's temp prompt dir as `KEY='value'` lines.
36
+ 2. The launch script sources `secrets.env` with `set -a` so the values are exported into the `prepareWorktree` phase only. Under `sdx`, they are forwarded into the sandbox via `-e KEY` flags.
37
+ 3. After `prepareWorktree` completes, the script removes every name in `BUILD_SECRET_NAMES` from the environment and removes the entire prompt dir before executing the agent.
38
38
 
39
39
  Net effect: by the time the agent process exists, the values are gone from the environment and the file is gone from disk.
40
40
 
@@ -46,8 +46,8 @@ Net effect: by the time the agent process exists, the values are gone from the e
46
46
 
47
47
  The "preLaunch never sees build secrets" contract is enforced differently per runner:
48
48
 
49
- - `runner: "safehouse"`: `preLaunch` runs immediately after `cd`, before `secrets.env` is sourced into the launch shell. `.groundcrew/setup.sh` then runs inside its own profile-neutral `safehouse-clearance` wrap with `--env-pass=NPM_TOKEN,BUF_TOKEN`; build secrets are unset on the host before the agent's Safehouse wrap is executed.
50
- - `runner: "none"`: `secrets.env` is sourced first, `.groundcrew/setup.sh` runs on the host, build-secret names are unset, then `preLaunch` runs against a clean env, then the agent is executed.
49
+ - `runner: "safehouse"`: `preLaunch` runs immediately after `cd`, before `secrets.env` is sourced into the launch shell. `prepareWorktree` then runs inside its own profile-neutral `safehouse-clearance` wrap with `--env-pass=NPM_TOKEN,BUF_TOKEN`; build secrets are unset on the host before the agent's Safehouse wrap is executed.
50
+ - `runner: "none"`: `secrets.env` is sourced first, `prepareWorktree` runs on the host, build-secret names are unset, then `preLaunch` runs against a clean env, then the agent is executed.
51
51
 
52
52
  Under the default `safehouse` runner, the agent runs under a sanitized env allowlist. Exports from `preLaunch` land in the launch shell but are stripped before reaching the agent unless they are forwarded. `preLaunchEnv` is the supported way to forward them:
53
53
 
@@ -1,62 +1,59 @@
1
- # Agent prompt: generate `.groundcrew/setup.sh`
1
+ # Agent prompt: generate `.groundcrew/config.json`
2
2
 
3
- When onboarding a new repository to groundcrew, an operator needs to author `.groundcrew/setup.sh` — the per-worktree setup hook. The hook has a non-obvious contract: the `--deps-only` branch must skip anything interactive or one-time-only, and several things that look like setup (codegen, db seeds, husky, pre-commit, local-package linking) belong only in the no-flag branch.
3
+ When onboarding a repository to Groundcrew, an operator can ask a coding agent
4
+ to author the repo-local `prepareWorktree` hook. The hook has a narrow contract:
5
+ it is recurring, non-interactive worktree preparation for unattended agents.
4
6
 
5
- To hand the job to a coding agent (Claude Code, Cursor, etc.) without re-explaining the rules, open the agent at the target repo's root and paste the prompt below. For the full contract this prompt encodes, see [Per-repo setup hook](../README.md#per-repo-setup-hook) in the README.
7
+ Paste this prompt at the target repo root:
6
8
 
7
9
  ```text
8
- You're adding a per-worktree setup hook for this repository. Produce a single
9
- file at `.groundcrew/setup.sh` and smoke-test it.
10
-
11
- Context: groundcrew launches each agent in a fresh git worktree per ticket and
12
- invokes `bash .groundcrew/setup.sh --deps-only` before the agent starts. The
13
- flag tells the script "you're being called by automation; skip anything
14
- interactive or one-time-only." The same hook also runs inside the sdx sandbox.
15
-
16
- Script requirements:
17
-
18
- - Start with `set -euo pipefail`.
19
- - Branch on `$1`:
20
- - `--deps-only` → recurring per-worktree work only (lockfile install,
21
- codegen the agent needs to navigate). NO prompts, NO global installs, NO
22
- runtime-version-manager bootstrap (`nvm`, `pyenv`, `rustup`, `mise`,
23
- `asdf` — assume the host has the runtime).
24
- - No flag → full interactive bootstrap for first-time onboarding or
25
- SessionStart-hook reuse (husky install, pre-commit install, db seed,
26
- local-package linking).
27
- - Fast. The operator pays this cost on every ticket spinup, and each
28
- worktree starts fresh (`node_modules` / `.venv` / `target` are
29
- gitignored). Use the package manager's frozen-lockfile install (`npm
30
- clean-install`, `uv sync --frozen`, `cargo fetch`, etc.) and trust its
31
- global cache a "fresh" install in a new worktree should resolve from
32
- `~/.cache/uv`, `~/.npm`, etc. rather than re-downloading. Setup failures
33
- are logged but don't block the agent, so exit non-zero on real problems
34
- so the operator sees them.
35
-
36
- Detect this repo's stack and install accordingly. Examples:
37
-
38
- - `package.json` + `pnpm-lock.yaml` / `package-lock.json` / `yarn.lock` →
39
- matching Node package manager's frozen-lockfile install
40
- - `pyproject.toml` + `uv.lock` → `uv sync --dev`; `poetry.lock` → poetry;
41
- `Pipfile.lock` pipenv; bare `requirements.txt` → pip + venv
10
+ You're adding Groundcrew's repo-local prepareWorktree hook for this repository.
11
+ Produce `.groundcrew/config.json` and smoke-test the command.
12
+
13
+ Context: Groundcrew launches each agent in a fresh git worktree per ticket. If
14
+ `.groundcrew/config.json` contains `hooks.prepareWorktree`, Groundcrew runs that
15
+ command from the repo root after creating the worktree and before launching the
16
+ agent. The same command runs under Safehouse, sdx, or the host runner.
17
+
18
+ Hook requirements:
19
+
20
+ - JSON shape:
21
+ {
22
+ "version": 1,
23
+ "hooks": {
24
+ "prepareWorktree": "<command>"
25
+ }
26
+ }
27
+ - The command must be non-interactive and idempotent.
28
+ - Include only recurring worktree preparation the agent needs, such as lockfile
29
+ installs, dependency downloads, or type/code generation required for
30
+ navigation and tests.
31
+ - Do NOT include prompts, global installs, auth setup, runtime-version-manager
32
+ bootstrap (`nvm`, `pyenv`, `rustup`, `mise`, `asdf`), db seeds, husky,
33
+ pre-commit, or local package linking.
34
+ - Keep it fast. Each ticket starts from a fresh worktree, so use frozen-lockfile
35
+ installs (`npm ci`, `pnpm install --frozen-lockfile`, `uv sync --frozen`,
36
+ `cargo fetch`, `go mod download`, etc.) and trust global package-manager
37
+ caches.
38
+
39
+ Detect this repo's stack and write the shortest command that prepares the root
40
+ worktree:
41
+
42
+ - `package.json` + `package-lock.json` → `npm ci`
43
+ - `package.json` + `pnpm-lock.yaml` → `pnpm install --frozen-lockfile`
44
+ - `package.json` + `yarn.lock` → `yarn install --frozen-lockfile`
45
+ - `pyproject.toml` + `uv.lock` → `uv sync --dev --frozen`
46
+ - `poetry.lock` → `poetry install`
42
47
  - `Cargo.lock` → `cargo fetch`
43
48
  - `go.mod` → `go mod download`
44
49
  - `Gemfile.lock` → `bundle install --jobs=4`
45
- - Multiple lockfiles → polyglot; install each under its own guard.
46
- - No install step (docs repo, polyglot monorepo with per-package setup)
47
- do not create the script. Groundcrew skips the hook silently when the
48
- file is absent.
49
-
50
- Put codegen-the-agent-doesn't-need, db seeds, husky install, pre-commit
51
- install, and local-package linking ONLY in the no-flag branch — never in
52
- `--deps-only`.
50
+ - Multiple lockfiles → combine each required root-level prep command with `&&`.
51
+ - No recurring root worktree prep do not create the file.
53
52
 
54
53
  Verify before reporting done:
55
54
 
56
- 1. `bash .groundcrew/setup.sh --deps-only` exits 0 with no interactive prompts.
57
- 2. The output has no runtime-bootstrap warnings (`nvm not found`, `Python not
58
- on PATH`, etc.) — if you see them, the script is doing too much; strip
59
- those branches.
55
+ 1. Run the exact `hooks.prepareWorktree` command from the repo root.
56
+ 2. Confirm it exits 0 with no prompts and no runtime-bootstrap warnings.
60
57
 
61
58
  Do NOT commit. Report exactly what you wrote so the operator can review.
62
59
  ```
@@ -1,46 +1,81 @@
1
- # Setup Hooks
1
+ # Prepare Worktree Hooks
2
2
 
3
- If `.groundcrew/setup.sh` exists in the repo root, groundcrew runs `bash .groundcrew/setup.sh --deps-only` before each agent launch. Otherwise nothing runs. Same convention applies inside the sdx sandbox, overridable per model via `models.definitions.<name>.sandbox.setupCommand`.
3
+ Groundcrew can run one repo-preparation hook after it creates a ticket worktree
4
+ and before it launches the agent. Add a repo-local `.groundcrew/config.json`:
4
5
 
5
- There is no implicit `npm install`, `uv sync`, or anything else. Groundcrew is language-agnostic, so opt in by adding the script.
6
+ ```json
7
+ {
8
+ "version": 1,
9
+ "hooks": {
10
+ "prepareWorktree": "npm ci && npm run codegen:types"
11
+ }
12
+ }
13
+ ```
14
+
15
+ If the file or hook is absent, Groundcrew skips this phase. There is no
16
+ implicit `npm install`, `uv sync`, or legacy setup script convention.
17
+
18
+ `prepareWorktree` must be non-interactive, idempotent, and limited to recurring
19
+ worktree preparation the agent needs: lockfile installs, dependency downloads,
20
+ or type/code generation required for navigation and tests. Do not put human
21
+ onboarding in this hook: no prompts, global installs, auth setup, runtime
22
+ manager bootstrap (`nvm`, `pyenv`, `rustup`, `mise`, `asdf`), db seeds, husky,
23
+ pre-commit, or local package linking.
24
+
25
+ The hook runs from the repo root under every runner:
26
+
27
+ - `safehouse`: inside a profile-neutral Safehouse wrap before the agent wrap.
28
+ - `sdx`: inside the Docker Sandbox before the agent command.
29
+ - `none`: on the host shell before the agent command.
6
30
 
7
- The `--deps-only` flag tells the script it is being called by an automated system before an agent launches. Skip anything interactive or one-time-only. The same script handles both modes; branch on `$1`:
31
+ Hook failures are advisory. Groundcrew logs the non-zero exit and still launches
32
+ the agent so a flaky package registry or stale lockfile does not block the
33
+ session.
8
34
 
9
- - With `--deps-only`: do the cheap recurring work this worktree needs, such as lockfile installs or type generation. No prompts, no global installs, no `nvm` / `pyenv` bootstrap.
10
- - Without the flag: full interactive bootstrap for first-time onboarding or another tool's SessionStart hook.
35
+ ## Defaults
11
36
 
12
- Setup failures are advisory. Groundcrew logs the non-zero exit and still launches the agent so a flaky network or stale lockfile does not block the session.
37
+ For repos without local config, set a fallback in `crew.config.ts`:
38
+
39
+ ```ts
40
+ export default {
41
+ defaults: {
42
+ hooks: {
43
+ prepareWorktree: "test ! -f package-lock.json || npm ci",
44
+ },
45
+ },
46
+ // ...
47
+ };
48
+ ```
49
+
50
+ Repo-local `.groundcrew/config.json` wins for that hook. A repo-local file
51
+ without `hooks.prepareWorktree` still falls back to the `crew.config.ts`
52
+ default.
13
53
 
14
54
  ## Examples
15
55
 
16
56
  Python with uv:
17
57
 
18
- ```bash
19
- #!/usr/bin/env bash
20
- set -euo pipefail
21
- if [ "${1:-}" = "--deps-only" ]; then
22
- uv sync --dev
23
- else
24
- uv sync --dev
25
- # Extra one-time bootstrap, such as pre-commit install or db seed.
26
- fi
58
+ ```json
59
+ {
60
+ "version": 1,
61
+ "hooks": {
62
+ "prepareWorktree": "uv sync --dev --frozen"
63
+ }
64
+ }
27
65
  ```
28
66
 
29
67
  Node with npm:
30
68
 
31
- ```bash
32
- #!/usr/bin/env bash
33
- set -euo pipefail
34
- if [ "${1:-}" = "--deps-only" ]; then
35
- npm clean-install
36
- else
37
- npm clean-install
38
- # Extra one-time bootstrap, such as husky install or codegen.
39
- fi
69
+ ```json
70
+ {
71
+ "version": 1,
72
+ "hooks": {
73
+ "prepareWorktree": "npm ci"
74
+ }
75
+ }
40
76
  ```
41
77
 
42
- Docs-only or polyglot repos can omit the script. With nothing at `.groundcrew/setup.sh`, groundcrew skips the hook silently.
43
-
44
- For a comprehensive real-world example with nvm bootstrap, hash-based skip-on-no-changes caching, and portable SHA-256 detection, see [this repo's own `.groundcrew/setup.sh`](../.groundcrew/setup.sh). It is also symlinked at `.claude/setup.sh` so the same script doubles as a Claude Code SessionStart hook for this repo; that symlink is local convenience, not part of groundcrew's contract.
78
+ Docs-only or manually prepared repos can omit the file.
45
79
 
46
- To scaffold `.groundcrew/setup.sh` with a coding agent, see [setup-hook-agent-prompt.md](./setup-hook-agent-prompt.md).
80
+ To scaffold `.groundcrew/config.json` with a coding agent, see
81
+ [setup-hook-agent-prompt.md](./setup-hook-agent-prompt.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",