@tablation/crew 0.123.0 → 0.124.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 +44 -7
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -406,7 +406,7 @@ function registerCodeServer(worktree, url, server = INTEL_SERVER_NAME) {
406
406
 
407
407
  // src/intel-serena.ts
408
408
  import { spawn, spawnSync } from "child_process";
409
- import { closeSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
409
+ import { closeSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, rmSync, symlinkSync, writeFileSync as writeFileSync4 } from "fs";
410
410
  import { dirname as dirname3, join as join5 } from "path";
411
411
 
412
412
  // src/ports.ts
@@ -575,6 +575,10 @@ function startSerena(d, s) {
575
575
  const p = serenaPaths(s.stateDir, s.route, String(s.ticket));
576
576
  try {
577
577
  prepareFolder(d, p.home, p.project, s.worktree);
578
+ if (s.languageServers && d.exists(s.languageServers) && d.link) {
579
+ const dest = join5(p.home, "language_servers");
580
+ if (!d.exists(dest)) d.link(s.languageServers, dest);
581
+ }
578
582
  const mainCache = join5(p.mainProject, "cache");
579
583
  const cache = join5(p.project, "cache");
580
584
  if (!d.exists(cache) && d.exists(mainCache)) d.copyDir(mainCache, cache);
@@ -625,6 +629,7 @@ function realSerenaDeps(onPath2, extraEnv = process.env) {
625
629
  spawnDetached: detached,
626
630
  // `&&` so the stamp exists only for an index that finished; a killed one is retried next start.
627
631
  spawnIndex: (args, env, log, stamp, stampText) => detached("sh", ["-c", 'serena "$@" && printf %s "$STAMP_TEXT" > "$STAMP_FILE"', "serena", ...args], { ...env, STAMP_FILE: stamp, STAMP_TEXT: stampText }, log),
632
+ link: (target, path) => symlinkSync(target, path),
628
633
  alive: (pid) => {
629
634
  try {
630
635
  process.kill(pid, 0);
@@ -12681,6 +12686,10 @@ var SANDBOX_CLAUDE_SHA256 = {
12681
12686
  };
12682
12687
  var SANDBOX_UID = 1e3;
12683
12688
  var CREW_PREFIX = "/opt/crew";
12689
+ var SANDBOX_UV_VERSION = "0.13.6";
12690
+ var SANDBOX_SERENA_VERSION = "2.1.0";
12691
+ var SERENA_IMAGE_HOME = `${CREW_PREFIX}/serena`;
12692
+ var SERENA_LANGUAGE_SERVERS_ENV = "CREW_SERENA_LANGUAGE_SERVERS";
12684
12693
  var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
12685
12694
  var KEEP_TAGS = 2;
12686
12695
  var CREDENTIAL_HELPER = `#!/bin/sh
@@ -12717,6 +12726,21 @@ function claudeInstallCommand() {
12717
12726
  `${CREW_PREFIX}/bin/claude --version | grep -F "${SANDBOX_CLAUDE_VERSION}"`
12718
12727
  ].join("; ");
12719
12728
  }
12729
+ function serenaInstallCommand() {
12730
+ return [
12731
+ `python3 -m venv ${CREW_PREFIX}/uv-venv`,
12732
+ `${CREW_PREFIX}/uv-venv/bin/pip install --no-cache-dir uv==${SANDBOX_UV_VERSION}`,
12733
+ `ln -sf ${CREW_PREFIX}/uv-venv/bin/uv ${CREW_PREFIX}/bin/uv`,
12734
+ `UV_TOOL_DIR=${CREW_PREFIX}/uv/tools UV_TOOL_BIN_DIR=${CREW_PREFIX}/bin UV_PYTHON_INSTALL_DIR=${CREW_PREFIX}/uv/python UV_CACHE_DIR=${CREW_PREFIX}/cache/uv ${CREW_PREFIX}/bin/uv tool install -p 3.13 serena-agent==${SANDBOX_SERENA_VERSION}`,
12735
+ `${CREW_PREFIX}/bin/serena --help >/dev/null`
12736
+ ].join("; ");
12737
+ }
12738
+ function serenaWarmCommand() {
12739
+ return [
12740
+ `mkdir -p ${SERENA_IMAGE_HOME}`,
12741
+ `SERENA_HOME=${SERENA_IMAGE_HOME} serena project index ${WARM_DIR}`
12742
+ ].join("; ");
12743
+ }
12720
12744
  function baseLayer() {
12721
12745
  const dockerfile = [
12722
12746
  `FROM ${SANDBOX_BASE_IMAGE}`,
@@ -12725,18 +12749,20 @@ function baseLayer() {
12725
12749
  ].join("\n");
12726
12750
  return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_OPENSHELL_VERSION })}`, dockerfile, files: {} };
12727
12751
  }
12728
- function crewLayer(base) {
12752
+ function crewLayer(base, intel) {
12729
12753
  const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
12730
12754
  const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
12731
12755
  const dockerfile = [
12732
12756
  `FROM ${base.tag}`,
12733
12757
  "USER root",
12734
- run2("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
12758
+ run2(`apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git${intel ? " python3 python3-venv nodejs" : ""} && rm -rf /var/lib/apt/lists/*`),
12735
12759
  `COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
12736
12760
  `COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
12737
12761
  run2(claudeInstallCommand()),
12762
+ ...intel ? [run2(`mkdir -p ${CREW_PREFIX}/cache && ${serenaInstallCommand()}`)] : [],
12738
12763
  run2(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
12739
12764
  run2(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
12765
+ ...intel ? [`ENV ${SERENA_LANGUAGE_SERVERS_ENV}=${SERENA_IMAGE_HOME}/language_servers UV_PYTHON_INSTALL_DIR=${CREW_PREFIX}/uv/python`] : [],
12740
12766
  `ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
12741
12767
  `LABEL io.crew.sandbox.layer="crew" io.crew.sandbox.claude="${SANDBOX_CLAUDE_VERSION}"`,
12742
12768
  ""
@@ -12790,6 +12816,7 @@ function repoLayer(repo, decl, crew, warm) {
12790
12816
  lines2.push(`COPY --chown=${SANDBOX_UID}:${SANDBOX_UID} warm/ ${WARM_DIR}/`, `WORKDIR ${WARM_DIR}`);
12791
12817
  }
12792
12818
  for (const step of decl.setup) lines2.push(run2(step));
12819
+ if (warming && decl.intel) lines2.push(run2(serenaWarmCommand()));
12793
12820
  if (warming) lines2.push("WORKDIR /sandbox", run2(`rm -rf ${WARM_DIR}`));
12794
12821
  lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
12795
12822
  const dockerfile = lines2.join("\n");
@@ -12797,7 +12824,7 @@ function repoLayer(repo, decl, crew, warm) {
12797
12824
  }
12798
12825
  function planSandbox(repo, decl, warm = {}) {
12799
12826
  const base = baseLayer();
12800
- const crew = crewLayer(base);
12827
+ const crew = crewLayer(base, decl.intel);
12801
12828
  const layer = repoLayer(repo, decl, crew, warm);
12802
12829
  return { repo, tag: layer.tag, claude: SANDBOX_CLAUDE_VERSION, layers: [base, crew, layer] };
12803
12830
  }
@@ -25400,7 +25427,7 @@ ${changed2.map((c) => ` - ${c}
25400
25427
  warm = readWarmFiles(r.dir, `${r.config.branch.remote}/${r.config.branch.base}`, r.config.sandbox.warm ?? []);
25401
25428
  } catch {
25402
25429
  }
25403
- out.set(r.name, { name: r.name, decl: r.config.sandbox, warm });
25430
+ out.set(r.name, { name: r.name, decl: intelFlavor(r.config.hooks.intel) === "serena" ? { ...r.config.sandbox, intel: "serena" } : r.config.sandbox, warm });
25404
25431
  }
25405
25432
  }
25406
25433
  return [...out.values()];
@@ -28496,7 +28523,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28496
28523
  }
28497
28524
  const pathWt = applyRunWorktree(repoWt.dir, repoWt.config, planWt);
28498
28525
  emit.emit(describeRunWorktreePlan(planWt));
28499
- if (pathWt && planIsUsable(planWt) && repoWt.config.hooks.intel) {
28526
+ if (pathWt && planIsUsable(planWt) && wantsCodeBridge(repoWt.config.hooks.intel)) {
28500
28527
  const shellWt = resolveShell(repoWt.config.shell ?? cfg.ship.shell);
28501
28528
  const paragraph = await startWorktreeIntel({
28502
28529
  worktree: pathWt,
@@ -28518,6 +28545,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28518
28545
  },
28519
28546
  runHook: async (name, cwd, env) => {
28520
28547
  const script = repoWt.config.hooks[name];
28548
+ if (name === "intel" && intelFlavor(script) === "serena") {
28549
+ return startSerena(realSerenaDeps((bin) => !!findOnPath(bin, pathFor(cfg.ship)), { ...process.env, PATH: pathFor(cfg.ship) }), {
28550
+ stateDir: cfg.ship.stateDir,
28551
+ route: routeWt.route,
28552
+ ticket: Number(nWt),
28553
+ worktree: cwd,
28554
+ mainCheckout: repoWt.dir
28555
+ });
28556
+ }
28521
28557
  return script ? runScript(script, { cwd, env, shell: shellWt, timeoutMs: name === "intel" ? 6e4 : void 0 }) : null;
28522
28558
  }
28523
28559
  });
@@ -28836,7 +28872,8 @@ ${where.join("\n")}`);
28836
28872
  route: routeP.route,
28837
28873
  ticket: Number(/(\d+)$/.exec(env.CREW_TICKET ?? "")?.[1] ?? 0),
28838
28874
  worktree: cwd,
28839
- mainCheckout: repoP.dir
28875
+ mainCheckout: repoP.dir,
28876
+ languageServers: process.env.CREW_SERENA_LANGUAGE_SERVERS
28840
28877
  });
28841
28878
  }
28842
28879
  if (name === "intel" && lspFlavor(script)) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.123.0",
3
+ "version": "0.124.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",