@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 +1 -1
- package/crew.config.example.ts +8 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/buildSecrets.d.ts +2 -2
- package/dist/lib/buildSecrets.js +2 -2
- package/dist/lib/config.d.ts +10 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +33 -9
- package/dist/lib/launchCommand.d.ts +7 -6
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +38 -30
- package/dist/lib/repositoryHooks.d.ts +8 -0
- package/dist/lib/repositoryHooks.d.ts.map +1 -0
- package/dist/lib/repositoryHooks.js +71 -0
- package/docs/adr/0001-groundcrew-uses-but-does-not-provision-sandboxes.md +1 -1
- package/docs/configuration.md +21 -1
- package/docs/credentials.md +6 -6
- package/docs/setup-hook-agent-prompt.md +46 -49
- package/docs/setup-hooks.md +64 -29
- package/package.json +1 -1
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
|
-
- [
|
|
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
|
|
package/crew.config.example.ts
CHANGED
|
@@ -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;
|
|
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
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
package/dist/lib/buildSecrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build-time secrets we shuttle into
|
|
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"];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAMrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAKrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -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 *
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
*
|
|
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;
|
|
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
|
|
36
|
+
function prepareWorktreeWithStatusReporting(prepareWorktreeCommand) {
|
|
42
37
|
return [
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
'if [ "$
|
|
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
|
|
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
|
|
200
|
-
* those (and the `none` runner) fall through to the unwrapped host
|
|
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).
|
|
211
|
-
* host shell because there is no groundcrew-managed sandbox to run them
|
|
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. **
|
|
241
|
-
*
|
|
242
|
-
* egress-restricted,
|
|
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 **
|
|
257
|
-
* them otherwise). After
|
|
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
|
-
* -
|
|
262
|
+
* - prepareWorktree wrap forwards build secrets only.
|
|
262
263
|
* - Agent wrap forwards `preLaunchEnv` names only. preLaunch credentials never
|
|
263
|
-
* reach the profile-neutral
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)}
|
|
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
|
|
325
|
-
|
|
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
|
|
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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`.
|
|
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. |
|
package/docs/credentials.md
CHANGED
|
@@ -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
|
|
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
|
|
37
|
-
3. After
|
|
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.
|
|
50
|
-
- `runner: "none"`: `secrets.env` is sourced first,
|
|
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/
|
|
1
|
+
# Agent prompt: generate `.groundcrew/config.json`
|
|
2
2
|
|
|
3
|
-
When onboarding a
|
|
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
|
-
|
|
7
|
+
Paste this prompt at the target repo root:
|
|
6
8
|
|
|
7
9
|
```text
|
|
8
|
-
You're adding
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Context:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `
|
|
41
|
-
|
|
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 →
|
|
46
|
-
- No
|
|
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.
|
|
57
|
-
2.
|
|
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
|
```
|
package/docs/setup-hooks.md
CHANGED
|
@@ -1,46 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Prepare Worktree Hooks
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
- Without the flag: full interactive bootstrap for first-time onboarding or another tool's SessionStart hook.
|
|
35
|
+
## Defaults
|
|
11
36
|
|
|
12
|
-
|
|
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
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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/
|
|
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