@tablation/crew 0.111.1 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6131,6 +6131,61 @@ var State = class _State {
6131
6131
 
6132
6132
  // src/environment.ts
6133
6133
  import { basename as basename6 } from "path";
6134
+
6135
+ // src/sandbox-brief.ts
6136
+ var SANDBOX_SKILL_SWAPS = [
6137
+ ["Worktree Setup", "Sandbox Setup"]
6138
+ ];
6139
+ var SANDBOX_STUB_DIR = "/sandbox/.crew-bin";
6140
+ var CONTAINER_ENGINES = ["docker", "podman", "nerdctl", "docker-compose"];
6141
+ var CONTAINER_ENGINE_MESSAGE = 'crew: this run is inside a sandbox, which has no container engine. Run the service as a process in hooks.setup instead (ports from hooks.ports, overrides from hooks.isolate) \u2014 see "Services inside the sandbox" in REPO_SPEC.md.';
6142
+ function swapSandboxSkills(text2) {
6143
+ let out = text2;
6144
+ for (const [host, sandbox] of SANDBOX_SKILL_SWAPS) out = out.split(`\`${host}\``).join(`\`${sandbox}\``);
6145
+ return out;
6146
+ }
6147
+ function containerEngineStubScript(dir = SANDBOX_STUB_DIR) {
6148
+ const body = `#!/bin/sh
6149
+ printf '%s\\n' ${shSingle(CONTAINER_ENGINE_MESSAGE)} >&2
6150
+ exit 125
6151
+ `;
6152
+ return [
6153
+ `mkdir -p ${shSingle(dir)}`,
6154
+ ...CONTAINER_ENGINES.map((e) => `printf '%b' ${shSingle(body)} > ${shSingle(`${dir}/${e}`)} && chmod 755 ${shSingle(`${dir}/${e}`)}`)
6155
+ ].join(" && ");
6156
+ }
6157
+ function shSingle(s) {
6158
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6159
+ }
6160
+ function containerEngineUse(hook2) {
6161
+ const names = CONTAINER_ENGINES.map((e) => e.replace(/-/g, "\\-")).join("|");
6162
+ const re = new RegExp(`(?:^|[;&|(\`]|&&|\\|\\||\\$\\()\\s*(?:(?:then|do|else|elif|if|while|until|time|exec)\\s+)*(?:sudo\\s+)?(?:\\S*/)?(${names})(?=\\s|$)`, "m");
6163
+ const m = re.exec(hook2.split("\n").filter((l) => !/^\s*#/.test(l)).join("\n"));
6164
+ return m ? m[1] : null;
6165
+ }
6166
+ function sandboxCheckoutSection(workDir, heading) {
6167
+ return [
6168
+ "",
6169
+ heading,
6170
+ "",
6171
+ `You are already inside a sandbox created for this run. \`${workDir}\` is a fresh`,
6172
+ "clone of the repository on a fresh branch, and it is your working directory. There is",
6173
+ "no primary checkout and no other worktree: do not cut a worktree, do not run `crew",
6174
+ "worktree`, and do not look for files to copy from a main checkout. The digest's",
6175
+ "`worktree` column does not apply; its `branch` column is the name your work is pushed",
6176
+ "under, so create it with `git checkout -b <branch>` in the clone.",
6177
+ "",
6178
+ "The sandbox has no container engine, and nothing installs at run time: a service the",
6179
+ "tests need is a process the repository's `setup` hook starts. Fetch the `Sandbox Setup`",
6180
+ "skill for setup, ports and the hand-off push."
6181
+ ];
6182
+ }
6183
+ function sandboxEnv(ship) {
6184
+ return ship.runner === "sandbox" ? { sandbox: { workDir: ship.sandbox?.workDir ?? DEFAULT_SANDBOX_WORKDIR } } : {};
6185
+ }
6186
+ var DEFAULT_SANDBOX_WORKDIR = "/sandbox/work";
6187
+
6188
+ // src/environment.ts
6134
6189
  var API_KEY_VAR = "CREW_API_KEY";
6135
6190
  function authorshipBullets(a, contract) {
6136
6191
  if (!a) return [];
@@ -6414,7 +6469,9 @@ function renderEnvironment(i) {
6414
6469
  ] : []
6415
6470
  );
6416
6471
  }
6417
- if (only) {
6472
+ if (only && i.sandbox) {
6473
+ lines2.push(...sandboxCheckoutSection(i.sandbox.workDir, "## Your checkout"));
6474
+ } else if (only) {
6418
6475
  lines2.push(...repoSection(only, key, "## Your worktree"));
6419
6476
  } else {
6420
6477
  lines2.push(
@@ -6427,8 +6484,9 @@ function renderEnvironment(i) {
6427
6484
  "ignore the others. A ticket whose repo is not listed here has no checkout on",
6428
6485
  "this machine and is not yours to work."
6429
6486
  );
6487
+ if (i.sandbox) lines2.push(...sandboxCheckoutSection(i.sandbox.workDir, "### Your checkout"));
6430
6488
  for (const r of repos) {
6431
- lines2.push(...repoSection(r, key, `### ${r.name} \u2014 \`${r.dir}\``));
6489
+ lines2.push(...i.sandbox ? ["", `### ${r.name}`] : repoSection(r, key, `### ${r.name} \u2014 \`${r.dir}\``));
6432
6490
  lines2.push("", `Hooks for ${r.name} \u2014 the only commands you should run here:`, "", hookTable(r.config));
6433
6491
  }
6434
6492
  }
@@ -9714,7 +9772,7 @@ var execFileP = promisify(execFile);
9714
9772
  var DEFAULT_SANDBOX_CONFIG = {
9715
9773
  bin: "openshell",
9716
9774
  mirrorDir: "/cache",
9717
- workDir: "/sandbox/work"
9775
+ workDir: DEFAULT_SANDBOX_WORKDIR
9718
9776
  };
9719
9777
  var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
9720
9778
  var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
@@ -9818,14 +9876,14 @@ var SandboxRunner = class {
9818
9876
  }
9819
9877
  try {
9820
9878
  const branch = `crew-run-${name.slice(-8)}`;
9821
- const clone = cloneScript({
9879
+ const clone = `${cloneScript({
9822
9880
  mirrorDir: cfg.mirrorDir,
9823
9881
  repo,
9824
9882
  workDir: cfg.workDir,
9825
9883
  origin,
9826
9884
  branch,
9827
9885
  lockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
9828
- });
9886
+ })} && ${containerEngineStubScript()}`;
9829
9887
  await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
9830
9888
  const harness = cfg.agentBin ?? basename8(plan.bin);
9831
9889
  const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
@@ -9839,7 +9897,8 @@ var SandboxRunner = class {
9839
9897
  rmSync12(tmp, { recursive: true, force: true });
9840
9898
  }
9841
9899
  }
9842
- const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
9900
+ const withStubs = `PATH=${shQuote2(SANDBOX_STUB_DIR)}:"$PATH"; export PATH; `;
9901
+ const command2 = viaUpload ? ["sh", "-c", `${withStubs}exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : ["sh", "-c", `${withStubs}exec "$@"`, "sh", harness, ...plan.args];
9843
9902
  const child = spawn2(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
9844
9903
  env: process.env,
9845
9904
  stdio: ["pipe", o.streamFd, "inherit"],
@@ -10032,6 +10091,7 @@ function planAgentRun(o) {
10032
10091
  if (e instanceof SkillIncludeError) throw new AgentError(`${e.message} \u2014 refusing to run an unscoped session`);
10033
10092
  throw e;
10034
10093
  }
10094
+ if (o.ship.runner === "sandbox") expanded = { ...expanded, text: swapSandboxSkills(expanded.text) };
10035
10095
  const prompt = assemblePrompt({
10036
10096
  roster: o.roster,
10037
10097
  environment: o.environment,
@@ -11724,7 +11784,13 @@ function syncAllPassengerCheckouts(cfg) {
11724
11784
 
11725
11785
  // src/sandbox-image.ts
11726
11786
  var SANDBOX_BASE_IMAGE = "nvcr.io/nvidia/base/ubuntu:24.04";
11727
- var SANDBOX_HARNESS_VERSION = "0.1.6";
11787
+ var SANDBOX_OPENSHELL_VERSION = "0.1.6";
11788
+ var SANDBOX_CLAUDE_VERSION = "2.1.309";
11789
+ var SANDBOX_CLAUDE_DIST = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases";
11790
+ var SANDBOX_CLAUDE_SHA256 = {
11791
+ x86_64: { platform: "linux-x64", sha256: "4d704a8aecbe038affa3000f498ae17c1b4f753add8facb8764120affc99b658" },
11792
+ aarch64: { platform: "linux-arm64", sha256: "9796dd26899337f687739a0b39a7353b639f062a3436a7b941fcf19a1b757a9b" }
11793
+ };
11728
11794
  var SANDBOX_UID = 1e3;
11729
11795
  var CREW_PREFIX = "/opt/crew";
11730
11796
  var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
@@ -11748,15 +11814,28 @@ var CREW_ENV = {
11748
11814
  XDG_CACHE_HOME: `${CREW_PREFIX}/cache`,
11749
11815
  npm_config_cache: `${CREW_PREFIX}/cache/npm`,
11750
11816
  npm_config_store_dir: `${CREW_PREFIX}/pnpm-store`,
11751
- PNPM_HOME: `${CREW_PREFIX}/pnpm`
11817
+ PNPM_HOME: `${CREW_PREFIX}/pnpm`,
11818
+ // A run cannot reach the download host, so an in-run update attempt is only noise.
11819
+ DISABLE_AUTOUPDATER: "1"
11752
11820
  };
11821
+ function claudeInstallCommand() {
11822
+ const cases = Object.entries(SANDBOX_CLAUDE_SHA256).map(([arch, { platform: platform2, sha256: sha2562 }]) => `${arch}) p=${platform2}; sum=${sha2562};;`).join(" ");
11823
+ return [
11824
+ `case "$(uname -m)" in ${cases} *) echo "no pinned Claude Code binary for $(uname -m)" >&2; exit 1;; esac`,
11825
+ `mkdir -p ${CREW_PREFIX}/bin`,
11826
+ `curl -fsSL --retry 3 -o ${CREW_PREFIX}/bin/claude "${SANDBOX_CLAUDE_DIST}/${SANDBOX_CLAUDE_VERSION}/$p/claude"`,
11827
+ `echo "$sum ${CREW_PREFIX}/bin/claude" | sha256sum -c -`,
11828
+ `chmod 0755 ${CREW_PREFIX}/bin/claude`,
11829
+ `${CREW_PREFIX}/bin/claude --version | grep -F "${SANDBOX_CLAUDE_VERSION}"`
11830
+ ].join("; ");
11831
+ }
11753
11832
  function baseLayer() {
11754
11833
  const dockerfile = [
11755
11834
  `FROM ${SANDBOX_BASE_IMAGE}`,
11756
- `LABEL io.crew.sandbox.layer="base" io.crew.sandbox.harness="${SANDBOX_HARNESS_VERSION}"`,
11835
+ `LABEL io.crew.sandbox.layer="base" io.crew.sandbox.openshell="${SANDBOX_OPENSHELL_VERSION}"`,
11757
11836
  ""
11758
11837
  ].join("\n");
11759
- return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_HARNESS_VERSION })}`, dockerfile, files: {} };
11838
+ return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_OPENSHELL_VERSION })}`, dockerfile, files: {} };
11760
11839
  }
11761
11840
  function crewLayer(base) {
11762
11841
  const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
@@ -11767,10 +11846,11 @@ function crewLayer(base) {
11767
11846
  run2("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
11768
11847
  `COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
11769
11848
  `COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
11849
+ run2(claudeInstallCommand()),
11770
11850
  run2(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
11771
11851
  run2(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
11772
11852
  `ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
11773
- `LABEL io.crew.sandbox.layer="crew"`,
11853
+ `LABEL io.crew.sandbox.layer="crew" io.crew.sandbox.claude="${SANDBOX_CLAUDE_VERSION}"`,
11774
11854
  ""
11775
11855
  ].join("\n");
11776
11856
  return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
@@ -11831,7 +11911,7 @@ function planSandbox(repo, decl, warm = {}) {
11831
11911
  const base = baseLayer();
11832
11912
  const crew = crewLayer(base);
11833
11913
  const layer = repoLayer(repo, decl, crew, warm);
11834
- return { repo, tag: layer.tag, layers: [base, crew, layer] };
11914
+ return { repo, tag: layer.tag, claude: SANDBOX_CLAUDE_VERSION, layers: [base, crew, layer] };
11835
11915
  }
11836
11916
  var sandboxStatePath = (stateDir) => join26(stateDir, "sandbox-images.json");
11837
11917
  function readSandboxState(stateDir) {
@@ -11856,8 +11936,8 @@ function sandboxImageStatus(state, plan) {
11856
11936
  return { status: "failed", detail: `last build of ${plan.tag} failed at ${s.failure.at}; log ${s.failure.logPath}${current ? `; running ${current.tag}` : ""}` };
11857
11937
  }
11858
11938
  if (!current) return { status: "missing", detail: `no image built yet (wants ${plan.tag}); run \`crew sandbox build\`` };
11859
- if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag}, declaration now builds ${plan.tag}` };
11860
- return { status: "ok", detail: `${current.tag} built ${current.builtAt}` };
11939
+ if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag} (claude ${current.claude ?? "unknown"}), declaration now builds ${plan.tag} (claude ${plan.claude})` };
11940
+ return { status: "ok", detail: `${current.tag} built ${current.builtAt}, claude ${current.claude ?? "unknown"}` };
11861
11941
  }
11862
11942
  var spawnBuildRunner = (engine = "docker") => (args, logPath) => new Promise((resolve7) => {
11863
11943
  const child = spawn4(engine, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -11920,7 +12000,7 @@ async function buildSandboxImage(plan, deps) {
11920
12000
  rmSync13(ctx, { recursive: true, force: true });
11921
12001
  }
11922
12002
  }
11923
- const tags = [{ tag: plan.tag, builtAt: now().toISOString() }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
12003
+ const tags = [{ tag: plan.tag, builtAt: now().toISOString(), claude: plan.claude }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
11924
12004
  const dropped = tags.slice(KEEP_TAGS);
11925
12005
  state[plan.repo] = { tags: tags.slice(0, KEEP_TAGS) };
11926
12006
  writeSandboxState(deps.stateDir, state);
@@ -13009,6 +13089,22 @@ async function runPoolMember(deps) {
13009
13089
  relayReported = null;
13010
13090
  continue;
13011
13091
  }
13092
+ if (body.action === "tokens") {
13093
+ if (body.leaseId !== current) {
13094
+ deps.log(`ignoring a tokens message for ${body.leaseId}; this member holds ${current ?? "no lease"}`);
13095
+ continue;
13096
+ }
13097
+ const applied = await deps.applyTokens(body.forges ?? []);
13098
+ if (!applied.ok) {
13099
+ deps.log(`tokens: ${applied.reason ?? "could not apply the delivered forge tokens"}`);
13100
+ continue;
13101
+ }
13102
+ deps.log(`tokens: replaced the forge tokens (${(body.forges ?? []).length} host${(body.forges ?? []).length === 1 ? "" : "s"})`);
13103
+ const r2 = await report("running", { leaseId: current });
13104
+ if (r2 === "credential") return POOL_EXIT.credential;
13105
+ if (r2 === "stopped") return POOL_EXIT.ok;
13106
+ continue;
13107
+ }
13012
13108
  if (current) {
13013
13109
  deps.log("ignoring a provision payload: a lease is already held");
13014
13110
  continue;
@@ -14023,6 +14119,27 @@ function realPoolMemberDeps(o) {
14023
14119
  if (outcome.code === PROVISION_EXIT.ok) return { ok: true };
14024
14120
  return { ok: false, reason: `crew provision exited ${outcome.code}: ${lines2.at(-1) ?? "no detail"}` };
14025
14121
  },
14122
+ async applyTokens(forges) {
14123
+ const want = new Map(forges.map((f) => [f.host.toLowerCase(), f.token]));
14124
+ for (const [host, token] of want) {
14125
+ const stored = provisionDeps.storedToken(host);
14126
+ if (stored === token) continue;
14127
+ const res = provisionDeps.crew(["forge", stored === null ? "set" : "rotate", host, "--token-stdin"], { input: `${token}
14128
+ ` });
14129
+ if (res.status !== 0) return { ok: false, reason: `forge ${host}: ${res.stderr.trim() || `crew forge exited ${res.status}`}` };
14130
+ }
14131
+ const cfg = loadShip(o.crewHome);
14132
+ if (!cfg) return { ok: false, reason: "no ship config to apply forge tokens to" };
14133
+ for (const host of Object.keys(cfg.ship.forges ?? {})) {
14134
+ if (want.has(host.toLowerCase())) continue;
14135
+ try {
14136
+ eraseForgeToken(cfg.ship.forges, host, cfg.ship.stateDir, env);
14137
+ } catch (e) {
14138
+ return { ok: false, reason: `forge ${host}: ${e.message}` };
14139
+ }
14140
+ }
14141
+ return { ok: true };
14142
+ },
14026
14143
  async reconcile() {
14027
14144
  const before = provisionDeps.installState();
14028
14145
  if (before.unitsStale || !before.running || !before.lingering) {
@@ -19204,6 +19321,10 @@ function checkRepoSpec(dir, opts = {}) {
19204
19321
  }
19205
19322
  const effective = resolveRepoConfig(parsed, opts.ship, dir);
19206
19323
  const readiness = validateEffective(effective);
19324
+ if (effective.sandbox) {
19325
+ const engine = effective.hooks.setup ? containerEngineUse(effective.hooks.setup) : null;
19326
+ if (engine) readiness.push(`hooks.setup runs \`${engine}\`, and a sandbox has no container engine \u2014 start the service as a process instead (REPO_SPEC.md, "Services inside the sandbox")`);
19327
+ }
19207
19328
  const hooks = Object.keys(effective.hooks).map((name) => ({
19208
19329
  name,
19209
19330
  label: hookLabel(effective, name),
@@ -21699,7 +21820,7 @@ async function buildEnvironment(route2, ship, ticket) {
21699
21820
  });
21700
21821
  const authorship = await tracker.authorshipProbe().catch(() => null);
21701
21822
  const attachments = await tracker.attachmentsProbe().catch(() => null);
21702
- return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
21823
+ return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name, ...sandboxEnv(ship) });
21703
21824
  }
21704
21825
 
21705
21826
  // src/control-api.ts
@@ -23861,7 +23982,8 @@ ${changed2.map((c) => ` - ${c}
23861
23982
  sourceTicket: ticket,
23862
23983
  authorship: await tracker.authorshipProbe().catch(() => null),
23863
23984
  attachments: await tracker.attachmentsProbe().catch(() => null),
23864
- shipName: cfg.ship.name
23985
+ shipName: cfg.ship.name,
23986
+ ...sandboxEnv(cfg.ship)
23865
23987
  });
23866
23988
  }
23867
23989
  async function releaserLine(tracker, routeName, repo, release) {
@@ -26581,7 +26703,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26581
26703
  `);
26582
26704
  for (const t of r.tags) {
26583
26705
  const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
26584
- process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
26706
+ process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago), claude ${t.claude ?? "unknown"}
26585
26707
  `);
26586
26708
  }
26587
26709
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.111.1",
3
+ "version": "0.113.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Sandbox Setup
3
+ description: How a lane agent prepares and hands off from a sandboxed run — the checkout is already cut, then the setup, ports and handoff hooks and the push. Fetch where the brief names the Worktree Setup skill and your environment section says you are in a sandbox.
4
+ ---
5
+ # Setting up inside a sandbox
6
+
7
+ A sandboxed run starts in a fresh clone on a fresh branch. Your working
8
+ directory is that clone. There is no primary checkout, no other worktree and no
9
+ `crew` command to cut one: skip every step of the lane policy that cuts a
10
+ worktree, copies files across from a main checkout, or runs `crew sync` or
11
+ `crew worktree`. The Environment section names the checkout's path.
12
+
13
+ ## Branch
14
+
15
+ The clone starts on a throwaway branch. Create the ticket's branch from it —
16
+ the digest's `branch` column, verbatim — with `git checkout -b <branch>`, and
17
+ commit there. Never commit on the throwaway branch.
18
+
19
+ ## Making it runnable
20
+
21
+ Run the **`setup`** hook from the checkout, verbatim. Nothing installs from a
22
+ network at run time: the toolchains and packages are already in the image, and
23
+ a download that is not is blocked. If setup needs something the image lacks,
24
+ that is the repository's `sandbox:` declaration to fix, not yours: say so on
25
+ the ticket and stop.
26
+
27
+ - **Services.** A service the tests need (a database, a queue) is a process the
28
+ `setup` hook starts inside this sandbox. The sandbox has no container engine:
29
+ `docker` and `podman` exit at once naming this pattern. Use the ports the
30
+ **`ports`** hook prints and export what the **`isolate`** hook prints; both
31
+ are the same hooks a host run uses.
32
+ - **`isolate`** — run it, and export what it prints, whenever the ticket touches
33
+ stored state. The sandbox's own services are already private to this run,
34
+ but the hook is where the repository says which settings point at them.
35
+ - **`handoff`** — a preview server cannot be reached from outside a sandbox, so
36
+ there is nothing to leave running. Skip it unless the repository's own
37
+ instructions say otherwise, and say in your progress comment that you did.
38
+
39
+ ## Running the suite
40
+
41
+ Run the **`test`** and **`build`** hooks in the foreground, as the lane policy
42
+ says. Stop every process `setup` started before you finish; the sandbox is
43
+ deleted when the run ends, so nothing outlives it, but a service still writing
44
+ when the push happens makes the result harder to judge.
45
+
46
+ ## Handing off
47
+
48
+ Commit on the ticket's branch and push it as a plain fast-forward to `origin`,
49
+ exactly as the lane policy says: `git push --set-upstream origin <branch>`.
50
+ Push credentials are already in place. The sandbox is deleted at the end of the
51
+ run, so **anything not pushed is gone**; there is no worktree for QA to pick up
52
+ and QA cuts its own from the remote branch you pushed. Put which hooks you ran,
53
+ and their results, in your progress comment.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Sandbox Setup
3
+ description: How a lane agent prepares and hands off from a sandboxed run — the checkout is already cut, then the setup, ports and handoff hooks and the push. Fetch where the brief names the Worktree Setup skill and your environment section says you are in a sandbox.
4
+ ---
5
+ # Setting up inside a sandbox
6
+
7
+ A sandboxed run starts in a fresh clone on a fresh branch. Your working
8
+ directory is that clone. There is no primary checkout, no other worktree and no
9
+ `crew` command to cut one: skip every step of the lane policy that cuts a
10
+ worktree, copies files across from a main checkout, or runs `crew sync` or
11
+ `crew worktree`. The Environment section names the checkout's path.
12
+
13
+ ## Branch
14
+
15
+ The clone starts on a throwaway branch. Create the ticket's branch from it —
16
+ the digest's `branch` column, verbatim — with `git checkout -b <branch>`, and
17
+ commit there. Never commit on the throwaway branch.
18
+
19
+ ## Making it runnable
20
+
21
+ Run the **`setup`** hook from the checkout, verbatim. Nothing installs from a
22
+ network at run time: the toolchains and packages are already in the image, and
23
+ a download that is not is blocked. If setup needs something the image lacks,
24
+ that is the repository's `sandbox:` declaration to fix, not yours: say so on
25
+ the ticket and stop.
26
+
27
+ - **Services.** A service the tests need (a database, a queue) is a process the
28
+ `setup` hook starts inside this sandbox. The sandbox has no container engine:
29
+ `docker` and `podman` exit at once naming this pattern. Use the ports the
30
+ **`ports`** hook prints and export what the **`isolate`** hook prints; both
31
+ are the same hooks a host run uses.
32
+ - **`isolate`** — run it, and export what it prints, whenever the ticket touches
33
+ stored state. The sandbox's own services are already private to this run,
34
+ but the hook is where the repository says which settings point at them.
35
+ - **`handoff`** — a preview server cannot be reached from outside a sandbox, so
36
+ there is nothing to leave running. Skip it unless the repository's own
37
+ instructions say otherwise, and say in your progress comment that you did.
38
+
39
+ ## Running the suite
40
+
41
+ Run the **`test`** and **`build`** hooks in the foreground, as the lane policy
42
+ says. Stop every process `setup` started before you finish; the sandbox is
43
+ deleted when the run ends, so nothing outlives it, but a service still writing
44
+ when the push happens makes the result harder to judge.
45
+
46
+ ## Handing off
47
+
48
+ Commit on the ticket's branch and push it as a plain fast-forward to `origin`,
49
+ exactly as the lane policy says: `git push --set-upstream origin <branch>`.
50
+ Push credentials are already in place. The sandbox is deleted at the end of the
51
+ run, so **anything not pushed is gone**; there is no worktree for QA to pick up
52
+ and QA cuts its own from the remote branch you pushed. Put which hooks you ran,
53
+ and their results, in your progress comment.