@tablation/crew 0.51.1 → 0.53.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 +140 -31
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1435,8 +1435,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
1435
1435
  "labels",
1436
1436
  "release"
1437
1437
  ]);
1438
- var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
1439
- var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag)\}/g;
1438
+ var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
1439
+ var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
1440
1440
  var WORKTREE_PLACEHOLDERS = ["dir", "issue", "number", "key", "slug", "role", "prefix", "tag"];
1441
1441
  function worktreeNameProblem(template) {
1442
1442
  const valid = `Valid placeholders: ${WORKTREE_PLACEHOLDERS.map((p) => `{${p}}`).join(" ")}.`;
@@ -1528,7 +1528,7 @@ function parseRepoConfig(text, file) {
1528
1528
  if (!PLACEHOLDER.test(branchName)) {
1529
1529
  PLACEHOLDER.lastIndex = 0;
1530
1530
  throw new RepoConfigError(
1531
- `${file}: branch.name ("${branchName}") names no placeholder \u2014 every ticket would get the same branch. Use at least one of {key} {number} {slug} {role}.`
1531
+ `${file}: branch.name ("${branchName}") names no placeholder \u2014 every ticket would get the same branch. Use at least one of {key} {number} {slug} {role} {prefix} {tag} {issue}.`
1532
1532
  );
1533
1533
  }
1534
1534
  PLACEHOLDER.lastIndex = 0;
@@ -1536,7 +1536,8 @@ function parseRepoConfig(text, file) {
1536
1536
  base: String(raw.branch?.base ?? "main"),
1537
1537
  name: branchName,
1538
1538
  push: String(raw.branch?.push ?? branchName),
1539
- remote: String(raw.branch?.remote ?? "origin")
1539
+ remote: String(raw.branch?.remote ?? "origin"),
1540
+ ...raw.branch?.lowercase !== void 0 ? { lowercase: parseLowercase(raw.branch.lowercase, file) } : {}
1540
1541
  };
1541
1542
  const copyRaw = raw.worktrees?.copy;
1542
1543
  if (copyRaw !== void 0 && !Array.isArray(copyRaw)) {
@@ -1733,9 +1734,13 @@ function slugify(title, maxLength = 40) {
1733
1734
  const s = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1734
1735
  return s || "work";
1735
1736
  }
1737
+ function parseLowercase(v, where) {
1738
+ if (typeof v !== "boolean") throw new RepoConfigError(`${where}: branch.lowercase must be true or false`);
1739
+ return v;
1740
+ }
1736
1741
  function renderBranchName(template, ctx) {
1737
1742
  const number = ctx.key.replace(/^\D+/, "");
1738
- return template.replace(PLACEHOLDER, (_m, name) => {
1743
+ const rendered = template.replace(PLACEHOLDER, (_m, name) => {
1739
1744
  switch (name) {
1740
1745
  case "key":
1741
1746
  return ctx.key;
@@ -1749,14 +1754,16 @@ function renderBranchName(template, ctx) {
1749
1754
  return ctx.tag ?? ctx.key;
1750
1755
  case "prefix":
1751
1756
  return ctx.prefix ?? "";
1757
+ case "issue":
1758
+ return (ctx.tag ?? ctx.key).toLowerCase();
1752
1759
  default:
1753
1760
  return "";
1754
1761
  }
1755
1762
  });
1763
+ return ctx.lowercase ? rendered.toLowerCase() : rendered;
1756
1764
  }
1757
1765
  function renderWorktreeName(template, ctx) {
1758
- const issue = (ctx.tag ?? ctx.key).toLowerCase();
1759
- return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
1766
+ return renderBranchName(template.replace(/\{dir\}/g, ctx.dir), ctx);
1760
1767
  }
1761
1768
  function defaultWorktreePrefix(dir) {
1762
1769
  const name = basename2(dir.replace(/[/\\]+$/, ""));
@@ -1856,7 +1863,8 @@ function resolveRepoConfig(repo, ship, dir) {
1856
1863
  repo?.branch.remote === "origin" ? void 0 : repo?.branch.remote,
1857
1864
  ship?.branch?.remote,
1858
1865
  "origin"
1859
- )
1866
+ ),
1867
+ ...pick("branch.lowercase", repo?.branch.lowercase, ship?.branch?.lowercase, false) ? { lowercase: true } : {}
1860
1868
  },
1861
1869
  // Unlike every other field here, platform has no ship-level fallback: what
1862
1870
  // a repo needs of a host is a fact about that repo alone (its build
@@ -2055,7 +2063,7 @@ function parseWorktreeName(v, where, file) {
2055
2063
  return String(v);
2056
2064
  }
2057
2065
  var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName", "toolchain"]);
2058
- var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
2066
+ var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
2059
2067
  var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
2060
2068
  "mode",
2061
2069
  "ci",
@@ -2139,8 +2147,12 @@ function parseBranchOverride(raw, where, file) {
2139
2147
  const name = raw.name !== void 0 ? String(raw.name) : void 0;
2140
2148
  const push = raw.push !== void 0 ? String(raw.push) : void 0;
2141
2149
  const remote = raw.remote !== void 0 ? String(raw.remote) : void 0;
2142
- if (base === void 0 && name === void 0 && push === void 0 && remote === void 0) return void 0;
2143
- return { base, name, push, remote };
2150
+ if (raw.lowercase !== void 0 && typeof raw.lowercase !== "boolean") {
2151
+ throw new ConfigError(`${file}: ${where}.branch.lowercase must be true or false`);
2152
+ }
2153
+ const lowercase = raw.lowercase;
2154
+ if (base === void 0 && name === void 0 && push === void 0 && remote === void 0 && lowercase === void 0) return void 0;
2155
+ return { base, name, push, remote, ...lowercase !== void 0 ? { lowercase } : {} };
2144
2156
  }
2145
2157
  function mergeRouteRelease(route, override) {
2146
2158
  const provider = override?.ci?.provider ?? route.ci?.provider;
@@ -6355,13 +6367,13 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
6355
6367
 
6356
6368
  // src/ticket-branch.ts
6357
6369
  import { basename as basename5 } from "path";
6358
- function branchRenderer(t, role) {
6370
+ function branchRenderer(t, role, lowercase) {
6359
6371
  const { tag, prefix } = ticketBranchContext(t);
6360
- return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
6372
+ return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix, lowercase });
6361
6373
  }
6362
6374
  function worktreeForTicket(dir, cfg, t, role) {
6363
6375
  const { prefix } = ticketBranchContext(t);
6364
- const branchName = branchRenderer(t, role)(effectiveBranchTemplate(cfg, prefix));
6376
+ const branchName = branchRenderer(t, role, cfg.branch.lowercase)(effectiveBranchTemplate(cfg, prefix));
6365
6377
  const number = t.issue_id.replace(/^\D+/, "");
6366
6378
  const { tag } = ticketBranchContext(t);
6367
6379
  return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
@@ -6380,10 +6392,10 @@ function branchTemplates(cfg, t) {
6380
6392
  return { name, push };
6381
6393
  }
6382
6394
  function pushedBranchForTicket(cfg, t, role) {
6383
- return branchRenderer(t, role)(branchTemplates(cfg, t).push);
6395
+ return branchRenderer(t, role, cfg.branch.lowercase)(branchTemplates(cfg, t).push);
6384
6396
  }
6385
6397
  function existingBranchForTicket(dir, cfg, t, role) {
6386
- return branchForIssue(dir, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role));
6398
+ return branchForIssue(dir, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role, cfg.branch.lowercase));
6387
6399
  }
6388
6400
  function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
6389
6401
  const local = existingBranchForTicket(dir, cfg, t, role);
@@ -6394,7 +6406,7 @@ function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
6394
6406
  fetchedDirs.add(dir);
6395
6407
  fetchRemote(dir, remote);
6396
6408
  }
6397
- const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role));
6409
+ const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role, cfg.branch.lowercase));
6398
6410
  return found ? { branch: found, where: "remote" } : null;
6399
6411
  }
6400
6412
  function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
@@ -6712,14 +6724,14 @@ function writeDigest(o, d, role, stateDir) {
6712
6724
  };
6713
6725
  const fetchedDirs = /* @__PURE__ */ new Set();
6714
6726
  const dirFor = (t) => dirForRepo(o.route, t.repo_id);
6715
- const render2 = (t) => branchRenderer(t, role);
6727
+ const render2 = (t, lowercase) => branchRenderer(t, role, lowercase);
6716
6728
  const input = {
6717
6729
  dirFor,
6718
6730
  repoStop: repoStopFor(openShipAttention(o.state), o.route),
6719
6731
  branchFor: (t) => {
6720
6732
  const cfg = repoFor(dirFor(t) ?? o.route.dir);
6721
6733
  const { prefix } = ticketBranchContext(t);
6722
- return render2(t)(effectiveBranchTemplate(cfg, prefix));
6734
+ return render2(t, cfg.branch.lowercase)(effectiveBranchTemplate(cfg, prefix));
6723
6735
  },
6724
6736
  // ISSUE-1028: the agent previously had to derive this itself from the
6725
6737
  // Environment section's own worked example, which is rendered before
@@ -7210,7 +7222,7 @@ function repoPrefix(repo) {
7210
7222
  function branchExample(repo, key) {
7211
7223
  const prefix = repoPrefix(repo);
7212
7224
  const template = effectiveBranchTemplate(repo, prefix);
7213
- return renderBranchName(template, { key, title: "Fix the widget", role: "dev", prefix });
7225
+ return renderBranchName(template, { key, title: "Fix the widget", role: "dev", prefix, lowercase: repo.branch.lowercase });
7214
7226
  }
7215
7227
  function repoSection(r, key, heading) {
7216
7228
  const repo = r.config;
@@ -8957,7 +8969,7 @@ var describeRunWorktreePlan = (p) => {
8957
8969
  var planIsUsable = (p) => p.action !== "leave-dirty" && p.action !== "leave-unpushed";
8958
8970
  function newBranchName(cfg, t, role) {
8959
8971
  const { prefix } = ticketBranchContext(t);
8960
- return branchRenderer(t, role)(effectiveBranchTemplate(cfg, prefix));
8972
+ return branchRenderer(t, role, cfg.branch.lowercase)(effectiveBranchTemplate(cfg, prefix));
8961
8973
  }
8962
8974
  function planRunWorktree(dir, cfg, t, role, opts = {}) {
8963
8975
  const { remote, base } = cfg.branch;
@@ -9721,9 +9733,9 @@ function reviewProblems(repo, contract) {
9721
9733
  if (!reviewApplies(repo, contract)) return [];
9722
9734
  const problems = [];
9723
9735
  const template = repo.branch.push ?? repo.branch.name;
9724
- if (!/\{(key|number|tag)\}/.test(template)) {
9736
+ if (!/\{(key|number|tag|issue)\}/.test(template)) {
9725
9737
  problems.push(
9726
- `branch.push "${template}" names none of {key}, {number} or {tag} \u2014 with release.mode "external" and a reviewing status, the pushed branch is the only link back to its ticket`
9738
+ `branch.push "${template}" names none of {key}, {number}, {tag} or {issue} \u2014 with release.mode "external" and a reviewing status, the pushed branch is the only link back to its ticket`
9727
9739
  );
9728
9740
  }
9729
9741
  return problems;
@@ -11608,7 +11620,8 @@ function keyIsAdmin(route) {
11608
11620
  return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
11609
11621
  }
11610
11622
  var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
11611
- var MEMBER_NO_KEY = "no agent key for this seat \u2014 minting needs a workspace admin key";
11623
+ var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
11624
+ var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Mint agent keys"; ask a workspace admin to add it to your project role (or run sync with an admin key)';
11612
11625
  function seatKeySlug(workspaceSlug, agentId) {
11613
11626
  return `${workspaceSlug}#agent:${agentId}`;
11614
11627
  }
@@ -11712,6 +11725,10 @@ async function mintMissingSeatKeys(route, o) {
11712
11725
  },
11713
11726
  body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
11714
11727
  });
11728
+ if (res.status === 403) {
11729
+ out.push({ role, action: "forbidden" });
11730
+ break;
11731
+ }
11715
11732
  if (!res.ok) {
11716
11733
  out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
11717
11734
  continue;
@@ -11722,13 +11739,15 @@ async function mintMissingSeatKeys(route, o) {
11722
11739
  continue;
11723
11740
  }
11724
11741
  try {
11725
- await store.set(host, slug, {
11742
+ const session = {
11726
11743
  apiKey: body.key,
11727
11744
  shipId: o.shipName ?? "",
11728
11745
  workspaceId: resolved.workspaceId,
11729
11746
  identityId: agentId,
11730
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
11731
- });
11747
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
11748
+ ...body.id ? { keyId: body.id } : {}
11749
+ };
11750
+ await store.set(host, slug, session);
11732
11751
  } catch (e) {
11733
11752
  let revoked = false;
11734
11753
  if (body.id) {
@@ -11764,10 +11783,92 @@ function describeMintOutcome(o) {
11764
11783
  return `${o.role}: agent key already stored`;
11765
11784
  case "no_agent":
11766
11785
  return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
11786
+ case "forbidden":
11787
+ return FORBIDDEN_LINE;
11767
11788
  case "failed":
11768
11789
  return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
11769
11790
  }
11770
11791
  }
11792
+ async function revokeSeatKeys(route, o) {
11793
+ const out = [];
11794
+ if (!route.resolved) return out;
11795
+ const resolved = route.resolved;
11796
+ const doFetch = o.fetchImpl ?? fetch;
11797
+ const targets = [
11798
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
11799
+ ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
11800
+ ];
11801
+ let store;
11802
+ try {
11803
+ store = (o.getStore ?? getSessionStore2)();
11804
+ } catch (e) {
11805
+ return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
11806
+ }
11807
+ const host = hostFromUrl2(route.baseUrl);
11808
+ const base = route.baseUrl.replace(/\/+$/, "");
11809
+ for (const { role, agentId } of targets) {
11810
+ if (!agentId) {
11811
+ out.push({ role, action: "no_agent" });
11812
+ continue;
11813
+ }
11814
+ const slug = slugFor(route, agentId);
11815
+ try {
11816
+ const session = await store.get(host, slug);
11817
+ if (!session?.apiKey) {
11818
+ out.push({ role, action: "not_stored" });
11819
+ continue;
11820
+ }
11821
+ if (!session.keyId) {
11822
+ out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
11823
+ continue;
11824
+ }
11825
+ if (o.dryRun) {
11826
+ out.push({ role, action: "revoked", detail: "dry run" });
11827
+ continue;
11828
+ }
11829
+ const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
11830
+ method: "DELETE",
11831
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11832
+ });
11833
+ if (!del2.ok && del2.status !== 404) {
11834
+ out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
11835
+ continue;
11836
+ }
11837
+ await store.clear(host, slug);
11838
+ out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
11839
+ } catch (e) {
11840
+ out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
11841
+ }
11842
+ }
11843
+ return out;
11844
+ }
11845
+ function describeRevokeOutcome(o) {
11846
+ switch (o.action) {
11847
+ case "revoked":
11848
+ return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
11849
+ case "not_stored":
11850
+ return `${o.role}: no agent key stored on this ship`;
11851
+ case "no_agent":
11852
+ return `${o.role}: no linked Agents row`;
11853
+ case "failed":
11854
+ return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
11855
+ }
11856
+ }
11857
+ async function canMintAgentKeys(route, o = {}) {
11858
+ const resolved = route.resolved;
11859
+ if (!resolved) return void 0;
11860
+ const agentId = seatAgentId(route, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route, r)).find(Boolean);
11861
+ if (!agentId) return void 0;
11862
+ try {
11863
+ const res = await (o.fetchImpl ?? fetch)(`${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
11864
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11865
+ });
11866
+ if (res.ok) return true;
11867
+ return res.status === 403 ? false : void 0;
11868
+ } catch {
11869
+ return void 0;
11870
+ }
11871
+ }
11771
11872
 
11772
11873
  // src/logbook.ts
11773
11874
  import { createHash as createHash3 } from "crypto";
@@ -13666,7 +13767,7 @@ function usage() {
13666
13767
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
13667
13768
  crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
13668
13769
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
13669
- crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
13770
+ crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
13670
13771
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
13671
13772
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
13672
13773
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
@@ -15187,7 +15288,7 @@ ${changed2.map((c) => ` - ${c}
15187
15288
  break;
15188
15289
  }
15189
15290
  if (sub !== "sync") {
15190
- process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)\n");
15291
+ process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt; --revoke-seat-keys deletes this ship's seat keys server-side and from the keychain, so the next sync mints fresh ones)\n");
15191
15292
  process.stderr.write(`crew agents prompt <role> [route] print a persona's current prompt (roles: ${ROLE_NAMES.join(", ")})
15192
15293
  `);
15193
15294
  process.exit(2);
@@ -15270,7 +15371,12 @@ ${changed2.map((c) => ` - ${c}
15270
15371
  }
15271
15372
  if (readOnly) {
15272
15373
  process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
15273
- process.stdout.write("seat keys: not minted (needs a workspace admin key)\n");
15374
+ }
15375
+ if (flag("revoke-seat-keys")) {
15376
+ for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
15377
+ process.stdout.write(`${describeRevokeOutcome(o)}
15378
+ `);
15379
+ }
15274
15380
  } else {
15275
15381
  for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
15276
15382
  process.stdout.write(`${describeMintOutcome(o)}
@@ -16045,7 +16151,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16045
16151
  const branch = found.branch ?? branchForIssue(target.dir, `ISSUE-${n}`, {
16046
16152
  name: target.config.branch.name,
16047
16153
  push: target.config.branch.push
16048
- }, (t) => renderBranchName(t, { key: `ISSUE-${n}` }));
16154
+ }, (t) => renderBranchName(t, { key: `ISSUE-${n}`, lowercase: target.config.branch.lowercase }));
16049
16155
  const pids = pidsInWorktree(wt, num);
16050
16156
  if (pids.length) {
16051
16157
  emit.emit(`${dryRun ? "would kill" : "killing"} processes still inside ${wt}: ${pids.join(" ")}`);
@@ -16675,6 +16781,9 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16675
16781
  const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
16676
16782
  const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
16677
16783
  process.stdout.write(`api key role: ${keyRole ?? "unknown"}
16784
+ `);
16785
+ const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
16786
+ process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
16678
16787
  `);
16679
16788
  for (const role of SEAT_KEY_ROLES) {
16680
16789
  if (!route.resolved?.seats[role]) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.51.1",
3
+ "version": "0.53.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",