@tablation/crew 0.46.1 → 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 +53 -7
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2256,6 +2256,7 @@ function parseResolved(raw, m, where) {
2256
2256
  holds: holds2,
2257
2257
  contract: raw.contract ?? void 0,
2258
2258
  ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
2259
+ ...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
2259
2260
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
2260
2261
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
2261
2262
  };
@@ -7548,6 +7549,8 @@ function needsBaselineBackfill(model, row) {
7548
7549
  function isPromptGateRejection(e) {
7549
7550
  return e instanceof TablationApiError && e.status === 422;
7550
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.";
7551
7554
  async function syncPersonas(route, opts) {
7552
7555
  if (!route.resolved) {
7553
7556
  throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
@@ -7679,7 +7682,24 @@ async function syncPersonas(route, opts) {
7679
7682
  }
7680
7683
  }
7681
7684
  const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
7682
- 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;
7683
7703
  }
7684
7704
  async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
7685
7705
  const seats2 = route.resolved.seats;
@@ -10870,6 +10890,18 @@ function slugFor(route, agentId) {
10870
10890
  function seatAgentId(route, role) {
10871
10891
  return route.resolved?.agentPersonas?.[role]?.agentId;
10872
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
+ }
10873
10905
  async function resolveSeatKey(route, role, getStore = getSessionStore2) {
10874
10906
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10875
10907
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
@@ -10888,12 +10920,15 @@ async function mintMissingSeatKeys(route, o) {
10888
10920
  if (!route.resolved) return out;
10889
10921
  const resolved = route.resolved;
10890
10922
  const doFetch = o.fetchImpl ?? fetch;
10891
- 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
+ ];
10892
10927
  let store;
10893
10928
  try {
10894
10929
  store = (o.getStore ?? getSessionStore2)();
10895
10930
  } catch (e) {
10896
- return seated.map((role) => ({
10931
+ return targets.map(({ role }) => ({
10897
10932
  role,
10898
10933
  action: "failed",
10899
10934
  detail: `OS keychain unavailable: ${e.message}`
@@ -10901,8 +10936,7 @@ async function mintMissingSeatKeys(route, o) {
10901
10936
  }
10902
10937
  const host = hostFromUrl2(route.baseUrl);
10903
10938
  const base = route.baseUrl.replace(/\/+$/, "");
10904
- for (const role of seated) {
10905
- const agentId = seatAgentId(route, role);
10939
+ for (const { role, agentId } of targets) {
10906
10940
  if (!agentId) {
10907
10941
  out.push({ role, action: "no_agent" });
10908
10942
  continue;
@@ -13277,7 +13311,9 @@ ${changed2.map((c) => ` - ${c}
13277
13311
  return { scope, tests: "skipped", outcome: "skipped", detail: note };
13278
13312
  }
13279
13313
  if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
13280
- 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 });
13281
13317
  if (!dryRun) {
13282
13318
  const force2 = command === "deploy" || flag("force");
13283
13319
  let seen = precheckTickets.get(c.route);
@@ -14237,12 +14273,14 @@ ${changed2.map((c) => ` - ${c}
14237
14273
  const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
14238
14274
  raw.agentPersonas = personaResult.agentPersonas;
14239
14275
  raw.agentSkills = skillResult.agentSkills;
14276
+ if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
14240
14277
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
14241
14278
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
14242
14279
  `);
14243
14280
  }
14244
14281
  if (!dryRun) {
14245
14282
  target.resolved.agentPersonas = personaResult.agentPersonas;
14283
+ if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
14246
14284
  }
14247
14285
  for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
14248
14286
  process.stdout.write(`${describeMintOutcome(o)}
@@ -14708,7 +14746,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14708
14746
  // role as "diverged by definition" on the next sync even though
14709
14747
  // nothing about its prompt actually changed.
14710
14748
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
14711
- ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {}
14749
+ ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
14750
+ ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
14712
14751
  };
14713
14752
  if (dryRun) {
14714
14753
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
@@ -15613,6 +15652,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
15613
15652
  const id = await resolveSeatKey(route, role);
15614
15653
  process.stdout.write(
15615
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})`}
15616
15662
  `
15617
15663
  );
15618
15664
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.46.1",
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",