@tablation/crew 0.100.2 → 0.101.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 +41 -7
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -19476,12 +19476,12 @@ async function runOnePassUnguarded(o) {
19476
19476
  live.delete(role);
19477
19477
  continue;
19478
19478
  }
19479
- started.push(role);
19480
- let ready = () => {
19479
+ let settle = () => {
19481
19480
  };
19482
19481
  const settled = new Promise((resolve7) => {
19483
- ready = resolve7;
19482
+ settle = resolve7;
19484
19483
  });
19484
+ const ready = (tookTicket2) => settle(tookTicket2);
19485
19485
  const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
19486
19486
  breaker.recordSuccess(role);
19487
19487
  clearShipAttention(o.state, `role_parked:${role}`);
@@ -19527,18 +19527,20 @@ async function runOnePassUnguarded(o) {
19527
19527
  }
19528
19528
  }).finally(() => {
19529
19529
  lock.release();
19530
- ready(false);
19530
+ settle(void 0);
19531
19531
  });
19532
19532
  if (o.inFlight) {
19533
19533
  const set = o.inFlight;
19534
19534
  set.add(run2);
19535
19535
  void run2.finally(() => set.delete(run2));
19536
19536
  }
19537
+ const reported = TICKET_ROLES.has(role) ? await settled : true;
19538
+ const tookTicket = reported !== false;
19539
+ if (tookTicket) started.push(role);
19537
19540
  if (roleCap(role) <= 1) {
19538
19541
  live.delete(role);
19539
19542
  continue;
19540
19543
  }
19541
- const tookTicket = await settled;
19542
19544
  if (tookTicket && count + 1 < roleCap(role)) live.set(role, count + 1);
19543
19545
  else live.delete(role);
19544
19546
  }
@@ -19768,6 +19770,8 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19768
19770
  );
19769
19771
  return;
19770
19772
  }
19773
+ } else {
19774
+ hooks?.ready(true);
19771
19775
  }
19772
19776
  }
19773
19777
  const plan = planAgentRun({
@@ -19969,6 +19973,7 @@ async function execute(h, args, streaming, after) {
19969
19973
  var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
19970
19974
  async function handlePeriscopes(h, q) {
19971
19975
  if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
19976
+ if (!q.token) return err(401, "a bearer token is required");
19972
19977
  const down = /^\/periscopes\/([^/]+)\/down$/.exec(q.path);
19973
19978
  const isUp = q.path === "/periscopes" && q.method === "POST";
19974
19979
  const isList = q.path === "/periscopes" && q.method === "GET";
@@ -19985,6 +19990,7 @@ async function handlePeriscopes(h, q) {
19985
19990
  const named = q.query.get("route") ?? void 0;
19986
19991
  const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
19987
19992
  const rows = [];
19993
+ const hosting = [];
19988
19994
  for (const c of candidates2) {
19989
19995
  if ("status" in c) return c;
19990
19996
  const denied2 = await member(c.workspaceId);
@@ -19997,12 +20003,21 @@ async function handlePeriscopes(h, q) {
19997
20003
  if (r.code !== 0) return err(failureStatus(r), cliMessage(r));
19998
20004
  try {
19999
20005
  const list = JSON.parse(r.stdout);
20000
- if (Array.isArray(list)) for (const e of list) rows.push({ route: c.route, ...e });
20006
+ const mine = Array.isArray(list) ? list : [];
20007
+ for (const e of mine) rows.push({ route: c.route, ...e });
20008
+ hosting.push({
20009
+ route: c.route,
20010
+ workspaceId: c.workspaceId,
20011
+ previewHostSet: !!h.previewHost,
20012
+ maxPeriscopes: h.maxPeriscopes,
20013
+ live: mine.length,
20014
+ repos: h.previewRepos?.(c.route) ?? []
20015
+ });
20001
20016
  } catch {
20002
20017
  return err(502, "the list command did not answer JSON");
20003
20018
  }
20004
20019
  }
20005
- return reply(200, { periscopes: rows });
20020
+ return reply(200, { periscopes: rows, hosting });
20006
20021
  }
20007
20022
  const b = q.body;
20008
20023
  if (!b) return err(400, "body must be a small JSON object");
@@ -20132,6 +20147,7 @@ var ControlAuth = class {
20132
20147
  async verifyIn(token, workspaceId) {
20133
20148
  if (!token) return "unauthorized";
20134
20149
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
20150
+ for (const k of this.o.workspaceSecrets?.(workspaceId) ?? []) if (k && secretsMatch(token, k)) return "ok";
20135
20151
  const now = (this.o.now ?? Date.now)();
20136
20152
  const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
20137
20153
  const hit = this.cache.get(id);
@@ -20461,6 +20477,9 @@ async function startControlServer(deps) {
20461
20477
  if (!deps.periscopeMcp) return send(res, err2(404, "not found"));
20462
20478
  return await deps.periscopeMcp(req, res, decodeURIComponent(mcp[1]));
20463
20479
  }
20480
+ if (isPeriscopePath(new URL(req.url ?? "/", "http://ship").pathname.replace(/\/+$/, "") || "/")) {
20481
+ return await send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
20482
+ }
20464
20483
  const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
20465
20484
  const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
20466
20485
  if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
@@ -22975,6 +22994,10 @@ ${cu.tail.trimEnd()}
22975
22994
  return [];
22976
22995
  }
22977
22996
  }),
22997
+ workspaceSecrets: (ws) => {
22998
+ const sec = readPersistedTunnel(cfg.ship.stateDir, ws)?.mcpSecret;
22999
+ return sec ? [sec] : [];
23000
+ },
22978
23001
  userAgent: cfg.ship.userAgent
22979
23002
  });
22980
23003
  const controlServer = await startControlServer({
@@ -22988,6 +23011,17 @@ ${cu.tail.trimEnd()}
22988
23011
  maxPeriscopes: cfg.ship.maxPeriscopes,
22989
23012
  run: runCrew,
22990
23013
  verifyIn: (token, ws) => controlAuth.verifyIn(token, ws),
23014
+ previewRepos: (routeName) => {
23015
+ const r = controlRoutes.find((x) => x.route === routeName);
23016
+ if (!r) return [];
23017
+ return reposOf(r).filter((t) => {
23018
+ try {
23019
+ return !!loadRepoConfig(t.dir)?.hooks.preview;
23020
+ } catch {
23021
+ return false;
23022
+ }
23023
+ }).map((t) => t.name);
23024
+ },
22991
23025
  mcp: {
22992
23026
  onUp: async (r, done) => {
22993
23027
  const id = typeof done.ticketRecordId === "string" ? done.ticketRecordId : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.100.2",
3
+ "version": "0.101.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",