@tablation/crew 0.126.6 → 0.126.8

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 +243 -122
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12004,6 +12004,21 @@ function slugFor(route2, agentId) {
12004
12004
  const [workspaceSlug] = splitRoute(route2.route);
12005
12005
  return seatKeySlug(workspaceSlug, agentId);
12006
12006
  }
12007
+ function withFoundSeats(route2, found, previousReleaseAgentId) {
12008
+ const resolved = route2.resolved;
12009
+ if (!resolved) return route2;
12010
+ const releaseSeat2 = found.releaseSeat ?? resolved.releaseSeat;
12011
+ const releaseAgentId = resolved.releaseAgentId ?? previousReleaseAgentId;
12012
+ return {
12013
+ ...route2,
12014
+ resolved: {
12015
+ ...resolved,
12016
+ seats: found.seats,
12017
+ ...releaseSeat2 ? { releaseSeat: releaseSeat2 } : {},
12018
+ ...releaseAgentId ? { releaseAgentId } : {}
12019
+ }
12020
+ };
12021
+ }
12007
12022
  function seatAgentId(route2, role) {
12008
12023
  return route2.resolved?.agentPersonas?.[role]?.agentId;
12009
12024
  }
@@ -12496,6 +12511,15 @@ printf '%s
12496
12511
  ' "$tip" > "$out/$name.tip"
12497
12512
  echo "updated $tip"
12498
12513
  `;
12514
+ var SHOW_SCRIPT = `
12515
+ set -e
12516
+ export GIT_TERMINAL_PROMPT=0
12517
+ m=${VOLUME_ROOT}/"$1"; base="$2"; path="$3"
12518
+ git config --global --add safe.directory '*'
12519
+ git -C "$m" rev-parse --verify --quiet "refs/heads/$base^{commit}" >/dev/null || { echo "the mirror has no branch $base" >&2; exit 3; }
12520
+ git -C "$m" cat-file -e "refs/heads/$base:$path" 2>/dev/null || exit 4
12521
+ git -C "$m" show "refs/heads/$base:$path"
12522
+ `;
12499
12523
  var SandboxCache = class {
12500
12524
  s;
12501
12525
  exec;
@@ -12582,6 +12606,20 @@ var SandboxCache = class {
12582
12606
  if (!m) throw new Error(`unexpected output materializing ${repo.host}/${repo.path}: ${stdout.trim().slice(0, 200)}`);
12583
12607
  return { tip: m[2], changed: m[1] === "updated" };
12584
12608
  }
12609
+ /**
12610
+ * The text of `path` at the tip of `base` in the repository's mirror, or
12611
+ * null when the mirror has no such file. Throws when the mirror or the
12612
+ * branch is missing, so the caller can tell "no contract" from "no mirror".
12613
+ */
12614
+ async showFile(repo, base, path) {
12615
+ try {
12616
+ const { stdout } = await this.helper(this.s.helperImage, SHOW_SCRIPT, [mirrorRelPath(repo), base, path]);
12617
+ return stdout;
12618
+ } catch (e) {
12619
+ if (e.code === 4) return null;
12620
+ throw e;
12621
+ }
12622
+ }
12585
12623
  /** Mirrors in the volume with their last refresh; empty when the volume does not exist. */
12586
12624
  async list() {
12587
12625
  try {
@@ -14055,7 +14093,7 @@ async function runPoolMember(deps) {
14055
14093
  await deps.sleep(backoffMs(failures++));
14056
14094
  }
14057
14095
  };
14058
- let reported;
14096
+ let reported2;
14059
14097
  let busy = false;
14060
14098
  const report = async (state, o = {}) => {
14061
14099
  const body = {
@@ -14078,7 +14116,7 @@ async function runPoolMember(deps) {
14078
14116
  if (r.status === 401) return "credential";
14079
14117
  if (r.status === 409) deps.log(`status ${state}: the platform refused the transition (409); not retrying`);
14080
14118
  else if (r.status >= 400) deps.log(`status ${state}: platform answered ${r.status}; not retrying`);
14081
- reported = body;
14119
+ reported2 = body;
14082
14120
  deps.store.write({ ...deps.store.read(), lastState: state });
14083
14121
  return "ok";
14084
14122
  };
@@ -14161,7 +14199,7 @@ async function runPoolMember(deps) {
14161
14199
  if (r === "stopped") return POOL_EXIT.ok;
14162
14200
  }
14163
14201
  }
14164
- let relayReported = reported?.state === "running" ? reported.relayUrl : null;
14202
+ let relayReported = reported2?.state === "running" ? reported2.relayUrl : null;
14165
14203
  let reregister = false;
14166
14204
  while (!reregister) {
14167
14205
  if (deps.stopping()) return POOL_EXIT.ok;
@@ -15184,13 +15222,30 @@ function isPidAlive(pid) {
15184
15222
  }
15185
15223
  var CONTROL_ONLY_HASH = "control-only";
15186
15224
  var TUNNEL_REJECTED_AFTER = 2;
15225
+ var CONTROL_RETRY_MS = 60 * 60 * 1e3;
15226
+ var RELAY_CONTROL_UNSUPPORTED_KEY = "relay_control_unsupported";
15227
+ function relayControlUnsupportedMessage(workspaceId) {
15228
+ return `the relay refused the control-path forward for workspace ${workspaceId}, so the passengers tunnel runs without the control API (remote ship transport and periscopes will not reach this ship); the relay is too old for \`-path-control\` (crew-relay 0.2.0+) \u2014 deploy it with scripts/deploy-relay.sh`;
15229
+ }
15230
+ function controlRefusedTunnels(stateDir, now = Date.now()) {
15231
+ const out = [];
15232
+ for (const workspaceId of readAllPersistedWorkspaceIds(stateDir)) {
15233
+ const at = readPersistedTunnel(stateDir, workspaceId)?.controlRefusedAt;
15234
+ if (at && now - Date.parse(at) < CONTROL_RETRY_MS) out.push(workspaceId);
15235
+ }
15236
+ return out;
15237
+ }
15187
15238
  function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
15188
15239
  const alive = deps.isPidAlive ?? isPidAlive;
15189
15240
  const plannedIds = new Set(plans.map((p) => p.workspaceId));
15190
15241
  for (const plan of plans) {
15191
15242
  const existing = readPersistedTunnel(stateDir, plan.workspaceId);
15192
15243
  const mountsHash = plan.mountsHash ?? CONTROL_ONLY_HASH;
15193
- const controlUnchanged = existing?.controlSocket === deps.controlSocket;
15244
+ const refusedAt = existing?.controlRefusedAt;
15245
+ const refusalFresh = refusedAt !== void 0 && Date.now() - Date.parse(refusedAt) < CONTROL_RETRY_MS;
15246
+ let controlSocket = deps.controlSocket && refusalFresh ? void 0 : deps.controlSocket;
15247
+ let controlRefusedAt = refusalFresh ? refusedAt : void 0;
15248
+ const controlUnchanged = existing?.controlSocket === controlSocket;
15194
15249
  const currentSecret = mcpSecrets.get(plan.workspaceId);
15195
15250
  const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
15196
15251
  const slug2 = existing?.slug ?? generateTunnelSlug();
@@ -15215,7 +15270,12 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15215
15270
  continue;
15216
15271
  }
15217
15272
  const diedQuickly = existing !== void 0 && existing.status === "connecting" && !alive(existing.pid) && controlUnchanged && secretUnchanged && slugUnchanged && existing.mountsHash === mountsHash;
15218
- const quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
15273
+ let quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
15274
+ if (diedQuickly && controlSocket && plan.port !== void 0) {
15275
+ controlSocket = void 0;
15276
+ controlRefusedAt = (/* @__PURE__ */ new Date()).toISOString();
15277
+ quickExits = 0;
15278
+ }
15219
15279
  let settledStatus = "connecting";
15220
15280
  const tunnel = startTunnel({
15221
15281
  relayHost,
@@ -15224,7 +15284,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15224
15284
  slug: slug2,
15225
15285
  apiKey,
15226
15286
  localPort: plan.port,
15227
- controlSocket: deps.controlSocket,
15287
+ controlSocket,
15228
15288
  privateKeyPath,
15229
15289
  mcpSecret: currentSecret,
15230
15290
  spawnFn: deps.spawnFn,
@@ -15237,7 +15297,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15237
15297
  containerName: plan.containerName ?? "",
15238
15298
  workspaceId: plan.workspaceId,
15239
15299
  mountsHash,
15240
- controlSocket: deps.controlSocket,
15300
+ controlSocket,
15301
+ controlRefusedAt,
15241
15302
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15242
15303
  status: settledStatus,
15243
15304
  mcpSecret: currentSecret,
@@ -19076,6 +19137,73 @@ function runForge(a, io) {
19076
19137
  `, err: "" };
19077
19138
  }
19078
19139
 
19140
+ // src/mirror-contract.ts
19141
+ import { existsSync as existsSync40 } from "fs";
19142
+
19143
+ // src/passenger-mirror.ts
19144
+ import { mkdirSync as mkdirSync29 } from "fs";
19145
+ import { dirname as dirname25 } from "path";
19146
+ function mirrorRepoOf(spec) {
19147
+ const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
19148
+ if (fromOrigin) return fromOrigin;
19149
+ if (!spec.remote) return null;
19150
+ return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
19151
+ }
19152
+ async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
19153
+ const out = [];
19154
+ for (const spec of specs) {
19155
+ if (!spec.mirror) continue;
19156
+ const base = { name: spec.name, workspaceId: spec.workspaceId };
19157
+ const repo = mirrorRepoOf(spec);
19158
+ if (!repo) {
19159
+ out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
19160
+ continue;
19161
+ }
19162
+ try {
19163
+ const parent = dirname25(spec.dir);
19164
+ mkdirSync29(parent, { recursive: true });
19165
+ mkdirSync29(spec.dir, { recursive: true });
19166
+ const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
19167
+ out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
19168
+ } catch (e) {
19169
+ const err3 = e;
19170
+ out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
19171
+ }
19172
+ }
19173
+ return out;
19174
+ }
19175
+ function mirrorOutcome(m) {
19176
+ if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
19177
+ return { action: "cloned", detail: `${m.action} ${m.detail}` };
19178
+ }
19179
+
19180
+ // src/mirror-contract.ts
19181
+ function lacksCheckout(ship, dir) {
19182
+ return ship.runner === "sandbox" && !existsSync40(dir);
19183
+ }
19184
+ function mirrorRepoOfTarget(t) {
19185
+ return mirrorRepoOf({ remote: t.remote, forgeHost: t.forgeHost });
19186
+ }
19187
+ async function readMirrorContract(cache, repo, base) {
19188
+ for (const name of REPO_CONFIG_FILENAMES) {
19189
+ const text2 = await cache.showFile(repo, base, name);
19190
+ if (text2 !== null) return parseRepoConfig(text2, `${repo.host}/${repo.path}:${base}:${name}`);
19191
+ }
19192
+ return null;
19193
+ }
19194
+ function noCheckoutReleaseNote(repo, mode) {
19195
+ if (mode === "local" || mode === null) {
19196
+ return `${repo}: no checkout on this ship; release.mode "local" repositories are released by the host that owns the deploy hook, which takes a ticket at verified`;
19197
+ }
19198
+ return `${repo}: no checkout on this ship; release.mode "${mode}" needs the sandboxed release over a mirror-fed clone, which is not built (CREW-2940)`;
19199
+ }
19200
+ var reported = /* @__PURE__ */ new Set();
19201
+ function firstReport(key) {
19202
+ if (reported.has(key)) return false;
19203
+ reported.add(key);
19204
+ return true;
19205
+ }
19206
+
19079
19207
  // src/whoami.ts
19080
19208
  async function buildWhoami(routes, deps = {}) {
19081
19209
  const doFetch = deps.fetch ?? fetch;
@@ -19247,7 +19375,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
19247
19375
  }
19248
19376
 
19249
19377
  // src/watch.ts
19250
- import { watch as watchFile, existsSync as existsSync40, statSync as statSync11, openSync as openSync5, readSync as readSync2, closeSync as closeSync5 } from "fs";
19378
+ import { watch as watchFile, existsSync as existsSync41, statSync as statSync11, openSync as openSync5, readSync as readSync2, closeSync as closeSync5 } from "fs";
19251
19379
  var E = "\x1B[";
19252
19380
  var dim = (s) => `${E}2m${s}${E}0m`;
19253
19381
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -19348,7 +19476,7 @@ function renderHeaders(states, now = Date.now()) {
19348
19476
  return states.map((s) => renderHeader(s, now)).join("");
19349
19477
  }
19350
19478
  function readFrom(file, from) {
19351
- if (!existsSync40(file)) return { events: [], offset: from };
19479
+ if (!existsSync41(file)) return { events: [], offset: from };
19352
19480
  const size = statSync11(file).size;
19353
19481
  if (size < from) return readFrom(file, 0);
19354
19482
  if (size === from) return { events: [], offset: from };
@@ -19591,8 +19719,8 @@ function nextRoles(decision, ran) {
19591
19719
  }
19592
19720
 
19593
19721
  // src/ship-name.ts
19594
- import { existsSync as existsSync41, mkdirSync as mkdirSync29, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync32 } from "fs";
19595
- import { dirname as dirname25 } from "path";
19722
+ import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync32 } from "fs";
19723
+ import { dirname as dirname26 } from "path";
19596
19724
  import { parseDocument as parseDocument5 } from "yaml";
19597
19725
  var ShipNameError = class extends Error {
19598
19726
  };
@@ -19653,7 +19781,7 @@ async function syncRouteName(i) {
19653
19781
  return { status: "ok", ...base, action, name: i.local };
19654
19782
  }
19655
19783
  function readSyncedName(resolvedFile) {
19656
- if (!existsSync41(resolvedFile)) return void 0;
19784
+ if (!existsSync42(resolvedFile)) return void 0;
19657
19785
  try {
19658
19786
  const v = JSON.parse(readFileSync37(resolvedFile, "utf8")).shipName;
19659
19787
  return typeof v === "string" ? v : void 0;
@@ -19662,7 +19790,7 @@ function readSyncedName(resolvedFile) {
19662
19790
  }
19663
19791
  }
19664
19792
  function writeSyncedName(resolvedFile, name) {
19665
- if (!existsSync41(resolvedFile)) return;
19793
+ if (!existsSync42(resolvedFile)) return;
19666
19794
  let raw;
19667
19795
  try {
19668
19796
  raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
@@ -19671,7 +19799,7 @@ function writeSyncedName(resolvedFile, name) {
19671
19799
  }
19672
19800
  if (raw.shipName === name) return;
19673
19801
  raw.shipName = name;
19674
- mkdirSync29(dirname25(resolvedFile), { recursive: true });
19802
+ mkdirSync30(dirname26(resolvedFile), { recursive: true });
19675
19803
  const tmp = `${resolvedFile}.tmp${process.pid}`;
19676
19804
  writeFileSync32(tmp, `${JSON.stringify(raw, null, 2)}
19677
19805
  `);
@@ -20364,8 +20492,8 @@ function describeCapability(i) {
20364
20492
  }
20365
20493
 
20366
20494
  // src/agents-sync-run.ts
20367
- import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, writeFileSync as writeFileSync33 } from "fs";
20368
- import { dirname as dirname26 } from "path";
20495
+ import { existsSync as existsSync43, mkdirSync as mkdirSync31, readFileSync as readFileSync38, writeFileSync as writeFileSync33 } from "fs";
20496
+ import { dirname as dirname27 } from "path";
20369
20497
  async function runAgentsSync(target, o) {
20370
20498
  const dryRun = o.dryRun ?? false;
20371
20499
  const force = o.force ?? false;
@@ -20398,11 +20526,11 @@ async function runAgentsSync(target, o) {
20398
20526
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
20399
20527
  if (!dryRun) {
20400
20528
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
20401
- const raw = existsSync42(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
20529
+ const raw = existsSync43(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
20402
20530
  raw.agentPersonas = personaResult.agentPersonas;
20403
20531
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
20404
20532
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
20405
- mkdirSync30(dirname26(resolvedPath), { recursive: true });
20533
+ mkdirSync31(dirname27(resolvedPath), { recursive: true });
20406
20534
  writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
20407
20535
  `);
20408
20536
  if (target.resolved) {
@@ -21025,7 +21153,7 @@ ${run3.err}` });
21025
21153
  }
21026
21154
 
21027
21155
  // src/sample-ticket.ts
21028
- import { basename as basename12, dirname as dirname27, join as join44 } from "path";
21156
+ import { basename as basename12, dirname as dirname28, join as join44 } from "path";
21029
21157
  var SAMPLE_REPO = "crew-sample";
21030
21158
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
21031
21159
  var SAMPLE_MARKER = "sample: true";
@@ -21048,7 +21176,7 @@ has something safe to work on: its test and build hooks always pass, it has no
21048
21176
  remote, and \`crew sample-ticket --remove\` deletes it.
21049
21177
  `;
21050
21178
  function sampleDir(existing, reposBase, workspace) {
21051
- if (existing.length > 0) return join44(dirname27(existing[0]), SAMPLE_REPO);
21179
+ if (existing.length > 0) return join44(dirname28(existing[0]), SAMPLE_REPO);
21052
21180
  return join44(reposBase, workspace, SAMPLE_REPO);
21053
21181
  }
21054
21182
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -21093,8 +21221,8 @@ function planSampleTicket(i) {
21093
21221
  }
21094
21222
 
21095
21223
  // src/worktree-sweep.ts
21096
- import { existsSync as existsSync43, readFileSync as readFileSync40, readdirSync as readdirSync14 } from "fs";
21097
- import { basename as basename13, dirname as dirname28, join as join45, resolve as resolve5 } from "path";
21224
+ import { existsSync as existsSync44, readFileSync as readFileSync40, readdirSync as readdirSync14 } from "fs";
21225
+ import { basename as basename13, dirname as dirname29, join as join45, resolve as resolve5 } from "path";
21098
21226
  function planWorktreeSweep(target, tickets, contract) {
21099
21227
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
21100
21228
  const verified = contract.statuses.verified;
@@ -21158,7 +21286,7 @@ function worktreeTicketNumbers(target) {
21158
21286
  return [...nums];
21159
21287
  }
21160
21288
  function siblingWorktreeDirs(target) {
21161
- const parent = dirname28(target.dir.replace(/[/\\]+$/, ""));
21289
+ const parent = dirname29(target.dir.replace(/[/\\]+$/, ""));
21162
21290
  const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
21163
21291
  const registered = new Set(worktrees(target.dir).map((w) => resolve5(w.path)));
21164
21292
  let names;
@@ -21206,7 +21334,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
21206
21334
  let removed = 0;
21207
21335
  let keptBranches = 0;
21208
21336
  for (const a of actions) {
21209
- if (!dryRun && base && !a.keepBranch && existsSync43(a.path) && isHandBackMerge(a.path, base)) {
21337
+ if (!dryRun && base && !a.keepBranch && existsSync44(a.path) && isHandBackMerge(a.path, base)) {
21210
21338
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
21211
21339
  log.emit(
21212
21340
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -21214,7 +21342,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
21214
21342
  );
21215
21343
  }
21216
21344
  }
21217
- if (!dryRun && existsSync43(a.path)) {
21345
+ if (!dryRun && existsSync44(a.path)) {
21218
21346
  let dirty = false;
21219
21347
  try {
21220
21348
  dirty = status(a.path).length > 0;
@@ -21225,7 +21353,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
21225
21353
  continue;
21226
21354
  }
21227
21355
  }
21228
- if (!dryRun && a.requirePushed && a.branch && existsSync43(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
21356
+ if (!dryRun && a.requirePushed && a.branch && existsSync44(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
21229
21357
  refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
21230
21358
  continue;
21231
21359
  }
@@ -21379,12 +21507,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
21379
21507
  }
21380
21508
 
21381
21509
  // src/stream-sweep.ts
21382
- import { existsSync as existsSync44, readdirSync as readdirSync15, rmSync as rmSync20, statSync as statSync12 } from "fs";
21510
+ import { existsSync as existsSync45, readdirSync as readdirSync15, rmSync as rmSync20, statSync as statSync12 } from "fs";
21383
21511
  import { join as join46 } from "path";
21384
21512
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
21385
21513
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
21386
21514
  const dir = join46(stateDir, "streams");
21387
- if (!existsSync44(dir)) return [];
21515
+ if (!existsSync45(dir)) return [];
21388
21516
  const cutoff = now - retentionDays * DAY_MS2;
21389
21517
  return readdirSync15(dir).map((name) => join46(dir, name)).filter((path) => {
21390
21518
  try {
@@ -21412,7 +21540,7 @@ function applyStreamSweep(paths, dryRun, log) {
21412
21540
  return removed;
21413
21541
  }
21414
21542
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
21415
- if (!existsSync44(stateDir)) return [];
21543
+ if (!existsSync45(stateDir)) return [];
21416
21544
  const cutoff = now - retentionDays * DAY_MS2;
21417
21545
  return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join46(stateDir, name)).filter((path) => {
21418
21546
  try {
@@ -21424,11 +21552,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
21424
21552
  }
21425
21553
 
21426
21554
  // src/cli.ts
21427
- import { realpathSync as realpathSync3, readFileSync as readFileSync46, existsSync as existsSync51, mkdirSync as mkdirSync38, writeFileSync as writeFileSync38, rmSync as rmSync25, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
21555
+ import { realpathSync as realpathSync3, readFileSync as readFileSync46, existsSync as existsSync52, mkdirSync as mkdirSync38, writeFileSync as writeFileSync38, rmSync as rmSync25, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
21428
21556
  import { createInterface } from "readline/promises";
21429
21557
 
21430
21558
  // src/periscope-cmd.ts
21431
- import { existsSync as existsSync45, mkdirSync as mkdirSync31, readFileSync as readFileSync41, rmSync as rmSync21, writeFileSync as writeFileSync34 } from "fs";
21559
+ import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync41, rmSync as rmSync21, writeFileSync as writeFileSync34 } from "fs";
21432
21560
  import { join as join47 } from "path";
21433
21561
  var PeriscopeError = class extends Error {
21434
21562
  };
@@ -21446,7 +21574,7 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
21446
21574
  }
21447
21575
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
21448
21576
  try {
21449
- mkdirSync31(priorDir(stateDir), { recursive: true });
21577
+ mkdirSync32(priorDir(stateDir), { recursive: true });
21450
21578
  writeFileSync34(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
21451
21579
  } catch {
21452
21580
  }
@@ -21454,7 +21582,7 @@ function writePeriscopePrior(stateDir, ticketRecordId, prior) {
21454
21582
  function readPeriscopePrior(stateDir, ticketRecordId) {
21455
21583
  try {
21456
21584
  const p = priorPath(stateDir, ticketRecordId);
21457
- return existsSync45(p) ? JSON.parse(readFileSync41(p, "utf8")) : void 0;
21585
+ return existsSync46(p) ? JSON.parse(readFileSync41(p, "utf8")) : void 0;
21458
21586
  } catch {
21459
21587
  return void 0;
21460
21588
  }
@@ -22009,8 +22137,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
22009
22137
 
22010
22138
  // src/ssh-keys.ts
22011
22139
  import { execFileSync as execFileSync13 } from "child_process";
22012
- import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync42, chmodSync as chmodSync3 } from "fs";
22013
- import { dirname as dirname29, join as join49 } from "path";
22140
+ import { existsSync as existsSync47, mkdirSync as mkdirSync33, readFileSync as readFileSync42, chmodSync as chmodSync3 } from "fs";
22141
+ import { dirname as dirname30, join as join49 } from "path";
22014
22142
  var SshKeyError = class extends Error {
22015
22143
  };
22016
22144
  function shipSshKeyPathFor(stateDir) {
@@ -22019,8 +22147,8 @@ function shipSshKeyPathFor(stateDir) {
22019
22147
  function ensureShipSshKeypair(stateDir) {
22020
22148
  const privateKeyPath = shipSshKeyPathFor(stateDir);
22021
22149
  const publicKeyPath = `${privateKeyPath}.pub`;
22022
- if (!existsSync46(privateKeyPath)) {
22023
- mkdirSync32(dirname29(privateKeyPath), { recursive: true });
22150
+ if (!existsSync47(privateKeyPath)) {
22151
+ mkdirSync33(dirname30(privateKeyPath), { recursive: true });
22024
22152
  try {
22025
22153
  execFileSync13("ssh-keygen", [
22026
22154
  "-t",
@@ -22040,7 +22168,7 @@ function ensureShipSshKeypair(stateDir) {
22040
22168
  }
22041
22169
  chmodSync3(privateKeyPath, 384);
22042
22170
  }
22043
- if (!existsSync46(publicKeyPath)) {
22171
+ if (!existsSync47(publicKeyPath)) {
22044
22172
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
22045
22173
  }
22046
22174
  const publicKey = readFileSync42(publicKeyPath, "utf8").trim();
@@ -22207,46 +22335,9 @@ async function unregisterEndpoint(workspaceId, info, d) {
22207
22335
  import { createServer as createHttpServer } from "http";
22208
22336
  import { spawn as spawn9 } from "child_process";
22209
22337
  import { randomBytes } from "crypto";
22210
- import { existsSync as existsSync47, mkdirSync as mkdirSync34, readFileSync as readFileSync43, writeFileSync as writeFileSync35, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
22338
+ import { existsSync as existsSync48, mkdirSync as mkdirSync34, readFileSync as readFileSync43, writeFileSync as writeFileSync35, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
22211
22339
  import { join as join50 } from "path";
22212
22340
 
22213
- // src/passenger-mirror.ts
22214
- import { mkdirSync as mkdirSync33 } from "fs";
22215
- import { dirname as dirname30 } from "path";
22216
- function mirrorRepoOf(spec) {
22217
- const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
22218
- if (fromOrigin) return fromOrigin;
22219
- if (!spec.remote) return null;
22220
- return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
22221
- }
22222
- async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
22223
- const out = [];
22224
- for (const spec of specs) {
22225
- if (!spec.mirror) continue;
22226
- const base = { name: spec.name, workspaceId: spec.workspaceId };
22227
- const repo = mirrorRepoOf(spec);
22228
- if (!repo) {
22229
- out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
22230
- continue;
22231
- }
22232
- try {
22233
- const parent = dirname30(spec.dir);
22234
- mkdirSync33(parent, { recursive: true });
22235
- mkdirSync33(spec.dir, { recursive: true });
22236
- const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
22237
- out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
22238
- } catch (e) {
22239
- const err3 = e;
22240
- out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
22241
- }
22242
- }
22243
- return out;
22244
- }
22245
- function mirrorOutcome(m) {
22246
- if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
22247
- return { action: "cloned", detail: `${m.action} ${m.detail}` };
22248
- }
22249
-
22250
22341
  // src/secret-compare.ts
22251
22342
  import { timingSafeEqual } from "crypto";
22252
22343
  function secretsMatch(a, b) {
@@ -22342,7 +22433,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
22342
22433
  }
22343
22434
  function readPersistedSyncDaemon(stateDir, workspaceId) {
22344
22435
  const path = syncDaemonStatePath(stateDir, workspaceId);
22345
- if (!existsSync47(path)) return void 0;
22436
+ if (!existsSync48(path)) return void 0;
22346
22437
  try {
22347
22438
  return JSON.parse(readFileSync43(path, "utf8"));
22348
22439
  } catch {
@@ -22373,7 +22464,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
22373
22464
  }
22374
22465
  function readAllPersistedSyncWorkspaceIds(stateDir) {
22375
22466
  const dir = join50(stateDir, "passengers");
22376
- if (!existsSync47(dir)) return [];
22467
+ if (!existsSync48(dir)) return [];
22377
22468
  const suffix = "-sync.json";
22378
22469
  return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
22379
22470
  }
@@ -22893,7 +22984,7 @@ function contractFixers(dir, baseRef, tickets, locate) {
22893
22984
  }
22894
22985
 
22895
22986
  // src/daemon.ts
22896
- import { existsSync as existsSync48, mkdirSync as mkdirSync36, readFileSync as readFileSync44, readdirSync as readdirSync17, rmSync as rmSync22, statSync as statSync14, writeFileSync as writeFileSync36 } from "fs";
22987
+ import { existsSync as existsSync49, mkdirSync as mkdirSync36, readFileSync as readFileSync44, readdirSync as readdirSync17, rmSync as rmSync22, statSync as statSync14, writeFileSync as writeFileSync36 } from "fs";
22897
22988
  import { join as join52 } from "path";
22898
22989
  import { randomBytes as randomBytes2 } from "crypto";
22899
22990
  var DEFAULT_FLOOR_MS = 5e3;
@@ -22991,7 +23082,7 @@ function computeTreeSignature(paths) {
22991
23082
  }
22992
23083
  function makeStaleChecker(paths, opts = {}) {
22993
23084
  const initial = computeTreeSignature(paths);
22994
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync48(opts.reloadSentinel);
23085
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync49(opts.reloadSentinel);
22995
23086
  }
22996
23087
  function reloadSentinelFor(stateDir) {
22997
23088
  return join52(stateDir, "reload-requested");
@@ -23242,8 +23333,8 @@ async function runOnePassUnguarded(o) {
23242
23333
  set.add(run3);
23243
23334
  void run3.finally(() => set.delete(run3));
23244
23335
  }
23245
- const reported = TICKET_ROLES.has(role) ? await settled : true;
23246
- const tookTicket = reported !== false;
23336
+ const reported2 = TICKET_ROLES.has(role) ? await settled : true;
23337
+ const tookTicket = reported2 !== false;
23247
23338
  if (tookTicket) started.push(role);
23248
23339
  if (roleCap(role) <= 1) {
23249
23340
  live.delete(role);
@@ -23580,7 +23671,7 @@ import { createHash as createHash10 } from "crypto";
23580
23671
  import {
23581
23672
  chmodSync as chmodSync4,
23582
23673
  closeSync as closeSync6,
23583
- existsSync as existsSync49,
23674
+ existsSync as existsSync50,
23584
23675
  fstatSync,
23585
23676
  openSync as openSync6,
23586
23677
  readSync as readSync3,
@@ -23951,7 +24042,7 @@ function lastLines2(path, n) {
23951
24042
  }
23952
24043
  function readTail(live, opts) {
23953
24044
  const max = opts.maxBytes ?? MAX_TAIL_BYTES;
23954
- if (!existsSync49(live)) {
24045
+ if (!existsSync50(live)) {
23955
24046
  return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
23956
24047
  }
23957
24048
  const cur = opts.after ? decodeCursor(opts.after) : null;
@@ -24108,7 +24199,7 @@ async function route(deps, method, url, req, abort) {
24108
24199
  const run3 = decodeURIComponent(p.slice("/streams/".length));
24109
24200
  if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
24110
24201
  const file = join53(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
24111
- if (!existsSync49(file)) return err2(404, "no such run");
24202
+ if (!existsSync50(file)) return err2(404, "no such run");
24112
24203
  return tailReply(file, url.searchParams, false, abort);
24113
24204
  }
24114
24205
  if ((p === "/pause" || p === "/resume") && post3) {
@@ -24152,7 +24243,7 @@ function send(res, r) {
24152
24243
  res.end(body);
24153
24244
  }
24154
24245
  async function socketIsStale(path) {
24155
- if (!existsSync49(path)) return false;
24246
+ if (!existsSync50(path)) return false;
24156
24247
  return new Promise((resolve8) => {
24157
24248
  const c = createConnection(path);
24158
24249
  c.once("connect", () => {
@@ -24171,7 +24262,7 @@ async function startControlServer(deps) {
24171
24262
  return null;
24172
24263
  }
24173
24264
  if (await socketIsStale(path)) rmSync23(path, { force: true });
24174
- else if (existsSync49(path)) {
24265
+ else if (existsSync50(path)) {
24175
24266
  log(`control API not started: ${path} is in use by another process`);
24176
24267
  return null;
24177
24268
  }
@@ -24264,7 +24355,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
24264
24355
 
24265
24356
  // src/periscope-endpoints.ts
24266
24357
  import { randomBytes as randomBytes3 } from "crypto";
24267
- import { chmodSync as chmodSync5, existsSync as existsSync50, mkdirSync as mkdirSync37, readdirSync as readdirSync19, readFileSync as readFileSync45, rmSync as rmSync24, writeFileSync as writeFileSync37 } from "fs";
24358
+ import { chmodSync as chmodSync5, existsSync as existsSync51, mkdirSync as mkdirSync37, readdirSync as readdirSync19, readFileSync as readFileSync45, rmSync as rmSync24, writeFileSync as writeFileSync37 } from "fs";
24268
24359
  import { join as join54 } from "path";
24269
24360
  var secretDir = (stateDir) => join54(stateDir, "periscope-mcp");
24270
24361
  var secretFile = (stateDir, id) => join54(secretDir(stateDir), `${id}.json`);
@@ -24307,7 +24398,7 @@ function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
24307
24398
  }
24308
24399
  function listPeriscopeEndpoints(stateDir) {
24309
24400
  const dir = endpointDir(stateDir);
24310
- if (!existsSync50(dir)) return [];
24401
+ if (!existsSync51(dir)) return [];
24311
24402
  const out = [];
24312
24403
  for (const name of readdirSync19(dir)) {
24313
24404
  if (!name.endsWith(".json")) continue;
@@ -25290,7 +25381,7 @@ if (command === "passenger-sync-daemon") {
25290
25381
  } else {
25291
25382
  const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "deprovision" || command === "connect" && flag("sign-out");
25292
25383
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset") || command === "deprovision";
25293
- if (configless && !existsSync51(findConfigFile(CREW_HOME))) {
25384
+ if (configless && !existsSync52(findConfigFile(CREW_HOME))) {
25294
25385
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
25295
25386
  } else {
25296
25387
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -25477,6 +25568,15 @@ if (command === "passenger-sync-daemon") {
25477
25568
  since: (/* @__PURE__ */ new Date()).toISOString()
25478
25569
  }).then(() => void 0).catch(() => void 0));
25479
25570
  } else if (status2 === "connected") clearShipAttention(state, rejectedKey);
25571
+ const controlKey = `${RELAY_CONTROL_UNSUPPORTED_KEY}:${workspaceId}`;
25572
+ if (info?.controlRefusedAt) {
25573
+ pending.push(raiseShipAttention({ state, emit, route: owningRoute, ship: cfg.ship }, {
25574
+ kind: RELAY_CONTROL_UNSUPPORTED_KEY,
25575
+ key: controlKey,
25576
+ message: relayControlUnsupportedMessage(workspaceId),
25577
+ since: (/* @__PURE__ */ new Date()).toISOString()
25578
+ }).then(() => void 0).catch(() => void 0));
25579
+ } else if (status2 === "connected") clearShipAttention(state, controlKey);
25480
25580
  }
25481
25581
  });
25482
25582
  for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
@@ -25634,7 +25734,7 @@ if (command === "passenger-sync-daemon") {
25634
25734
  if (host === "linux" && hasSystemd) {
25635
25735
  const label = labelFor(CREW_HOME, "run");
25636
25736
  const staleTimer = join56(homedir11(), ".config", "systemd", "user", `${label}.timer`);
25637
- if (existsSync51(staleTimer)) {
25737
+ if (existsSync52(staleTimer)) {
25638
25738
  process.stdout.write(
25639
25739
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
25640
25740
  `
@@ -25663,7 +25763,7 @@ if (command === "passenger-sync-daemon") {
25663
25763
  const { settled: doctorRepos, errors: refusedRepos } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
25664
25764
  for (const e of refusedRepos) problems.push(`repo ${e.repo}: ${e.detail}`);
25665
25765
  for (const r of doctorRepos) {
25666
- if (!existsSync51(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
25766
+ if (!existsSync52(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
25667
25767
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
25668
25768
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
25669
25769
  }
@@ -25690,7 +25790,7 @@ if (command === "passenger-sync-daemon") {
25690
25790
  }
25691
25791
  async function doctorFix(o) {
25692
25792
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
25693
- const unitExists = (p) => existsSync51(p);
25793
+ const unitExists = (p) => existsSync52(p);
25694
25794
  const crontab = o.host === "linux" ? spawnSync11("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
25695
25795
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
25696
25796
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -25842,9 +25942,25 @@ ${changed2.map((c) => ` - ${c}
25842
25942
  }
25843
25943
  }
25844
25944
  const precheckTickets = /* @__PURE__ */ new Map();
25945
+ const scope0 = (c, t) => releaseLockScope(c.route, t.name);
25845
25946
  async function releasePhase(c, target, opts = {}) {
25846
25947
  const scope = releaseLockScope(c.route, target.name);
25847
25948
  const remit = emit.forRoute(c.route);
25949
+ if (lacksCheckout(cfg.ship, target.dir)) {
25950
+ let mode = null;
25951
+ const settings = cacheSettingsOf(cfg.ship.sandbox);
25952
+ const mirror = mirrorRepoOfTarget(target);
25953
+ if (settings && mirror) {
25954
+ try {
25955
+ mode = (await readMirrorContract(new SandboxCache(settings), mirror, c.branch?.base ?? "main"))?.release.mode ?? null;
25956
+ } catch (e) {
25957
+ if (firstReport(`${scope0(c, target)}:contract`)) remit.warn(`${target.name}: could not read the contract from the mirror: ${e.message}`, { step: "release" });
25958
+ }
25959
+ }
25960
+ const note = noCheckoutReleaseNote(target.name, mode);
25961
+ if (firstReport(`${scope0(c, target)}:release`)) remit.emit(note, { step: "release" });
25962
+ return { scope: releaseLockScope(c.route, target.name), tests: "skipped", outcome: "skipped", detail: note };
25963
+ }
25848
25964
  const relLock = dryRun ? void 0 : state.acquire(`release-${scope}`);
25849
25965
  if (relLock && !relLock.ok) {
25850
25966
  remit.emit(`a previous release (pid ${relLock.heldBy}) is still running \u2014 skipping`, { step: "release" });
@@ -26104,10 +26220,10 @@ ${changed2.map((c) => ` - ${c}
26104
26220
  if (outcome.gateRed && !dryRun) {
26105
26221
  try {
26106
26222
  const g = outcome.gateRed;
26107
- const reported = await tracker.events("gate_red");
26223
+ const reported2 = await tracker.events("gate_red");
26108
26224
  const seat = releaseSeat(c);
26109
26225
  for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
26110
- if (alreadyReported(t, g, reported)) continue;
26226
+ if (alreadyReported(t, g, reported2)) continue;
26111
26227
  await tracker.postEvent(t.id, gateRedComment(g), seat, "gate_red");
26112
26228
  remit.warn(`test gate red for ${g.count} cycles \u2014 noted on the ticket`, { ticket: t.issue_id, step: "merge" });
26113
26229
  }
@@ -26118,10 +26234,10 @@ ${changed2.map((c) => ` - ${c}
26118
26234
  }
26119
26235
  if (outcome.localRefSkips?.length && !dryRun) {
26120
26236
  try {
26121
- const reported = await tracker.events("local_ref_skip");
26237
+ const reported2 = await tracker.events("local_ref_skip");
26122
26238
  const seat = releaseSeat(c);
26123
26239
  for (const s of outcome.localRefSkips) {
26124
- if (localRefSkipReported(s.ticket, s.problem, reported)) continue;
26240
+ if (localRefSkipReported(s.ticket, s.problem, reported2)) continue;
26125
26241
  await tracker.postEvent(s.ticket.id, localRefSkipComment(cfg.ship.name, s.problem), seat, "local_ref_skip");
26126
26242
  }
26127
26243
  } catch (e) {
@@ -27191,7 +27307,7 @@ ${cu.tail.trimEnd()}
27191
27307
  `);
27192
27308
  if (!dryRun) {
27193
27309
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
27194
- const raw = existsSync51(resolvedPath) ? JSON.parse(readFileSync46(resolvedPath, "utf8")) : {};
27310
+ const raw = existsSync52(resolvedPath) ? JSON.parse(readFileSync46(resolvedPath, "utf8")) : {};
27195
27311
  raw.agentSkills = result.agentSkills;
27196
27312
  mkdirSync38(dirOf2(resolvedPath), { recursive: true });
27197
27313
  writeFileSync38(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -27296,8 +27412,8 @@ ${cu.tail.trimEnd()}
27296
27412
  try {
27297
27413
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
27298
27414
  const dir = repoDirOf(configText, dirname34(cfg.configFile), a.route, a.name);
27299
- if (!existsSync51(dir)) throw new ReposError(`${dir} does not exist on disk`);
27300
- const files = { exists: (r) => existsSync51(resolvePath(dir, r)), list: (r) => {
27415
+ if (!existsSync52(dir)) throw new ReposError(`${dir} does not exist on disk`);
27416
+ const files = { exists: (r) => existsSync52(resolvePath(dir, r)), list: (r) => {
27301
27417
  try {
27302
27418
  return readdirSync20(resolvePath(dir, r));
27303
27419
  } catch {
@@ -27415,7 +27531,7 @@ ${cu.tail.trimEnd()}
27415
27531
  if (sub === "list" && !routeName) usage2();
27416
27532
  if (sub === "add") {
27417
27533
  try {
27418
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync51(resolvePath(p.replace(/^~(?=\/|$)/, homedir11()))));
27534
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync52(resolvePath(p.replace(/^~(?=\/|$)/, homedir11()))));
27419
27535
  routeName = a.route;
27420
27536
  addPath = a.path;
27421
27537
  addName = a.name;
@@ -27445,7 +27561,7 @@ ${cu.tail.trimEnd()}
27445
27561
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
27446
27562
  if (sub === "list" || sub === "add") {
27447
27563
  const liveRoute = cfg.routes.find((r) => r.route === routeName);
27448
- if (liveRoute?.resolved?.reposModelId && existsSync51(resolvedFile) && !dryRun) {
27564
+ if (liveRoute?.resolved?.reposModelId && existsSync52(resolvedFile) && !dryRun) {
27449
27565
  try {
27450
27566
  await hydrateApiKeys([liveRoute]);
27451
27567
  persistRepoRows(cfg.ship.stateDir, liveRoute.route, await new Tracker(liveRoute, cfg.ship).repoRowsForMap());
@@ -27454,7 +27570,7 @@ ${cu.tail.trimEnd()}
27454
27570
  }
27455
27571
  }
27456
27572
  let resolvedRemotes;
27457
- if (existsSync51(resolvedFile)) {
27573
+ if (existsSync52(resolvedFile)) {
27458
27574
  try {
27459
27575
  resolvedRemotes = JSON.parse(readFileSync46(resolvedFile, "utf8"));
27460
27576
  } catch {
@@ -27463,7 +27579,7 @@ ${cu.tail.trimEnd()}
27463
27579
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
27464
27580
  const text2 = configText;
27465
27581
  const base = dirname34(cfg.configFile);
27466
- const isCheckout = (d) => existsSync51(`${d}/.git`);
27582
+ const isCheckout = (d) => existsSync52(`${d}/.git`);
27467
27583
  try {
27468
27584
  if (sub === "list") {
27469
27585
  if (flag("json")) {
@@ -27473,7 +27589,7 @@ ${cu.tail.trimEnd()}
27473
27589
  base,
27474
27590
  resolved: resolvedRemotes,
27475
27591
  originOf,
27476
- exists: (d) => existsSync51(d),
27592
+ exists: (d) => existsSync52(d),
27477
27593
  isCheckout,
27478
27594
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
27479
27595
  try {
@@ -27495,7 +27611,7 @@ ${cu.tail.trimEnd()}
27495
27611
  for (const r of repos) {
27496
27612
  const origin2 = originOf(r.dir);
27497
27613
  const row = matchRepoName(origin2, resolvedRemotes);
27498
- const note = !existsSync51(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
27614
+ const note = !existsSync52(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
27499
27615
  process.stdout.write(`${r.name} ${r.dir} ${note}
27500
27616
  `);
27501
27617
  }
@@ -27507,9 +27623,9 @@ ${cu.tail.trimEnd()}
27507
27623
  ` : `${human}
27508
27624
  `);
27509
27625
  const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
27510
- if (existsSync51(plan2.target) && (existsSync51(`${plan2.target}/.git`) || readdirSync20(plan2.target).length > 0)) {
27626
+ if (existsSync52(plan2.target) && (existsSync52(`${plan2.target}/.git`) || readdirSync20(plan2.target).length > 0)) {
27511
27627
  throw new ReposError(
27512
- existsSync51(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
27628
+ existsSync52(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
27513
27629
  );
27514
27630
  }
27515
27631
  if (dryRun) {
@@ -27534,7 +27650,7 @@ ${cu.tail.trimEnd()}
27534
27650
  break;
27535
27651
  }
27536
27652
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir11()));
27537
- const top = existsSync51(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
27653
+ const top = existsSync52(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
27538
27654
  if (!top) throw new ReposError(`${target} is not a git checkout`);
27539
27655
  const origin = originOf(top);
27540
27656
  const plan = planRepoAdd({
@@ -27586,7 +27702,7 @@ ${cu.tail.trimEnd()}
27586
27702
  const created = await rowTracker.createRepoRow(rowPayload);
27587
27703
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
27588
27704
  `);
27589
- if (existsSync51(resolvedFile)) {
27705
+ if (existsSync52(resolvedFile)) {
27590
27706
  const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
27591
27707
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
27592
27708
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
@@ -28008,7 +28124,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28008
28124
  }
28009
28125
  }
28010
28126
  let previous;
28011
- const hadPreviousFile = existsSync51(resolvedPath);
28127
+ const hadPreviousFile = existsSync52(resolvedPath);
28012
28128
  if (hadPreviousFile) {
28013
28129
  try {
28014
28130
  previous = JSON.parse(readFileSync46(resolvedPath, "utf8"));
@@ -28153,7 +28269,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28153
28269
  let mintOutcomes = [];
28154
28270
  if (!dryRun && previous?.agentPersonas && existingRoute?.resolved) {
28155
28271
  try {
28156
- mintOutcomes = await mintMissingSeatKeys({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
28272
+ mintOutcomes = await mintMissingSeatKeys(withFoundSeats(existingRoute, found, previous.releaseAgentId), { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
28157
28273
  for (const o of mintOutcomes) process.stderr.write(`${describeMintOutcome(o)}
28158
28274
  `);
28159
28275
  for (const p of await probeSeatReads({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent })) {
@@ -28888,7 +29004,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28888
29004
  prepare: async (path) => {
28889
29005
  for (const f of repoWt.config.worktrees.copy) {
28890
29006
  const src = join56(repoWt.dir, f);
28891
- if (!existsSync51(src) || existsSync51(join56(path, f))) continue;
29007
+ if (!existsSync52(src) || existsSync52(join56(path, f))) continue;
28892
29008
  mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
28893
29009
  copyFileSync2(src, join56(path, f));
28894
29010
  }
@@ -29111,7 +29227,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29111
29227
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
29112
29228
  const resumed = await runPeriscopeResume({
29113
29229
  find: async () => hit && { row: hit.row, tag: hit.tag },
29114
- worktreeExists: (p) => existsSync51(p),
29230
+ worktreeExists: (p) => existsSync52(p),
29115
29231
  prepareDropFile,
29116
29232
  startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: hit.row.host || "localhost", inject: hit.repo.config.periscope?.inspector !== "repo" }),
29117
29233
  caps: periscopeCaps(cfg.ship),
@@ -29208,7 +29324,7 @@ ${where.join("\n")}`);
29208
29324
  prepare: async (path) => {
29209
29325
  for (const f of repoP.config.worktrees.copy) {
29210
29326
  const src = join56(repoP.dir, f);
29211
- if (!existsSync51(src)) continue;
29327
+ if (!existsSync52(src)) continue;
29212
29328
  mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
29213
29329
  copyFileSync2(src, join56(path, f));
29214
29330
  }
@@ -29700,7 +29816,7 @@ ${where.join("\n")}`);
29700
29816
  }
29701
29817
  if (row) {
29702
29818
  await tracker.deleteRepoRow(row.id);
29703
- if (existsSync51(resolvedFile)) {
29819
+ if (existsSync52(resolvedFile)) {
29704
29820
  const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
29705
29821
  if (raw.repoNames) delete raw.repoNames[row.id];
29706
29822
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
@@ -29708,13 +29824,13 @@ ${where.join("\n")}`);
29708
29824
  `);
29709
29825
  }
29710
29826
  }
29711
- if (existsSync51(join56(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
29827
+ if (existsSync52(join56(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
29712
29828
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
29713
29829
  break;
29714
29830
  }
29715
29831
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
29716
- const fresh = !existsSync51(join56(dir, ".git"));
29717
- if (existsSync51(dir) && fresh && readdirSync20(dir).length > 0) fail2(`${dir} exists and is not a git checkout \u2014 move it aside`);
29832
+ const fresh = !existsSync52(join56(dir, ".git"));
29833
+ if (existsSync52(dir) && fresh && readdirSync20(dir).length > 0) fail2(`${dir} exists and is not a git checkout \u2014 move it aside`);
29718
29834
  if (dryRun) {
29719
29835
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
29720
29836
  break;
@@ -29740,7 +29856,7 @@ ${where.join("\n")}`);
29740
29856
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
29741
29857
  });
29742
29858
  repoId = (await tracker.createRepoRow(payload)).id;
29743
- if (existsSync51(resolvedFile)) {
29859
+ if (existsSync52(resolvedFile)) {
29744
29860
  const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
29745
29861
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
29746
29862
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
@@ -30096,7 +30212,7 @@ watermark: ${state.watermark()}
30096
30212
  }
30097
30213
  for (const r of repos) {
30098
30214
  const remote = r.config.branch.remote;
30099
- if (existsSync51(r.dir)) {
30215
+ if (existsSync52(r.dir)) {
30100
30216
  pushAuth.push({
30101
30217
  route: c.route,
30102
30218
  repo: r.name,
@@ -30161,6 +30277,9 @@ watermark: ${state.watermark()}
30161
30277
  for (const t of rejectedTunnels(cfg.ship.stateDir)) {
30162
30278
  problems.push({ severity: "attention", kind: TUNNEL_REJECTED_KEY, message: tunnelRejectedMessage(t.workspaceId, t.quickExits), remedy: "rotate the slug with `crew rotate-passenger-url`" });
30163
30279
  }
30280
+ for (const workspaceId of controlRefusedTunnels(cfg.ship.stateDir)) {
30281
+ problems.push({ severity: "attention", kind: RELAY_CONTROL_UNSUPPORTED_KEY, message: relayControlUnsupportedMessage(workspaceId), remedy: "deploy crew-relay 0.2.0 or later (scripts/deploy-relay.sh in the crew-relay repository)" });
30282
+ }
30164
30283
  const previewProblem2 = await previewHostProblem(cfg);
30165
30284
  if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
30166
30285
  if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
@@ -30183,6 +30302,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
30183
30302
  for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
30184
30303
  `);
30185
30304
  for (const t of rejectedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${tunnelRejectedMessage(t.workspaceId, t.quickExits)}
30305
+ `);
30306
+ for (const workspaceId of controlRefusedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${relayControlUnsupportedMessage(workspaceId)}
30186
30307
  `);
30187
30308
  process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
30188
30309
  `);
@@ -30197,7 +30318,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
30197
30318
  }
30198
30319
  try {
30199
30320
  const evFile = eventFileFor(cfg.ship.stateDir);
30200
- const evSize = existsSync51(evFile) ? statSync16(evFile).size : 0;
30321
+ const evSize = existsSync52(evFile) ? statSync16(evFile).size : 0;
30201
30322
  process.stdout.write(
30202
30323
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
30203
30324
  ` : "events log: ok\n"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.126.6",
3
+ "version": "0.126.8",
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",