@tablation/crew 0.97.0 → 0.99.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 +163 -21
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -684,11 +684,17 @@ var claudePair = {
684
684
  },
685
685
  removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
686
686
  };
687
+ var inferenceBaseUrl = (baseUrl, workspaceId) => `${baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/inference`;
688
+ function workspaceSettings(baseUrl) {
689
+ const blank = BILLING_VARS_TO_UNSET.filter((v) => v !== "ANTHROPIC_AUTH_TOKEN");
690
+ return JSON.stringify({ env: { ...Object.fromEntries(blank.map((v) => [v, ""])), ANTHROPIC_BASE_URL: baseUrl } });
691
+ }
687
692
  function subscriptionSettings() {
688
693
  return JSON.stringify({ env: Object.fromEntries(BILLING_VARS_TO_UNSET.map((v) => [v, ""])) });
689
694
  }
690
695
  function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
691
696
  if (spec.auth === "subscription") return "subscription (settings override)";
697
+ if (spec.auth === "workspace") return `workspace${spec.inference ? ` (${new URL(spec.inference.baseUrl).host})` : " (endpoint resolved per route at run time)"}`;
692
698
  let fromSettings;
693
699
  try {
694
700
  const parsed = JSON.parse(readFileSync6(join6(home, ".claude", "settings.json"), "utf8"));
@@ -702,6 +708,9 @@ function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
702
708
  const shell = BILLING_VARS_TO_UNSET.find((v) => v.startsWith("ANTHROPIC_API") && env[v]);
703
709
  return shell ? `inherit: ${shell} in env (unset at exec)` : "inherit: subscription login";
704
710
  }
711
+ function withoutProviderVars(env) {
712
+ return Object.fromEntries(Object.entries(env).filter(([k]) => !BILLING_VARS_TO_UNSET.includes(k) && k !== "ANTHROPIC_BASE_URL" && !/^(CLAUDE_CODE_USE_|ANTHROPIC_(VERTEX|BEDROCK|FOUNDRY))/.test(k)));
713
+ }
705
714
  var claudeAdapter = {
706
715
  harness: "claude",
707
716
  displayName: "Claude Code",
@@ -757,6 +766,7 @@ var claudeAdapter = {
757
766
  }
758
767
  })] : [],
759
768
  ...spec.auth === "subscription" ? ["--settings", subscriptionSettings()] : [],
769
+ ...spec.auth === "workspace" && spec.inference ? ["--settings", workspaceSettings(spec.inference.baseUrl)] : [],
760
770
  ...spec.args
761
771
  ];
762
772
  return wrapInvocation(spec, { model: o.model, args });
@@ -767,7 +777,11 @@ var claudeAdapter = {
767
777
  // blocks at all — without it, Agent Log Cycles reporting has nothing
768
778
  // to report (ISSUE-558).
769
779
  ...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
770
- ...spec.env
780
+ // CREW-2796: under `workspace` the only route to a model is the endpoint. The provider
781
+ // variables are already unset at exec (`unsetEnv`), and a spec-level `env:` entry must not
782
+ // smuggle one back in.
783
+ ...spec.auth === "workspace" ? withoutProviderVars(spec.env) : spec.env,
784
+ ...spec.auth === "workspace" && spec.inference ? { ANTHROPIC_BASE_URL: spec.inference.baseUrl, ANTHROPIC_AUTH_TOKEN: spec.inference.token } : {}
771
785
  };
772
786
  },
773
787
  provider: () => "anthropic",
@@ -1588,6 +1602,28 @@ import { dirname as dirname3, join as join9 } from "path";
1588
1602
  var ForgeError = class extends Error {
1589
1603
  };
1590
1604
  var DEFAULT_FORGE_USERNAME = "x-access-token";
1605
+ var DEFAULT_FORGE_HOST = "github.com";
1606
+ var FORGE_DESCRIPTORS = {
1607
+ github: {
1608
+ kind: "github",
1609
+ httpsUrl: (host, remote) => `https://${host}/${remote}.git`,
1610
+ sshUrl: (host, remote) => `git@${host}:${remote}.git`,
1611
+ credentialUsername: DEFAULT_FORGE_USERNAME
1612
+ },
1613
+ gitlab: {
1614
+ kind: "gitlab",
1615
+ httpsUrl: (host, remote) => `https://${host}/${remote}.git`,
1616
+ sshUrl: (host, remote) => `git@${host}:${remote}.git`,
1617
+ credentialUsername: "oauth2"
1618
+ }
1619
+ };
1620
+ function normalizeForgeHost(host) {
1621
+ const h = host?.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/\/+$/, "");
1622
+ return h || DEFAULT_FORGE_HOST;
1623
+ }
1624
+ function forgeDescriptorFor(host) {
1625
+ return normalizeForgeHost(host).includes("gitlab") ? FORGE_DESCRIPTORS.gitlab : FORGE_DESCRIPTORS.github;
1626
+ }
1591
1627
  var DEFAULT_KEYCHAIN_SERVICE = "crew-forge";
1592
1628
  function defaultTokenFilePath(stateDir, host) {
1593
1629
  return join9(stateDir, "forges", `${host.replace(/[^a-z0-9.-]/gi, "_")}.token`);
@@ -1800,7 +1836,7 @@ function parseForges(raw) {
1800
1836
  throw new ForgeError(`${where}.token.source must be one of: ${Object.keys(TOKEN_SOURCES).join(", ")}`);
1801
1837
  }
1802
1838
  out[host.toLowerCase()] = {
1803
- username: typeof e.username === "string" && e.username ? e.username : DEFAULT_FORGE_USERNAME,
1839
+ username: typeof e.username === "string" && e.username ? e.username : forgeDescriptorFor(host).credentialUsername,
1804
1840
  token
1805
1841
  };
1806
1842
  }
@@ -2918,6 +2954,7 @@ function parseResolved(raw, m, where) {
2918
2954
  reposModelId: raw.reposModelId,
2919
2955
  repoNames: raw.repoNames,
2920
2956
  repoRemotes: raw.repoRemotes,
2957
+ repoForgeHosts: raw.repoForgeHosts,
2921
2958
  models: {
2922
2959
  issues: m.req(raw.models?.issues, `${where}.resolved.models.issues`),
2923
2960
  comments: m.req(raw.models?.comments, `${where}.resolved.models.comments`),
@@ -3168,10 +3205,10 @@ function parseToolchains(raw, base, missing, file) {
3168
3205
  const maxRunMinutes = parseMaxRunMinutes(entryRaw.maxRunMinutes, `${where}.maxRunMinutes`, missing);
3169
3206
  let auth;
3170
3207
  if (entryRaw.auth !== void 0) {
3171
- if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit") {
3172
- missing.add(`${where}.auth must be subscription|inherit (got "${entryRaw.auth}")`);
3208
+ if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit" && entryRaw.auth !== "workspace") {
3209
+ missing.add(`${where}.auth must be subscription|inherit|workspace (got "${entryRaw.auth}")`);
3173
3210
  } else if (harness !== "claude") {
3174
- missing.add(`${where}.auth is only supported on the claude harness`);
3211
+ missing.add(`${where}.auth is only supported on the claude harness (${entryRaw.auth === "workspace" ? "workspace is not available for codex or gemini yet" : "got " + harness})`);
3175
3212
  } else {
3176
3213
  auth = entryRaw.auth;
3177
3214
  }
@@ -3423,23 +3460,29 @@ function reposOf(r) {
3423
3460
  if (r.dirIsCloneBase) {
3424
3461
  const [workspace2] = splitRoute(r.route);
3425
3462
  const remotes2 = r.resolved?.repoRemotes ?? {};
3463
+ const hosts = r.resolved?.repoForgeHosts ?? {};
3426
3464
  return Object.entries(r.resolved?.repoNames ?? {}).map(([id, name]) => ({
3427
3465
  name,
3428
3466
  dir: defaultRepoDir(r.reposBasePath, workspace2, name),
3429
- remote: remotes2[id]
3467
+ remote: remotes2[id],
3468
+ ...hosts[id] ? { forgeHost: hosts[id] } : {}
3430
3469
  }));
3431
3470
  }
3432
3471
  if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
3433
3472
  const remotes = r.resolved?.repoRemotes ?? {};
3434
3473
  const remoteByName = {};
3474
+ const hostByName = {};
3435
3475
  for (const [id, name] of Object.entries(r.resolved.repoNames)) {
3436
3476
  if (remotes[id]) remoteByName[name] = remotes[id];
3477
+ const h = r.resolved?.repoForgeHosts?.[id];
3478
+ if (h) hostByName[name] = h;
3437
3479
  }
3438
3480
  const [workspace] = splitRoute(r.route);
3439
3481
  return trackerNames.map((name) => ({
3440
3482
  name,
3441
3483
  dir: defaultRepoDir(r.reposBasePath, workspace, name),
3442
- remote: remoteByName[name]
3484
+ remote: remoteByName[name],
3485
+ ...hostByName[name] ? { forgeHost: hostByName[name] } : {}
3443
3486
  }));
3444
3487
  }
3445
3488
  function passengerRepoTargets(r) {
@@ -3455,10 +3498,12 @@ function passengerCheckoutTargets(r) {
3455
3498
  const remotes = r.resolved?.repoRemotes ?? {};
3456
3499
  return passengerRepoTargets(r).map((t) => {
3457
3500
  const id = repoIdForName(r, t.name);
3501
+ const forgeHost = t.forgeHost ?? (id ? r.resolved?.repoForgeHosts?.[id] : void 0);
3458
3502
  return {
3459
3503
  name: t.name,
3460
3504
  dir: passengerCheckoutDir(r.reposBasePath, workspace, t.name),
3461
- remote: t.remote ?? (id ? remotes[id] : void 0)
3505
+ remote: t.remote ?? (id ? remotes[id] : void 0),
3506
+ ...forgeHost ? { forgeHost } : {}
3462
3507
  };
3463
3508
  });
3464
3509
  }
@@ -4766,8 +4811,10 @@ function setForgeTokenHosts(hosts) {
4766
4811
  }
4767
4812
  function cloneUrlFor(remote, opts = {}) {
4768
4813
  const origin = opts.originUrl?.trim() ?? "";
4769
- const scheme = tokenHosts.has("github.com") ? "https" : /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
4770
- return scheme === "https" ? `https://github.com/${remote}.git` : `git@github.com:${remote}.git`;
4814
+ const host = normalizeForgeHost(opts.host);
4815
+ const scheme = tokenHosts.has(host) ? "https" : /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
4816
+ const forge = forgeDescriptorFor(host);
4817
+ return scheme === "https" ? forge.httpsUrl(host, remote) : forge.sshUrl(host, remote);
4771
4818
  }
4772
4819
  function originUrlOf(dir, remoteName) {
4773
4820
  try {
@@ -4939,6 +4986,16 @@ function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ ne
4939
4986
  message: `${route2}: the tracker refused the ${role} seat's run report (${status2}) \u2014 runs and ticket spend are not being recorded; check the seat key's role`
4940
4987
  };
4941
4988
  }
4989
+ var workspaceAuthKey = (role) => `workspace_auth:${role}`;
4990
+ var workspaceAllowanceKey = (role) => `workspace_allowance:${role}`;
4991
+ function workspaceAllowanceItem(route2, role, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
4992
+ return {
4993
+ kind: "workspace_allowance",
4994
+ key: workspaceAllowanceKey(role),
4995
+ since: nowIso,
4996
+ message: `${route2}: the ${role} seat is paused \u2014 the workspace is past its AI allowance; it resumes on its own once the limit is raised`
4997
+ };
4998
+ }
4942
4999
  var diskLowKey = (repo) => `disk_low:${repo}`;
4943
5000
  var LOW_DISK_BYTES = 2 * 1024 ** 3;
4944
5001
  var gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
@@ -5220,7 +5277,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
5220
5277
  const raw = (err3.stderr ? String(err3.stderr) : err3.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
5221
5278
  const detail = raw || "push dry-run failed";
5222
5279
  const f = o.forge;
5223
- if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
5280
+ if (!f) return { state: "failed", detail: hostOfRemote(o.remoteUrl) ? `${hostOfRemote(o.remoteUrl)}: ${detail}` : detail, hint: pushAuthHint(o.remoteUrl) };
5224
5281
  if (!f.hasToken) {
5225
5282
  return {
5226
5283
  state: "failed",
@@ -9379,6 +9436,37 @@ var AgentError = class extends Error {
9379
9436
  };
9380
9437
  var AgentSpawnError = class extends AgentError {
9381
9438
  };
9439
+ var WorkspaceAuthRefused = class extends AgentSpawnError {
9440
+ };
9441
+ var WorkspaceAllowanceExhausted = class extends AgentError {
9442
+ };
9443
+ var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
9444
+ function streamShowsAllowanceRefusal(streamPath) {
9445
+ let raw;
9446
+ try {
9447
+ raw = readFileSync19(streamPath, "utf8");
9448
+ } catch {
9449
+ return false;
9450
+ }
9451
+ if (!raw.includes(ALLOWANCE_REFUSAL_MARKER)) return false;
9452
+ for (const line of raw.split("\n")) {
9453
+ if (!line.includes(ALLOWANCE_REFUSAL_MARKER)) continue;
9454
+ let ev;
9455
+ try {
9456
+ ev = JSON.parse(line);
9457
+ } catch {
9458
+ continue;
9459
+ }
9460
+ if (typeof ev !== "object" || ev === null) continue;
9461
+ const e = ev;
9462
+ if (e.type === "result" && e.is_error === true) {
9463
+ if (typeof e.result === "string" && e.result.includes(ALLOWANCE_REFUSAL_MARKER)) return true;
9464
+ } else if (e.type === "assistant" && (e.isApiErrorMessage === true || e.error !== void 0)) {
9465
+ if (JSON.stringify(e.message ?? "").includes(ALLOWANCE_REFUSAL_MARKER)) return true;
9466
+ }
9467
+ }
9468
+ return false;
9469
+ }
9382
9470
  var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9383
9471
  function planAgentRun(o) {
9384
9472
  const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
@@ -9423,7 +9511,17 @@ function planAgentRun(o) {
9423
9511
  const streamsDir = join22(o.stateDir, "streams");
9424
9512
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9425
9513
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9426
- const { toolchain, model } = seat;
9514
+ const { model } = seat;
9515
+ let toolchain = seat.toolchain;
9516
+ if (toolchain.auth === "workspace") {
9517
+ if (!o.apiKey || o.seatKind === "ship") {
9518
+ throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
9519
+ }
9520
+ if (!o.route.resolved) {
9521
+ throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
9522
+ }
9523
+ toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
9524
+ }
9427
9525
  const adapter = adapterFor(toolchain.harness);
9428
9526
  const invocation = adapter.invocation(toolchain, {
9429
9527
  role: o.role,
@@ -9468,6 +9566,7 @@ function planAgentRun(o) {
9468
9566
  args: invocation.args,
9469
9567
  toolchain: toolchain.name,
9470
9568
  harness: toolchain.harness,
9569
+ workspaceAuth: toolchain.auth === "workspace",
9471
9570
  provider: toolchainProvider(toolchain),
9472
9571
  maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
9473
9572
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
@@ -13090,7 +13189,8 @@ function planRepoClone(i) {
13090
13189
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13091
13190
  const target = i.dir ? expandDir(i.dir, i.base) : join29(expandDir(basePath, i.base), i.route.split("/")[0], name);
13092
13191
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
13093
- return { ...add, remote, target };
13192
+ const forgeHost = i.resolved?.repoForgeHosts?.[id];
13193
+ return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
13094
13194
  }
13095
13195
 
13096
13196
  // src/doctor-fix.ts
@@ -15216,7 +15316,7 @@ function runForge(a, io) {
15216
15316
  const doc = parseDocument4(io.readFile(io.configFile));
15217
15317
  doc.setIn(["ship", "forges", host, "token"], tok);
15218
15318
  io.writeFile(io.configFile, doc.toString());
15219
- io.forges = { ...io.forges ?? {}, [host]: { username: "x-access-token", token: tok } };
15319
+ io.forges = { ...io.forges ?? {}, [host]: { username: forgeDescriptorFor(host).credentialUsername, token: tok } };
15220
15320
  existing = forgeFor(io.forges, host);
15221
15321
  }
15222
15322
  if (existing.entry.token.source === "env") {
@@ -16213,6 +16313,10 @@ async function discover(o) {
16213
16313
  repos.filter((r) => r.remote).map((r) => [r.id, r.remote])
16214
16314
  );
16215
16315
  if (Object.keys(remotes).length > 0) out.repoRemotes = remotes;
16316
+ const forgeHosts = Object.fromEntries(
16317
+ repos.filter((r) => r.forge_host?.trim()).map((r) => [r.id, r.forge_host.trim().toLowerCase()])
16318
+ );
16319
+ if (Object.keys(forgeHosts).length > 0) out.repoForgeHosts = forgeHosts;
16216
16320
  }
16217
16321
  if (o.area) {
16218
16322
  if (!out.areaModelId) problems.push(`--area given but this project has no "Projects" table`);
@@ -18366,7 +18470,8 @@ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
18366
18470
  gitRemoteName: effective.branch.remote,
18367
18471
  base: effective.branch.base,
18368
18472
  originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
18369
- cloneScheme: cfg.ship.cloneScheme
18473
+ cloneScheme: cfg.ship.cloneScheme,
18474
+ ...target.forgeHost ? { forgeHost: target.forgeHost } : {}
18370
18475
  });
18371
18476
  }
18372
18477
  }
@@ -18376,7 +18481,7 @@ function syncAllPassengerCheckouts(cfg) {
18376
18481
  return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
18377
18482
  name: spec.name,
18378
18483
  workspaceId: spec.workspaceId,
18379
- outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme })
18484
+ outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
18380
18485
  }));
18381
18486
  }
18382
18487
 
@@ -18749,7 +18854,7 @@ function createSyncDaemonServer(targets, secret) {
18749
18854
  try {
18750
18855
  const synced = targets.map((t) => ({
18751
18856
  name: t.name,
18752
- outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base, { originUrl: t.originUrl, scheme: t.cloneScheme })
18857
+ outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base, { originUrl: t.originUrl, scheme: t.cloneScheme, host: t.forgeHost })
18753
18858
  }));
18754
18859
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ synced }));
18755
18860
  } catch (error) {
@@ -18912,6 +19017,7 @@ import { join as join42 } from "path";
18912
19017
  import { randomBytes as randomBytes2 } from "crypto";
18913
19018
  var DEFAULT_FLOOR_MS = 5e3;
18914
19019
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
19020
+ var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
18915
19021
  var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
18916
19022
  async function runDaemonLoop(o) {
18917
19023
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
@@ -19045,6 +19151,10 @@ var SpawnBreaker = class {
19045
19151
  this.states.set(role, { failures, retryAt: this.now() + retryInMs, lastMessage: message });
19046
19152
  return { parked, firstOfMessage: prev?.lastMessage !== message, failures, retryInMs };
19047
19153
  }
19154
+ /** CREW-2796: holds `role` back for `ms` without counting a failed start (an allowance pause). */
19155
+ pause(role, ms3) {
19156
+ this.states.set(role, { failures: 0, retryAt: this.now() + ms3, lastMessage: "paused" });
19157
+ }
19048
19158
  /** A run for `role` spawned: forget its earlier failed starts. */
19049
19159
  recordSuccess(role) {
19050
19160
  this.states.delete(role);
@@ -19172,8 +19282,27 @@ async function runOnePassUnguarded(o) {
19172
19282
  const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
19173
19283
  breaker.recordSuccess(role);
19174
19284
  clearShipAttention(o.state, `role_parked:${role}`);
19285
+ clearShipAttention(o.state, workspaceAuthKey(role));
19286
+ clearShipAttention(o.state, workspaceAllowanceKey(role));
19175
19287
  }).catch(async (e) => {
19176
19288
  const message = e.message;
19289
+ if (e instanceof WorkspaceAllowanceExhausted) {
19290
+ breaker.pause(role, ALLOWANCE_PAUSE_MS);
19291
+ emit.warn(`${role} paused: ${message}`, { step: "agent", role });
19292
+ await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, workspaceAllowanceItem(o.route.route, role));
19293
+ return;
19294
+ }
19295
+ if (e instanceof WorkspaceAuthRefused) {
19296
+ breaker.recordFailure(role, message);
19297
+ emit.warn(`${role} agent refused: ${message}`, { step: "agent", role });
19298
+ await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, {
19299
+ kind: "workspace_auth",
19300
+ key: workspaceAuthKey(role),
19301
+ message: `${o.route.route}: ${message}`,
19302
+ since: (/* @__PURE__ */ new Date()).toISOString()
19303
+ });
19304
+ return;
19305
+ }
19177
19306
  if (!(e instanceof AgentSpawnError)) {
19178
19307
  emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
19179
19308
  return;
@@ -19421,7 +19550,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19421
19550
  const target = repoTargetFor(route2, result.ticket.repo_id);
19422
19551
  ticketRepo = target?.name;
19423
19552
  try {
19424
- if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme })) {
19553
+ if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme, host: target.forgeHost })) {
19425
19554
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
19426
19555
  }
19427
19556
  } catch (e) {
@@ -19440,6 +19569,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19440
19569
  roster: rosterFor(decision, route2, role),
19441
19570
  environment: await buildEnvironment(route2, ship, ticketHint),
19442
19571
  apiKey: identity.apiKey,
19572
+ seatKind: identity.kind,
19443
19573
  cycle: emit.cycle,
19444
19574
  runId,
19445
19575
  ticket: ticketHint,
@@ -19484,6 +19614,9 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19484
19614
  await postRunKilled(route2, tracker, workingId, memberId, result.killed.limitMinutes, emit);
19485
19615
  } else {
19486
19616
  await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
19617
+ if (plan.workspaceAuth && streamShowsAllowanceRefusal(plan.streamPath)) {
19618
+ throw new WorkspaceAllowanceExhausted("the inference endpoint refused the run: the workspace is past its AI allowance");
19619
+ }
19487
19620
  }
19488
19621
  } finally {
19489
19622
  await settleRunEnd({
@@ -21734,7 +21867,7 @@ ${cu.tail.trimEnd()}
21734
21867
  const target = repoTargetFor(w.route, result.ticket.repo_id);
21735
21868
  fleetTicketRepo = target?.name;
21736
21869
  try {
21737
- if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
21870
+ if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme, host: target.forgeHost })) {
21738
21871
  wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
21739
21872
  }
21740
21873
  } catch (e) {
@@ -21951,7 +22084,7 @@ ${cu.tail.trimEnd()}
21951
22084
  const target = repoTargetFor(route2, result.ticket.repo_id);
21952
22085
  ticketRepo = target?.name;
21953
22086
  try {
21954
- if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
22087
+ if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme, host: target.forgeHost })) {
21955
22088
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
21956
22089
  }
21957
22090
  } catch (e) {
@@ -22691,7 +22824,7 @@ ${cu.tail.trimEnd()}
22691
22824
  }
22692
22825
  say("cloning", { remote: plan2.remote, target: plan2.target }, `cloning ${plan2.remote} into ${plan2.target}`);
22693
22826
  try {
22694
- ensureRepoCheckout(plan2.target, plan2.remote, { scheme: cfg.ship.cloneScheme ?? "https" });
22827
+ ensureRepoCheckout(plan2.target, plan2.remote, { scheme: cfg.ship.cloneScheme ?? "https", host: plan2.forgeHost });
22695
22828
  } catch (e) {
22696
22829
  if (!(e instanceof GitError)) throw e;
22697
22830
  const authHint = /authentication failed|could not read (username|password)|permission denied|terminal prompts disabled|repository not found/i.test(e.message) ? "\n hint: git could not authenticate \u2014 run `gh auth login` (or add an SSH key and set ship.cloneScheme: ssh), then re-run" : "";
@@ -23281,6 +23414,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23281
23414
  reposModelId: found.reposModelId,
23282
23415
  repoNames: found.repoNames,
23283
23416
  repoRemotes: found.repoRemotes,
23417
+ repoForgeHosts: found.repoForgeHosts,
23284
23418
  models: found.models,
23285
23419
  seats: found.seats,
23286
23420
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
@@ -25554,6 +25688,14 @@ ${foreign.join("\n")}
25554
25688
  `);
25555
25689
  }
25556
25690
  for (const t of shipToolchains(cfg.ship).filter((t2) => t2.harness === "claude")) {
25691
+ if (t.auth === "workspace") {
25692
+ for (const r of cfg.routes.filter((r2) => r2.enabled)) {
25693
+ const inference = r.resolved ? { baseUrl: inferenceBaseUrl(r.baseUrl, r.resolved.workspaceId), token: "" } : void 0;
25694
+ process.stdout.write(`auth: ${t.name}: ${r.route}: ${describeClaudeAuth({ ...t, inference })}
25695
+ `);
25696
+ }
25697
+ continue;
25698
+ }
25557
25699
  process.stdout.write(`auth: ${t.name}: ${describeClaudeAuth(t)}
25558
25700
  `);
25559
25701
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.97.0",
3
+ "version": "0.99.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",