@tablation/crew 0.126.11 → 0.127.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 (2) hide show
  1. package/dist/cli.js +65 -17
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10821,7 +10821,8 @@ var SandboxRunner = class {
10821
10821
  }
10822
10822
  const create2 = ["sandbox", "create", "--name", name, "--detach"];
10823
10823
  if (policyFile) create2.push("--policy", policyFile);
10824
- if (cfg.image) create2.push("--from", cfg.image);
10824
+ const image = cfg.image ?? plan.sandboxImage;
10825
+ if (image) create2.push("--from", image);
10825
10826
  if (cfg.cacheVolume) {
10826
10827
  create2.push("--driver-config-json", JSON.stringify({
10827
10828
  docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
@@ -11041,6 +11042,15 @@ function sandboxLspPluginFor(route2, repo) {
11041
11042
  return void 0;
11042
11043
  }
11043
11044
  }
11045
+ function sandboxImageOf(route2, repo, stateDir) {
11046
+ let declares = false;
11047
+ try {
11048
+ const dir = reposOf(route2).find((r) => r.name === repo)?.dir ?? route2.dir;
11049
+ declares = Boolean(loadRepoConfig(dir)?.sandbox);
11050
+ } catch {
11051
+ }
11052
+ return sandboxImageForRun(readSandboxState(stateDir), repo, declares) ?? void 0;
11053
+ }
11044
11054
  var AgentSpawnError = class extends AgentError {
11045
11055
  };
11046
11056
  var WorkspaceAuthRefused = class extends AgentSpawnError {
@@ -11181,6 +11191,7 @@ function planAgentRun(o) {
11181
11191
  const code = codeBridgeFor(o, adapter.harness);
11182
11192
  const sandboxLsp = o.ship.runner === "sandbox" && adapter.harness === "claude" ? sandboxLspPluginFor(o.route, o.repo) : void 0;
11183
11193
  const sandboxOrigin = o.ship.runner === "sandbox" ? sandboxOriginFor(o.route, o.repo) : void 0;
11194
+ const sandboxImage = o.ship.runner === "sandbox" ? sandboxImageOf(o.route, o.repo, o.stateDir) : void 0;
11184
11195
  const sandboxFiles = [...code?.files ?? [], ...sandboxLsp?.files ?? []];
11185
11196
  const invocation = adapter.invocation(toolchain, {
11186
11197
  code,
@@ -11234,6 +11245,7 @@ function planAgentRun(o) {
11234
11245
  runner: "sandbox",
11235
11246
  ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {},
11236
11247
  ...sandboxOrigin ? { sandboxOrigin } : {},
11248
+ ...sandboxImage ? { sandboxImage } : {},
11237
11249
  sandboxPolicy: sandboxPolicyInputFor(o.route, toolchain, { harnessBin: invocation.bin, repo: o.repo, sandbox: o.ship.sandbox }),
11238
11250
  ...sandboxFiles.length ? { sandboxFiles } : {}
11239
11251
  } : {},
@@ -13104,6 +13116,8 @@ function planSandbox(repo, decl, warm = {}, bridge) {
13104
13116
  const layer = repoLayer(repo, decl, crew, warm);
13105
13117
  return { repo, tag: layer.tag, claude: SANDBOX_CLAUDE_VERSION, layers: [base, crew, layer] };
13106
13118
  }
13119
+ var SHIP_SANDBOX_KEY = "@ship";
13120
+ var shipSandboxRef = () => ({ name: SHIP_SANDBOX_KEY, decl: { packages: [], setup: [] } });
13107
13121
  var sandboxStatePath = (stateDir) => join32(stateDir, "sandbox-images.json");
13108
13122
  function readSandboxState(stateDir) {
13109
13123
  try {
@@ -13123,6 +13137,9 @@ function writeSandboxState(stateDir, state) {
13123
13137
  function sandboxImageFor(state, repo) {
13124
13138
  return state[repo]?.tags[0]?.tag ?? null;
13125
13139
  }
13140
+ function sandboxImageForRun(state, repo, declares) {
13141
+ return (declares && repo ? sandboxImageFor(state, repo) : null) ?? sandboxImageFor(state, SHIP_SANDBOX_KEY);
13142
+ }
13126
13143
  function sandboxImageStatus(state, plan) {
13127
13144
  const s = state[plan.repo];
13128
13145
  const current = s?.tags[0];
@@ -13595,7 +13612,7 @@ function doctorSelftestLines(stored, toolchains) {
13595
13612
  // src/cli.ts
13596
13613
  import { resolve as resolve7, dirname as dirname34, join as join56, basename as basename16 } from "path";
13597
13614
  import { fileURLToPath as fileURLToPath2 } from "url";
13598
- import { hostname as hostname4, homedir as homedir11 } from "os";
13615
+ import { hostname as hostname5, homedir as homedir11 } from "os";
13599
13616
  import { spawnSync as spawnSync11 } from "child_process";
13600
13617
  import { parse as parseYaml2 } from "yaml";
13601
13618
 
@@ -17463,7 +17480,7 @@ function flagWizardAnswers(f, env) {
17463
17480
  // src/release-run.ts
17464
17481
  import { readFileSync as readFileSync36, writeFileSync as writeFileSync31, existsSync as existsSync39, copyFileSync, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync3, rmSync as rmSync19 } from "fs";
17465
17482
  import { dirname as dirname24, join as join43 } from "path";
17466
- import { tmpdir as tmpdir5 } from "os";
17483
+ import { tmpdir as tmpdir5, hostname as hostname4 } from "os";
17467
17484
 
17468
17485
  // src/release.ts
17469
17486
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -18150,6 +18167,17 @@ async function confirmExternalReleased(o, closures) {
18150
18167
  return { ...c, confirmed, version };
18151
18168
  });
18152
18169
  }
18170
+ var mergeFailed = (r) => typeof r === "object" && ("conflict" in r || "error" in r);
18171
+ function ensureCommitterIdentity(o) {
18172
+ if (gitOk(o.cwd, ["var", "GIT_COMMITTER_IDENT"])) return;
18173
+ const ship = hostname4().split(".")[0] || "ship";
18174
+ git(o.cwd, ["config", "user.name", `Crew (${ship})`]);
18175
+ git(o.cwd, ["config", "user.email", `crew@${ship}`]);
18176
+ o.emit.warn(`no git committer identity here \u2014 set a checkout-local one, Crew (${ship}), for the release commit`);
18177
+ }
18178
+ function isConflict(message, unmergedPaths) {
18179
+ return unmergedPaths.length > 0 || /CONFLICT \(|Automatic merge failed|unmerged (files|paths)/i.test(message);
18180
+ }
18153
18181
  function mergeOne(o, c) {
18154
18182
  const subject = c.entries[0] ?? `${c.ticket.title ?? c.ticket.issue_id} (${c.ticket.issue_id})`;
18155
18183
  if (o.dryRun) {
@@ -18157,7 +18185,11 @@ function mergeOne(o, c) {
18157
18185
  return { applied: true };
18158
18186
  }
18159
18187
  const before = headSha(o.cwd);
18188
+ const clearFailure = () => {
18189
+ o.attention?.clear(`release_merge_failed:${o.scope ?? "release"}`);
18190
+ };
18160
18191
  try {
18192
+ ensureCommitterIdentity(o);
18161
18193
  git(o.cwd, ["merge", "--squash", c.branch]);
18162
18194
  if (!git(o.cwd, ["diff", "--cached", "--name-only"])) {
18163
18195
  git(o.cwd, ["reset", "--hard", before]);
@@ -18165,6 +18197,7 @@ function mergeOne(o, c) {
18165
18197
  `${c.branch} is already contained in the integration branch \u2014 nothing to apply`,
18166
18198
  { ticket: c.ticket.issue_id }
18167
18199
  );
18200
+ clearFailure();
18168
18201
  return "noop";
18169
18202
  }
18170
18203
  const tip = gitOk(o.cwd, ["rev-parse", "--verify", "-q", `${c.branch}^{commit}`]);
@@ -18176,12 +18209,25 @@ ${BRANCH_TIP_TRAILER}: ${tip}` : "";
18176
18209
  Closes ${c.ticket.issue_id}.${trailer}`]);
18177
18210
  const sha = headSha(o.cwd);
18178
18211
  o.emit.emit(`merged ${c.branch}`, { ticket: c.ticket.issue_id });
18212
+ clearFailure();
18179
18213
  return { applied: true, sha };
18180
18214
  } catch (e) {
18181
18215
  const paths = (gitOk(o.cwd, ["diff", "--name-only", "--diff-filter=U"]) ?? "").split("\n").filter(Boolean);
18182
18216
  gitOk(o.cwd, ["reset", "--hard", before]);
18183
- o.emit.error(`could not merge ${c.branch}: ${e.message}`, { ticket: c.ticket.issue_id });
18184
- return { conflict: { candidate: c, paths, message: e.message } };
18217
+ const message = e.message;
18218
+ o.emit.error(`could not merge ${c.branch}: ${message}`, { ticket: c.ticket.issue_id });
18219
+ if (!isConflict(message, paths)) {
18220
+ if (o.attention) {
18221
+ void o.attention.raise({
18222
+ kind: "release_merge_failed",
18223
+ key: `release_merge_failed:${o.scope ?? "release"}`,
18224
+ message: `${o.scope ?? "release"}: could not merge ${c.branch} (${c.ticket.issue_id}) \u2014 not a conflict, so the ticket stays verified and is retried: ${message.slice(0, 400)}`,
18225
+ since: (/* @__PURE__ */ new Date()).toISOString()
18226
+ });
18227
+ }
18228
+ return { error: message };
18229
+ }
18230
+ return { conflict: { candidate: c, paths, message } };
18185
18231
  }
18186
18232
  }
18187
18233
  async function bump(o, size) {
@@ -18576,7 +18622,7 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
18576
18622
  for (const c of applied) {
18577
18623
  gitOk(o.cwd, ["reset", "--hard", preMerge]);
18578
18624
  const m = mergeOne(o, c);
18579
- if (typeof m === "object" && "conflict" in m) continue;
18625
+ if (mergeFailed(m)) continue;
18580
18626
  const r = await hook(o, name);
18581
18627
  if (r && r.code !== 0) {
18582
18628
  o.emit.warn(`${c.branch} fails the ${name} hook on its own`, { ticket: c.ticket.issue_id });
@@ -18588,8 +18634,8 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
18588
18634
  for (const [i, j] of pairs) {
18589
18635
  gitOk(o.cwd, ["reset", "--hard", preMerge]);
18590
18636
  const a = mergeOne(o, applied[i]);
18591
- const b = typeof a === "object" && "conflict" in a ? a : mergeOne(o, applied[j]);
18592
- if (typeof b === "object" && "conflict" in b) continue;
18637
+ const b = mergeFailed(a) ? a : mergeOne(o, applied[j]);
18638
+ if (mergeFailed(b)) continue;
18593
18639
  const r = await hook(o, name);
18594
18640
  if (r && r.code !== 0) {
18595
18641
  o.emit.warn(`${applied[j].branch} fails the ${name} hook together with ${applied[i].branch}`, { ticket: applied[j].ticket.issue_id });
@@ -18606,7 +18652,7 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
18606
18652
  let clean = true;
18607
18653
  for (const c of applied.slice(0, mid)) {
18608
18654
  const m = mergeOne(o, c);
18609
- if (typeof m === "object" && "conflict" in m) {
18655
+ if (mergeFailed(m)) {
18610
18656
  clean = false;
18611
18657
  break;
18612
18658
  }
@@ -18677,6 +18723,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
18677
18723
  conflicts.push(r.conflict);
18678
18724
  continue;
18679
18725
  }
18726
+ if (typeof r === "object" && "error" in r) continue;
18680
18727
  if (typeof r === "object" && r.applied) {
18681
18728
  applied++;
18682
18729
  c.sha = r.sha;
@@ -25385,7 +25432,7 @@ if (command === "passenger-sync-daemon") {
25385
25432
  };
25386
25433
  if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
25387
25434
  const flagged = flagWizardAnswers(wizardFlags, {
25388
- hostname: hostname4(),
25435
+ hostname: hostname5(),
25389
25436
  platform: hostPlatform(),
25390
25437
  pathEnv: process.env.PATH ?? "",
25391
25438
  nodePath: process.execPath,
@@ -25410,7 +25457,7 @@ if (command === "passenger-sync-daemon") {
25410
25457
  let answers;
25411
25458
  try {
25412
25459
  answers = await runWizard(prompter, {
25413
- hostname: hostname4(),
25460
+ hostname: hostname5(),
25414
25461
  platform: hostPlatform(),
25415
25462
  pathEnv: process.env.PATH ?? "",
25416
25463
  nodePath: process.execPath,
@@ -25894,6 +25941,7 @@ ${changed2.map((c) => ` - ${c}
25894
25941
  }
25895
25942
  return [...out.values()];
25896
25943
  }
25944
+ const withShipSandbox = (declared) => cfg.ship.runner === "sandbox" ? [shipSandboxRef(), ...declared] : declared;
25897
25945
  async function resolveOneRepo(c, t) {
25898
25946
  {
25899
25947
  const id = repoIdForName(c, t.name);
@@ -27162,7 +27210,7 @@ ${cu.tail.trimEnd()}
27162
27210
  onPass: (result) => {
27163
27211
  if (!sandboxBuilding) {
27164
27212
  sandboxBuilding = true;
27165
- void sandboxReposOf(cfg.routes.filter((r) => r.enabled)).then((repos) => repos.length ? sandboxBuildTick(repos, sandboxTickDeps(emit, route2)) : null).catch((err3) => emit.warn(`sandbox image build tick failed: ${err3 instanceof Error ? err3.message : String(err3)}`)).finally(() => {
27213
+ void sandboxReposOf(cfg.routes.filter((r) => r.enabled)).then(withShipSandbox).then((repos) => repos.length ? sandboxBuildTick(repos, sandboxTickDeps(emit, route2)) : null).catch((err3) => emit.warn(`sandbox image build tick failed: ${err3 instanceof Error ? err3.message : String(err3)}`)).finally(() => {
27166
27214
  sandboxBuilding = false;
27167
27215
  });
27168
27216
  }
@@ -27967,7 +28015,7 @@ ${reconstruction.prompt}
27967
28015
  let result;
27968
28016
  try {
27969
28017
  result = await loginWithDeviceCode(`${baseUrl}/api`, {
27970
- deviceName: `crew on ${hostname4()}`,
28018
+ deviceName: `crew on ${hostname5()}`,
27971
28019
  // Lets the approval page preselect the workspace this connect is for.
27972
28020
  // A bare `crew connect` has none and passes nothing.
27973
28021
  ...workspace ? { workspaceSlug: workspace } : {},
@@ -28696,7 +28744,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28696
28744
  }
28697
28745
  throw e;
28698
28746
  }
28699
- const declared = await sandboxReposOf(routesS);
28747
+ const declared = withShipSandbox(await sandboxReposOf(routesS));
28700
28748
  if (subS === "images") {
28701
28749
  const stateS = readSandboxState(cfg.ship.stateDir);
28702
28750
  const rows = declared.map((r) => {
@@ -28723,8 +28771,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28723
28771
  const only = positional[2];
28724
28772
  const targets = only ? declared.filter((r) => r.name === only) : declared;
28725
28773
  if (targets.length === 0) {
28726
- process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
28727
- ` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
28774
+ process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section (${SHIP_SANDBOX_KEY} is the ship's crew layer, built only on a ship.runner: sandbox ship)
28775
+ ` : "crew: this ship is not a sandbox ship and no repository declares a sandbox: section in its .crew.yaml\n");
28728
28776
  process.exit(2);
28729
28777
  }
28730
28778
  const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
@@ -30823,7 +30871,7 @@ ${foreign.join("\n")}
30823
30871
  }
30824
30872
  }
30825
30873
  {
30826
- const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
30874
+ const declaredS = withShipSandbox(await sandboxReposOf(cfg.routes.filter((r) => r.enabled)));
30827
30875
  const stateS = readSandboxState(cfg.ship.stateDir);
30828
30876
  for (const r of declaredS) {
30829
30877
  const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl, r.warm, r.bridge));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.126.11",
3
+ "version": "0.127.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",