@tablation/crew 0.46.0 → 0.47.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 +174 -71
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1963,6 +1963,11 @@ var ROLE_LABEL = {
1963
1963
  };
1964
1964
  var ConfigError = class extends Error {
1965
1965
  };
1966
+ function parseCloneScheme(v) {
1967
+ if (v === void 0 || v === null || v === "") return void 0;
1968
+ if (v === "ssh" || v === "https") return v;
1969
+ throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
1970
+ }
1966
1971
  var SHIP_KEYS = /* @__PURE__ */ new Set([
1967
1972
  "name",
1968
1973
  "platform",
@@ -1987,7 +1992,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
1987
1992
  "relayPort",
1988
1993
  "relayHttpPort",
1989
1994
  "relayPublicDomain",
1990
- "passengerImage"
1995
+ "passengerImage",
1996
+ "cloneScheme"
1991
1997
  ]);
1992
1998
  var ROUTE_KEYS = /* @__PURE__ */ new Set([
1993
1999
  "route",
@@ -2250,6 +2256,7 @@ function parseResolved(raw, m, where) {
2250
2256
  holds: holds2,
2251
2257
  contract: raw.contract ?? void 0,
2252
2258
  ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
2259
+ ...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
2253
2260
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
2254
2261
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
2255
2262
  };
@@ -2355,7 +2362,8 @@ Create the first of those:
2355
2362
  relayPort: shipRaw.relayPort !== void 0 ? Number(shipRaw.relayPort) : 2222,
2356
2363
  relayHttpPort: shipRaw.relayHttpPort !== void 0 ? Number(shipRaw.relayHttpPort) : 443,
2357
2364
  relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0,
2358
- passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0
2365
+ passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0,
2366
+ cloneScheme: parseCloneScheme(shipRaw.cloneScheme)
2359
2367
  },
2360
2368
  routes,
2361
2369
  crewHome,
@@ -4020,17 +4028,22 @@ import {
4020
4028
  closeSync as closeSync2,
4021
4029
  writeSync,
4022
4030
  statSync as statSync4,
4023
- unlinkSync as unlinkSync2
4031
+ unlinkSync as unlinkSync2,
4032
+ readFileSync as readFileSync9,
4033
+ writeFileSync as writeFileSync6
4024
4034
  } from "fs";
4025
4035
  import { dirname as dirname5, basename as basename4 } from "path";
4026
4036
  var GitError = class extends Error {
4027
4037
  };
4038
+ function streamText(x) {
4039
+ return String(x ?? "").trim();
4040
+ }
4028
4041
  function gitRaw(cwd, args) {
4029
4042
  try {
4030
4043
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4031
4044
  } catch (e) {
4032
4045
  const err = e;
4033
- const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
4046
+ const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4034
4047
  throw new GitError(`git ${args.join(" ")}: ${why || err.message.trim()}`);
4035
4048
  }
4036
4049
  }
@@ -4039,7 +4052,7 @@ function git(cwd, args) {
4039
4052
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4040
4053
  } catch (e) {
4041
4054
  const err = e;
4042
- const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
4055
+ const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4043
4056
  throw new GitError(`git ${args.join(" ")}: ${why || err.message.trim()}`);
4044
4057
  }
4045
4058
  }
@@ -4382,10 +4395,19 @@ function worktrees(cwd) {
4382
4395
  if (current.path) list.push({ path: current.path, branch: current.branch ?? null, prunable: current.prunable ?? false });
4383
4396
  return list;
4384
4397
  }
4385
- function cloneUrlFor(remote) {
4386
- return `git@github.com:${remote}.git`;
4398
+ function cloneUrlFor(remote, opts = {}) {
4399
+ const origin = opts.originUrl?.trim() ?? "";
4400
+ const scheme = /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
4401
+ return scheme === "https" ? `https://github.com/${remote}.git` : `git@github.com:${remote}.git`;
4402
+ }
4403
+ function originUrlOf(dir, remoteName) {
4404
+ try {
4405
+ return git(dir, ["remote", "get-url", remoteName]) || null;
4406
+ } catch {
4407
+ return null;
4408
+ }
4387
4409
  }
4388
- function ensureRepoCheckout(dir, remote) {
4410
+ function ensureRepoCheckout(dir, remote, opts = {}) {
4389
4411
  if (existsSync9(`${dir}/.git`)) return false;
4390
4412
  if (!remote) {
4391
4413
  throw new GitError(
@@ -4393,12 +4415,13 @@ function ensureRepoCheckout(dir, remote) {
4393
4415
  );
4394
4416
  }
4395
4417
  mkdirSync5(dirname5(dir), { recursive: true });
4418
+ const url = cloneUrlFor(remote, opts);
4396
4419
  try {
4397
- execFileSync("git", ["clone", cloneUrlFor(remote), dir], { stdio: ["ignore", "pipe", "pipe"] });
4420
+ execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4398
4421
  } catch (e) {
4399
4422
  const err = e;
4400
- const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
4401
- throw new GitError(`git clone ${cloneUrlFor(remote)} ${dir}: ${why || err.message.trim()}`);
4423
+ const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4424
+ throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
4402
4425
  }
4403
4426
  return true;
4404
4427
  }
@@ -4445,10 +4468,31 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4445
4468
  }
4446
4469
  }
4447
4470
  }
4448
- function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base) {
4471
+ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4449
4472
  const release = acquireCheckoutLock(dir);
4450
4473
  try {
4451
- if (ensureRepoCheckout(dir, githubRemote)) {
4474
+ if (!existsSync9(`${dir}/.git`) && githubRemote) {
4475
+ const url = cloneUrlFor(githubRemote, clone);
4476
+ const marker = cloneFailureMarker(dir);
4477
+ if (existsSync9(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
4478
+ return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4479
+ }
4480
+ try {
4481
+ ensureRepoCheckout(dir, githubRemote, clone);
4482
+ } catch (e) {
4483
+ if (!(e instanceof GitError)) throw e;
4484
+ writeFileSync6(marker, `${url}
4485
+ ${e.message}`);
4486
+ return { action: "clone-failed", detail: e.message, repeat: false };
4487
+ }
4488
+ try {
4489
+ unlinkSync2(marker);
4490
+ } catch {
4491
+ }
4492
+ if (currentBranch(dir) !== base) git(dir, ["checkout", base]);
4493
+ return { action: "cloned", detail: `cloned ${githubRemote} into ${dir}` };
4494
+ }
4495
+ if (ensureRepoCheckout(dir, githubRemote, clone)) {
4452
4496
  if (currentBranch(dir) !== base) git(dir, ["checkout", base]);
4453
4497
  return { action: "cloned", detail: `cloned ${githubRemote} into ${dir}` };
4454
4498
  }
@@ -4457,6 +4501,9 @@ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base) {
4457
4501
  release();
4458
4502
  }
4459
4503
  }
4504
+ function cloneFailureMarker(dir) {
4505
+ return `${dir}.clone-failed`;
4506
+ }
4460
4507
 
4461
4508
  // src/ship-attention.ts
4462
4509
  var RELEASE_STALE_AFTER_INTERVALS = 3;
@@ -6220,7 +6267,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
6220
6267
  }
6221
6268
 
6222
6269
  // src/poll.ts
6223
- import { writeFileSync as writeFileSync6 } from "fs";
6270
+ import { writeFileSync as writeFileSync7 } from "fs";
6224
6271
  import { join as join10 } from "path";
6225
6272
 
6226
6273
  // src/stalled.ts
@@ -6560,7 +6607,7 @@ function writeDigest(o, d, role, stateDir) {
6560
6607
  };
6561
6608
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
6562
6609
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
6563
- writeFileSync6(path, text);
6610
+ writeFileSync7(path, text);
6564
6611
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
6565
6612
  data: { role, bytes: Buffer.byteLength(text) }
6566
6613
  });
@@ -6944,7 +6991,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
6944
6991
 
6945
6992
  // src/agent.ts
6946
6993
  import { spawn as spawn2 } from "child_process";
6947
- import { readFileSync as readFileSync11, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6994
+ import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream } from "fs";
6948
6995
  import { finished } from "stream/promises";
6949
6996
  import { join as join13, dirname as dirname6 } from "path";
6950
6997
  import { createHash as createHash2 } from "crypto";
@@ -7361,13 +7408,13 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7361
7408
  }
7362
7409
 
7363
7410
  // src/skills.ts
7364
- import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
7411
+ import { readFileSync as readFileSync11, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
7365
7412
  import { join as join12 } from "path";
7366
7413
  import { parse as parseYaml } from "yaml";
7367
7414
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
7368
7415
 
7369
7416
  // src/agents.ts
7370
- import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
7417
+ import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
7371
7418
  import { join as join11 } from "path";
7372
7419
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
7373
7420
  var PERSONA_NAME = {
@@ -7384,10 +7431,10 @@ var AgentsSyncError = class extends Error {
7384
7431
  function personaDefaultPrompt(promptsDir, role) {
7385
7432
  const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
7386
7433
  if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7387
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync9(briefPath, "utf8");
7434
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
7388
7435
  const commonPath = join11(promptsDir, "personas", "common.md");
7389
- if (!existsSync10(commonPath)) return readFileSync9(briefPath, "utf8");
7390
- return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
7436
+ if (!existsSync10(commonPath)) return readFileSync10(briefPath, "utf8");
7437
+ return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
7391
7438
  }
7392
7439
  async function resolveAgentId(route, role, opts) {
7393
7440
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -7502,6 +7549,8 @@ function needsBaselineBackfill(model, row) {
7502
7549
  function isPromptGateRejection(e) {
7503
7550
  return e instanceof TablationApiError && e.status === 422;
7504
7551
  }
7552
+ var RELEASE_AGENT_NAME = "Release agent";
7553
+ var RELEASE_AGENT_PROMPT = "Principal for the crew release phase (merge, deploy, release comments). It runs no model; this row only gives those tracker writes an agent identity.";
7505
7554
  async function syncPersonas(route, opts) {
7506
7555
  if (!route.resolved) {
7507
7556
  throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
@@ -7633,7 +7682,24 @@ async function syncPersonas(route, opts) {
7633
7682
  }
7634
7683
  }
7635
7684
  const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
7636
- return { outcomes, agentPersonas, crewLinks };
7685
+ const releaseAgentId = opts.dryRun ? route.resolved.releaseAgentId : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent);
7686
+ return { outcomes, agentPersonas, crewLinks, ...releaseAgentId ? { releaseAgentId } : {} };
7687
+ }
7688
+ async function linkReleaseSeat(route, client2, agentsModel, userAgent) {
7689
+ const seatId = route.resolved.releaseSeat;
7690
+ if (!seatId) return void 0;
7691
+ const rows = await client2.records.list(agentsModel.id, { limit: 200 });
7692
+ const agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id ?? (await client2.records.create(agentsModel.id, {
7693
+ name: RELEASE_AGENT_NAME,
7694
+ prompt: RELEASE_AGENT_PROMPT,
7695
+ ...promptSourceFields(agentsModel, RELEASE_AGENT_PROMPT)
7696
+ })).id;
7697
+ const crewModelId = route.resolved.models.crew;
7698
+ const crewModel = await client2.dataModels.get(crewModelId);
7699
+ const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
7700
+ const seatRow = await client2.records.get(crewModelId, seatId);
7701
+ if (seatRow[CREW_AGENT_COLUMN] !== agentId) await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
7702
+ return agentId;
7637
7703
  }
7638
7704
  async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
7639
7705
  const seats2 = route.resolved.seats;
@@ -7733,7 +7799,7 @@ function describeCrewLink(o) {
7733
7799
  var SkillSyncError = class extends Error {
7734
7800
  };
7735
7801
  function parseSkillFile(path) {
7736
- const text = readFileSync10(path, "utf8");
7802
+ const text = readFileSync11(path, "utf8");
7737
7803
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7738
7804
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7739
7805
  const frontmatter = match[1];
@@ -7994,10 +8060,10 @@ function planAgentRun(o) {
7994
8060
  let ageSeconds;
7995
8061
  if (existsSync12(digestPath2)) {
7996
8062
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7997
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync11(digestPath2, "utf8");
8063
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
7998
8064
  }
7999
- const common = usesCommon ? readFileSync11(commonPath, "utf8") : "";
8000
- const brief = readFileSync11(briefPath, "utf8");
8065
+ const common = usesCommon ? readFileSync12(commonPath, "utf8") : "";
8066
+ const brief = readFileSync12(briefPath, "utf8");
8001
8067
  const effectiveCommon = o.divergedPrompt ? "" : common;
8002
8068
  const effectiveBrief = o.divergedPrompt ?? brief;
8003
8069
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -8223,7 +8289,7 @@ function spawnAgent(plan, emit) {
8223
8289
  provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
8224
8290
  };
8225
8291
  try {
8226
- writeFileSync7(`${plan.streamPath}.meta.json`, JSON.stringify({
8292
+ writeFileSync8(`${plan.streamPath}.meta.json`, JSON.stringify({
8227
8293
  route: plan.route,
8228
8294
  role: plan.role,
8229
8295
  ticket: plan.ticket ?? null,
@@ -8630,7 +8696,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
8630
8696
 
8631
8697
  // src/connect-wizard.ts
8632
8698
  import { delimiter, dirname as dirname8, join as join16 } from "path";
8633
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
8699
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
8634
8700
  import { stringify } from "yaml";
8635
8701
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
8636
8702
  function shouldRunWizard(opts) {
@@ -8715,7 +8781,7 @@ ${routeBlock}`;
8715
8781
  function writeNewConfig(path, text) {
8716
8782
  mkdirSync7(dirname8(path), { recursive: true });
8717
8783
  try {
8718
- writeFileSync8(path, text, { mode: 384, flag: "wx" });
8784
+ writeFileSync9(path, text, { mode: 384, flag: "wx" });
8719
8785
  return true;
8720
8786
  } catch (e) {
8721
8787
  if (e.code === "EEXIST") return false;
@@ -8747,7 +8813,7 @@ Wrote ${o.configPath}.
8747
8813
  }
8748
8814
 
8749
8815
  // src/release-run.ts
8750
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8816
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8751
8817
  import { dirname as dirname9, join as join17 } from "path";
8752
8818
  import { tmpdir as tmpdir2 } from "os";
8753
8819
 
@@ -9412,7 +9478,7 @@ async function bump(o, size) {
9412
9478
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
9413
9479
  return null;
9414
9480
  }
9415
- const current = JSON.parse(readFileSync12(first, "utf8")).version;
9481
+ const current = JSON.parse(readFileSync13(first, "utf8")).version;
9416
9482
  if (!current) {
9417
9483
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
9418
9484
  return null;
@@ -9425,13 +9491,13 @@ async function bump(o, size) {
9425
9491
  return version;
9426
9492
  }
9427
9493
  for (const f of files) {
9428
- const src = readFileSync12(f, "utf8");
9494
+ const src = readFileSync13(f, "utf8");
9429
9495
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
9430
9496
  if (out === src) {
9431
9497
  o.emit.error(`no version field in ${f}`);
9432
9498
  return null;
9433
9499
  }
9434
- writeFileSync9(f, out);
9500
+ writeFileSync10(f, out);
9435
9501
  }
9436
9502
  return version;
9437
9503
  }
@@ -9444,8 +9510,8 @@ function writeChangelog(o, version, entries) {
9444
9510
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
9445
9511
  return;
9446
9512
  }
9447
- const existing = existsSync16(path) ? readFileSync12(path, "utf8") : "# Changelog\n";
9448
- writeFileSync9(path, insertChangelogSection(existing, section));
9513
+ const existing = existsSync16(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
9514
+ writeFileSync10(path, insertChangelogSection(existing, section));
9449
9515
  }
9450
9516
  async function confirm(o, expected) {
9451
9517
  if (!o.repo.hooks.released) return void 0;
@@ -10824,6 +10890,18 @@ function slugFor(route, agentId) {
10824
10890
  function seatAgentId(route, role) {
10825
10891
  return route.resolved?.agentPersonas?.[role]?.agentId;
10826
10892
  }
10893
+ async function resolveReleaseKey(route, getStore = getSessionStore2) {
10894
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10895
+ const agentId = route.resolved?.releaseAgentId;
10896
+ if (!agentId) return ship("release seat has no linked Agents row (run `crew agents sync`)");
10897
+ try {
10898
+ const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
10899
+ if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
10900
+ return ship("no agent key stored for the release seat (run `crew agents sync`)");
10901
+ } catch {
10902
+ return ship("OS keychain unavailable");
10903
+ }
10904
+ }
10827
10905
  async function resolveSeatKey(route, role, getStore = getSessionStore2) {
10828
10906
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10829
10907
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
@@ -10842,12 +10920,15 @@ async function mintMissingSeatKeys(route, o) {
10842
10920
  if (!route.resolved) return out;
10843
10921
  const resolved = route.resolved;
10844
10922
  const doFetch = o.fetchImpl ?? fetch;
10845
- const seated = SEAT_KEY_ROLES.filter((r) => resolved.seats[r]);
10923
+ const targets = [
10924
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
10925
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
10926
+ ];
10846
10927
  let store;
10847
10928
  try {
10848
10929
  store = (o.getStore ?? getSessionStore2)();
10849
10930
  } catch (e) {
10850
- return seated.map((role) => ({
10931
+ return targets.map(({ role }) => ({
10851
10932
  role,
10852
10933
  action: "failed",
10853
10934
  detail: `OS keychain unavailable: ${e.message}`
@@ -10855,8 +10936,7 @@ async function mintMissingSeatKeys(route, o) {
10855
10936
  }
10856
10937
  const host = hostFromUrl2(route.baseUrl);
10857
10938
  const base = route.baseUrl.replace(/\/+$/, "");
10858
- for (const role of seated) {
10859
- const agentId = seatAgentId(route, role);
10939
+ for (const { role, agentId } of targets) {
10860
10940
  if (!agentId) {
10861
10941
  out.push({ role, action: "no_agent" });
10862
10942
  continue;
@@ -11414,13 +11494,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
11414
11494
  }
11415
11495
 
11416
11496
  // src/cli.ts
11417
- import { readFileSync as readFileSync17, existsSync as existsSync24, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
11497
+ import { readFileSync as readFileSync18, existsSync as existsSync24, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
11418
11498
  import { createInterface } from "readline/promises";
11419
11499
  import { dirname as dirOf, resolve as resolvePath } from "path";
11420
11500
 
11421
11501
  // src/ssh-keys.ts
11422
11502
  import { execFileSync as execFileSync3 } from "child_process";
11423
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync13, chmodSync } from "fs";
11503
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
11424
11504
  import { dirname as dirname11, join as join19 } from "path";
11425
11505
  var SshKeyError = class extends Error {
11426
11506
  };
@@ -11454,7 +11534,7 @@ function ensureShipSshKeypair(stateDir) {
11454
11534
  if (!existsSync20(publicKeyPath)) {
11455
11535
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
11456
11536
  }
11457
- const publicKey = readFileSync13(publicKeyPath, "utf8").trim();
11537
+ const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
11458
11538
  return { privateKeyPath, publicKeyPath, publicKey };
11459
11539
  }
11460
11540
  function sshKeygenAvailable() {
@@ -11652,7 +11732,9 @@ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
11652
11732
  dir: target.dir,
11653
11733
  remote: target.remote,
11654
11734
  gitRemoteName: effective.branch.remote,
11655
- base: effective.branch.base
11735
+ base: effective.branch.base,
11736
+ originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
11737
+ cloneScheme: cfg.ship.cloneScheme
11656
11738
  });
11657
11739
  }
11658
11740
  }
@@ -11662,13 +11744,13 @@ function syncAllPassengerCheckouts(cfg) {
11662
11744
  return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
11663
11745
  name: spec.name,
11664
11746
  workspaceId: spec.workspaceId,
11665
- outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base)
11747
+ outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme })
11666
11748
  }));
11667
11749
  }
11668
11750
 
11669
11751
  // src/tunnel.ts
11670
11752
  import { spawn as spawn3 } from "child_process";
11671
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
11753
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
11672
11754
  import { randomInt } from "crypto";
11673
11755
  import { join as join20 } from "path";
11674
11756
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -11743,7 +11825,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
11743
11825
  const path = tunnelStatePath(stateDir, workspaceId);
11744
11826
  if (!existsSync21(path)) return void 0;
11745
11827
  try {
11746
- return JSON.parse(readFileSync14(path, "utf8"));
11828
+ return JSON.parse(readFileSync15(path, "utf8"));
11747
11829
  } catch {
11748
11830
  return void 0;
11749
11831
  }
@@ -11751,7 +11833,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
11751
11833
  function writePersistedTunnel(stateDir, state) {
11752
11834
  const path = tunnelStatePath(stateDir, state.workspaceId);
11753
11835
  mkdirSync10(join20(stateDir, "passengers"), { recursive: true });
11754
- writeFileSync10(path, `${JSON.stringify(state, null, 2)}
11836
+ writeFileSync11(path, `${JSON.stringify(state, null, 2)}
11755
11837
  `);
11756
11838
  }
11757
11839
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -11970,7 +12052,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
11970
12052
  import { createServer as createHttpServer } from "http";
11971
12053
  import { spawn as spawn4 } from "child_process";
11972
12054
  import { randomBytes } from "crypto";
11973
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
12055
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
11974
12056
  import { join as join21 } from "path";
11975
12057
 
11976
12058
  // src/secret-compare.ts
@@ -12025,7 +12107,7 @@ function createSyncDaemonServer(targets, secret) {
12025
12107
  try {
12026
12108
  const synced = targets.map((t) => ({
12027
12109
  name: t.name,
12028
- outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base)
12110
+ outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base, { originUrl: t.originUrl, scheme: t.cloneScheme })
12029
12111
  }));
12030
12112
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ synced }));
12031
12113
  } catch (error) {
@@ -12066,7 +12148,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
12066
12148
  const path = syncDaemonStatePath(stateDir, workspaceId);
12067
12149
  if (!existsSync22(path)) return void 0;
12068
12150
  try {
12069
- return JSON.parse(readFileSync15(path, "utf8"));
12151
+ return JSON.parse(readFileSync16(path, "utf8"));
12070
12152
  } catch {
12071
12153
  return void 0;
12072
12154
  }
@@ -12074,7 +12156,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
12074
12156
  function writePersistedSyncDaemon(stateDir, state) {
12075
12157
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
12076
12158
  mkdirSync11(join21(stateDir, "passengers"), { recursive: true });
12077
- writeFileSync11(path, `${JSON.stringify(state, null, 2)}
12159
+ writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12078
12160
  `);
12079
12161
  }
12080
12162
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -12152,7 +12234,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12152
12234
  }
12153
12235
 
12154
12236
  // src/daemon.ts
12155
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
12237
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12156
12238
  import { join as join22 } from "path";
12157
12239
  var DEFAULT_FLOOR_MS = 5e3;
12158
12240
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -12212,12 +12294,12 @@ function daemonStampFor(stateDir) {
12212
12294
  function noteDaemonStarted(stateDir, now = Date.now()) {
12213
12295
  mkdirSync12(stateDir, { recursive: true });
12214
12296
  rmSync8(reloadSentinelFor(stateDir), { force: true });
12215
- writeFileSync12(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
12297
+ writeFileSync13(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
12216
12298
  `);
12217
12299
  }
12218
12300
  function readDaemonStarted(stateDir) {
12219
12301
  try {
12220
- const v = JSON.parse(readFileSync16(daemonStampFor(stateDir), "utf8")).startedAt;
12302
+ const v = JSON.parse(readFileSync17(daemonStampFor(stateDir), "utf8")).startedAt;
12221
12303
  return typeof v === "number" ? v : null;
12222
12304
  } catch {
12223
12305
  return null;
@@ -12854,6 +12936,15 @@ if (command === "passenger-sync-daemon") {
12854
12936
  }
12855
12937
  try {
12856
12938
  for (const r of syncAllPassengerCheckouts(cfg)) {
12939
+ if (r.outcome.action === "clone-failed") {
12940
+ if (!r.outcome.repeat) {
12941
+ emit.warn(
12942
+ `passenger checkout for ${r.name} could not be cloned (not retried until its clone URL changes): ${r.outcome.detail}`,
12943
+ { step: "passengers", data: { workspaceId: r.workspaceId, repo: r.name } }
12944
+ );
12945
+ }
12946
+ continue;
12947
+ }
12857
12948
  if (r.outcome.action === "diverged" || r.outcome.action === "ff-failed") {
12858
12949
  emit.warn(
12859
12950
  `passenger checkout for ${r.name} is stale: ${r.outcome.detail}`,
@@ -13084,7 +13175,7 @@ if (command === "passenger-sync-daemon") {
13084
13175
  for (const a of plan.actions) {
13085
13176
  if (a.kind === "enable") {
13086
13177
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
13087
- writeFileSync13(cfg.configFile, planEnable(readFileSync17(cfg.configFile, "utf8"), a.route));
13178
+ writeFileSync14(cfg.configFile, planEnable(readFileSync18(cfg.configFile, "utf8"), a.route));
13088
13179
  enabledSound.add(a.route);
13089
13180
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
13090
13181
  } else {
@@ -13220,7 +13311,9 @@ ${changed2.map((c) => ` - ${c}
13220
13311
  return { scope, tests: "skipped", outcome: "skipped", detail: note };
13221
13312
  }
13222
13313
  if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
13223
- const tracker = new Tracker(c, cfg.ship);
13314
+ const releaseIdentity = await resolveReleaseKey(c);
13315
+ if (releaseIdentity.kind === "ship") remit.warn(`release writes as the ship, not its own agent: ${releaseIdentity.reason}`, { step: "release" });
13316
+ const tracker = new Tracker(c, cfg.ship, { apiKey: releaseIdentity.apiKey });
13224
13317
  if (!dryRun) {
13225
13318
  const force2 = command === "deploy" || flag("force");
13226
13319
  let seen = precheckTickets.get(c.route);
@@ -14177,15 +14270,17 @@ ${changed2.map((c) => ` - ${c}
14177
14270
  `);
14178
14271
  if (!dryRun) {
14179
14272
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
14180
- const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
14273
+ const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
14181
14274
  raw.agentPersonas = personaResult.agentPersonas;
14182
14275
  raw.agentSkills = skillResult.agentSkills;
14276
+ if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
14183
14277
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
14184
- writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
14278
+ writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
14185
14279
  `);
14186
14280
  }
14187
14281
  if (!dryRun) {
14188
14282
  target.resolved.agentPersonas = personaResult.agentPersonas;
14283
+ if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
14189
14284
  }
14190
14285
  for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
14191
14286
  process.stdout.write(`${describeMintOutcome(o)}
@@ -14235,10 +14330,10 @@ ${changed2.map((c) => ` - ${c}
14235
14330
  `);
14236
14331
  if (!dryRun) {
14237
14332
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
14238
- const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
14333
+ const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
14239
14334
  raw.agentSkills = result.agentSkills;
14240
14335
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
14241
- writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
14336
+ writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
14242
14337
  `);
14243
14338
  }
14244
14339
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -14251,7 +14346,7 @@ ${changed2.map((c) => ` - ${c}
14251
14346
  process.exit(2);
14252
14347
  };
14253
14348
  if (sub !== "add" && sub !== "list") usage2();
14254
- const configText = readFileSync17(cfg.configFile, "utf8");
14349
+ const configText = readFileSync18(cfg.configFile, "utf8");
14255
14350
  let routeName = positional[2];
14256
14351
  let addPath = ".";
14257
14352
  let addName;
@@ -14275,7 +14370,7 @@ ${changed2.map((c) => ` - ${c}
14275
14370
  let resolvedRemotes;
14276
14371
  if (existsSync24(resolvedFile)) {
14277
14372
  try {
14278
- resolvedRemotes = JSON.parse(readFileSync17(resolvedFile, "utf8"));
14373
+ resolvedRemotes = JSON.parse(readFileSync18(resolvedFile, "utf8"));
14279
14374
  } catch {
14280
14375
  }
14281
14376
  }
@@ -14347,14 +14442,14 @@ ${changed2.map((c) => ` - ${c}
14347
14442
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
14348
14443
  `);
14349
14444
  if (existsSync24(resolvedFile)) {
14350
- const raw = JSON.parse(readFileSync17(resolvedFile, "utf8"));
14445
+ const raw = JSON.parse(readFileSync18(resolvedFile, "utf8"));
14351
14446
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
14352
14447
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
14353
- writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
14448
+ writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
14354
14449
  `);
14355
14450
  }
14356
14451
  }
14357
- writeFileSync13(cfg.configFile, plan.text);
14452
+ writeFileSync14(cfg.configFile, plan.text);
14358
14453
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
14359
14454
  `);
14360
14455
  } catch (e) {
@@ -14596,7 +14691,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14596
14691
  const hadPreviousFile = existsSync24(resolvedPath);
14597
14692
  if (hadPreviousFile) {
14598
14693
  try {
14599
- previous = JSON.parse(readFileSync17(resolvedPath, "utf8"));
14694
+ previous = JSON.parse(readFileSync18(resolvedPath, "utf8"));
14600
14695
  } catch {
14601
14696
  }
14602
14697
  }
@@ -14651,7 +14746,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14651
14746
  // role as "diverged by definition" on the next sync even though
14652
14747
  // nothing about its prompt actually changed.
14653
14748
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
14654
- ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {}
14749
+ ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
14750
+ ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
14655
14751
  };
14656
14752
  if (dryRun) {
14657
14753
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
@@ -14660,7 +14756,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14660
14756
  mkdirSync13(dirname12(resolvedPath), { recursive: true });
14661
14757
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
14662
14758
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
14663
- writeFileSync13(tmpPath, `${JSON.stringify(resolved, null, 2)}
14759
+ writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
14664
14760
  `);
14665
14761
  renameSync2(tmpPath, resolvedPath);
14666
14762
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -14686,7 +14782,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14686
14782
  `);
14687
14783
  } else {
14688
14784
  mkdirSync13(dirname12(apiKeyFile), { recursive: true });
14689
- writeFileSync13(apiKeyFile, `${apiKeyVar}=${apiKey}
14785
+ writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
14690
14786
  `, { mode: 384 });
14691
14787
  process.stderr.write(`API key written to ${apiKeyFile}
14692
14788
  `);
@@ -15224,7 +15320,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15224
15320
  break;
15225
15321
  }
15226
15322
  mkdirSync13(cfg.ship.stateDir, { recursive: true });
15227
- writeFileSync13(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
15323
+ writeFileSync14(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
15228
15324
  `);
15229
15325
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
15230
15326
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -15275,7 +15371,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15275
15371
  }
15276
15372
  case "log": {
15277
15373
  try {
15278
- const text = readFileSync17(cfg.ship.logFile, "utf8").split("\n");
15374
+ const text = readFileSync18(cfg.ship.logFile, "utf8").split("\n");
15279
15375
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
15280
15376
  `);
15281
15377
  } catch {
@@ -15556,6 +15652,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
15556
15652
  const id = await resolveSeatKey(route, role);
15557
15653
  process.stdout.write(
15558
15654
  `seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
15655
+ `
15656
+ );
15657
+ }
15658
+ if (route.resolved?.releaseSeat) {
15659
+ const id = await resolveReleaseKey(route);
15660
+ process.stdout.write(
15661
+ `seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
15559
15662
  `
15560
15663
  );
15561
15664
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.46.0",
3
+ "version": "0.47.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",