@mastra/factory 0.11.1 → 0.12.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/factory.d.ts +2 -22
  3. package/dist/factory.d.ts.map +1 -1
  4. package/dist/factory.js +15 -58
  5. package/dist/factory.js.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/integrations/base.d.ts +5 -13
  9. package/dist/integrations/base.d.ts.map +1 -1
  10. package/dist/integrations/github/config.d.ts +4 -4
  11. package/dist/integrations/github/config.d.ts.map +1 -1
  12. package/dist/integrations/github/config.js +3 -3
  13. package/dist/integrations/github/config.js.map +1 -1
  14. package/dist/integrations/github/integration.d.ts.map +1 -1
  15. package/dist/integrations/github/integration.js +2 -4
  16. package/dist/integrations/github/integration.js.map +1 -1
  17. package/dist/integrations/github/reconcile-worker.d.ts +0 -5
  18. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  19. package/dist/integrations/github/reconcile-worker.js +0 -7
  20. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  21. package/dist/integrations/github/routes.d.ts +6 -5
  22. package/dist/integrations/github/routes.d.ts.map +1 -1
  23. package/dist/integrations/github/routes.js +36 -263
  24. package/dist/integrations/github/routes.js.map +1 -1
  25. package/dist/integrations/github/sandbox-release.d.ts +17 -47
  26. package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
  27. package/dist/integrations/github/sandbox-release.js +24 -103
  28. package/dist/integrations/github/sandbox-release.js.map +1 -1
  29. package/dist/integrations/github/sandbox.d.ts +25 -139
  30. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  31. package/dist/integrations/github/sandbox.js +24 -212
  32. package/dist/integrations/github/sandbox.js.map +1 -1
  33. package/dist/integrations/github/webhook.d.ts.map +1 -1
  34. package/dist/integrations/github/webhook.js +1 -2
  35. package/dist/integrations/github/webhook.js.map +1 -1
  36. package/dist/integrations/platform/github/event-worker.d.ts +0 -2
  37. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  38. package/dist/integrations/platform/github/event-worker.js +0 -7
  39. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  40. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  41. package/dist/integrations/platform/github/integration.js +5 -7
  42. package/dist/integrations/platform/github/integration.js.map +1 -1
  43. package/dist/routes/config.d.ts.map +1 -1
  44. package/dist/routes/config.js +3 -1
  45. package/dist/routes/config.js.map +1 -1
  46. package/dist/routes/fs.d.ts +4 -6
  47. package/dist/routes/fs.d.ts.map +1 -1
  48. package/dist/routes/fs.js +28 -34
  49. package/dist/routes/fs.js.map +1 -1
  50. package/dist/routes/skills.d.ts.map +1 -1
  51. package/dist/routes/skills.js +6 -3
  52. package/dist/routes/skills.js.map +1 -1
  53. package/dist/routes/surface.d.ts +4 -9
  54. package/dist/routes/surface.d.ts.map +1 -1
  55. package/dist/routes/surface.js +3 -5
  56. package/dist/routes/surface.js.map +1 -1
  57. package/dist/sandbox/materialization.d.ts +36 -0
  58. package/dist/sandbox/materialization.d.ts.map +1 -0
  59. package/dist/sandbox/materialization.js +18 -0
  60. package/dist/sandbox/materialization.js.map +1 -0
  61. package/dist/sandbox/session-retirement.d.ts +8 -6
  62. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  63. package/dist/sandbox/session-retirement.js +28 -123
  64. package/dist/sandbox/session-retirement.js.map +1 -1
  65. package/dist/sandbox/session-sandbox.d.ts +117 -0
  66. package/dist/sandbox/session-sandbox.d.ts.map +1 -0
  67. package/dist/sandbox/session-sandbox.js +132 -0
  68. package/dist/sandbox/session-sandbox.js.map +1 -0
  69. package/dist/sandbox/workdir.d.ts +30 -0
  70. package/dist/sandbox/workdir.d.ts.map +1 -0
  71. package/dist/sandbox/workdir.js +48 -0
  72. package/dist/sandbox/workdir.js.map +1 -0
  73. package/dist/session/filesystem-capture.d.ts +2 -1
  74. package/dist/session/filesystem-capture.d.ts.map +1 -1
  75. package/dist/session/filesystem-capture.js +45 -4
  76. package/dist/session/filesystem-capture.js.map +1 -1
  77. package/dist/session/first-exec-capture.d.ts +1 -0
  78. package/dist/session/first-exec-capture.d.ts.map +1 -1
  79. package/dist/session/first-exec-capture.js +1 -1
  80. package/dist/session/first-exec-capture.js.map +1 -1
  81. package/dist/storage/domains/integrations/base.d.ts +1 -1
  82. package/dist/storage/domains/integrations/base.js +1 -1
  83. package/dist/storage/domains/integrations/base.js.map +1 -1
  84. package/dist/storage/domains/source-control/base.d.ts +0 -144
  85. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  86. package/dist/storage/domains/source-control/base.js +3 -290
  87. package/dist/storage/domains/source-control/base.js.map +1 -1
  88. package/dist/storage/domains/source-control/inmemory.d.ts +1 -63
  89. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  90. package/dist/storage/domains/source-control/inmemory.js +0 -89
  91. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  92. package/dist/workspace.d.ts +5 -37
  93. package/dist/workspace.d.ts.map +1 -1
  94. package/dist/workspace.js +107 -245
  95. package/dist/workspace.js.map +1 -1
  96. package/package.json +4 -4
  97. package/dist/sandbox/base-checkpoint-triggers.d.ts +0 -77
  98. package/dist/sandbox/base-checkpoint-triggers.d.ts.map +0 -1
  99. package/dist/sandbox/base-checkpoint-triggers.js +0 -137
  100. package/dist/sandbox/base-checkpoint-triggers.js.map +0 -1
  101. package/dist/sandbox/base-checkpoint.d.ts +0 -59
  102. package/dist/sandbox/base-checkpoint.d.ts.map +0 -1
  103. package/dist/sandbox/base-checkpoint.js +0 -145
  104. package/dist/sandbox/base-checkpoint.js.map +0 -1
  105. package/dist/sandbox/fleet.d.ts +0 -258
  106. package/dist/sandbox/fleet.d.ts.map +0 -1
  107. package/dist/sandbox/fleet.js +0 -356
  108. package/dist/sandbox/fleet.js.map +0 -1
  109. package/dist/sandbox/reattach.d.ts +0 -3
  110. package/dist/sandbox/reattach.d.ts.map +0 -1
  111. package/dist/sandbox/reattach.js +0 -16
  112. package/dist/sandbox/reattach.js.map +0 -1
@@ -1,58 +1,6 @@
1
1
  import { timedPhase } from "../../timing.js";
2
- import { reportProgress } from "../../sandbox/fleet.js";
3
- import { createHash } from "crypto";
4
2
  //#region src/integrations/github/sandbox.ts
5
3
  /**
6
- * Repo materialization for GitHub-backed repositories.
7
- *
8
- * A GitHub repo is never cloned onto the server host. Instead each project gets
9
- * its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and
10
- * the repo is cloned *inside* that sandbox. The agent's file tools and command
11
- * tools then operate entirely against the remote checkout.
12
- *
13
- * - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's
14
- * provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.
15
- * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`
16
- * (re-open) inside the sandbox, using a short-lived installation token that is
17
- * scrubbed from the git remote afterwards so it never persists in the VM.
18
- *
19
- * This module owns everything git/GitHub: clone/pull, commit/push, worktrees,
20
- * and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in
21
- * the fleet module.
22
- */
23
- /** Adapt a per-(project,user) sandbox binding row to the fleet's persistence seam. */
24
- function bindingStore(row, storage, seedCheckpointName) {
25
- return {
26
- sandboxId: row.sandboxId,
27
- ...seedCheckpointName ? { seedCheckpointName } : {},
28
- setSandboxId: (id) => id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({
29
- id: row.id,
30
- sandboxId: id
31
- }),
32
- clear: () => storage.clearBinding({ id: row.id })
33
- };
34
- }
35
- /**
36
- * Provision a new sandbox (persisting its provider id on first open) or
37
- * reattach to the stored one. Returns a started, live sandbox.
38
- */
39
- async function ensureProjectSandbox(options) {
40
- const { fleet, row, storage, token, onProgress, seedCheckpointName } = options;
41
- return fleet.ensureSandbox(bindingStore(row, storage, seedCheckpointName), { GH_TOKEN: token }, onProgress, { actingUserId: row.userId });
42
- }
43
- /**
44
- * Tear down a user's sandbox for a project: stop the live VM (best-effort) and
45
- * clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)
46
- * binding row so the next open re-provisions cleanly.
47
- *
48
- * @param row the per-(project,user) sandbox binding to tear down
49
- * @param sandbox an already-reattached live sandbox to stop, when available
50
- */
51
- async function teardownProjectSandbox(options) {
52
- const { fleet, row, storage, sandbox } = options;
53
- return fleet.teardownSandbox(bindingStore(row, storage), sandbox);
54
- }
55
- /**
56
4
  * Single-quote a string for safe POSIX shell interpolation. Wraps the value in
57
5
  * single quotes and escapes any embedded single quote using the canonical
58
6
  * close-quote / escaped-quote / reopen-quote sequence (`'\''`). This is the
@@ -190,13 +138,12 @@ function cleanUrl(repoFullName) {
190
138
  * Materialize the repo inside the user's sandbox. Clones on first open, pulls on
191
139
  * re-open. Always scrubs the install token from the remote afterwards and sets
192
140
  * `materialized_at` on the per-user sandbox binding row.
193
- *
194
141
  */
195
142
  async function materializeRepo(options) {
196
143
  return timedPhase("workspace.materialize", () => materializeRepoImpl(options));
197
144
  }
198
145
  async function materializeRepoImpl(options) {
199
- const { row: sandboxRow, repoInfo, sandbox, token, storage, onProgress, skipPullOnExistingCheckout } = options;
146
+ const { row: sandboxRow, repoInfo, sandbox, token, storage } = options;
200
147
  const workdir = sandboxRow.sandboxWorkdir;
201
148
  const repo = repoInfo.repoFullName;
202
149
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, "clone-failed");
@@ -207,18 +154,10 @@ async function materializeRepoImpl(options) {
207
154
  let tokenInRemote = false;
208
155
  try {
209
156
  if (!alreadyMaterialized) {
210
- reportProgress(onProgress, {
211
- phase: "cloning",
212
- message: `Cloning ${repo} (first open can take a minute)…`
213
- });
214
157
  await sh(sandbox, `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`);
215
158
  const clone = await gitTransfer(sandbox, `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`, {
216
159
  phase: "repository clone",
217
- beforeRetry: async (attempt) => {
218
- reportProgress(onProgress, {
219
- phase: "cloning",
220
- message: `Lost the connection to github.com — retrying the clone (attempt ${attempt + 1})…`
221
- });
160
+ beforeRetry: async () => {
222
161
  await sh(sandbox, `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`);
223
162
  }
224
163
  });
@@ -227,68 +166,21 @@ async function materializeRepoImpl(options) {
227
166
  throw classifyGitFailure(clone, "clone-failed");
228
167
  }
229
168
  tokenInRemote = true;
230
- } else if (skipPullOnExistingCheckout) {
231
- const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);
232
- if (setUrl.exitCode !== 0) throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, "pull-failed");
233
- tokenInRemote = true;
234
169
  } else {
235
- reportProgress(onProgress, {
236
- phase: "pulling",
237
- message: `Updating ${repo} to the latest changes…`
238
- });
239
170
  const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);
240
171
  if (setUrl.exitCode !== 0) throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, "pull-failed");
241
172
  tokenInRemote = true;
242
- const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, {
243
- phase: "repository pull",
244
- beforeRetry: async (attempt) => reportProgress(onProgress, {
245
- phase: "pulling",
246
- message: `Lost the connection to github.com — retrying the update (attempt ${attempt + 1})…`
247
- })
248
- });
173
+ const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: "repository pull" });
249
174
  if (pull.exitCode !== 0) {
250
175
  if (!isBenignNonFastForward(pull)) throw classifyGitFailure(pull, "pull-failed");
251
- reportProgress(onProgress, {
252
- phase: "pulling",
253
- message: isDeletedUpstreamRef(pull) ? "Workspace could not be updated from its remote — keeping the existing checkout as-is." : "Workspace has local changes that diverge from the remote — keeping them as-is."
254
- });
255
176
  }
256
177
  }
257
178
  } catch (primary) {
258
179
  throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, "pull-failed");
259
180
  }
260
181
  await scrubRemote(sandbox, workdir, repo, tokenInRemote);
261
- reportProgress(onProgress, {
262
- phase: "finalizing",
263
- message: "Finalizing workspace…"
264
- });
265
182
  await storage.markMaterialized({ id: sandboxRow.id });
266
183
  }
267
- /**
268
- * Reset a pooled workdir that a new session just claimed: the previous
269
- * session's branch and dirty state must not leak into the new session, so
270
- * force-checkout the default branch, drop all local modifications, and delete
271
- * every other local branch (a surviving branch would otherwise hand the next
272
- * session for the same branch the previous session's stale tip instead of a
273
- * fresh fork from the base branch). When the claimed VM was reaped and
274
- * re-provisioned there is no checkout yet and this is a no-op (the clone path
275
- * handles it). A wedged checkout falls back to wiping the workdir so
276
- * `materializeRepo` re-clones inside the same VM instead of permanently
277
- * failing the session.
278
- */
279
- async function recycleClaimedWorkdir(sandbox, workdir, defaultBranch) {
280
- if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, "clone-failed");
281
- const w = shellQuote(workdir);
282
- if ((await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`)).exitCode !== 0) return;
283
- const recycle = await sh(sandbox, `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`, { phase: "claimed workdir recycle" });
284
- let failure = recycle;
285
- if (recycle.exitCode === 0) {
286
- const sweep = await sh(sandbox, `set -e; for ref in $(git -C ${w} for-each-ref --format='%(refname)' refs/heads); do [ "$ref" = ${shellQuote(`refs/heads/${defaultBranch}`)} ] || git -C ${w} update-ref --no-deref -d "$ref"; done`, { phase: "claimed workdir branch sweep" });
287
- if (sweep.exitCode === 0) return;
288
- failure = sweep;
289
- }
290
- if ((await sh(sandbox, `rm -rf ${w}`)).exitCode !== 0) throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${failure.stderr}`, "clone-failed");
291
- }
292
184
  /** Check out a session's branch inside its isolated repository clone. */
293
185
  async function checkoutSessionBranch(sandbox, workdir, options) {
294
186
  return timedPhase("workspace.checkout", () => checkoutSessionBranchImpl(sandbox, workdir, options));
@@ -521,7 +413,7 @@ async function pushBranch(sandbox, workdir, branch, token, repoFullName) {
521
413
  * error, so callers can safely commit-then-push without first diffing.
522
414
  *
523
415
  * @param sandbox the live sandbox containing the checkout
524
- * @param workdir the worktree (or repo) path to commit in
416
+ * @param workdir the session workdir to commit in
525
417
  * @param message the commit message (quoted; arbitrary text is safe)
526
418
  * @param identity authorship identity for the commit
527
419
  */
@@ -534,81 +426,21 @@ async function commitAll(sandbox, workdir, message, identity) {
534
426
  if (commit.exitCode !== 0) throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, "commit-failed");
535
427
  return { committed: true };
536
428
  }
537
- /** Error raised when a worktree cannot be created/reused inside the sandbox. */
538
- var WorktreeError = class extends Error {
429
+ /** Error raised when the org's setup or teardown command fails in the sandbox. */
430
+ var SetupCommandError = class extends Error {
539
431
  code;
540
432
  constructor(message, code) {
541
433
  super(message);
542
434
  this.code = code;
543
- this.name = "WorktreeError";
435
+ this.name = "SetupCommandError";
544
436
  }
545
437
  };
546
438
  /**
547
- * Reduce a (already ref-validated) branch name to a filesystem-safe directory
548
- * segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.
549
- * This only affects the *directory name*, never the git branch itself.
550
- *
551
- * Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),
552
- * so an 8-char hash of the original branch is appended whenever the sanitized
553
- * form differs from the input. That keeps clean names (`main`) readable while
554
- * guaranteeing distinct branches never share a worktree directory.
555
- */
556
- function safeBranchDir(branch) {
557
- const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
558
- if (sanitized === branch) return sanitized;
559
- return `${sanitized}-${createHash("sha256").update(branch).digest("hex").slice(0, 8)}`;
560
- }
561
- /**
562
- * Compute the absolute worktree path for a branch, server-side only. Worktrees
563
- * live alongside the repo checkout under a sibling `worktrees/` directory so the
564
- * repo's `.git` is shared. Never derived from client-supplied paths.
565
- */
566
- function computeWorktreePath(repoWorkdir, branch) {
567
- return `${repoWorkdir.replace(/\/+$/, "").split("/").slice(0, -1).join("/") || ""}/worktrees/${safeBranchDir(branch)}`;
568
- }
569
- /**
570
- * Create (or reuse) a git worktree + branch inside the sandbox for a unit of
571
- * work. Idempotent: if a worktree already exists at the computed path it is
572
- * reused. The branch is created from the freshly fetched `origin/<baseBranch>`
573
- * — never the sandbox's possibly stale local ref — so new worktrees always
574
- * start from the latest remote state.
575
- *
576
- * @param sandbox live sandbox containing the base checkout
577
- * @param repoWorkdir the base repo checkout path inside the sandbox
578
- * @param branch the feature branch (ref-validated server-side)
579
- * @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)
580
- * @param token short-lived installation token used only for the base-branch fetch
581
- * @param repoFullName `owner/repo` used to build the tokenized remote URL
582
- */
583
- async function ensureWorktree(sandbox, repoWorkdir, { branch, baseBranch, token, repoFullName }) {
584
- if (!isValidGitRef(branch)) throw new WorktreeError(`Invalid branch name '${branch}'.`, "invalid-branch");
585
- if (!isValidGitRef(baseBranch)) throw new WorktreeError(`Invalid base branch name '${baseBranch}'.`, "invalid-branch");
586
- const worktreePath = computeWorktreePath(repoWorkdir, branch);
587
- if ((await sh(sandbox, `test -e ${shellQuote(`${worktreePath}/.git`)}`)).exitCode === 0) return {
588
- worktreePath,
589
- branch,
590
- baseBranch,
591
- reused: true
592
- };
593
- const baseRef = `origin/${baseBranch}`;
594
- await withInstallToken(sandbox, repoWorkdir, repoFullName, token, async () => {
595
- const fetch = await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} fetch origin ${shellQuote(`+refs/heads/${baseBranch}:refs/remotes/${baseRef}`)}`);
596
- if (fetch.exitCode !== 0) throw classifyGitFailure(fetch, "pull-failed");
597
- });
598
- const add = await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree add --no-track -B ${shellQuote(branch)} ${shellQuote(worktreePath)} ${shellQuote(baseRef)}`);
599
- if (add.exitCode !== 0) throw new WorktreeError(`git worktree add failed: ${add.stderr.trim() || add.stdout.trim()}`, "worktree-failed");
600
- return {
601
- worktreePath,
602
- branch,
603
- baseBranch,
604
- reused: false
605
- };
606
- }
607
- /**
608
- * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a
609
- * freshly created worktree. Called before the worktree is handed to any agent
610
- * run so the checkout is ready to build/test. A non-zero exit is a hard error —
611
- * starting agent work in a half-set-up tree is worse than failing the request.
439
+ * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside the
440
+ * freshly materialized session workdir. Called before the checkout is handed
441
+ * to any agent run so it is ready to build/test. A non-zero exit is a hard
442
+ * error — starting agent work in a half-set-up tree is worse than failing the
443
+ * request.
612
444
  *
613
445
  * Security model: the command is intentionally arbitrary shell — that is the
614
446
  * feature (install deps, build, seed fixtures). It is only configurable by
@@ -620,22 +452,22 @@ async function ensureWorktree(sandbox, repoWorkdir, { branch, baseBranch, token,
620
452
  * server host, so it grants no privilege beyond what sandbox access already
621
453
  * provides.
622
454
  *
623
- * @param sandbox live sandbox containing the worktree
624
- * @param worktreePath the server-computed worktree path the command runs in
625
- * @param command the org-configured setup shell command
455
+ * @param sandbox live sandbox containing the checkout
456
+ * @param workdir the server-resolved session workdir the command runs in
457
+ * @param command the org-configured setup shell command
626
458
  */
627
- async function runWorktreeLifecycleCommand(sandbox, worktreePath, command, options) {
628
- const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\n}`, {
629
- phase: `worktree ${options.phase}`,
459
+ async function runLifecycleCommand(sandbox, workdir, command, options) {
460
+ const result = await sh(sandbox, `cd ${shellQuote(workdir)} && { ${command}\n}`, {
461
+ phase: `${options.phase} command`,
630
462
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
631
463
  });
632
464
  if (result.exitCode !== 0) {
633
465
  const detail = (result.stderr.trim() || result.stdout.trim()).slice(-1800);
634
- throw new WorktreeError(`${options.phase === "setup" ? "Setup" : "Teardown"} command failed (exit ${result.exitCode}): ${detail}`, options.phase === "setup" ? "setup-failed" : "teardown-failed");
466
+ throw new SetupCommandError(`${options.phase === "setup" ? "Setup" : "Teardown"} command failed (exit ${result.exitCode}): ${detail}`, options.phase === "setup" ? "setup-failed" : "teardown-failed");
635
467
  }
636
468
  }
637
- async function runWorktreeSetup(sandbox, worktreePath, command) {
638
- return runWorktreeLifecycleCommand(sandbox, worktreePath, command, { phase: "setup" });
469
+ async function runSetupCommand(sandbox, workdir, command) {
470
+ return runLifecycleCommand(sandbox, workdir, command, { phase: "setup" });
639
471
  }
640
472
  /**
641
473
  * Run the repository's best-effort teardown command from the materialized
@@ -643,33 +475,13 @@ async function runWorktreeSetup(sandbox, worktreePath, command) {
643
475
  * so the retirement coordinator can log them while still continuing with
644
476
  * scrub, pooling/destruction, cache invalidation, and row deletion.
645
477
  */
646
- async function runWorktreeTeardown(sandbox, worktreePath, command, options = {}) {
647
- return runWorktreeLifecycleCommand(sandbox, worktreePath, command, {
478
+ async function runTeardownCommand(sandbox, workdir, command, options = {}) {
479
+ return runLifecycleCommand(sandbox, workdir, command, {
648
480
  phase: "teardown",
649
481
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
650
482
  });
651
483
  }
652
484
  /**
653
- * Remove a worktree (and its local feature branch) from the sandbox. The
654
- * checkout is removed with `--force` — the caller owns confirming that any
655
- * uncommitted work in it can be discarded. Idempotent: a worktree whose
656
- * directory is already gone only has its metadata pruned.
657
- *
658
- * @param sandbox live sandbox containing the base checkout
659
- * @param repoWorkdir the base repo checkout path inside the sandbox
660
- * @param branch the worktree's feature branch (ref-validated)
661
- * @param worktreePath the persisted, server-computed worktree path
662
- */
663
- async function removeWorktree(sandbox, repoWorkdir, { branch, worktreePath }) {
664
- if (!isValidGitRef(branch)) throw new WorktreeError(`Invalid branch name '${branch}'.`, "invalid-branch");
665
- const remove = await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree remove --force ${shellQuote(worktreePath)}`);
666
- if (remove.exitCode !== 0) {
667
- await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree prune`);
668
- if ((await sh(sandbox, `test -e ${shellQuote(worktreePath)}`)).exitCode === 0) throw new WorktreeError(`git worktree remove failed: ${remove.stderr.trim() || remove.stdout.trim()}`, "worktree-failed");
669
- }
670
- await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} branch -D ${shellQuote(branch)}`);
671
- }
672
- /**
673
485
  * Preflight that `gh` is installed in the sandbox. Only called on the PR path so
674
486
  * a missing `gh` never blocks clone/open. Surfaces an actionable error naming
675
487
  * the sandbox template requirement.
@@ -712,6 +524,6 @@ async function createPullRequest(sandbox, workdir, { token, base, head, title, b
712
524
  return { url };
713
525
  }
714
526
  //#endregion
715
- export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, WorktreeError, checkoutSessionBranch, commitAll, computeWorktreePath, configureGitIdentity, createPullRequest, ensureProjectSandbox, ensureWorktree, hasExistingCheckout, isValidGitRef, materializeRepo, pushBranch, recycleClaimedWorkdir, removeWorktree, resolveGitIdentity, runWorktreeSetup, runWorktreeTeardown, safeBranchDir, sh, shellQuote, teardownProjectSandbox, withInstallToken };
527
+ export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, SetupCommandError, checkoutSessionBranch, commitAll, configureGitIdentity, createPullRequest, hasExistingCheckout, isValidGitRef, materializeRepo, pushBranch, resolveGitIdentity, runSetupCommand, runTeardownCommand, sh, shellQuote, withInstallToken };
716
528
 
717
529
  //# sourceMappingURL=sandbox.js.map