@tablation/crew 0.46.1 → 0.47.1

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 +69 -11
  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
  };
@@ -3481,7 +3482,7 @@ function describeRelease(o, route) {
3481
3482
  level: "fail",
3482
3483
  headline: `${route}: deploy FAILED`,
3483
3484
  detail: withHookTail(
3484
- "the version is merged to the base branch but not live \u2014 run `crew deploy` to retry; do not re-release",
3485
+ "the version is merged to the base branch but not live \u2014 if the deploy target was the problem, run `crew deploy` to retry (do not re-release); if the merged code itself failed to build, fix forward on a ticket branch, and merging it clears the not-retrying guard",
3485
3486
  o.hookFailure
3486
3487
  )
3487
3488
  };
@@ -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;
@@ -10029,7 +10049,7 @@ ${n.detail ?? n.headline}
10029
10049
 
10030
10050
  Filed automatically \u2014 no \`notify\` hook is configured for route \`${route}\`, so this is the crew's built-in fallback rather than a silent log line. Configure \`hooks.notify\` on this route to replace it.`;
10031
10051
  }
10032
- async function applyFailureAlert(writer, n, route, openTickets, memberId, log, dryRun) {
10052
+ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, dryRun, placement = {}) {
10033
10053
  const existing = findOpenFailureAlert(openTickets, n.headline);
10034
10054
  if (existing) {
10035
10055
  const label = displayKey(existing);
@@ -10056,7 +10076,9 @@ Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
10056
10076
  description: ticketBody(n, route),
10057
10077
  report_type: "bug",
10058
10078
  severity: "s2",
10059
- reporter_name: "Crew release"
10079
+ reporter_name: "Crew release",
10080
+ ...placement.repo_id ? { repo_id: placement.repo_id } : {},
10081
+ ...placement.project_id ? { project_id: placement.project_id } : {}
10060
10082
  }, memberId);
10061
10083
  const label = created.issue_id ? displayKey(created) : created.id;
10062
10084
  log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
@@ -10870,6 +10892,18 @@ function slugFor(route, agentId) {
10870
10892
  function seatAgentId(route, role) {
10871
10893
  return route.resolved?.agentPersonas?.[role]?.agentId;
10872
10894
  }
10895
+ async function resolveReleaseKey(route, getStore = getSessionStore2) {
10896
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10897
+ const agentId = route.resolved?.releaseAgentId;
10898
+ if (!agentId) return ship("release seat has no linked Agents row (run `crew agents sync`)");
10899
+ try {
10900
+ const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
10901
+ if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
10902
+ return ship("no agent key stored for the release seat (run `crew agents sync`)");
10903
+ } catch {
10904
+ return ship("OS keychain unavailable");
10905
+ }
10906
+ }
10873
10907
  async function resolveSeatKey(route, role, getStore = getSessionStore2) {
10874
10908
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
10875
10909
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
@@ -10888,12 +10922,15 @@ async function mintMissingSeatKeys(route, o) {
10888
10922
  if (!route.resolved) return out;
10889
10923
  const resolved = route.resolved;
10890
10924
  const doFetch = o.fetchImpl ?? fetch;
10891
- const seated = SEAT_KEY_ROLES.filter((r) => resolved.seats[r]);
10925
+ const targets = [
10926
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
10927
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
10928
+ ];
10892
10929
  let store;
10893
10930
  try {
10894
10931
  store = (o.getStore ?? getSessionStore2)();
10895
10932
  } catch (e) {
10896
- return seated.map((role) => ({
10933
+ return targets.map(({ role }) => ({
10897
10934
  role,
10898
10935
  action: "failed",
10899
10936
  detail: `OS keychain unavailable: ${e.message}`
@@ -10901,8 +10938,7 @@ async function mintMissingSeatKeys(route, o) {
10901
10938
  }
10902
10939
  const host = hostFromUrl2(route.baseUrl);
10903
10940
  const base = route.baseUrl.replace(/\/+$/, "");
10904
- for (const role of seated) {
10905
- const agentId = seatAgentId(route, role);
10941
+ for (const { role, agentId } of targets) {
10906
10942
  if (!agentId) {
10907
10943
  out.push({ role, action: "no_agent" });
10908
10944
  continue;
@@ -13277,7 +13313,9 @@ ${changed2.map((c) => ` - ${c}
13277
13313
  return { scope, tests: "skipped", outcome: "skipped", detail: note };
13278
13314
  }
13279
13315
  if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
13280
- const tracker = new Tracker(c, cfg.ship);
13316
+ const releaseIdentity = await resolveReleaseKey(c);
13317
+ if (releaseIdentity.kind === "ship") remit.warn(`release writes as the ship, not its own agent: ${releaseIdentity.reason}`, { step: "release" });
13318
+ const tracker = new Tracker(c, cfg.ship, { apiKey: releaseIdentity.apiKey });
13281
13319
  if (!dryRun) {
13282
13320
  const force2 = command === "deploy" || flag("force");
13283
13321
  let seen = precheckTickets.get(c.route);
@@ -13453,7 +13491,17 @@ ${changed2.map((c) => ` - ${c}
13453
13491
  const notified = await notify(c, cfg.ship, news, remit, dryRun);
13454
13492
  if (!notified && news.level === "fail") {
13455
13493
  const memberId = releaseSeat(c);
13456
- await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun).catch((e) => {
13494
+ const placement = {};
13495
+ try {
13496
+ const repoId = repoIdForName(c, target.name);
13497
+ if (repoId) {
13498
+ placement.repo_id = repoId;
13499
+ const projectId = (await tracker.repoRow(repoId))?.project_id;
13500
+ if (projectId) placement.project_id = projectId;
13501
+ }
13502
+ } catch {
13503
+ }
13504
+ await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun, placement).catch((e) => {
13457
13505
  remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
13458
13506
  });
13459
13507
  }
@@ -14237,12 +14285,14 @@ ${changed2.map((c) => ` - ${c}
14237
14285
  const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
14238
14286
  raw.agentPersonas = personaResult.agentPersonas;
14239
14287
  raw.agentSkills = skillResult.agentSkills;
14288
+ if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
14240
14289
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
14241
14290
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
14242
14291
  `);
14243
14292
  }
14244
14293
  if (!dryRun) {
14245
14294
  target.resolved.agentPersonas = personaResult.agentPersonas;
14295
+ if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
14246
14296
  }
14247
14297
  for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
14248
14298
  process.stdout.write(`${describeMintOutcome(o)}
@@ -14708,7 +14758,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14708
14758
  // role as "diverged by definition" on the next sync even though
14709
14759
  // nothing about its prompt actually changed.
14710
14760
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
14711
- ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {}
14761
+ ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
14762
+ ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
14712
14763
  };
14713
14764
  if (dryRun) {
14714
14765
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
@@ -15613,6 +15664,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
15613
15664
  const id = await resolveSeatKey(route, role);
15614
15665
  process.stdout.write(
15615
15666
  `seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
15667
+ `
15668
+ );
15669
+ }
15670
+ if (route.resolved?.releaseSeat) {
15671
+ const id = await resolveReleaseKey(route);
15672
+ process.stdout.write(
15673
+ `seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
15616
15674
  `
15617
15675
  );
15618
15676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.46.1",
3
+ "version": "0.47.1",
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",