@clipboard-health/groundcrew 4.47.6 → 4.48.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.
Files changed (56) hide show
  1. package/README.md +1 -0
  2. package/crew.config.example.ts +2 -2
  3. package/dist/commands/doctor.js +0 -25
  4. package/dist/commands/openWorkspace.d.ts.map +1 -1
  5. package/dist/commands/openWorkspace.js +25 -18
  6. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  7. package/dist/commands/resumeWorkspace.js +7 -15
  8. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  9. package/dist/commands/setupWorkspace.js +23 -20
  10. package/dist/lib/agentLaunch.d.ts +14 -11
  11. package/dist/lib/agentLaunch.d.ts.map +1 -1
  12. package/dist/lib/agentLaunch.js +102 -51
  13. package/dist/lib/cmuxAgentHookInstall.d.ts +17 -0
  14. package/dist/lib/cmuxAgentHookInstall.d.ts.map +1 -0
  15. package/dist/lib/cmuxAgentHookInstall.js +42 -0
  16. package/dist/lib/codexConfigRelocation.d.ts +60 -0
  17. package/dist/lib/codexConfigRelocation.d.ts.map +1 -0
  18. package/dist/lib/codexConfigRelocation.js +70 -0
  19. package/dist/lib/commandRunner.d.ts +9 -0
  20. package/dist/lib/commandRunner.d.ts.map +1 -1
  21. package/dist/lib/commandRunner.js +1 -0
  22. package/dist/lib/config.d.ts +31 -12
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +18 -2
  25. package/dist/lib/gitCommonDir.d.ts +7 -0
  26. package/dist/lib/gitCommonDir.d.ts.map +1 -0
  27. package/dist/lib/gitCommonDir.js +15 -0
  28. package/dist/lib/host.d.ts +0 -14
  29. package/dist/lib/host.d.ts.map +1 -1
  30. package/dist/lib/host.js +1 -8
  31. package/dist/lib/launchCommand.d.ts +39 -51
  32. package/dist/lib/launchCommand.d.ts.map +1 -1
  33. package/dist/lib/launchCommand.js +117 -183
  34. package/dist/lib/localRunner.d.ts +4 -5
  35. package/dist/lib/localRunner.d.ts.map +1 -1
  36. package/dist/lib/localRunner.js +4 -23
  37. package/dist/lib/repositoryHooks.js +13 -0
  38. package/docs/configuration.md +3 -3
  39. package/docs/credentials.md +8 -1
  40. package/docs/runners.md +10 -49
  41. package/docs/setup-hooks.md +39 -0
  42. package/docs/spec/e2e-scenario-catalog.md +164 -0
  43. package/docs/spec/v2-design.md +353 -0
  44. package/docs/task-sources.md +3 -3
  45. package/package.json +10 -13
  46. package/static/demo.tape +0 -2
  47. package/static/render-demo.sh +0 -1
  48. package/dist/lib/clearanceHosts.d.ts +0 -31
  49. package/dist/lib/clearanceHosts.d.ts.map +0 -1
  50. package/dist/lib/clearanceHosts.js +0 -96
  51. package/dist/lib/srtLaunch.d.ts +0 -61
  52. package/dist/lib/srtLaunch.d.ts.map +0 -1
  53. package/dist/lib/srtLaunch.js +0 -107
  54. package/dist/lib/srtPolicy.d.ts +0 -118
  55. package/dist/lib/srtPolicy.d.ts.map +0 -1
  56. package/dist/lib/srtPolicy.js +0 -371
package/README.md CHANGED
@@ -159,6 +159,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
159
159
  - [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
160
160
  - [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
161
161
  - [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
162
+ - [Operator host setup](./docs/setup-hooks.md#unsandboxedhooks-operator-only-host-setup): `crew.config.ts` `knownRepositories[].unsandboxedHooks.prepareWorktree` runs trusted, per-repo setup on the host outside the sandbox.
162
163
  - [Task sources](./docs/task-sources.md): custom shell/Jira/local-plan adapters.
163
164
  - [Development](./docs/development.md): local source workflow and README/demo asset regeneration.
164
165
  - [Troubleshooting](./docs/troubleshooting.md): common operational pitfalls and fixes.
@@ -131,7 +131,7 @@ export default {
131
131
  // // Install via task-sources/jira (see task-sources/jira/README.md):
132
132
  // // cp task-sources/jira/jira.sh ~/.config/groundcrew/jira.sh
133
133
  // // Open local task-store directories for read/write inside the
134
- // // safehouse/srt sandbox when this source owns the launched task.
134
+ // // safehouse sandbox when this source owns the launched task.
135
135
  // sandboxWritePaths: ["~/plans"],
136
136
  // commands: {
137
137
  // verify: "~/.config/groundcrew/jira.sh verify",
@@ -179,7 +179,7 @@ export default {
179
179
  // // deny-by-default policy. Examples: `agent-browser` so the
180
180
  // // chrome-devtools MCP server can drive a headless Chrome;
181
181
  // // `browser-native-messaging` for `claude --chrome`. Ignored by the
182
- // // srt/sdx/none runners.
182
+ // // sdx/none runners.
183
183
  // local: { safehouse: { enable: ["agent-browser"] } },
184
184
  //
185
185
  // // Groundcrew does not create or authenticate sdx sandboxes. For an sdx
@@ -343,22 +343,6 @@ function localCapabilityCheck(host, resolved) {
343
343
  : "sdx runner requires `sbx` (Docker Sandboxes) on PATH (install from https://docs.docker.com/ai/sandboxes/)",
344
344
  };
345
345
  }
346
- if (resolved === "srt") {
347
- const missingLinuxDeps = host.isLinux
348
- ? [
349
- host.hasBubblewrap ? undefined : "bubblewrap",
350
- host.hasSocat ? undefined : "socat",
351
- host.hasRipgrep ? undefined : "ripgrep (rg)",
352
- ].filter((name) => name !== undefined)
353
- : [];
354
- const ok = host.isSrtSupported && missingLinuxDeps.length === 0;
355
- return {
356
- name: "local runner (srt)",
357
- ok,
358
- required: false,
359
- hint: srtRunnerHint(ok, host.isSrtSupported, missingLinuxDeps),
360
- };
361
- }
362
346
  // resolved === "none"
363
347
  return {
364
348
  name: "local runner (none)",
@@ -367,15 +351,6 @@ function localCapabilityCheck(host, resolved) {
367
351
  hint: "WARNING: local.runner='none' — agent runs unsandboxed on the host. Only use this when you understand the implications.",
368
352
  };
369
353
  }
370
- function srtRunnerHint(ok, isSrtSupported, missingLinuxDeps) {
371
- if (ok) {
372
- return "ready (beta: @anthropic-ai/sandbox-runtime is a research preview)";
373
- }
374
- if (!isSrtSupported) {
375
- return "srt runner requires macOS or Linux/WSL";
376
- }
377
- return `srt runner on Linux requires ${missingLinuxDeps.join(", ")} on PATH (Debian/Ubuntu: \`apt install bubblewrap socat ripgrep\`; on Ubuntu 24.04+ also \`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\`)`;
378
- }
379
354
  function reportLocalCapability(arguments_) {
380
355
  writeOutput();
381
356
  writeOutput("Local runner");
@@ -1 +1 @@
1
- {"version":3,"file":"openWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/openWorkspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAetF,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA6JD,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA8Gf;AA4ID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAE3E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
1
+ {"version":3,"file":"openWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/openWorkspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAgBtF,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAwJD,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA2Hf;AA4ID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAE3E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
4
  import { inferAgentCommandName } from "../lib/launchCommand.js";
@@ -7,7 +7,7 @@ import { resolvePullRequest } from "../lib/pullRequests.js";
7
7
  import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
8
8
  import { recordRunState, readRunState } from "../lib/runState.js";
9
9
  import { seedLaunchWorkspaceTrust } from "../lib/seedLaunchWorkspaceTrust.js";
10
- import { stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
10
+ import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
11
11
  import { normalizePlainTaskId } from "../lib/taskId.js";
12
12
  import { debug, errorMessage, log, okMark } from "../lib/util.js";
13
13
  import { failIfWorkspaceAlreadyLive } from "../lib/workspaceLiveness.js";
@@ -106,15 +106,11 @@ async function rollback(arguments_) {
106
106
  catch (error) {
107
107
  log(`Worktree teardown failed during rollback: ${errorMessage(error)}`);
108
108
  }
109
- for (const dir of [arguments_.promptDir, arguments_.srtSettingsDir]) {
110
- if (dir !== undefined) {
111
- try {
112
- rmSync(dir, { recursive: true, force: true });
113
- }
114
- catch {
115
- // already gone
116
- }
117
- }
109
+ try {
110
+ removeStagedPrompt(arguments_.promptDir);
111
+ }
112
+ catch {
113
+ // already gone
118
114
  }
119
115
  }
120
116
  export async function openWorkspace(config, options) {
@@ -154,19 +150,27 @@ export async function openWorkspace(config, options) {
154
150
  task: target.task,
155
151
  text: options.promptText ?? "",
156
152
  });
157
- let srtSettingsDir;
153
+ let cleanupAgentLaunch;
158
154
  try {
155
+ const repositoryEntry = config.workspace.repositories.find((entry) => entry.name === repository);
159
156
  const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
160
157
  worktreeDir: launchDir,
158
+ // Spread-conditional: exactOptionalPropertyTypes forbids an explicit
159
+ // `undefined` for an optional field, and the lookup yields undefined for
160
+ // repos with no hooks. Mirrors setupWorkspace so `crew open` honors the
161
+ // same per-repo operator hooks as `crew setup`.
162
+ ...(repositoryEntry?.hooks === undefined ? {} : { perRepoHooks: repositoryEntry.hooks }),
161
163
  defaultHooks: config.defaults.hooks,
162
164
  });
163
- const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(stagedPrompt.directory);
165
+ const prepareWorktreeUnsandboxedCommand = repositoryEntry?.unsandboxedHooks?.prepareWorktree;
166
+ const secretsFile = prepareWorktreeCommand === undefined && prepareWorktreeUnsandboxedCommand === undefined
167
+ ? undefined
168
+ : stageBuildSecrets(stagedPrompt.directory);
164
169
  seedLaunchWorkspaceTrust({
165
170
  agentCommandName: inferAgentCommandName(definition.cmd),
166
171
  launchDir,
167
172
  });
168
- let launchCommand;
169
- ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
173
+ const launch = composeAgentLaunch({
170
174
  runner,
171
175
  networkEgress,
172
176
  task: target.task,
@@ -176,12 +180,14 @@ export async function openWorkspace(config, options) {
176
180
  workingDir: launchDir,
177
181
  secretsFile,
178
182
  prepareWorktreeCommand,
183
+ prepareWorktreeUnsandboxedCommand,
179
184
  sandboxName,
180
185
  workspaceKind,
181
186
  readOnlyDirs: config.local.readOnlyDirs,
182
187
  omitPromptArgument,
183
- }));
184
- const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
188
+ });
189
+ cleanupAgentLaunch = launch.cleanup;
190
+ const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launch.command);
185
191
  await openAgentWorkspace({
186
192
  config,
187
193
  name: target.task,
@@ -193,7 +199,8 @@ export async function openWorkspace(config, options) {
193
199
  });
194
200
  }
195
201
  catch (error) {
196
- await rollback({ config, entry: created, promptDir: stagedPrompt.directory, srtSettingsDir });
202
+ cleanupAgentLaunch?.();
203
+ await rollback({ config, entry: created, promptDir: stagedPrompt.directory });
197
204
  throw error;
198
205
  }
199
206
  recordRunState({
@@ -1 +1 @@
1
- {"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAsBzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyKD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAyGf;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,EAAoC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAsBzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyKD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAiGf;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
@@ -170,14 +170,9 @@ export async function resumeWorkspace(config, options) {
170
170
  text: renderResumePrompt(context),
171
171
  });
172
172
  const secretsFile = stageBuildSecrets(stagedPrompt.directory);
173
- // Resume stages srt settings exactly like setup (a relocating agent such as
174
- // codex needs its config home re-seeded to authenticate on the resumed launch).
175
- // Composition runs inside the try so a pre-launch failure still cleans up the
176
- // staged prompt (and any srt settings) dir.
177
- let srtSettingsDir;
173
+ let cleanupAgentLaunch;
178
174
  try {
179
- let launchCommand;
180
- const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
175
+ const taskSourceWritePaths = runner === "safehouse"
181
176
  ? taskSourceWritePathsForCompletion({
182
177
  config,
183
178
  taskId: context.completionTaskId,
@@ -188,7 +183,7 @@ export async function resumeWorkspace(config, options) {
188
183
  agentCommandName: inferAgentCommandName(launchDefinition.cmd),
189
184
  launchDir,
190
185
  });
191
- ({ launchCommand, srtSettingsDir } = composeAgentLaunch({
186
+ const launch = composeAgentLaunch({
192
187
  runner,
193
188
  networkEgress,
194
189
  task,
@@ -206,8 +201,9 @@ export async function resumeWorkspace(config, options) {
206
201
  }),
207
202
  taskSourceWritePaths,
208
203
  safehouseEnableFeatures: config.local.safehouse.enable,
209
- }));
210
- const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launchCommand);
204
+ });
205
+ cleanupAgentLaunch = launch.cleanup;
206
+ const launchCmd = stageWorkspaceLaunchCommand(stagedPrompt.directory, launch.command);
211
207
  await openAgentWorkspace({
212
208
  config,
213
209
  name: task,
@@ -219,12 +215,8 @@ export async function resumeWorkspace(config, options) {
219
215
  });
220
216
  }
221
217
  catch (error) {
218
+ cleanupAgentLaunch?.();
222
219
  removeStagedPrompt(stagedPrompt.directory);
223
- // The launch command tears down the settings dir after srt exits; on the
224
- // pre-launch failure path it never ran, so clean it up here.
225
- if (srtSettingsDir !== undefined) {
226
- removeStagedPrompt(srtSettingsDir);
227
- }
228
220
  throw error;
229
221
  }
230
222
  recordRunState({
@@ -1 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA0BnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAsJf;AA8MD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA2BnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA4Jf;AA2MD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -1,4 +1,3 @@
1
- import { rmSync } from "node:fs";
2
1
  import { loadConfig } from "../lib/config.js";
3
2
  import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
4
3
  import { inferAgentCommandName, workerEnvironmentForTask } from "../lib/launchCommand.js";
@@ -8,7 +7,7 @@ import { resolvePrepareWorktreeCommand } from "../lib/repositoryHooks.js";
8
7
  import { recordRunState } from "../lib/runState.js";
9
8
  import { seedLaunchWorkspaceTrust } from "../lib/seedLaunchWorkspaceTrust.js";
10
9
  import { sourceSupportsMarkDone } from "../lib/sourceCapabilities.js";
11
- import { stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
10
+ import { removeStagedPrompt, stageBuildSecrets, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
12
11
  import { taskSourceWritePathsForCompletion } from "../lib/taskSourceFilesystem.js";
13
12
  import { naturalIdFromCanonical } from "../lib/taskSource.js";
14
13
  import { debug, errorMessage, log, okMark } from "../lib/util.js";
@@ -72,7 +71,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
72
71
  // Without rollback the next tick hits "Worktree already exists" and
73
72
  // the task strands forever.
74
73
  let promptDir;
75
- let srtSettingsDir;
74
+ let cleanupAgentLaunch;
76
75
  try {
77
76
  await assertLaunchReady(readinessPromise);
78
77
  const taskDetails = options.details;
@@ -85,7 +84,9 @@ export async function setupWorkspace(config, options, runOptions = {}) {
85
84
  workspaceContinuationInstruction: renderWorkspaceContinuationInstruction(accessHint),
86
85
  });
87
86
  promptDir = stagedPrompt.directory;
88
- const perRepoHooks = config.workspace.repositories.find((entry) => entry.name === repository)?.hooks;
87
+ const repositoryEntry = config.workspace.repositories.find((entry) => entry.name === repository);
88
+ const perRepoHooks = repositoryEntry?.hooks;
89
+ const prepareWorktreeUnsandboxedCommand = repositoryEntry?.unsandboxedHooks?.prepareWorktree;
89
90
  const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
90
91
  worktreeDir: launchDir,
91
92
  // Spread-conditional rather than a direct assignment: under
@@ -94,10 +95,12 @@ export async function setupWorkspace(config, options, runOptions = {}) {
94
95
  ...(perRepoHooks === undefined ? {} : { perRepoHooks }),
95
96
  defaultHooks: config.defaults.hooks,
96
97
  });
97
- const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
98
+ const secretsFile = prepareWorktreeCommand === undefined && prepareWorktreeUnsandboxedCommand === undefined
99
+ ? undefined
100
+ : stageBuildSecrets(promptDir);
98
101
  const completionTaskId = options.completionTaskId ?? task;
99
102
  const completionMarkDoneSupported = options.completionMarkDoneSupported ?? true;
100
- const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
103
+ const taskSourceWritePaths = runner === "safehouse"
101
104
  ? taskSourceWritePathsForCompletion({
102
105
  config,
103
106
  taskId: completionTaskId,
@@ -108,7 +111,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
108
111
  agentCommandName: inferAgentCommandName(definition.cmd),
109
112
  launchDir,
110
113
  });
111
- const { launchCommand, srtSettingsDir: stagedSrtSettingsDir } = composeAgentLaunch({
114
+ const launch = composeAgentLaunch({
112
115
  runner,
113
116
  networkEgress,
114
117
  task,
@@ -118,6 +121,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
118
121
  workingDir: launchDir,
119
122
  secretsFile,
120
123
  prepareWorktreeCommand,
124
+ prepareWorktreeUnsandboxedCommand,
121
125
  sandboxName,
122
126
  workspaceKind,
123
127
  readOnlyDirs: config.local.readOnlyDirs,
@@ -128,8 +132,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
128
132
  taskSourceWritePaths,
129
133
  safehouseEnableFeatures: config.local.safehouse.enable,
130
134
  });
131
- srtSettingsDir = stagedSrtSettingsDir;
132
- const launchCmd = stageWorkspaceLaunchCommand(promptDir, launchCommand);
135
+ cleanupAgentLaunch = launch.cleanup;
136
+ const launchCmd = stageWorkspaceLaunchCommand(promptDir, launch.command);
133
137
  debug("Opening workspace...");
134
138
  await openAgentWorkspace({
135
139
  config,
@@ -162,7 +166,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
162
166
  }
163
167
  }
164
168
  catch (error) {
165
- await rollbackWorktree({ config, entry: created, promptDir, srtSettingsDir });
169
+ cleanupAgentLaunch?.();
170
+ await rollbackWorktree({ config, entry: created, promptDir });
166
171
  recordFailedToLaunch({ config, options, paths: { worktreeDir, branchName }, error });
167
172
  throw error;
168
173
  }
@@ -295,16 +300,14 @@ async function rollbackWorktree(arguments_) {
295
300
  log(`Worktree teardown failed during rollback: ${errorMessage(error)}`);
296
301
  }
297
302
  finally {
298
- // Both temp dirs are normally removed by the launch command; clean them
299
- // here for the pre-launch failure path. Silent on retry races.
300
- for (const dir of [arguments_.promptDir, arguments_.srtSettingsDir]) {
301
- if (dir !== undefined) {
302
- try {
303
- rmSync(dir, { recursive: true, force: true });
304
- }
305
- catch {
306
- // already gone
307
- }
303
+ // The prompt dir is normally removed by the launch command; clean it here
304
+ // for the pre-launch failure path. Silent on retry races.
305
+ if (arguments_.promptDir !== undefined) {
306
+ try {
307
+ removeStagedPrompt(arguments_.promptDir);
308
+ }
309
+ catch {
310
+ // already gone
308
311
  }
309
312
  }
310
313
  }
@@ -1,13 +1,11 @@
1
1
  import { type LocalRunner, type AgentDefinition, type NetworkEgressSetting, type ResolvedConfig } from "./config.ts";
2
2
  import { type WorkerEnvironment } from "./launchCommand.ts";
3
3
  import type { WorkspaceKind } from "./workspaceAdapter.ts";
4
- /**
5
- * Stage any srt settings and build the workspace launch command — the assembly
6
- * shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
7
- * (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
8
- * `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
9
- * Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
10
- */
4
+ export interface ComposedAgentLaunch {
5
+ command: string;
6
+ cleanup: () => void;
7
+ }
8
+ /** Build the workspace launch command shared by fresh runs and resumes. */
11
9
  export declare function composeAgentLaunch(input: {
12
10
  runner: LocalRunner;
13
11
  networkEgress: NetworkEgressSetting;
@@ -18,6 +16,7 @@ export declare function composeAgentLaunch(input: {
18
16
  workingDir: string;
19
17
  secretsFile?: string | undefined;
20
18
  prepareWorktreeCommand?: string | undefined;
19
+ prepareWorktreeUnsandboxedCommand?: string | undefined;
21
20
  sandboxName?: string | undefined;
22
21
  workspaceKind: WorkspaceKind;
23
22
  workerEnvironment?: WorkerEnvironment | undefined;
@@ -25,10 +24,14 @@ export declare function composeAgentLaunch(input: {
25
24
  taskSourceWritePaths?: readonly string[] | undefined;
26
25
  safehouseEnableFeatures?: readonly string[] | undefined;
27
26
  readOnlyDirs?: readonly string[] | undefined;
28
- }): {
29
- launchCommand: string;
30
- srtSettingsDir: string | undefined;
31
- };
27
+ /**
28
+ * Test-only seam: overrides `os.homedir()` when staging a relocated agent
29
+ * config home (codex's `CODEX_HOME` under safehouse). Production callers
30
+ * must omit this so specs can seed a fixture home instead of touching the
31
+ * real `~/.codex`.
32
+ */
33
+ homeDir?: string;
34
+ }): ComposedAgentLaunch;
32
35
  interface PreparedAgentLaunch {
33
36
  runner: LocalRunner;
34
37
  /** Resolved `config.local.networkEgress`, threaded into `composeAgentLaunch`. */
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,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;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACxD,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9C,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CA2ChE;AA4DD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,oBAAoB,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,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,CAsD/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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,CAahB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAcA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAW5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,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,iCAAiC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACxD,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,mBAAmB,CAgDtB;AAkHD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,oBAAoB,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,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,CAsD/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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,CAahB"}
@@ -1,60 +1,65 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
  import { ensureClearance, resolveSafehouseCmuxIntegration, safehouseCmuxIntegrationWarningLines, } from "@clipboard-health/clearance";
3
5
  import { clearanceAllowHostsFilesFromEnvironment } from "./clearanceAllowlist.js";
6
+ import { installCmuxAgentHooks } from "./cmuxAgentHookInstall.js";
4
7
  import { cmuxAgentHookSettingsJson } from "./cmuxAgentHooks.js";
5
8
  import { shellSingleQuote } from "./shell.js";
6
9
  import { hasPreLaunchEnv, } from "./config.js";
7
10
  import { detectHostCapabilities } from "./host.js";
8
11
  import { buildLaunchCommand, inferAgentCommandName, } from "./launchCommand.js";
12
+ import { agentConfigRelocation, stageRelocatedAgentConfigHome, } from "./codexConfigRelocation.js";
13
+ import { resolveGitCommonDir } from "./gitCommonDir.js";
9
14
  import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
10
15
  import { sandboxNameFor } from "./sandboxName.js";
11
- import { buildAndStageSrtLaunch, resolveGitCommonDir } from "./srtLaunch.js";
12
- import { debug, sleep, writeError } from "./util.js";
16
+ import { debug, readEnvironmentVariable, sleep, writeError } from "./util.js";
13
17
  import { resolveWorkspaceKind, workspaces } from "./workspaces.js";
14
- /**
15
- * Stage any srt settings and build the workspace launch command — the assembly
16
- * shared verbatim by `setupWorkspace` (fresh runs) and `resumeWorkspace`
17
- * (resumes). `worktreeDir` is the checkout root (srt grants + `{{worktree}}`);
18
- * `workingDir` is the agent cwd (the worktree root, or its `workdir` subproject).
19
- * Returns `srtSettingsDir` so callers can tear it down on a pre-launch failure.
20
- */
18
+ /** Build the workspace launch command shared by fresh runs and resumes. */
21
19
  export function composeAgentLaunch(input) {
22
- const staged = input.runner === "srt"
23
- ? buildAndStageSrtLaunch({
24
- task: input.task,
25
- worktreeDir: input.worktreeDir,
20
+ const homeDir = input.homeDir ?? os.homedir();
21
+ const safehouseAgentIntegration = input.runner === "safehouse"
22
+ ? safehouseAgentIntegrationFor({
23
+ workspaceKind: input.workspaceKind,
26
24
  definition: input.definition,
27
- taskSourceWritePaths: input.taskSourceWritePaths,
28
- readOnlyDirs: input.readOnlyDirs,
25
+ task: input.task,
26
+ homeDir,
29
27
  })
30
28
  : undefined;
31
- const safehouseAgentIntegration = input.runner === "safehouse"
32
- ? safehouseAgentIntegrationFor(input.workspaceKind, input.definition)
33
- : undefined;
34
- const launchCommand = buildLaunchCommand({
35
- definition: input.definition,
36
- promptFile: input.promptFile,
37
- worktreeDir: input.worktreeDir,
38
- workingDir: input.workingDir,
39
- secretsFile: input.secretsFile,
40
- prepareWorktreeCommand: input.prepareWorktreeCommand,
41
- runner: input.runner,
42
- networkEgress: input.networkEgress,
43
- sandboxName: input.sandboxName,
44
- srtPrepareSettingsFile: staged?.prepareFile,
45
- srtAgentSettingsFile: staged?.agentFile,
46
- srtSettingsDir: staged?.directory,
47
- srtAgentConfigDirEnv: staged?.agentConfigDirEnv,
48
- workerEnvironment: input.workerEnvironment,
49
- omitPromptArgument: input.omitPromptArgument,
50
- safehouseAddDirs: input.runner === "safehouse" ? resolveSafehouseAddDirs(input.worktreeDir) : undefined,
51
- safehouseAgentAddDirs: input.runner === "safehouse" ? (input.taskSourceWritePaths ?? []) : undefined,
52
- safehouseEnableFeatures: input.runner === "safehouse" ? input.safehouseEnableFeatures : undefined,
53
- // Safehouse rejects nonexistent --add-dirs-ro paths, so drop absent ones.
54
- safehouseAgentAddDirsReadOnly: input.runner === "safehouse" ? (input.readOnlyDirs ?? []).filter(existsSync) : undefined,
55
- safehouseAgentIntegration,
56
- });
57
- return { launchCommand, srtSettingsDir: staged?.directory };
29
+ function cleanup() {
30
+ for (const teardownPath of safehouseAgentIntegration?.teardownPaths ?? []) {
31
+ rmSync(teardownPath, { recursive: true, force: true });
32
+ }
33
+ }
34
+ try {
35
+ return {
36
+ command: buildLaunchCommand({
37
+ definition: input.definition,
38
+ promptFile: input.promptFile,
39
+ worktreeDir: input.worktreeDir,
40
+ workingDir: input.workingDir,
41
+ secretsFile: input.secretsFile,
42
+ prepareWorktreeCommand: input.prepareWorktreeCommand,
43
+ prepareWorktreeUnsandboxedCommand: input.prepareWorktreeUnsandboxedCommand,
44
+ runner: input.runner,
45
+ networkEgress: input.networkEgress,
46
+ sandboxName: input.sandboxName,
47
+ workerEnvironment: input.workerEnvironment,
48
+ omitPromptArgument: input.omitPromptArgument,
49
+ safehouseAddDirs: input.runner === "safehouse" ? resolveSafehouseAddDirs(input.worktreeDir) : undefined,
50
+ safehouseAgentAddDirs: input.runner === "safehouse" ? (input.taskSourceWritePaths ?? []) : undefined,
51
+ safehouseEnableFeatures: input.runner === "safehouse" ? input.safehouseEnableFeatures : undefined,
52
+ // Safehouse rejects nonexistent --add-dirs-ro paths, so drop absent ones.
53
+ safehouseAgentAddDirsReadOnly: input.runner === "safehouse" ? (input.readOnlyDirs ?? []).filter(existsSync) : undefined,
54
+ safehouseAgentIntegration,
55
+ }),
56
+ cleanup,
57
+ };
58
+ }
59
+ catch (error) {
60
+ cleanup();
61
+ throw error;
62
+ }
58
63
  }
59
64
  /**
60
65
  * Filesystem paths the safehouse sandbox must be granted (read/write) beyond
@@ -67,28 +72,38 @@ export function composeAgentLaunch(input) {
67
72
  * whose store lives outside the worktree tree (e.g. graft's `~/carrot/.git`)
68
73
  * gets git access. This is the path the bare cwd grant fundamentally cannot
69
74
  * cover, and the reason this resolution exists.
70
- * Gated to the safehouse runner at the call site (srt fences its own equivalent
71
- * surface worktree root + git common dir through its settings file; sdx/none
72
- * don't use it). Deduped defensively in case git resolves either path to the
73
- * same directory in an unusual checkout shape.
75
+ * Gated to the safehouse runner at the call site. Deduped defensively in case
76
+ * git resolves either path to the same directory in an unusual checkout shape.
74
77
  */
75
78
  function resolveSafehouseAddDirs(worktreeDir) {
76
79
  return [...new Set([worktreeDir, resolveGitCommonDir(worktreeDir)])];
77
80
  }
78
- function safehouseAgentIntegrationFor(workspaceKind, definition) {
79
- if (workspaceKind !== "cmux") {
81
+ function safehouseAgentIntegrationFor(input) {
82
+ if (input.workspaceKind !== "cmux") {
80
83
  return undefined;
81
84
  }
82
- const agentCommandName = inferAgentCommandName(definition.cmd);
85
+ const agentCommandName = inferAgentCommandName(input.definition.cmd);
83
86
  const isClaudeAgent = agentCommandName === "claude";
84
87
  const cmuxIntegration = resolveSafehouseCmuxIntegration();
85
88
  if (isClaudeAgent) {
86
89
  warnOnCmuxIntegrationDrift({ unreviewedEnvNames: cmuxIntegration.unreviewedEnvNames });
87
90
  }
91
+ const relocatedCmuxHooksHome = stageSafehouseCmuxHooksHome({
92
+ agent: agentCommandName,
93
+ task: input.task,
94
+ homeDir: input.homeDir,
95
+ });
88
96
  return {
89
97
  addDirsReadOnly: cmuxIntegration.addDirsReadOnly,
90
98
  envPass: cmuxIntegration.envPass,
91
- commandPreludes: isClaudeAgent ? [cmuxIntegration.claudeCommandPrelude] : [],
99
+ commandPreludes: [
100
+ ...(isClaudeAgent ? [cmuxIntegration.claudeCommandPrelude] : []),
101
+ ...(relocatedCmuxHooksHome === undefined
102
+ ? []
103
+ : [
104
+ `export ${relocatedCmuxHooksHome.configDirEnv.name}=${shellSingleQuote(relocatedCmuxHooksHome.configDirEnv.value)}`,
105
+ ]),
106
+ ],
92
107
  ...(isClaudeAgent
93
108
  ? {
94
109
  agentArgs: [
@@ -97,8 +112,44 @@ function safehouseAgentIntegrationFor(workspaceKind, definition) {
97
112
  ],
98
113
  }
99
114
  : {}),
115
+ ...(relocatedCmuxHooksHome === undefined
116
+ ? {}
117
+ : {
118
+ addDirs: [relocatedCmuxHooksHome.configDir],
119
+ writeBackFiles: relocatedCmuxHooksHome.writeBackFiles,
120
+ teardownPaths: [relocatedCmuxHooksHome.parentDir],
121
+ }),
100
122
  };
101
123
  }
124
+ /**
125
+ * Relocate + seed a writable config home for a cmux-hosted agent whose hook
126
+ * activation is file-driven (codex reads `$CODEX_HOME/config.toml` +
127
+ * `hooks.json`, unlike Claude's `--settings` flag) and best-effort install
128
+ * cmux's lifecycle hooks into it. Undefined for agents with no registered
129
+ * relocation (claude), which keep reporting status via `--settings` instead.
130
+ */
131
+ function stageSafehouseCmuxHooksHome(input) {
132
+ const relocation = agentConfigRelocation(input.agent);
133
+ if (relocation === undefined) {
134
+ return undefined;
135
+ }
136
+ const parentDir = mkdtempSync(path.join(os.tmpdir(), `groundcrew-safehouse-${input.task}-`));
137
+ try {
138
+ const staged = stageRelocatedAgentConfigHome({
139
+ agent: input.agent,
140
+ relocation,
141
+ parentDir,
142
+ sourceConfigDir: readEnvironmentVariable(relocation.configDirEnv) ??
143
+ path.join(input.homeDir, relocation.sourceHomeRelativeDir),
144
+ });
145
+ installCmuxAgentHooks({ agent: input.agent, configDir: staged.configDir });
146
+ return { ...staged, parentDir };
147
+ }
148
+ catch (error) {
149
+ rmSync(parentDir, { recursive: true, force: true });
150
+ throw error;
151
+ }
152
+ }
102
153
  function warnOnCmuxIntegrationDrift(input) {
103
154
  for (const warningLine of safehouseCmuxIntegrationWarningLines({
104
155
  commandName: "groundcrew",
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Best-effort `cmux hooks <agent> install --yes` against a relocated,
3
+ * writable config home (e.g. codex's `CODEX_HOME`) so a file-driven agent —
4
+ * whose hook activation reads `$CODEX_HOME/config.toml` + `hooks.json` rather
5
+ * than an `agentArgs` flag — reports its lifecycle to the cmux sidebar the
6
+ * same way Claude already does via `--settings`.
7
+ *
8
+ * Never throws: an agent with no registered config relocation, a missing
9
+ * `cmux` CLI, or a non-zero install exit all degrade to that agent simply
10
+ * emitting no live status — never to a failed launch. A short timeout keeps a
11
+ * hung `cmux` binary from stalling every launch on this agent.
12
+ */
13
+ export declare function installCmuxAgentHooks(input: {
14
+ agent: string;
15
+ configDir: string;
16
+ }): void;
17
+ //# sourceMappingURL=cmuxAgentHookInstall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmuxAgentHookInstall.d.ts","sourceRoot":"","sources":["../../src/lib/cmuxAgentHookInstall.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0BvF"}