@clipboard-health/groundcrew 4.30.0 → 4.31.1

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
@@ -50,8 +50,8 @@ crew init --global --project-dir ~/dev --repo OWNER/REPO --agent claude
50
50
 
51
51
  # 3. Run the clone commands printed by `crew init`.
52
52
 
53
- # 4. Set the clearance egress proxy allowlist.
54
- export CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts"
53
+ # 4. Safehouse runs use groundcrew's bundled clearance allowlist automatically.
54
+ # Add extra hosts later via CLEARANCE_ALLOW_HOSTS or CLEARANCE_ALLOW_HOSTS_FILES.
55
55
 
56
56
  # 5. Using Linear? Export your API key. (Jira and other trackers: see Task Pickup.)
57
57
  export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
@@ -145,7 +145,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
145
145
 
146
146
  ## Reference
147
147
 
148
- - [Configuration](./docs/configuration.md): discovery order, repo layout, full config table, prompt customization.
148
+ - [Configuration](./docs/configuration.md): discovery order, repo layout, scripted/sparse-checkout (graft) worktrees, full config table, prompt customization.
149
149
  - [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
150
150
  - [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
151
151
  - [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
@@ -26,6 +26,29 @@ export default {
26
26
  // form to point a repo at a different parent directory:
27
27
  // { name: "other-org/other-repo", projectDirOverride: "~/work" }
28
28
  knownRepositories: ["your-org/your-repo"],
29
+ // A knownRepositories entry can also be an object that provisions the
30
+ // worktree with a custom command instead of `git worktree add` — e.g. a
31
+ // sparse checkout via `graft`. `repo` is a logical name (task token +
32
+ // worktree dir basename); the physical clone is the command's concern.
33
+ // Templates interpolate ${branch} ${dir} ${baseRef} ${repo} ${task}.
34
+ //
35
+ // {
36
+ // name: "billing",
37
+ // provision: {
38
+ // create: "graft new ${branch} billing --from ${baseRef} --dir ${dir}",
39
+ // remove: "graft rm ${branch} -f",
40
+ // },
41
+ // // Optional: run the agent, prepareWorktree hook, and
42
+ // // .groundcrew/config.json lookup in this subdirectory of the
43
+ // // checkout (relative, no ".."). Use it when the checkout is a
44
+ // // monorepo whose project lives in a subdir.
45
+ // workdir: "services/billing",
46
+ // },
47
+ //
48
+ // Set up graft once outside groundcrew:
49
+ // graft repo add ~/dev/owner/monorepo
50
+ // graft alias add billing services/billing libs/common
51
+ // `crew doctor` does not parse or validate these shell templates.
29
52
  },
30
53
  agents: {
31
54
  default: "claude",
@@ -1 +1 @@
1
- {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAuID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA0Ff;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
1
+ {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAuID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA2Ef;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
@@ -2,14 +2,12 @@ import { fetchResolvedIssue } from "../lib/adapters/linear/fetch.js";
2
2
  import { getLinearClient } from "../lib/adapters/linear/client.js";
3
3
  import { isLinearEnabled } from "../lib/buildSources.js";
4
4
  import { loadConfig } from "../lib/config.js";
5
- import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
6
- import { buildLaunchCommand } from "../lib/launchCommand.js";
5
+ import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
7
6
  import { readRunState, recordRunState } from "../lib/runState.js";
8
- import { buildAndStageSrtLaunch } from "../lib/srtLaunch.js";
9
7
  import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
10
8
  import { errorMessage, log } from "../lib/util.js";
11
9
  import { workspaces } from "../lib/workspaces.js";
12
- import { worktrees } from "../lib/worktrees.js";
10
+ import { resolveLaunchDir, worktrees } from "../lib/worktrees.js";
13
11
  function parseArguments(argv) {
14
12
  const [task, ...extras] = argv;
15
13
  if (task === undefined || task.length === 0 || extras.length > 0 || task.startsWith("-")) {
@@ -124,50 +122,36 @@ export async function resumeWorkspace(config, options) {
124
122
  purpose: "resumes",
125
123
  });
126
124
  await ensureReady();
125
+ const worktreeDir = context.worktree.dir;
126
+ const launchDir = resolveLaunchDir(config, context.repository, worktreeDir);
127
127
  const stagedPrompt = stagePromptText({
128
128
  prefix: "groundcrew-resume",
129
129
  task,
130
130
  text: renderResumePrompt(context),
131
131
  });
132
132
  const secretsFile = stageBuildSecrets(stagedPrompt.directory);
133
- // Resume must stage srt settings exactly like setup, or `buildLaunchCommand`
134
- // throws under the srt runner and a relocating agent (codex) needs its
135
- // config home re-seeded so it authenticates on the resumed launch.
136
- let srtPrepareSettingsFile;
137
- let srtAgentSettingsFile;
133
+ // Resume stages srt settings exactly like setup (a relocating agent such as
134
+ // codex needs its config home re-seeded to authenticate on the resumed launch).
135
+ // Composition runs inside the try so a pre-launch failure still cleans up the
136
+ // staged prompt (and any srt settings) dir.
138
137
  let srtSettingsDir;
139
- let srtAgentConfigDirEnv;
140
- if (runner === "srt") {
141
- const staged = buildAndStageSrtLaunch({
142
- config,
143
- repository: context.repository,
138
+ try {
139
+ let launchCommand;
140
+ ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
141
+ runner,
144
142
  task,
145
- worktreeDir: context.worktree.dir,
146
143
  definition,
147
- });
148
- srtPrepareSettingsFile = staged.prepareFile;
149
- srtAgentSettingsFile = staged.agentFile;
150
- srtSettingsDir = staged.directory;
151
- srtAgentConfigDirEnv = staged.agentConfigDirEnv;
152
- }
153
- const launchCommand = buildLaunchCommand({
154
- definition,
155
- promptFile: stagedPrompt.file,
156
- worktreeDir: context.worktree.dir,
157
- secretsFile,
158
- runner,
159
- sandboxName,
160
- srtPrepareSettingsFile,
161
- srtAgentSettingsFile,
162
- srtSettingsDir,
163
- srtAgentConfigDirEnv,
164
- });
165
- const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
166
- try {
144
+ promptFile: stagedPrompt.file,
145
+ worktreeDir,
146
+ workingDir: launchDir,
147
+ secretsFile,
148
+ sandboxName,
149
+ }));
150
+ const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
167
151
  await openAgentWorkspace({
168
152
  config,
169
153
  name: task,
170
- cwd: context.worktree.dir,
154
+ cwd: launchDir,
171
155
  command: launchCmd,
172
156
  agent: context.agent,
173
157
  color: definition.color,
@@ -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;AAmBnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA0If;AA8ID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,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;AAsBnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA0Hf;AA8ID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
@@ -1,17 +1,15 @@
1
1
  import { rmSync } from "node:fs";
2
2
  import { loadConfig } from "../lib/config.js";
3
- import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
3
+ import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
4
  import { createBoard } from "../lib/board.js";
5
5
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
6
- import { buildLaunchCommand } from "../lib/launchCommand.js";
7
6
  import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
8
7
  import { recordRunState } from "../lib/runState.js";
9
- import { buildAndStageSrtLaunch } from "../lib/srtLaunch.js";
10
8
  import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
11
9
  import { naturalIdFromCanonical } from "../lib/taskSource.js";
12
10
  import { debug, errorMessage, log, okMark } from "../lib/util.js";
13
11
  import { workspaces } from "../lib/workspaces.js";
14
- import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
12
+ import { isWorktreeAlreadyExistsError, resolveLaunchDir, worktrees, } from "../lib/worktrees.js";
15
13
  function stagePrompt(input) {
16
14
  return stagePromptFromTemplate({
17
15
  config: input.config,
@@ -53,7 +51,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
53
51
  }
54
52
  throw error;
55
53
  }
56
- const { branchName, dir: launchDir } = created;
54
+ const { branchName, dir: worktreeDir } = created;
55
+ const launchDir = resolveLaunchDir(config, repository, worktreeDir);
57
56
  const worktreeName = `${repository}-${task}`;
58
57
  // Anything that fails after the worktree is on disk must roll it back
59
58
  // (the worktree and the just-created branch). `workspaces.open` cleans
@@ -81,35 +80,18 @@ export async function setupWorkspace(config, options, runOptions = {}) {
81
80
  defaultHooks: config.defaults.hooks,
82
81
  });
83
82
  const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
84
- let srtPrepareSettingsFile;
85
- let srtAgentSettingsFile;
86
- let srtAgentConfigDirEnv;
87
- if (runner === "srt") {
88
- const staged = buildAndStageSrtLaunch({
89
- config,
90
- repository,
91
- task,
92
- worktreeDir: launchDir,
93
- definition,
94
- });
95
- srtPrepareSettingsFile = staged.prepareFile;
96
- srtAgentSettingsFile = staged.agentFile;
97
- srtSettingsDir = staged.directory;
98
- srtAgentConfigDirEnv = staged.agentConfigDirEnv;
99
- }
100
- const launchCommand = buildLaunchCommand({
83
+ const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
84
+ runner,
85
+ task,
101
86
  definition,
102
87
  promptFile: stagedPrompt.file,
103
- worktreeDir: launchDir,
88
+ worktreeDir,
89
+ workingDir: launchDir,
104
90
  secretsFile,
105
91
  prepareWorktreeCommand,
106
- runner,
107
92
  sandboxName,
108
- srtPrepareSettingsFile,
109
- srtAgentSettingsFile,
110
- srtSettingsDir,
111
- srtAgentConfigDirEnv,
112
93
  });
94
+ srtSettingsDir = stagedSrtSettingsDir;
113
95
  const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
114
96
  debug("Opening workspace...");
115
97
  await openAgentWorkspace({
@@ -126,7 +108,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
126
108
  task,
127
109
  repository,
128
110
  agent,
129
- worktreeDir: launchDir,
111
+ worktreeDir,
130
112
  branchName,
131
113
  workspaceName: task,
132
114
  state: "running",
@@ -147,7 +129,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
147
129
  task,
148
130
  repository,
149
131
  agent,
150
- worktreeDir: launchDir,
132
+ worktreeDir,
151
133
  branchName,
152
134
  workspaceName: task,
153
135
  state: "failed-to-launch",
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, HookCommands, AgentDefinition, ResolvedConfig, SourceConfig, } from "./lib/config.ts";
9
+ export type { Config, HookCommands, AgentDefinition, KnownRepository, 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 { fetchBlockersForTask, 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,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,oBAAoB,EACpB,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,kBAAkB,EAClB,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,UAAU,GAChB,MAAM,qBAAqB,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,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,oBAAoB,EACpB,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,kBAAkB,EAClB,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,UAAU,GAChB,MAAM,qBAAqB,CAAC"}
@@ -1,4 +1,25 @@
1
1
  import { type LocalRunner, type AgentDefinition, type ResolvedConfig } from "./config.ts";
2
+ /**
3
+ * Stage any srt settings and build the workspace launch command — the assembly
4
+ * shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
5
+ * (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
6
+ * `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
7
+ * Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
8
+ */
9
+ export declare function composeAgentLaunch(input: {
10
+ runner: LocalRunner;
11
+ task: string;
12
+ definition: AgentDefinition;
13
+ promptFile: string;
14
+ worktreeDir: string;
15
+ workingDir: string;
16
+ secretsFile?: string | undefined;
17
+ prepareWorktreeCommand?: string | undefined;
18
+ sandboxName?: string | undefined;
19
+ }): {
20
+ launchCommand: string;
21
+ srtSettingsDir: string | undefined;
22
+ };
2
23
  interface PreparedAgentLaunch {
3
24
  runner: LocalRunner;
4
25
  sandboxName: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B;AAqBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AASrB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAwBhE;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
@@ -1,10 +1,44 @@
1
1
  import { ensureClearance } from "@clipboard-health/clearance";
2
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
2
3
  import { hasPreLaunchEnv, } from "./config.js";
3
4
  import { detectHostCapabilities } from "./host.js";
5
+ import { buildLaunchCommand } from "./launchCommand.js";
4
6
  import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
5
7
  import { sandboxNameFor } from "./sandboxName.js";
8
+ import { buildAndStageSrtLaunch } from "./srtLaunch.js";
6
9
  import { debug, sleep } from "./util.js";
7
10
  import { workspaces } from "./workspaces.js";
11
+ /**
12
+ * Stage any srt settings and build the workspace launch command — the assembly
13
+ * shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
14
+ * (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
15
+ * `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
16
+ * Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
17
+ */
18
+ export function composeAgentLaunch(input) {
19
+ const staged = input.runner === "srt"
20
+ ? buildAndStageSrtLaunch({
21
+ task: input.task,
22
+ worktreeDir: input.worktreeDir,
23
+ definition: input.definition,
24
+ })
25
+ : undefined;
26
+ const launchCommand = buildLaunchCommand({
27
+ definition: input.definition,
28
+ promptFile: input.promptFile,
29
+ worktreeDir: input.worktreeDir,
30
+ workingDir: input.workingDir,
31
+ secretsFile: input.secretsFile,
32
+ prepareWorktreeCommand: input.prepareWorktreeCommand,
33
+ runner: input.runner,
34
+ sandboxName: input.sandboxName,
35
+ srtPrepareSettingsFile: staged?.prepareFile,
36
+ srtAgentSettingsFile: staged?.agentFile,
37
+ srtSettingsDir: staged?.directory,
38
+ srtAgentConfigDirEnv: staged?.agentConfigDirEnv,
39
+ });
40
+ return { launchCommand, srtSettingsDir: staged?.directory };
41
+ }
8
42
  export async function prepareAgentLaunch(input) {
9
43
  const host = await detectHostCapabilities(input.signal);
10
44
  const runner = resolveLocalRunner(input.config.local.runner, host);
@@ -44,6 +78,9 @@ async function alreadyReady() {
44
78
  }
45
79
  async function ensureSafehouseClearance(signal) {
46
80
  await ensureClearance({
81
+ envOverrides: {
82
+ CLEARANCE_ALLOW_HOSTS_FILES: clearanceAllowHostsFilesFromEnvironment(),
83
+ },
47
84
  logger: debug,
48
85
  ...(signal === undefined
49
86
  ? {}
@@ -0,0 +1,8 @@
1
+ interface ClearanceAllowHostsFilesInput {
2
+ defaultFile?: string | undefined;
3
+ existingFiles?: string | undefined;
4
+ }
5
+ export declare function clearanceAllowHostsFilesValue(input?: ClearanceAllowHostsFilesInput): string;
6
+ export declare function clearanceAllowHostsFilesFromEnvironment(): string;
7
+ export {};
8
+ //# sourceMappingURL=clearanceAllowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clearanceAllowlist.d.ts","sourceRoot":"","sources":["../../src/lib/clearanceAllowlist.ts"],"names":[],"mappings":"AAOA,UAAU,6BAA6B;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAMD,wBAAgB,6BAA6B,CAAC,KAAK,GAAE,6BAAkC,GAAG,MAAM,CAa/F;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAIhE"}
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import { splitPathList } from "./pathList.js";
3
+ import { readEnvironmentVariable } from "./util.js";
4
+ const CLEARANCE_ALLOW_HOSTS_FILES = "CLEARANCE_ALLOW_HOSTS_FILES";
5
+ function groundcrewClearanceAllowHostsFile() {
6
+ return path.resolve(import.meta.dirname, "..", "..", "clearance-allow-hosts");
7
+ }
8
+ export function clearanceAllowHostsFilesValue(input = {}) {
9
+ const defaultFile = input.defaultFile ?? groundcrewClearanceAllowHostsFile();
10
+ const files = [defaultFile, ...splitPathList(input.existingFiles)];
11
+ const seen = new Set();
12
+ const uniqueFiles = [];
13
+ for (const file of files) {
14
+ if (seen.has(file)) {
15
+ continue;
16
+ }
17
+ seen.add(file);
18
+ uniqueFiles.push(file);
19
+ }
20
+ return uniqueFiles.join(path.delimiter);
21
+ }
22
+ export function clearanceAllowHostsFilesFromEnvironment() {
23
+ return clearanceAllowHostsFilesValue({
24
+ existingFiles: readEnvironmentVariable(CLEARANCE_ALLOW_HOSTS_FILES),
25
+ });
26
+ }
@@ -14,7 +14,7 @@
14
14
  * validates.
15
15
  */
16
16
  import { readFileSync } from "node:fs";
17
- import path from "node:path";
17
+ import { splitPathList } from "./pathList.js";
18
18
  import { debug } from "./util.js";
19
19
  /**
20
20
  * Parse and validate clearance allow-host sources into a de-duplicated list of
@@ -51,15 +51,6 @@ export function collectAllowedDomains(input) {
51
51
  }
52
52
  return domains;
53
53
  }
54
- function splitPathList(value) {
55
- if (value === undefined || value.length === 0) {
56
- return [];
57
- }
58
- return value
59
- .split(path.delimiter)
60
- .map((entry) => entry.trim())
61
- .filter((entry) => entry.length > 0);
62
- }
63
54
  /**
64
55
  * Split a host source into candidate tokens. Handles env-style
65
56
  * comma/whitespace separators and file-style newline lists with `#` comments
@@ -129,14 +129,42 @@ type UserAgentDefinition = EnabledUserAgentDefinition;
129
129
  * Linear's default "In Progress" / "In Review" status names disambiguate
130
130
  * `started` workflow states; unmatched statuses fall back to `state.type`.
131
131
  */
132
+ /**
133
+ * Scripted provisioning templates for a repository. Both run via `sh -c` in
134
+ * place of the native git porcelain: `create` replaces `git worktree add`,
135
+ * `remove` replaces `git worktree remove`. Grouping them in one object makes the
136
+ * native/scripted split structural — an entry either has `provision` (scripted)
137
+ * or it doesn't (native) — and removes the need for a both-or-neither check.
138
+ */
139
+ export interface ProvisionScripts {
140
+ /** Shell template run in place of `git worktree add`. */
141
+ create: string;
142
+ /** Shell template run in place of `git worktree remove`. */
143
+ remove: string;
144
+ }
132
145
  /**
133
146
  * A configured repository. The bare-string form keeps the repo under
134
147
  * `workspace.projectDir`; the object form's optional `projectDirOverride`
135
- * overrides that parent directory so repos can live in more than one place.
148
+ * overrides that parent directory so repos can live in more than one place. When
149
+ * a `provision` block is present (a *scripted* entry), groundcrew runs its
150
+ * templates via `sh -c` in place of `git worktree add`/`remove`; the `name` is
151
+ * then a logical handle and the physical clone is the template's concern (e.g.
152
+ * graft's own registry).
136
153
  */
137
154
  export interface KnownRepository {
155
+ /** Logical repo name: the token tickets reference and the worktree dir basename. */
138
156
  name: string;
157
+ /** Overrides the parent directory the source repo lives under (defaults to `projectDir`). */
139
158
  projectDirOverride?: string;
159
+ /** Scripted provisioning templates; presence marks this a scripted entry. Mutually exclusive with `projectDirOverride`. */
160
+ provision?: ProvisionScripts;
161
+ /**
162
+ * Project subdirectory within the worktree. When set, the agent cwd, the
163
+ * `prepareWorktree` hook, and the `.groundcrew/config.json` lookup re-root to
164
+ * `<worktree>/<workdir>`. The worktree root itself (identity, sandbox access)
165
+ * is unchanged. Relative, no `..`.
166
+ */
167
+ workdir?: string;
140
168
  }
141
169
  export interface Config {
142
170
  /**
@@ -246,8 +274,10 @@ export interface ResolvedConfig {
246
274
  projectDir: string;
247
275
  /** Resolved worktree root; unset means "use projectDir". */
248
276
  worktreeDir?: string;
249
- /** Repository names only — the union's `projectDirOverride`s are lifted out. */
277
+ /** Repository names only — derived; what name-matching consumers read. */
250
278
  knownRepositories: string[];
279
+ /** Normalized full entries carrying any `projectDirOverride`/`provision`. */
280
+ repositories: KnownRepository[];
251
281
  /** name -> resolved parent dir, only for entries that override projectDir. */
252
282
  repositoryDirs?: Record<string, string>;
253
283
  };
@@ -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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"}
@@ -466,46 +466,65 @@ function normalizeSources(raw) {
466
466
  * per-repo options hang off — add new `KnownRepository` fields here.
467
467
  */
468
468
  function normalizeKnownRepository(entry, index) {
469
+ const label = `workspace.knownRepositories[${index}]`;
469
470
  if (typeof entry === "string") {
471
+ requireString(entry, label);
470
472
  return { name: entry };
471
473
  }
472
- requireObject(entry, `workspace.knownRepositories[${index}]`);
473
- requireString(entry.name, `workspace.knownRepositories[${index}].name`);
474
- if (entry.projectDirOverride === undefined) {
475
- return { name: entry.name };
474
+ requireObject(entry, label);
475
+ requireString(entry.name, `${label}.name`);
476
+ const recipe = { name: entry.name };
477
+ if (entry.projectDirOverride !== undefined) {
478
+ requireString(entry.projectDirOverride, `${label}.projectDirOverride`);
479
+ recipe.projectDirOverride = expandHome(entry.projectDirOverride);
480
+ }
481
+ if (entry.provision !== undefined) {
482
+ requireObject(entry.provision, `${label}.provision`);
483
+ const create = normalizeOptionalString(entry.provision.create, `${label}.provision.create`);
484
+ const remove = normalizeOptionalString(entry.provision.remove, `${label}.provision.remove`);
485
+ if (create === undefined || remove === undefined) {
486
+ fail(`${label}.provision must define both \`create\` and \`remove\` templates`);
487
+ }
488
+ recipe.provision = { create, remove };
489
+ }
490
+ const workdir = normalizeOptionalString(entry.workdir, `${label}.workdir`);
491
+ if (workdir !== undefined) {
492
+ recipe.workdir = workdir;
476
493
  }
477
- requireString(entry.projectDirOverride, `workspace.knownRepositories[${index}].projectDirOverride`);
478
- return { name: entry.name, projectDirOverride: expandHome(entry.projectDirOverride) };
494
+ return recipe;
479
495
  }
480
496
  /**
481
497
  * Flatten the loose `(string | KnownRepository)[]` union into the strict
482
498
  * resolved shape: a `string[]` of names every downstream consumer reads, plus
483
499
  * a separate `repositoryDirs` map holding only the entries that override
484
- * `projectDir`. Types are validated here, at the resolution edge, before any
485
- * `expandHome` runs (which would otherwise throw a raw TypeError on a
486
- * non-string `worktreeDir`).
500
+ * `projectDir`. Types are validated at the resolution edge: every path is
501
+ * `requireString`-checked before its `expandHome` runs (here for `projectDir`/
502
+ * `worktreeDir`, and in `normalizeKnownRepository` for `projectDirOverride`),
503
+ * which would otherwise throw a raw TypeError on a non-string value.
487
504
  */
488
505
  function normalizeWorkspace(workspace) {
489
506
  requireObject(workspace, "workspace");
490
507
  requireString(workspace.projectDir, "workspace.projectDir");
508
+ const entries = workspace.knownRepositories;
509
+ if (!Array.isArray(entries) || entries.length === 0) {
510
+ fail("workspace.knownRepositories must be a non-empty array");
511
+ }
512
+ const repositories = entries.map((entry, index) => normalizeKnownRepository(entry, index));
491
513
  // Track the first index each name was seen at so a duplicate (which would
492
514
  // silently overwrite its `projectDirOverride` in `repositoryDirs`) fails
493
515
  // loudly instead of resolving order-dependently.
494
516
  const seen = new Map();
495
517
  const repositoryDirs = {};
496
- const entries = Array.isArray(workspace.knownRepositories) ? workspace.knownRepositories : [];
497
- entries.forEach((entry, index) => {
498
- const { name, projectDirOverride } = normalizeKnownRepository(entry, index);
499
- const previous = seen.get(name);
518
+ repositories.forEach((recipe, index) => {
519
+ const previous = seen.get(recipe.name);
500
520
  if (previous !== undefined) {
501
- fail(`workspace.knownRepositories[${index}] duplicates ${JSON.stringify(name)} from workspace.knownRepositories[${previous}]. Configure distinct repository names.`);
521
+ fail(`workspace.knownRepositories[${index}] duplicates ${JSON.stringify(recipe.name)} from workspace.knownRepositories[${previous}]. Configure distinct repository names.`);
502
522
  }
503
- seen.set(name, index);
504
- if (projectDirOverride !== undefined) {
505
- repositoryDirs[name] = projectDirOverride;
523
+ seen.set(recipe.name, index);
524
+ if (recipe.projectDirOverride !== undefined) {
525
+ repositoryDirs[recipe.name] = recipe.projectDirOverride;
506
526
  }
507
527
  });
508
- const names = [...seen.keys()];
509
528
  let worktreeDir;
510
529
  if (workspace.worktreeDir !== undefined) {
511
530
  requireString(workspace.worktreeDir, "workspace.worktreeDir");
@@ -514,7 +533,8 @@ function normalizeWorkspace(workspace) {
514
533
  return {
515
534
  projectDir: expandHome(workspace.projectDir),
516
535
  ...(worktreeDir === undefined ? {} : { worktreeDir }),
517
- knownRepositories: names,
536
+ knownRepositories: repositories.map((recipe) => recipe.name),
537
+ repositories,
518
538
  ...(Object.keys(repositoryDirs).length === 0 ? {} : { repositoryDirs }),
519
539
  };
520
540
  }
@@ -606,12 +626,19 @@ function validate(config) {
606
626
  requireString(config.git.remote, "git.remote");
607
627
  requireString(config.git.defaultBranch, "git.defaultBranch");
608
628
  requireString(config.workspace.projectDir, "workspace.projectDir");
609
- if (!Array.isArray(config.workspace.knownRepositories) ||
610
- config.workspace.knownRepositories.length === 0) {
611
- fail("workspace.knownRepositories must be a non-empty array");
612
- }
613
- config.workspace.knownRepositories.forEach((repository, index) => {
614
- requireString(repository, `workspace.knownRepositories[${index}]`);
629
+ config.workspace.repositories.forEach((recipe, index) => {
630
+ const label = `workspace.knownRepositories[${index}]`;
631
+ if (recipe.projectDirOverride !== undefined && recipe.provision !== undefined) {
632
+ fail(`${label}.projectDirOverride cannot be combined with \`provision\`: a scripted entry has no groundcrew-managed clone, so \`projectDirOverride\` would be ignored`);
633
+ }
634
+ if (recipe.workdir !== undefined) {
635
+ if (path.isAbsolute(recipe.workdir)) {
636
+ fail(`${label}.workdir must be a relative path`);
637
+ }
638
+ if (recipe.workdir.split("/").includes("..")) {
639
+ fail(`${label}.workdir must not contain '..' segments`);
640
+ }
641
+ }
615
642
  });
616
643
  requirePositiveInt(config.orchestrator.maximumInProgress, "orchestrator.maximumInProgress");
617
644
  requirePositiveInt(config.orchestrator.pollIntervalMilliseconds, "orchestrator.pollIntervalMilliseconds");
@@ -40,6 +40,13 @@ interface LaunchCommandArguments {
40
40
  definition: AgentDefinition;
41
41
  promptFile: string;
42
42
  worktreeDir: string;
43
+ /**
44
+ * Directory the agent and prepareWorktree hook cwd into (the `cd`/`-w`
45
+ * target). Equals `worktreeDir` unless the repo recipe sets a `workdir`, in
46
+ * which case it is the subproject dir. The `{{worktree}}` template and the srt
47
+ * filesystem grants keep using `worktreeDir` (the whole checkout).
48
+ */
49
+ workingDir: string;
43
50
  /**
44
51
  * Optional path to a `KEY='value'` env file containing build-time
45
52
  * secrets (see `BUILD_SECRET_NAMES`). Sourced on the host shell before
@@ -1 +1 @@
1
- {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,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;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAqMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,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;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA6B7E"}
1
+ {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AA0MD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;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;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA6B7E"}
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { BUILD_SECRET_NAMES, hasPreLaunchEnv, } from "./config.js";
5
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
5
6
  import { shellSingleQuote } from "./shell.js";
6
7
  export { shellSingleQuote } from "./shell.js";
7
8
  /**
@@ -100,6 +101,9 @@ function unsetEnvironmentLine(names) {
100
101
  function unsetSecretsLine() {
101
102
  return unsetEnvironmentLine(BUILD_SECRET_NAMES);
102
103
  }
104
+ function safehouseClearanceWrapperCommand() {
105
+ return `CLEARANCE_ALLOW_HOSTS_FILES=${shellSingleQuote(clearanceAllowHostsFilesFromEnvironment())} ${shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH)}`;
106
+ }
103
107
  function trapCleanupLine(promptDir) {
104
108
  const cleanupCmd = `rm -rf ${shellSingleQuote(promptDir)}`;
105
109
  return `trap ${shellSingleQuote(cleanupCmd)} EXIT`;
@@ -107,12 +111,13 @@ function trapCleanupLine(promptDir) {
107
111
  /**
108
112
  * Shared head of every host-shell `&&` chain: arm the `EXIT` trap that wipes
109
113
  * `promptDir` (must come before any link that can fail, including the `cd`),
110
- * then `cd` into the worktree. Kept separate from secret sourcing so the
111
- * safehouse path can splice `preLaunch` between the `cd` and the secrets
112
- * source preLaunch must never see build-time secrets in env.
114
+ * then `cd` into the working directory (the worktree root, or its `workdir`
115
+ * subproject). Kept separate from secret sourcing so the safehouse path can
116
+ * splice `preLaunch` between the `cd` and the secrets source — preLaunch must
117
+ * never see build-time secrets in env.
113
118
  */
114
119
  function hostTrapAndCd(arguments_) {
115
- return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.worktreeDir)}`];
120
+ return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.workingDir)}`];
116
121
  }
117
122
  /**
118
123
  * Optional source-of-secrets line. Returns `[]` when no `secretsFile` is
@@ -301,7 +306,7 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
301
306
  sandboxName: "",
302
307
  });
303
308
  const lines = [
304
- ...hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir }),
309
+ ...hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir }),
305
310
  ...hostSourceSecrets(arguments_.secretsFile),
306
311
  ];
307
312
  if (arguments_.prepareWorktreeCommand !== undefined) {
@@ -362,14 +367,14 @@ function buildSafehouseLaunchCommand(arguments_) {
362
367
  const prepareWorktreeEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
363
368
  const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
364
369
  const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
365
- const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
370
+ const safehouseWrapper = safehouseClearanceWrapperCommand();
366
371
  // Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
367
372
  // has already run (line below) so the promptDir wipe is a no-op on the happy
368
373
  // path. Keeps the failure-window between shim creation and the explicit
369
374
  // post-wrap cleanup covered for both targets without an unarmed window.
370
375
  const shimAndPromptCleanup = `rm -rf "$_safehouse_shim_dir"; rm -rf ${shellSingleQuote(promptDir)}`;
371
376
  const shimAndPromptTrap = `trap ${shellSingleQuote(shimAndPromptCleanup)} EXIT`;
372
- const lines = hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir });
377
+ const lines = hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir });
373
378
  // Scrub inherited env before preLaunch (build secrets are copied out of
374
379
  // `process.env`, which the launch shell inherits), then source secrets and
375
380
  // read the staged prompt. See `hostPreLaunchSourceAndReadPrompt`.
@@ -486,7 +491,7 @@ function buildSrtLaunchCommand(arguments_) {
486
491
  const cleanup = `rm -rf ${shellSingleQuote(arguments_.srtSettingsDir)}; rm -rf ${shellSingleQuote(promptDir)}`;
487
492
  const lines = [
488
493
  `trap ${shellSingleQuote(cleanup)} EXIT`,
489
- `cd ${shellSingleQuote(arguments_.worktreeDir)}`,
494
+ `cd ${shellSingleQuote(arguments_.workingDir)}`,
490
495
  ...hostPreLaunchSourceAndReadPrompt({
491
496
  definition: arguments_.definition,
492
497
  worktreeDir: arguments_.worktreeDir,
@@ -528,6 +533,6 @@ function buildSdxLaunchCommand(arguments_) {
528
533
  if (arguments_.secretsFile !== undefined) {
529
534
  lines.push(sourceSecretsLine(arguments_.secretsFile));
530
535
  }
531
- lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.worktreeDir)} ${shellSingleQuote(arguments_.sandboxName)} sh -c ${shellSingleQuote(innerCommand)} sh "$_p"`);
536
+ lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.workingDir)} ${shellSingleQuote(arguments_.sandboxName)} sh -c ${shellSingleQuote(innerCommand)} sh "$_p"`);
532
537
  return lines.join(" && ");
533
538
  }
@@ -0,0 +1,2 @@
1
+ export declare function splitPathList(value: string | undefined): string[];
2
+ //# sourceMappingURL=pathList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathList.d.ts","sourceRoot":"","sources":["../../src/lib/pathList.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CASjE"}
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ export function splitPathList(value) {
3
+ const paths = [];
4
+ for (const entry of value?.split(path.delimiter) ?? []) {
5
+ const pathEntry = entry.trim();
6
+ if (pathEntry.length > 0) {
7
+ paths.push(pathEntry);
8
+ }
9
+ }
10
+ return paths;
11
+ }
@@ -1,4 +1,4 @@
1
- import { type AgentDefinition, type ResolvedConfig } from "./config.ts";
1
+ import type { AgentDefinition } from "./config.ts";
2
2
  export interface StagedSrtLaunch {
3
3
  /** Dedicated temp dir holding the settings files (and any relocated config home). */
4
4
  directory: string;
@@ -36,8 +36,6 @@ export interface StagedSrtLaunch {
36
36
  * command tears the whole dir down after srt exits.
37
37
  */
38
38
  export declare function buildAndStageSrtLaunch(input: {
39
- config: ResolvedConfig;
40
- repository: string;
41
39
  task: string;
42
40
  worktreeDir: string;
43
41
  definition: AgentDefinition;
@@ -1 +1 @@
1
- {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAqB,MAAM,aAAa,CAAC;AAK3F,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA8ClB"}
1
+ {"version":3,"file":"srtLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/srtLaunch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAuBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,eAAe,CA6ClB"}
@@ -1,11 +1,32 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
4
5
  import { collectAllowedDomains } from "./clearanceHosts.js";
5
- import { repositoryBaseDir } from "./config.js";
6
+ import { runCommand } from "./commandRunner.js";
6
7
  import { inferAgentCommandName } from "./launchCommand.js";
7
8
  import { agentConfigRelocation, buildSrtSettings } from "./srtPolicy.js";
8
9
  import { readEnvironmentVariable } from "./util.js";
10
+ /**
11
+ * Resolve the worktree's real git common dir — the shared `.git` the srt policy
12
+ * fences off (read-grant + narrow write-allow + write-denies). Derived from the
13
+ * worktree itself rather than assuming a `<projectDir>/<repo>/.git` clone, so
14
+ * scripted/sparse-checkout worktrees — whose checkout is owned by an external
15
+ * provisioner and whose `repo` is just an alias with no clone on disk — get the
16
+ * correct dir instead of a phantom path that breaks git access and leaves the
17
+ * real common dir unfenced. For native worktrees this returns the same
18
+ * `<projectDir>/<repo>/.git` as before. `--path-format=absolute` keeps the path
19
+ * absolute regardless of git version or cwd.
20
+ */
21
+ function resolveGitCommonDir(worktreeDir) {
22
+ return runCommand("git", [
23
+ "-C",
24
+ worktreeDir,
25
+ "rev-parse",
26
+ "--path-format=absolute",
27
+ "--git-common-dir",
28
+ ]);
29
+ }
9
30
  /**
10
31
  * Generate the srt policies for a launch and stage them, plus — for agents that
11
32
  * cannot run with a read-only config home (codex) — a relocated, writable
@@ -28,13 +49,12 @@ import { readEnvironmentVariable } from "./util.js";
28
49
  export function buildAndStageSrtLaunch(input) {
29
50
  const agent = inferAgentCommandName(input.definition.cmd);
30
51
  const homeDir = input.homeDir ?? os.homedir();
31
- const repoDir = path.resolve(repositoryBaseDir(input.config, input.repository), input.repository);
32
52
  const base = {
33
53
  worktreeDir: input.worktreeDir,
34
- gitCommonDir: path.join(repoDir, ".git"),
54
+ gitCommonDir: resolveGitCommonDir(input.worktreeDir),
35
55
  allowedDomains: collectAllowedDomains({
36
56
  hosts: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS"),
37
- files: readEnvironmentVariable("CLEARANCE_ALLOW_HOSTS_FILES"),
57
+ files: clearanceAllowHostsFilesFromEnvironment(),
38
58
  }),
39
59
  };
40
60
  const directory = mkdtempSync(path.join(os.tmpdir(), `groundcrew-srt-${input.task}-`));
@@ -30,6 +30,14 @@ export interface WorktreeSpec {
30
30
  task: string;
31
31
  }
32
32
  declare function branchNameForTask(config: ResolvedConfig, task: string): string;
33
+ /**
34
+ * The directory the agent and its setup hooks actually run in. Equals the
35
+ * worktree root unless the repo recipe sets a `workdir`, in which case it is
36
+ * `<worktreeDir>/<workdir>` — a monorepo subproject inside a sparse checkout.
37
+ * Pure path resolution; existence is enforced at create time by
38
+ * assertWorkdirPresent.
39
+ */
40
+ export declare function resolveLaunchDir(config: ResolvedConfig, repository: string, worktreeDir: string): string;
33
41
  export type WorktreeDirtiness = {
34
42
  kind: "dirty";
35
43
  modified: number;
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,EAAE,KAAK,cAAc,EAAsC,MAAM,aAAa,CAAC;AAItF,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAeD,iBAAS,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AA4QD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAEzE;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAuBD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
1
+ {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"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"}
@@ -8,11 +8,12 @@
8
8
  * the module owns creation, listing, removal, and teardown (workspace-close +
9
9
  * worktree-remove paired) so callers don't reach into git directly.
10
10
  */
11
- import { existsSync, readdirSync, rmSync } from "node:fs";
11
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
12
12
  import { userInfo } from "node:os";
13
13
  import path from "node:path";
14
+ import { applySubstitutions } from "./adapters/shell/invoke.js";
14
15
  import { runCommandAsync } from "./commandRunner.js";
15
- import { repositoryBaseDir, worktreeBaseDir } from "./config.js";
16
+ import { repositoryBaseDir, worktreeBaseDir, } from "./config.js";
16
17
  import { resolveDefaultBranch } from "./defaultBranch.js";
17
18
  import { assertPlainTaskId, isPlainTaskId } from "./taskId.js";
18
19
  import { debug, errorMessage, isVerbose } from "./util.js";
@@ -43,22 +44,88 @@ function branchPrefix(config) {
43
44
  function branchNameForTask(config, task) {
44
45
  return `${branchPrefix(config)}-${task}`;
45
46
  }
47
+ // Membership in knownRepositories is enforced by recipeFor (called first in
48
+ // basePaths), so this resolves the clone dir for a repo already known to exist
49
+ // in config and only guards against the clone being absent on disk.
46
50
  function repoDirFor(config, repository) {
47
- if (!config.workspace.knownRepositories.includes(repository)) {
48
- throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
49
- }
50
51
  const repoDir = path.resolve(repositoryBaseDir(config, repository), repository);
51
52
  if (!existsSync(repoDir)) {
52
53
  throw new Error(`Repository not found: ${repoDir}`);
53
54
  }
54
55
  return repoDir;
55
56
  }
57
+ function recipeFor(config, repository) {
58
+ const recipe = config.workspace.repositories.find((entry) => entry.name === repository);
59
+ if (recipe === undefined) {
60
+ throw new Error(`Repository "${repository}" is not in workspace.knownRepositories: ${config.workspace.knownRepositories.join(", ")}`);
61
+ }
62
+ return recipe;
63
+ }
64
+ /**
65
+ * The directory the agent and its setup hooks actually run in. Equals the
66
+ * worktree root unless the repo recipe sets a `workdir`, in which case it is
67
+ * `<worktreeDir>/<workdir>` — a monorepo subproject inside a sparse checkout.
68
+ * Pure path resolution; existence is enforced at create time by
69
+ * assertWorkdirPresent.
70
+ */
71
+ export function resolveLaunchDir(config, repository, worktreeDir) {
72
+ const recipe = recipeFor(config, repository);
73
+ return recipe.workdir === undefined ? worktreeDir : path.resolve(worktreeDir, recipe.workdir);
74
+ }
75
+ function assertWorkdirPresent(config, entry) {
76
+ const recipe = recipeFor(config, entry.repository);
77
+ if (recipe.workdir === undefined) {
78
+ return;
79
+ }
80
+ const launchDir = path.resolve(entry.dir, recipe.workdir);
81
+ if (!existsSync(launchDir) || !statSync(launchDir).isDirectory()) {
82
+ throw new Error(`Configured workdir "${recipe.workdir}" not found in worktree ${entry.dir}; the create template must produce it (looked for ${launchDir}).`);
83
+ }
84
+ }
85
+ function provisionerSubstitutions(config, arguments_) {
86
+ return {
87
+ branch: arguments_.branchName,
88
+ dir: arguments_.dir,
89
+ baseRef: `${config.git.remote}/${config.git.defaultBranch}`,
90
+ repo: arguments_.repository,
91
+ task: arguments_.task,
92
+ };
93
+ }
94
+ /**
95
+ * Runs a provisioner template (`create`/`remove`) with no timeout. Mirrors
96
+ * runLongGitCommand: under --verbose the child streams live; otherwise it is
97
+ * captured and discarded on success (failures still carry stderr via the
98
+ * thrown error).
99
+ */
100
+ async function runLongShellCommand(command, cwd, signal) {
101
+ const signalOption = signal === undefined ? {} : { signal };
102
+ if (isVerbose()) {
103
+ await runCommandAsync("sh", ["-c", command], {
104
+ cwd,
105
+ stdio: "inherit",
106
+ timeoutMs: 0,
107
+ ...signalOption,
108
+ });
109
+ return;
110
+ }
111
+ await runCommandAsync("sh", ["-c", command], {
112
+ cwd,
113
+ stdio: "captured",
114
+ timeoutMs: 0,
115
+ ...signalOption,
116
+ });
117
+ }
56
118
  function basePaths(config, repository, task) {
57
119
  // Tasks must match the same shape the worktree discovery regexes use,
58
120
  // so create()/list()/findByTask() agree on what's a valid worktree.
59
121
  // This also rejects traversal tokens before they reach path.resolve().
60
122
  assertPlainTaskId(task);
61
- const repoDir = repoDirFor(config, repository);
123
+ const recipe = recipeFor(config, repository);
124
+ // Scripted entries have no source clone — graft owns the checkout — so run
125
+ // templates with cwd = the worktree root and never resolve a clone dir.
126
+ const repoDir = recipe.provision === undefined
127
+ ? repoDirFor(config, repository)
128
+ : path.resolve(worktreeBaseDir(config));
62
129
  const hostWorktreeName = `${repository}-${task}`;
63
130
  const hostWorktreeDir = path.resolve(worktreeBaseDir(config), hostWorktreeName);
64
131
  return {
@@ -119,6 +186,24 @@ async function deleteBranchBestEffort(arguments_) {
119
186
  }
120
187
  async function createWorktree(config, spec, signal) {
121
188
  const base = basePaths(config, spec.repository, spec.task);
189
+ const recipe = recipeFor(config, spec.repository);
190
+ if (recipe.provision !== undefined) {
191
+ const command = applySubstitutions(recipe.provision.create, provisionerSubstitutions(config, {
192
+ branchName: base.branchName,
193
+ dir: base.hostWorktreeDir,
194
+ task: spec.task,
195
+ repository: spec.repository,
196
+ }));
197
+ debug(`Provisioning worktree ${spec.repository}-${spec.task} via create template...`);
198
+ await runLongShellCommand(command, base.repoDir, signal);
199
+ return {
200
+ repository: spec.repository,
201
+ task: spec.task,
202
+ branchName: base.branchName,
203
+ dir: base.hostWorktreeDir,
204
+ kind: "host",
205
+ };
206
+ }
122
207
  const defaultBranch = await resolveDefaultBranch({
123
208
  repoDir: base.repoDir,
124
209
  remote: config.git.remote,
@@ -188,6 +273,11 @@ function listWorktrees(config) {
188
273
  return entries;
189
274
  }
190
275
  async function removeWorktree(config, entry, options) {
276
+ const recipe = recipeFor(config, entry.repository);
277
+ if (recipe.provision !== undefined) {
278
+ await removeScriptedWorktree(config, entry, recipe.provision.remove, options);
279
+ return;
280
+ }
191
281
  const repoDir = path.resolve(repositoryBaseDir(config, entry.repository), entry.repository);
192
282
  if (existsSync(entry.dir)) {
193
283
  debug(`Removing worktree ${entry.dir}${options.force ? " (--force)" : ""}...`);
@@ -223,15 +313,7 @@ async function removeWorktree(config, entry, options) {
223
313
  removeOrphanWorktreeDirectory(config, entry);
224
314
  }
225
315
  else {
226
- const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
227
- if (dirtiness.kind === "dirty") {
228
- throw new Error(describeDirtyWorktree({
229
- task: entry.task,
230
- dir: entry.dir,
231
- modified: dirtiness.modified,
232
- untracked: dirtiness.untracked,
233
- }), { cause: error });
234
- }
316
+ const dirtiness = await throwIfWorktreeDirty(entry, options.signal, error);
235
317
  if (dirtiness.kind === "unknown") {
236
318
  const registration = await probeWorktreeRegistration({
237
319
  repoDir,
@@ -259,6 +341,31 @@ async function removeWorktree(config, entry, options) {
259
341
  ...signalProperty(options.signal),
260
342
  });
261
343
  }
344
+ async function removeScriptedWorktree(config, entry, removeTemplate, options) {
345
+ const worktreeRoot = path.resolve(worktreeBaseDir(config));
346
+ // A scripted worktree's teardown lives in the remove template (e.g. `graft rm`),
347
+ // which owns provisioner-side branch/metadata beyond the checkout dir. Run it
348
+ // even when the dir is already gone so that state is still cleaned up; only the
349
+ // dirtiness guard — which needs the dir to inspect — is skipped in that case.
350
+ const worktreeExists = existsSync(entry.dir);
351
+ if (worktreeExists && !options.force) {
352
+ // Keep the data-loss guard: a dirty worktree is not removed without --force.
353
+ // Fail closed when the dirtiness probe can't confirm the worktree is clean,
354
+ // so the remove template never runs over uncommitted work.
355
+ const dirtiness = await throwIfWorktreeDirty(entry, options.signal);
356
+ if (dirtiness.kind !== "clean") {
357
+ throw new Error(`Could not verify ${entry.dir} is clean; rerun with --force after manual inspection.`);
358
+ }
359
+ }
360
+ const command = applySubstitutions(removeTemplate, provisionerSubstitutions(config, {
361
+ branchName: entry.branchName,
362
+ dir: entry.dir,
363
+ task: entry.task,
364
+ repository: entry.repository,
365
+ }));
366
+ debug(`Removing worktree ${entry.dir} via remove template...`);
367
+ await runLongShellCommand(command, worktreeRoot, options.signal);
368
+ }
262
369
  async function probeWorktreeDirtiness(worktreeDir, signal) {
263
370
  let output;
264
371
  try {
@@ -285,6 +392,24 @@ async function probeWorktreeDirtiness(worktreeDir, signal) {
285
392
  }
286
393
  return { kind: "dirty", modified, untracked };
287
394
  }
395
+ /**
396
+ * Probe a worktree and, when it has uncommitted work, throw the data-loss guard
397
+ * error. Returns the dirtiness so the git-native path can still branch on
398
+ * `unknown`. `cause` chains the underlying git failure when called from a catch.
399
+ */
400
+ async function throwIfWorktreeDirty(entry, signal, cause) {
401
+ const dirtiness = await probeWorktreeDirtiness(entry.dir, signal);
402
+ if (dirtiness.kind === "dirty") {
403
+ const message = describeDirtyWorktree({
404
+ task: entry.task,
405
+ dir: entry.dir,
406
+ modified: dirtiness.modified,
407
+ untracked: dirtiness.untracked,
408
+ });
409
+ throw cause === undefined ? new Error(message) : new Error(message, { cause });
410
+ }
411
+ return dirtiness;
412
+ }
288
413
  function describeDirtyWorktree(arguments_) {
289
414
  const { task, dir, modified, untracked } = arguments_;
290
415
  const parts = [];
@@ -351,7 +476,15 @@ async function create(config, spec, signal) {
351
476
  if (existing !== undefined) {
352
477
  throw new WorktreeAlreadyExistsError(existing.dir);
353
478
  }
354
- return await createWorktree(config, spec, signal);
479
+ const entry = await createWorktree(config, spec, signal);
480
+ try {
481
+ assertWorkdirPresent(config, entry);
482
+ }
483
+ catch (error) {
484
+ await removeWorktree(config, entry, { force: true, ...signalProperty(signal) });
485
+ throw error;
486
+ }
487
+ return entry;
355
488
  }
356
489
  async function remove(config, entry, options) {
357
490
  await removeWorktree(config, entry, {
@@ -37,6 +37,42 @@ Clean up existing worktrees before switching it, or temporarily unset
37
37
  `worktreeDir` when you need `crew cleanup` to find worktrees created beside the
38
38
  repos.
39
39
 
40
+ ## Scripted Worktrees (Sparse Checkouts)
41
+
42
+ A `workspace.knownRepositories` entry can be an **object** instead of a string when you want groundcrew to provision the worktree with a custom command — for example a sparse checkout via `graft` — instead of `git worktree add`:
43
+
44
+ ```ts
45
+ workspace: {
46
+ projectDir: "~/dev/groundcrew",
47
+ knownRepositories: [
48
+ "your-org/your-repo",
49
+ {
50
+ name: "billing",
51
+ provision: {
52
+ create: "graft new ${branch} billing --from ${baseRef} --dir ${dir}",
53
+ remove: "graft rm ${branch} -f",
54
+ },
55
+ workdir: "services/billing",
56
+ },
57
+ ],
58
+ },
59
+ ```
60
+
61
+ - **`name`** is a logical name — it is the token matched in task descriptions and the basename of the per-task worktree directory. The physical clone is the command's concern, so several scripted entries can share one underlying clone.
62
+ - **`provision`** marks the entry as scripted. **`provision.create`** runs in place of `git worktree add`; **`provision.remove`** runs in place of `git worktree remove`. Both are required — a `provision` block missing either is rejected at config load. `projectDirOverride` (the per-repo source-directory override for native entries) cannot be combined with `provision`; a scripted entry has no groundcrew-managed clone, so it is rejected at config load.
63
+ - Both templates run on the host via `sh -c` with the working directory set to the worktree root (`worktreeDir`, defaulting to `projectDir`). They interpolate `${branch}`, `${dir}`, `${baseRef}` (`<remote>/<defaultBranch>`), `${repo}`, and `${task}`; each value is shell-quoted.
64
+ - **`workdir`** (optional) is a relative subdirectory of the worktree. When set, the agent's working directory, the `prepareWorktree` hook, and the `.groundcrew/config.json` lookup all re-root to `<worktree>/<workdir>` — use it when a sparse checkout materializes a monorepo whose project lives in a subdirectory (e.g. `uv sync` must run there). The worktree root is unchanged: it is still what create/remove/list operate on, and a sandboxed agent keeps full read/write access to the whole checkout. `workdir` must be relative with no `..` segments; if it is missing after checkout, `crew` fails fast.
65
+ - A dirty scripted worktree is still protected from data loss: `crew cleanup` refuses to run `remove` unless you pass `--force`. Orphan and branch cleanup are delegated to your `remove` template, since groundcrew does not track the scripted clone.
66
+
67
+ Set `graft` (or whatever tool your templates call) up once, outside groundcrew:
68
+
69
+ ```bash
70
+ graft repo add ~/dev/owner/monorepo
71
+ graft alias add billing services/billing libs/common
72
+ ```
73
+
74
+ `crew doctor` does not parse or validate provisioner templates; if your setup is more than a simple command, put it in a wrapper script and call that from `provision.create` / `provision.remove`.
75
+
40
76
  ## Config Discovery
41
77
 
42
78
  Resolution order:
package/docs/runners.md CHANGED
@@ -13,19 +13,21 @@
13
13
 
14
14
  ## Safehouse Clearance Allowlist
15
15
 
16
- Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Clearance refuses to start without an allowlist.
16
+ Only applies when `local.runner` resolves to `safehouse`. Groundcrew starts `clearance` on `http://127.0.0.1:19999` and runs the agent through the bundled `safehouse-clearance` wrapper. Groundcrew automatically points clearance at its shipped starter allowlist, so a fresh install does not need a `CLEARANCE_ALLOW_HOSTS_FILES` export.
17
17
 
18
- Shortest path:
18
+ Groundcrew ships that starter file at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`, covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling.
19
+
20
+ To add ad hoc hosts for one run, use `CLEARANCE_ALLOW_HOSTS`:
19
21
 
20
22
  ```bash
21
23
  CLEARANCE_ALLOW_HOSTS="api.openai.com,auth.openai.com,api.anthropic.com,mcp.linear.app,api.linear.app" \
22
24
  crew run --watch
23
25
  ```
24
26
 
25
- Groundcrew ships a starter file covering model APIs, Linear, Notion, Slack, Datadog, GitHub, npm, and common dev tooling at `$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts`. Point clearance at it, optionally with a personal file:
27
+ To keep personal hosts in a file, set `CLEARANCE_ALLOW_HOSTS_FILES` to only the additional files. Groundcrew prepends its shipped file automatically:
26
28
 
27
29
  ```bash
28
- CLEARANCE_ALLOW_HOSTS_FILES="$(npm root -g)/@clipboard-health/groundcrew/clearance-allow-hosts:$HOME/.config/clearance/personal-allow-hosts" \
30
+ CLEARANCE_ALLOW_HOSTS_FILES="$HOME/.config/clearance/personal-allow-hosts" \
29
31
  crew run --watch
30
32
  ```
31
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.30.0",
3
+ "version": "4.31.1",
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",
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "dependencies": {
71
71
  "@anthropic-ai/sandbox-runtime": "0.0.52",
72
- "@clipboard-health/clearance": "1.1.14",
72
+ "@clipboard-health/clearance": "1.2.0",
73
73
  "@linear/sdk": "86.0.0",
74
74
  "cosmiconfig": "9.0.1",
75
75
  "tslib": "2.8.1",