@tablation/crew 0.126.1 → 0.126.3

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.
package/dist/cli.js CHANGED
@@ -794,7 +794,7 @@ function shellInitScript(shell) {
794
794
 
795
795
  // src/config.ts
796
796
  import { readFileSync as readFileSync15, existsSync as existsSync15 } from "fs";
797
- import { resolve as resolve2, dirname as dirname7, isAbsolute as isAbsolute2, join as join17, basename as basename4 } from "path";
797
+ import { resolve as resolve2, dirname as dirname8, isAbsolute as isAbsolute2, join as join17, basename as basename4 } from "path";
798
798
  import { homedir as homedir6, tmpdir } from "os";
799
799
  import { parse as parse2 } from "yaml";
800
800
 
@@ -2661,7 +2661,7 @@ function explain(ship, required) {
2661
2661
 
2662
2662
  // src/repo-config.ts
2663
2663
  import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
2664
- import { join as join16, basename as basename3 } from "path";
2664
+ import { join as join16, basename as basename3, dirname as dirname7 } from "path";
2665
2665
  import { parse } from "yaml";
2666
2666
 
2667
2667
  // src/sandbox-policy.ts
@@ -2863,6 +2863,19 @@ var SPEC_VERSION = 1;
2863
2863
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
2864
2864
  var RepoConfigError = class extends Error {
2865
2865
  };
2866
+ function versionFileProblem(name, text2) {
2867
+ const remedy = `a version file that is not a JSON object with a string "version" needs hooks.bump instead of release.versionFiles (docs/REPO_SPEC.md)`;
2868
+ let doc;
2869
+ try {
2870
+ doc = JSON.parse(text2);
2871
+ } catch {
2872
+ return `release.versionFiles entry "${name}" is not a JSON document \u2014 ${remedy}`;
2873
+ }
2874
+ if (!doc || typeof doc !== "object" || Array.isArray(doc) || typeof doc.version !== "string") {
2875
+ return `release.versionFiles entry "${name}" has no string "version" field \u2014 ${remedy}`;
2876
+ }
2877
+ return null;
2878
+ }
2866
2879
  var HOOK_NAMES = [
2867
2880
  "test",
2868
2881
  "build",
@@ -3197,6 +3210,22 @@ function parseRepoConfig(text2, file) {
3197
3210
  if (versionFiles.length === 0) {
3198
3211
  throw new RepoConfigError(`${file}: release.versionFiles cannot be empty`);
3199
3212
  }
3213
+ if (versionFilesGiven) {
3214
+ for (const name of versionFiles) {
3215
+ const path = join16(dirname7(file), name);
3216
+ if (!existsSync14(path)) continue;
3217
+ let text3;
3218
+ try {
3219
+ text3 = readFileSync14(path, "utf8");
3220
+ } catch (e) {
3221
+ throw new RepoConfigError(
3222
+ `${file}: release.versionFiles entry "${name}" cannot be read (${e.code ?? "read error"})`
3223
+ );
3224
+ }
3225
+ const problem = versionFileProblem(name, text3);
3226
+ if (problem) throw new RepoConfigError(`${file}: ${problem}`);
3227
+ }
3228
+ }
3200
3229
  const inspectorRaw = raw.periscope?.inspector;
3201
3230
  if (inspectorRaw !== void 0 && inspectorRaw !== "proxy" && inspectorRaw !== "repo") {
3202
3231
  throw new RepoConfigError(`${file}: periscope.inspector must be "proxy" or "repo"`);
@@ -3234,6 +3263,15 @@ function loadRepoConfig(dir) {
3234
3263
  if (!file) return null;
3235
3264
  return parseRepoConfig(readFileSync14(file, "utf8"), file);
3236
3265
  }
3266
+ function loadRepoConfigTolerant(dir, refused) {
3267
+ try {
3268
+ return loadRepoConfig(dir);
3269
+ } catch (e) {
3270
+ if (!(e instanceof RepoConfigError)) throw e;
3271
+ refused?.(e);
3272
+ return null;
3273
+ }
3274
+ }
3237
3275
  function hookLabel(cfg, hook2) {
3238
3276
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
3239
3277
  }
@@ -3898,7 +3936,7 @@ function loadConfig(crewHome, configFile, opts = {}) {
3898
3936
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
3899
3937
 
3900
3938
  Create the first of those:
3901
- mkdir -p ${dirname7(defaultConfigPath())} && cp ${join17(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
3939
+ mkdir -p ${dirname8(defaultConfigPath())} && cp ${join17(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
3902
3940
  );
3903
3941
  }
3904
3942
  let raw;
@@ -3908,7 +3946,7 @@ Create the first of those:
3908
3946
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
3909
3947
  }
3910
3948
  if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
3911
- const base = dirname7(file);
3949
+ const base = dirname8(file);
3912
3950
  const missing = new Missing();
3913
3951
  rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
3914
3952
  const shipRaw = raw.ship ?? {};
@@ -4275,7 +4313,7 @@ function locateDir(cfg, dir, commonDirOf) {
4275
4313
  const direct = find(target);
4276
4314
  if (direct) return direct;
4277
4315
  const common = commonDirOf?.(target);
4278
- return common ? find(dirname7(resolve2(target, common))) : void 0;
4316
+ return common ? find(dirname8(resolve2(target, common))) : void 0;
4279
4317
  }
4280
4318
  function findRoute(cfg, route2) {
4281
4319
  if (!route2) {
@@ -4717,7 +4755,7 @@ function recoveredFailureHeadlines(o, route2) {
4717
4755
  }
4718
4756
 
4719
4757
  // src/install.ts
4720
- import { dirname as dirname8, join as join18, extname } from "path";
4758
+ import { dirname as dirname9, join as join18, extname } from "path";
4721
4759
  import { homedir as homedir7, userInfo } from "os";
4722
4760
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync16, mkdirSync as mkdirSync11, readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync5, writeFileSync as writeFileSync12, unlinkSync, realpathSync } from "fs";
4723
4761
  import { spawnSync as spawnSync5 } from "child_process";
@@ -4762,7 +4800,7 @@ function schedulerLogFor(crewLog, job) {
4762
4800
  return `${stem}.${suffix}${ext || ".log"}`;
4763
4801
  }
4764
4802
  function pathFor(ship) {
4765
- const nodeBinDir = dirname8(process.execPath);
4803
+ const nodeBinDir = dirname9(process.execPath);
4766
4804
  const parts = [nodeBinDir];
4767
4805
  if (ship.extraPath) parts.push(ship.extraPath);
4768
4806
  parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
@@ -4784,7 +4822,7 @@ function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
4784
4822
  const schedulerPath = pathFor(ship);
4785
4823
  if (findOnPath("docker", schedulerPath)) return void 0;
4786
4824
  const inShell = findOnPath("docker", shellPath);
4787
- return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname8(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
4825
+ return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname9(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
4788
4826
  }
4789
4827
  var USER_INSTALL = { root: false, user: "" };
4790
4828
  function installContext(env = process.env) {
@@ -5079,7 +5117,7 @@ async function applyInstall(plan, crewHome, dryRun, log) {
5079
5117
  }
5080
5118
  const before = existsSync16(path) ? readFileSync16(path, "utf8") : null;
5081
5119
  if (before !== plan.unitContent[path]) changed2 = true;
5082
- mkdirSync11(dirname8(path), { recursive: true });
5120
+ mkdirSync11(dirname9(path), { recursive: true });
5083
5121
  writeFileSync12(path, plan.unitContent[path]);
5084
5122
  log.emit(`wrote ${path}`);
5085
5123
  }
@@ -5308,7 +5346,7 @@ import {
5308
5346
  readdirSync as readdirSync4,
5309
5347
  rmSync as rmSync9
5310
5348
  } from "fs";
5311
- import { dirname as dirname9, basename as basename5, join as join19 } from "path";
5349
+ import { dirname as dirname10, basename as basename5, join as join19 } from "path";
5312
5350
  var GitError = class extends Error {
5313
5351
  };
5314
5352
  function streamText(x) {
@@ -5688,7 +5726,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
5688
5726
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
5689
5727
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
5690
5728
  gitOk(cwd, ["worktree", "remove", "--force", path]);
5691
- const root = dirname9(path);
5729
+ const root = dirname10(path);
5692
5730
  if (basename5(root).startsWith("crew-release-")) rmSync9(root, { recursive: true, force: true });
5693
5731
  }
5694
5732
  }
@@ -5768,7 +5806,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
5768
5806
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
5769
5807
  );
5770
5808
  }
5771
- mkdirSync12(dirname9(dir), { recursive: true });
5809
+ mkdirSync12(dirname10(dir), { recursive: true });
5772
5810
  const url = cloneUrlFor(remote, opts);
5773
5811
  try {
5774
5812
  execFileSync5("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -5793,7 +5831,7 @@ function sleepSync(ms3) {
5793
5831
  }
5794
5832
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
5795
5833
  const lockPath = checkoutLockPath(dir);
5796
- mkdirSync12(dirname9(lockPath), { recursive: true });
5834
+ mkdirSync12(dirname10(lockPath), { recursive: true });
5797
5835
  const deadline = Date.now() + timeoutMs;
5798
5836
  for (; ; ) {
5799
5837
  try {
@@ -5987,6 +6025,15 @@ function seatKeyUnreadableItem(route2, seat, storeError, nowIso = (/* @__PURE__
5987
6025
  message: `${route2}: the ${seat} seat's agent key cannot be read (${storeError}) \u2014 its runs are refused until the key store is readable. Resumes automatically once it is; \`crew doctor\` names the store in use`
5988
6026
  };
5989
6027
  }
6028
+ var repoContractRefusedKey = (repo) => `repo_contract_refused:${repo}`;
6029
+ function repoContractRefusedItem(repo, detail, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
6030
+ return {
6031
+ kind: "repo_contract_refused",
6032
+ key: repoContractRefusedKey(repo),
6033
+ since: nowIso,
6034
+ message: `${repo}: its repo contract is refused and the repository is skipped \u2014 ${detail}. Resumes automatically once the file is valid on its base; \`crew doctor\` names the problem`
6035
+ };
6036
+ }
5990
6037
  var agentLogRefusedKey = (route2) => `agent_log_refused:${route2}`;
5991
6038
  function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
5992
6039
  return {
@@ -6100,7 +6147,7 @@ function recheckBaseStops(state, route2) {
6100
6147
  for (const item of open) {
6101
6148
  const target = reposOf(route2).find((t) => baseUnsafeKey(t.name) === item.key);
6102
6149
  if (!target) continue;
6103
- const cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
6150
+ const cfg = resolveRepoConfig(loadRepoConfigTolerant(target.dir), void 0, target.dir);
6104
6151
  const outcome = refreshBaseBranch(target.dir, cfg.branch.remote, cfg.branch.base, true);
6105
6152
  if (outcome.action === "level" || outcome.action === "would-fast-forward") {
6106
6153
  clearShipAttention(state, item.key);
@@ -6305,7 +6352,7 @@ import { createHash as createHash6 } from "crypto";
6305
6352
  import { spawn as spawn5 } from "child_process";
6306
6353
  import { execFileSync as execFileSync8 } from "child_process";
6307
6354
  import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, renameSync as renameSync3, writeFileSync as writeFileSync20, rmSync as rmSync15 } from "fs";
6308
- import { dirname as dirname14, join as join32 } from "path";
6355
+ import { dirname as dirname15, join as join32 } from "path";
6309
6356
  import { fileURLToPath } from "url";
6310
6357
  import { tmpdir as tmpdir3 } from "os";
6311
6358
 
@@ -6330,7 +6377,7 @@ import { spawnSync as spawnSync6 } from "child_process";
6330
6377
  import { readFileSync as readFileSync25, existsSync as existsSync26, statSync as statSync8, mkdirSync as mkdirSync18, writeFileSync as writeFileSync19, createWriteStream, openSync as openSync4, closeSync as closeSync4, readSync } from "fs";
6331
6378
  import { StringDecoder } from "string_decoder";
6332
6379
  import { finished } from "stream/promises";
6333
- import { join as join29, dirname as dirname13, basename as basename9 } from "path";
6380
+ import { join as join29, dirname as dirname14, basename as basename9 } from "path";
6334
6381
  import { createHash as createHash4 } from "crypto";
6335
6382
 
6336
6383
  // src/state.ts
@@ -7235,7 +7282,7 @@ function renderEnvironment(i) {
7235
7282
 
7236
7283
  // src/intel-bridge.ts
7237
7284
  import { existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "fs";
7238
- import { dirname as dirname10, join as join22 } from "path";
7285
+ import { dirname as dirname11, join as join22 } from "path";
7239
7286
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7240
7287
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7241
7288
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -7255,7 +7302,7 @@ function intelDir(stateDir, route2, ticket) {
7255
7302
  }
7256
7303
  var attachFilePath = (stateDir, route2, ticket) => join22(intelDir(stateDir, route2, ticket), "url");
7257
7304
  function writeAttachFile(file, url) {
7258
- mkdirSync14(dirname10(file), { recursive: true });
7305
+ mkdirSync14(dirname11(file), { recursive: true });
7259
7306
  writeFileSync15(file, `${url}
7260
7307
  `);
7261
7308
  }
@@ -9300,7 +9347,7 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
9300
9347
 
9301
9348
  // src/run-worktree.ts
9302
9349
  import { existsSync as existsSync22 } from "fs";
9303
- import { dirname as dirname11, join as join24 } from "path";
9350
+ import { dirname as dirname12, join as join24 } from "path";
9304
9351
  var describeRunWorktreePlan = (p) => {
9305
9352
  switch (p.action) {
9306
9353
  case "cut-remote":
@@ -9357,7 +9404,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9357
9404
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
9358
9405
  return { action: "recut", path: existing.path, branch, base: baseRef };
9359
9406
  }
9360
- const path = join24(dirname11(dir), worktreeForTicket(dir, cfg, t, role));
9407
+ const path = join24(dirname12(dir), worktreeForTicket(dir, cfg, t, role));
9361
9408
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
9362
9409
  return { action: "cut-remote", path, branch: located.branch };
9363
9410
  }
@@ -10631,7 +10678,7 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
10631
10678
  import { spawn as spawn3, execFile, execFileSync as execFileSync6 } from "child_process";
10632
10679
  import { mkdtempSync, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18, rmSync as rmSync14 } from "fs";
10633
10680
  import { tmpdir as tmpdir2 } from "os";
10634
- import { basename as basename8, dirname as dirname12, join as join28 } from "path";
10681
+ import { basename as basename8, dirname as dirname13, join as join28 } from "path";
10635
10682
  import { promisify } from "util";
10636
10683
 
10637
10684
  // src/run-limit.ts
@@ -10760,7 +10807,7 @@ var SandboxRunner = class {
10760
10807
  throw new Error(`cannot generate the sandbox policy: ${e.message}`);
10761
10808
  }
10762
10809
  policyFile = policyPathFor(plan.streamPath);
10763
- mkdirSync17(dirname12(policyFile), { recursive: true });
10810
+ mkdirSync17(dirname13(policyFile), { recursive: true });
10764
10811
  writeFileSync18(policyFile, generated.yaml, { mode: 384 });
10765
10812
  }
10766
10813
  const create2 = ["sandbox", "create", "--name", name, "--detach"];
@@ -11022,7 +11069,7 @@ function codeBridgeFor(o, harness) {
11022
11069
  "--clone",
11023
11070
  o.ship.sandbox?.workDir ?? DEFAULT_SANDBOX_WORKDIR,
11024
11071
  "--state-dir",
11025
- dirname13(SANDBOX_INTEL_DIR),
11072
+ dirname14(SANDBOX_INTEL_DIR),
11026
11073
  "--ticket",
11027
11074
  number,
11028
11075
  ...seed ? ["--seed-cache", seed] : []
@@ -11036,7 +11083,7 @@ function codeBridgeFor(o, harness) {
11036
11083
  bridge.files = [{ path: bridge.settingsFile, content: text2 }];
11037
11084
  } else {
11038
11085
  const file = join29(intelDir(o.stateDir, o.route.route, number), "gemini-settings.json");
11039
- mkdirSync18(dirname13(file), { recursive: true });
11086
+ mkdirSync18(dirname14(file), { recursive: true });
11040
11087
  writeFileSync19(file, text2);
11041
11088
  bridge.settingsFile = file;
11042
11089
  }
@@ -11259,7 +11306,7 @@ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMi
11259
11306
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
11260
11307
  function openSink(path) {
11261
11308
  try {
11262
- mkdirSync18(dirname13(path), { recursive: true });
11309
+ mkdirSync18(dirname14(path), { recursive: true });
11263
11310
  return createWriteStream(path, { flags: "a" });
11264
11311
  } catch {
11265
11312
  return void 0;
@@ -11611,7 +11658,7 @@ function spawnAgent(plan, emit, hintMoved) {
11611
11658
  const id = identityOf(plan, adapter.harness);
11612
11659
  let streamFd;
11613
11660
  try {
11614
- mkdirSync18(dirname13(plan.streamPath), { recursive: true });
11661
+ mkdirSync18(dirname14(plan.streamPath), { recursive: true });
11615
11662
  streamFd = openSync4(plan.streamPath, "a");
11616
11663
  } catch (e) {
11617
11664
  void endSink(eventsSink);
@@ -12776,7 +12823,7 @@ function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec2, syn
12776
12823
  }
12777
12824
  return { started, recreated, stopped };
12778
12825
  }
12779
- function passengerCheckoutSyncSpecs(cfg, workspaceId) {
12826
+ function passengerCheckoutSyncSpecs(cfg, workspaceId, onRefused) {
12780
12827
  const specs = [];
12781
12828
  for (const route2 of cfg.routes) {
12782
12829
  if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
@@ -12788,8 +12835,14 @@ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
12788
12835
  for (const target of passengerCheckoutTargets(route2, mirrorArea)) {
12789
12836
  const ordinary = ordinaryByName.get(target.name);
12790
12837
  const override = route2.repoOverrides[target.name];
12838
+ let refused = false;
12839
+ const declared = ordinary ? loadRepoConfigTolerant(ordinary.dir, (e) => {
12840
+ refused = true;
12841
+ onRefused?.(target.name, e);
12842
+ }) : null;
12843
+ if (refused) continue;
12791
12844
  const effective = resolveRepoConfig(
12792
- ordinary ? loadRepoConfig(ordinary.dir) : null,
12845
+ declared,
12793
12846
  { branch: { ...route2.branch, ...override?.branch } },
12794
12847
  ordinary?.dir ?? target.dir
12795
12848
  );
@@ -12809,8 +12862,8 @@ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
12809
12862
  }
12810
12863
  return specs;
12811
12864
  }
12812
- function syncAllPassengerCheckouts(cfg) {
12813
- return passengerCheckoutSyncSpecs(cfg).filter((spec) => !spec.mirror).map((spec) => ({
12865
+ function syncAllPassengerCheckouts(cfg, onRefused) {
12866
+ return passengerCheckoutSyncSpecs(cfg, void 0, onRefused).filter((spec) => !spec.mirror).map((spec) => ({
12814
12867
  name: spec.name,
12815
12868
  workspaceId: spec.workspaceId,
12816
12869
  outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
@@ -12830,7 +12883,7 @@ var SANDBOX_UID = 1e3;
12830
12883
  var CREW_PREFIX = "/opt/crew";
12831
12884
  var SANDBOX_BRIDGE_PATH = `${CREW_PREFIX}/bin/intel-bridge`;
12832
12885
  function readBridgeBundle() {
12833
- const here = dirname14(fileURLToPath(import.meta.url));
12886
+ const here = dirname15(fileURLToPath(import.meta.url));
12834
12887
  for (const p of [join32(here, "sandbox-intel-bridge.cjs"), join32(here, "..", "dist", "sandbox-intel-bridge.cjs")]) {
12835
12888
  if (existsSync30(p)) return readFileSync27(p, "utf8");
12836
12889
  }
@@ -13055,7 +13108,7 @@ async function buildSandboxImage(plan, deps) {
13055
13108
  try {
13056
13109
  writeFileSync20(join32(ctx, "Dockerfile"), layer.dockerfile);
13057
13110
  for (const [name, content] of Object.entries(layer.files)) {
13058
- mkdirSync19(dirname14(join32(ctx, name)), { recursive: true });
13111
+ mkdirSync19(dirname15(join32(ctx, name)), { recursive: true });
13059
13112
  writeFileSync20(join32(ctx, name), content);
13060
13113
  }
13061
13114
  const code = await deps.run(["build", "--tag", layer.tag, "--file", join32(ctx, "Dockerfile"), ctx], logPath);
@@ -13468,7 +13521,7 @@ function doctorSelftestLines(stored, toolchains) {
13468
13521
  }
13469
13522
 
13470
13523
  // src/cli.ts
13471
- import { resolve as resolve7, dirname as dirname33, join as join55, basename as basename16 } from "path";
13524
+ import { resolve as resolve7, dirname as dirname34, join as join56, basename as basename16 } from "path";
13472
13525
  import { fileURLToPath as fileURLToPath2 } from "url";
13473
13526
  import { hostname as hostname4, homedir as homedir11 } from "os";
13474
13527
  import { spawnSync as spawnSync11 } from "child_process";
@@ -13667,7 +13720,7 @@ function rosterMarkdown(roster, meId) {
13667
13720
 
13668
13721
  // src/connect-resume.ts
13669
13722
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync29, rmSync as rmSync17, writeFileSync as writeFileSync22 } from "fs";
13670
- import { dirname as dirname15, join as join34 } from "path";
13723
+ import { dirname as dirname16, join as join34 } from "path";
13671
13724
  function lastSessionFile(stateDir) {
13672
13725
  return join34(stateDir, "connect-session.json");
13673
13726
  }
@@ -13687,7 +13740,7 @@ function readPointers(stateDir) {
13687
13740
  }
13688
13741
  function writePointers(stateDir, pointers) {
13689
13742
  const file = lastSessionFile(stateDir);
13690
- mkdirSync21(dirname15(file), { recursive: true });
13743
+ mkdirSync21(dirname16(file), { recursive: true });
13691
13744
  writeFileSync22(file, `${JSON.stringify(pointers, null, 2)}
13692
13745
  `, { mode: 384 });
13693
13746
  }
@@ -13774,7 +13827,7 @@ async function signOutSessions(o) {
13774
13827
 
13775
13828
  // src/reset.ts
13776
13829
  import { existsSync as existsSync32, mkdirSync as mkdirSync22, readFileSync as readFileSync30, readdirSync as readdirSync12, rmSync as rmSync18, statSync as statSync10, writeFileSync as writeFileSync23 } from "fs";
13777
- import { dirname as dirname16, join as join35 } from "path";
13830
+ import { dirname as dirname17, join as join35 } from "path";
13778
13831
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
13779
13832
  function clonedCheckoutsFile(stateDir) {
13780
13833
  return join35(stateDir, "cloned-checkouts.json");
@@ -13791,7 +13844,7 @@ function recordClonedCheckout(stateDir, dir) {
13791
13844
  const known = readClonedCheckouts(stateDir);
13792
13845
  if (known.includes(dir)) return;
13793
13846
  const file = clonedCheckoutsFile(stateDir);
13794
- mkdirSync22(dirname16(file), { recursive: true });
13847
+ mkdirSync22(dirname17(file), { recursive: true });
13795
13848
  writeFileSync23(file, `${JSON.stringify([...known, dir], null, 2)}
13796
13849
  `);
13797
13850
  }
@@ -13974,7 +14027,7 @@ async function runDeprovision(o, fx, deps) {
13974
14027
  // src/pool-member.ts
13975
14028
  import { mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync4, writeFileSync as writeFileSync24 } from "fs";
13976
14029
  import { homedir as homedir8 } from "os";
13977
- import { dirname as dirname17, join as join36 } from "path";
14030
+ import { dirname as dirname18, join as join36 } from "path";
13978
14031
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
13979
14032
  function backoffMs(attempt) {
13980
14033
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -14218,7 +14271,7 @@ function fileMemberStore(dir) {
14218
14271
  }
14219
14272
  },
14220
14273
  write(f) {
14221
- mkdirSync23(dirname17(file), { recursive: true, mode: 448 });
14274
+ mkdirSync23(dirname18(file), { recursive: true, mode: 448 });
14222
14275
  const tmp = `${file}.tmp`;
14223
14276
  writeFileSync24(tmp, `${JSON.stringify(f)}
14224
14277
  `, { mode: 384 });
@@ -14279,7 +14332,7 @@ import { hostname as hostname3 } from "os";
14279
14332
  import { execFileSync as execFileSync10 } from "child_process";
14280
14333
  import { createHash as createHash7 } from "crypto";
14281
14334
  import { existsSync as existsSync33, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync25 } from "fs";
14282
- import { dirname as dirname18, join as join37 } from "path";
14335
+ import { dirname as dirname19, join as join37 } from "path";
14283
14336
  function machineIdPathFor(stateDir) {
14284
14337
  return join37(stateDir, "machine-id");
14285
14338
  }
@@ -14313,7 +14366,7 @@ function ensureMachineId(stateDir, opts = {}) {
14313
14366
  const raw = (opts.read ?? readRawMachineId)();
14314
14367
  if (!raw) return void 0;
14315
14368
  const hashed = sha256(raw);
14316
- mkdirSync24(dirname18(path), { recursive: true });
14369
+ mkdirSync24(dirname19(path), { recursive: true });
14317
14370
  writeFileSync25(path, `${hashed}
14318
14371
  `, { mode: 384 });
14319
14372
  return hashed;
@@ -16425,7 +16478,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
16425
16478
  if (hit) return hit;
16426
16479
  const target = reposOf(o.route).find((r) => r.dir === dir);
16427
16480
  const nameOverride = target ? o.route.repoOverrides[target.name]?.worktreeName : void 0;
16428
- const cfg = resolveRepoConfig(loadRepoConfig(dir), nameOverride ? { worktrees: { nameOverride } } : void 0, dir);
16481
+ const cfg = resolveRepoConfig(loadRepoConfigTolerant(dir), nameOverride ? { worktrees: { nameOverride } } : void 0, dir);
16429
16482
  repoCache.set(dir, cfg);
16430
16483
  return cfg;
16431
16484
  };
@@ -16594,7 +16647,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
16594
16647
 
16595
16648
  // src/repo-refresh.ts
16596
16649
  import { existsSync as existsSync37, mkdirSync as mkdirSync26, readFileSync as readFileSync35, writeFileSync as writeFileSync29 } from "fs";
16597
- import { dirname as dirname20 } from "path";
16650
+ import { dirname as dirname21 } from "path";
16598
16651
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
16599
16652
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
16600
16653
  var lastAsked = /* @__PURE__ */ new Map();
@@ -16626,7 +16679,7 @@ function persistRepoRows(stateDir, routeName, rows) {
16626
16679
  if (!existsSync37(path)) return;
16627
16680
  const raw = JSON.parse(readFileSync35(path, "utf8"));
16628
16681
  applyRepoRows(raw, rows);
16629
- mkdirSync26(dirname20(path), { recursive: true });
16682
+ mkdirSync26(dirname21(path), { recursive: true });
16630
16683
  writeFileSync29(path, `${JSON.stringify(raw, null, 2)}
16631
16684
  `);
16632
16685
  }
@@ -17077,7 +17130,7 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
17077
17130
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
17078
17131
 
17079
17132
  // src/connect-wizard.ts
17080
- import { delimiter, dirname as dirname21, join as join41 } from "path";
17133
+ import { delimiter, dirname as dirname22, join as join41 } from "path";
17081
17134
  import { accessSync as accessSync3, constants as constants3, existsSync as existsSync38, mkdirSync as mkdirSync27, writeFileSync as writeFileSync30 } from "fs";
17082
17135
  import { stringify as stringify3 } from "yaml";
17083
17136
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
@@ -17126,9 +17179,9 @@ async function runWizard(p, env) {
17126
17179
  if (entered && isExec(entered)) agentBin = entered;
17127
17180
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
17128
17181
  }
17129
- const nodeDir = dirname21(env.nodePath);
17182
+ const nodeDir = dirname22(env.nodePath);
17130
17183
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
17131
- const baseDirs = [nodeDir, pnpmPath ? dirname21(pnpmPath) : void 0];
17184
+ const baseDirs = [nodeDir, pnpmPath ? dirname22(pnpmPath) : void 0];
17132
17185
  p.say(
17133
17186
  `
17134
17187
  Host Passengers lets this machine run the agent sessions that other people's
@@ -17139,7 +17192,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
17139
17192
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
17140
17193
  }
17141
17194
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
17142
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname21(dockerPath) : void 0]);
17195
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname22(dockerPath) : void 0]);
17143
17196
  if (extraPath) {
17144
17197
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
17145
17198
  }
@@ -17161,7 +17214,7 @@ routes:
17161
17214
  ${routeBlock}`;
17162
17215
  }
17163
17216
  function writeNewConfig(path, text2) {
17164
- mkdirSync27(dirname21(path), { recursive: true });
17217
+ mkdirSync27(dirname22(path), { recursive: true });
17165
17218
  try {
17166
17219
  writeFileSync30(path, text2, { mode: 384, flag: "wx" });
17167
17220
  return true;
@@ -17197,7 +17250,7 @@ Wrote ${o.configPath}.
17197
17250
  // src/connect-json.ts
17198
17251
  import { accessSync as accessSync4, constants as constants4 } from "fs";
17199
17252
  import { homedir as homedir10 } from "os";
17200
- import { dirname as dirname22, join as join42 } from "path";
17253
+ import { dirname as dirname23, join as join42 } from "path";
17201
17254
  import { delimiter as delimiter2 } from "path";
17202
17255
  var CHOICE_EXIT_CODE = 3;
17203
17256
  function createEmitter(json2, write) {
@@ -17263,7 +17316,7 @@ function flagWizardAnswers(f, env) {
17263
17316
  const agentBin = f.agentBin ?? found?.bin;
17264
17317
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
17265
17318
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
17266
- const extraPath = f.extraPath ?? deriveExtraPath([dirname22(env.nodePath), pnpmPath ? dirname22(pnpmPath) : void 0, dockerPath ? dirname22(dockerPath) : void 0]);
17319
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname23(env.nodePath), pnpmPath ? dirname23(pnpmPath) : void 0, dockerPath ? dirname23(dockerPath) : void 0]);
17267
17320
  return {
17268
17321
  answers: {
17269
17322
  name: f.shipName?.trim() || env.hostname,
@@ -17278,7 +17331,7 @@ function flagWizardAnswers(f, env) {
17278
17331
 
17279
17332
  // src/release-run.ts
17280
17333
  import { readFileSync as readFileSync36, writeFileSync as writeFileSync31, existsSync as existsSync39, copyFileSync, mkdirSync as mkdirSync28, mkdtempSync as mkdtempSync3, rmSync as rmSync19 } from "fs";
17281
- import { dirname as dirname23, join as join43 } from "path";
17334
+ import { dirname as dirname24, join as join43 } from "path";
17282
17335
  import { tmpdir as tmpdir5 } from "os";
17283
17336
 
17284
17337
  // src/release.ts
@@ -18025,7 +18078,13 @@ async function bump(o, size) {
18025
18078
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
18026
18079
  return null;
18027
18080
  }
18028
- const current = JSON.parse(readFileSync36(first, "utf8")).version;
18081
+ const firstText = readFileSync36(first, "utf8");
18082
+ const problem = versionFileProblem(o.repo.release.versionFiles[0], firstText);
18083
+ if (problem) {
18084
+ o.emit.error(problem);
18085
+ return null;
18086
+ }
18087
+ const current = JSON.parse(firstText).version;
18029
18088
  if (!current) {
18030
18089
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
18031
18090
  return null;
@@ -18164,7 +18223,7 @@ function openReleaseCheckout(o) {
18164
18223
  for (const f of o.repo.worktrees.copy) {
18165
18224
  const src = join43(o.cwd, f);
18166
18225
  if (!existsSync39(src)) continue;
18167
- mkdirSync28(dirname23(join43(dir, f)), { recursive: true });
18226
+ mkdirSync28(dirname24(join43(dir, f)), { recursive: true });
18168
18227
  copyFileSync(src, join43(dir, f));
18169
18228
  }
18170
18229
  return { dir, branch, close };
@@ -18503,8 +18562,11 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
18503
18562
  o.emit.emit(`${changed2.split("\n")[0]} changed in this release \u2014 continuing under the merged contract`);
18504
18563
  }
18505
18564
  } catch (e) {
18565
+ if (o.contractRefusal) throw new Error(`${o.contractRefusal}; the merged contract is also refused: ${e.message}`);
18506
18566
  o.emit.warn(`could not re-read the merged repo config: ${e.message}; releasing under the earlier one`);
18507
18567
  }
18568
+ } else if (o.contractRefusal) {
18569
+ throw new Error(`${o.contractRefusal}; nothing merged replaces it`);
18508
18570
  }
18509
18571
  }
18510
18572
  if (o.mergeOnly) {
@@ -19504,7 +19566,7 @@ function nextRoles(decision, ran) {
19504
19566
 
19505
19567
  // src/ship-name.ts
19506
19568
  import { existsSync as existsSync41, mkdirSync as mkdirSync29, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync32 } from "fs";
19507
- import { dirname as dirname24 } from "path";
19569
+ import { dirname as dirname25 } from "path";
19508
19570
  import { parseDocument as parseDocument5 } from "yaml";
19509
19571
  var ShipNameError = class extends Error {
19510
19572
  };
@@ -19583,7 +19645,7 @@ function writeSyncedName(resolvedFile, name) {
19583
19645
  }
19584
19646
  if (raw.shipName === name) return;
19585
19647
  raw.shipName = name;
19586
- mkdirSync29(dirname24(resolvedFile), { recursive: true });
19648
+ mkdirSync29(dirname25(resolvedFile), { recursive: true });
19587
19649
  const tmp = `${resolvedFile}.tmp${process.pid}`;
19588
19650
  writeFileSync32(tmp, `${JSON.stringify(raw, null, 2)}
19589
19651
  `);
@@ -20277,7 +20339,7 @@ function describeCapability(i) {
20277
20339
 
20278
20340
  // src/agents-sync-run.ts
20279
20341
  import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, writeFileSync as writeFileSync33 } from "fs";
20280
- import { dirname as dirname25 } from "path";
20342
+ import { dirname as dirname26 } from "path";
20281
20343
  async function runAgentsSync(target, o) {
20282
20344
  const dryRun = o.dryRun ?? false;
20283
20345
  const force = o.force ?? false;
@@ -20314,7 +20376,7 @@ async function runAgentsSync(target, o) {
20314
20376
  raw.agentPersonas = personaResult.agentPersonas;
20315
20377
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
20316
20378
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
20317
- mkdirSync30(dirname25(resolvedPath), { recursive: true });
20379
+ mkdirSync30(dirname26(resolvedPath), { recursive: true });
20318
20380
  writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
20319
20381
  `);
20320
20382
  if (target.resolved) {
@@ -20937,7 +20999,7 @@ ${run3.err}` });
20937
20999
  }
20938
21000
 
20939
21001
  // src/sample-ticket.ts
20940
- import { basename as basename12, dirname as dirname26, join as join44 } from "path";
21002
+ import { basename as basename12, dirname as dirname27, join as join44 } from "path";
20941
21003
  var SAMPLE_REPO = "crew-sample";
20942
21004
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
20943
21005
  var SAMPLE_MARKER = "sample: true";
@@ -20960,7 +21022,7 @@ has something safe to work on: its test and build hooks always pass, it has no
20960
21022
  remote, and \`crew sample-ticket --remove\` deletes it.
20961
21023
  `;
20962
21024
  function sampleDir(existing, reposBase, workspace) {
20963
- if (existing.length > 0) return join44(dirname26(existing[0]), SAMPLE_REPO);
21025
+ if (existing.length > 0) return join44(dirname27(existing[0]), SAMPLE_REPO);
20964
21026
  return join44(reposBase, workspace, SAMPLE_REPO);
20965
21027
  }
20966
21028
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -21006,7 +21068,7 @@ function planSampleTicket(i) {
21006
21068
 
21007
21069
  // src/worktree-sweep.ts
21008
21070
  import { existsSync as existsSync43, readFileSync as readFileSync40, readdirSync as readdirSync14 } from "fs";
21009
- import { basename as basename13, dirname as dirname27, join as join45, resolve as resolve5 } from "path";
21071
+ import { basename as basename13, dirname as dirname28, join as join45, resolve as resolve5 } from "path";
21010
21072
  function planWorktreeSweep(target, tickets, contract) {
21011
21073
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
21012
21074
  const verified = contract.statuses.verified;
@@ -21070,7 +21132,7 @@ function worktreeTicketNumbers(target) {
21070
21132
  return [...nums];
21071
21133
  }
21072
21134
  function siblingWorktreeDirs(target) {
21073
- const parent = dirname27(target.dir.replace(/[/\\]+$/, ""));
21135
+ const parent = dirname28(target.dir.replace(/[/\\]+$/, ""));
21074
21136
  const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
21075
21137
  const registered = new Set(worktrees(target.dir).map((w) => resolve5(w.path)));
21076
21138
  let names;
@@ -21922,7 +21984,7 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
21922
21984
  // src/ssh-keys.ts
21923
21985
  import { execFileSync as execFileSync13 } from "child_process";
21924
21986
  import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync42, chmodSync as chmodSync3 } from "fs";
21925
- import { dirname as dirname28, join as join49 } from "path";
21987
+ import { dirname as dirname29, join as join49 } from "path";
21926
21988
  var SshKeyError = class extends Error {
21927
21989
  };
21928
21990
  function shipSshKeyPathFor(stateDir) {
@@ -21932,7 +21994,7 @@ function ensureShipSshKeypair(stateDir) {
21932
21994
  const privateKeyPath = shipSshKeyPathFor(stateDir);
21933
21995
  const publicKeyPath = `${privateKeyPath}.pub`;
21934
21996
  if (!existsSync46(privateKeyPath)) {
21935
- mkdirSync32(dirname28(privateKeyPath), { recursive: true });
21997
+ mkdirSync32(dirname29(privateKeyPath), { recursive: true });
21936
21998
  try {
21937
21999
  execFileSync13("ssh-keygen", [
21938
22000
  "-t",
@@ -22124,7 +22186,7 @@ import { join as join50 } from "path";
22124
22186
 
22125
22187
  // src/passenger-mirror.ts
22126
22188
  import { mkdirSync as mkdirSync33 } from "fs";
22127
- import { dirname as dirname29 } from "path";
22189
+ import { dirname as dirname30 } from "path";
22128
22190
  function mirrorRepoOf(spec) {
22129
22191
  const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
22130
22192
  if (fromOrigin) return fromOrigin;
@@ -22142,7 +22204,7 @@ async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCa
22142
22204
  continue;
22143
22205
  }
22144
22206
  try {
22145
- const parent = dirname29(spec.dir);
22207
+ const parent = dirname30(spec.dir);
22146
22208
  mkdirSync33(parent, { recursive: true });
22147
22209
  mkdirSync33(spec.dir, { recursive: true });
22148
22210
  const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
@@ -22348,7 +22410,7 @@ import { createServer, request as httpRequest } from "http";
22348
22410
  import { request as httpsRequest } from "https";
22349
22411
  import { connect as netConnect } from "net";
22350
22412
  import { connect as tlsConnect } from "tls";
22351
- import { dirname as dirname30, isAbsolute as isAbsolute4 } from "path";
22413
+ import { dirname as dirname31, isAbsolute as isAbsolute4 } from "path";
22352
22414
  import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
22353
22415
 
22354
22416
  // src/periscope-inspector.ts
@@ -22462,7 +22524,7 @@ function parsePointedBody(raw) {
22462
22524
  return entry;
22463
22525
  }
22464
22526
  function appendPointed(file, entry, now = /* @__PURE__ */ new Date()) {
22465
- mkdirSync35(dirname30(file), { recursive: true });
22527
+ mkdirSync35(dirname31(file), { recursive: true });
22466
22528
  appendFileSync3(file, `${JSON.stringify({ ...entry, at: now.toISOString() })}
22467
22529
  `);
22468
22530
  }
@@ -22781,9 +22843,32 @@ function describeCause(err3) {
22781
22843
  return code && !first.includes(code) ? `${first} (${code})` : first;
22782
22844
  }
22783
22845
 
22846
+ // src/contract-refusal.ts
22847
+ import { join as join51 } from "path";
22848
+ function contractFixers(dir, baseRef, tickets, locate) {
22849
+ const out = [];
22850
+ for (const t of tickets) {
22851
+ const branch = locate(t);
22852
+ if (!branch) continue;
22853
+ const changed2 = gitOk(dir, ["diff", "--name-only", `${baseRef}...${branch}`, "--", ...REPO_CONFIG_FILENAMES]);
22854
+ const file = changed2?.split("\n").find((f) => f.trim() !== "");
22855
+ if (!file) continue;
22856
+ const text2 = gitOk(dir, ["show", `${branch}:${file}`]);
22857
+ if (text2 === null) continue;
22858
+ try {
22859
+ parseRepoConfig(text2, join51(dir, file));
22860
+ } catch (e) {
22861
+ if (e instanceof RepoConfigError) continue;
22862
+ throw e;
22863
+ }
22864
+ out.push({ issueId: t.issue_id, branch, file });
22865
+ }
22866
+ return out;
22867
+ }
22868
+
22784
22869
  // src/daemon.ts
22785
22870
  import { existsSync as existsSync48, mkdirSync as mkdirSync36, readFileSync as readFileSync44, readdirSync as readdirSync17, rmSync as rmSync22, statSync as statSync14, writeFileSync as writeFileSync36 } from "fs";
22786
- import { join as join51 } from "path";
22871
+ import { join as join52 } from "path";
22787
22872
  import { randomBytes as randomBytes2 } from "crypto";
22788
22873
  var DEFAULT_FLOOR_MS = 5e3;
22789
22874
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -22871,7 +22956,7 @@ function latestMtime(path) {
22871
22956
  if (!stat2.isDirectory()) return 0;
22872
22957
  let latest = stat2.mtimeMs;
22873
22958
  for (const entry of readdirSync17(path, { withFileTypes: true })) {
22874
- latest = Math.max(latest, latestMtime(join51(path, entry.name)));
22959
+ latest = Math.max(latest, latestMtime(join52(path, entry.name)));
22875
22960
  }
22876
22961
  return latest;
22877
22962
  }
@@ -22883,7 +22968,7 @@ function makeStaleChecker(paths, opts = {}) {
22883
22968
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync48(opts.reloadSentinel);
22884
22969
  }
22885
22970
  function reloadSentinelFor(stateDir) {
22886
- return join51(stateDir, "reload-requested");
22971
+ return join52(stateDir, "reload-requested");
22887
22972
  }
22888
22973
  function readReloadRequestedAt(stateDir) {
22889
22974
  try {
@@ -22893,7 +22978,7 @@ function readReloadRequestedAt(stateDir) {
22893
22978
  }
22894
22979
  }
22895
22980
  function daemonStampFor(stateDir) {
22896
- return join51(stateDir, "daemon.started");
22981
+ return join52(stateDir, "daemon.started");
22897
22982
  }
22898
22983
  function noteDaemonStarted(stateDir, now = Date.now()) {
22899
22984
  mkdirSync36(stateDir, { recursive: true });
@@ -23451,7 +23536,7 @@ async function buildEnvironment(route2, ship, ticket) {
23451
23536
  name: t.name,
23452
23537
  dir: t.dir,
23453
23538
  id,
23454
- config: resolveRepoConfig(loadRepoConfig(t.dir), {
23539
+ config: resolveRepoConfig(loadRepoConfigTolerant(t.dir), {
23455
23540
  hooks: { ...route2.hooks, ...o?.hooks },
23456
23541
  labels: { ...route2.labels, ...o?.labels },
23457
23542
  branch: { ...route2.branch, ...o?.branch },
@@ -23480,7 +23565,7 @@ import {
23480
23565
  } from "fs";
23481
23566
  import { createConnection } from "net";
23482
23567
  import { createServer as createServer2 } from "http";
23483
- import { basename as basename14, dirname as dirname31, join as join52 } from "path";
23568
+ import { basename as basename14, dirname as dirname32, join as join53 } from "path";
23484
23569
  import { execFile as execFile3, spawn as spawn12 } from "child_process";
23485
23570
 
23486
23571
  // src/periscope-control.ts
@@ -23709,7 +23794,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
23709
23794
  // src/control-api.ts
23710
23795
  var PAUSABLE = ["dev", "design", "qa"];
23711
23796
  var CONTROL_SOCKET_NAME = "control.sock";
23712
- var controlSocketFor = (stateDir) => join52(stateDir, CONTROL_SOCKET_NAME);
23797
+ var controlSocketFor = (stateDir) => join53(stateDir, CONTROL_SOCKET_NAME);
23713
23798
  var MAX_SOCKET_PATH = 100;
23714
23799
  var MAX_TAIL_BYTES = 1024 * 1024;
23715
23800
  var MAX_WAIT_SECONDS = 30;
@@ -23796,7 +23881,7 @@ function candidates(live, rotated) {
23796
23881
  const out = [live];
23797
23882
  if (!rotated) return out;
23798
23883
  try {
23799
- for (const n of readdirSync18(dirname31(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join52(dirname31(live), n));
23884
+ for (const n of readdirSync18(dirname32(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join53(dirname32(live), n));
23800
23885
  } catch {
23801
23886
  }
23802
23887
  return out;
@@ -23956,7 +24041,7 @@ async function tailReply(path, q2, rotated, abort) {
23956
24041
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
23957
24042
  let r = readTail(path, { after, tail: tail3, rotated });
23958
24043
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
23959
- await waitForChange(dirname31(path), deadline - Date.now(), abort);
24044
+ await waitForChange(dirname32(path), deadline - Date.now(), abort);
23960
24045
  r = readTail(path, { after: r.cursor, rotated });
23961
24046
  }
23962
24047
  return reply2(200, r);
@@ -23996,7 +24081,7 @@ async function route(deps, method, url, req, abort) {
23996
24081
  if (p.startsWith("/streams/") && get2) {
23997
24082
  const run3 = decodeURIComponent(p.slice("/streams/".length));
23998
24083
  if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
23999
- const file = join52(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
24084
+ const file = join53(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
24000
24085
  if (!existsSync49(file)) return err2(404, "no such run");
24001
24086
  return tailReply(file, url.searchParams, false, abort);
24002
24087
  }
@@ -24146,7 +24231,7 @@ function crewJson(args, timeoutMs = 6e4) {
24146
24231
 
24147
24232
  // src/periscope-mcp.ts
24148
24233
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
24149
- import { basename as basename15, dirname as dirname32, isAbsolute as isAbsolute5, join as join54, relative as relative2, resolve as resolve6, sep } from "path";
24234
+ import { basename as basename15, dirname as dirname33, isAbsolute as isAbsolute5, join as join55, relative as relative2, resolve as resolve6, sep } from "path";
24150
24235
  import { z } from "zod";
24151
24236
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24152
24237
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -24154,11 +24239,11 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
24154
24239
  // src/periscope-endpoints.ts
24155
24240
  import { randomBytes as randomBytes3 } from "crypto";
24156
24241
  import { chmodSync as chmodSync5, existsSync as existsSync50, mkdirSync as mkdirSync37, readdirSync as readdirSync19, readFileSync as readFileSync45, rmSync as rmSync24, writeFileSync as writeFileSync37 } from "fs";
24157
- import { join as join53 } from "path";
24158
- var secretDir = (stateDir) => join53(stateDir, "periscope-mcp");
24159
- var secretFile = (stateDir, id) => join53(secretDir(stateDir), `${id}.json`);
24160
- var endpointDir = (stateDir) => join53(stateDir, "periscope-endpoints");
24161
- var endpointFile = (stateDir, id) => join53(endpointDir(stateDir), `${id}.json`);
24242
+ import { join as join54 } from "path";
24243
+ var secretDir = (stateDir) => join54(stateDir, "periscope-mcp");
24244
+ var secretFile = (stateDir, id) => join54(secretDir(stateDir), `${id}.json`);
24245
+ var endpointDir = (stateDir) => join54(stateDir, "periscope-endpoints");
24246
+ var endpointFile = (stateDir, id) => join54(endpointDir(stateDir), `${id}.json`);
24162
24247
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
24163
24248
  mkdirSync37(secretDir(stateDir), { recursive: true, mode: 448 });
24164
24249
  const secret = randomBytes3(32).toString("base64url");
@@ -24284,10 +24369,10 @@ async function realAncestor(p) {
24284
24369
  }
24285
24370
  if (isLink) {
24286
24371
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
24287
- cur = resolve6(dirname32(cur), await readlink(cur));
24372
+ cur = resolve6(dirname33(cur), await readlink(cur));
24288
24373
  continue;
24289
24374
  }
24290
- const parent = dirname32(cur);
24375
+ const parent = dirname33(cur);
24291
24376
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
24292
24377
  tail3.push(basename15(cur));
24293
24378
  cur = parent;
@@ -24299,13 +24384,13 @@ async function resolveInWorktree(root, requested) {
24299
24384
  let abs;
24300
24385
  if (isAbsolute5(requested)) {
24301
24386
  abs = resolve6(requested);
24302
- if (isInside(root, abs)) abs = join54(realRoot, relative2(root, abs));
24387
+ if (isInside(root, abs)) abs = join55(realRoot, relative2(root, abs));
24303
24388
  } else {
24304
24389
  abs = resolve6(realRoot, requested);
24305
24390
  }
24306
24391
  if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
24307
24392
  const { real, rest } = await realAncestor(abs);
24308
- const final = rest ? join54(real, rest) : real;
24393
+ const final = rest ? join55(real, rest) : real;
24309
24394
  if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
24310
24395
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
24311
24396
  return final;
@@ -24373,7 +24458,7 @@ function createPeriscopeMcpServer(d, sc) {
24373
24458
  const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
24374
24459
  const lines2 = await Promise.all(entries.map(async (e) => {
24375
24460
  if (e.isDirectory()) return `[DIR] ${e.name}`;
24376
- const size = (await lstat(join54(abs, e.name))).size;
24461
+ const size = (await lstat(join55(abs, e.name))).size;
24377
24462
  return `[FILE] ${e.name} (${size} bytes)`;
24378
24463
  }));
24379
24464
  return text(lines2.join("\n") || "(empty)");
@@ -24398,7 +24483,7 @@ function createPeriscopeMcpServer(d, sc) {
24398
24483
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
24399
24484
  const abs = await inTree(path);
24400
24485
  if (abs === await realpath(root)) return text("path must name a file", true);
24401
- await mkdir(dirname32(abs), { recursive: true });
24486
+ await mkdir(dirname33(abs), { recursive: true });
24402
24487
  await inTree(path);
24403
24488
  await writeFile(abs, content);
24404
24489
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -24438,8 +24523,8 @@ function createPeriscopeMcpServer(d, sc) {
24438
24523
  inputSchema: { path: z.string() },
24439
24524
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
24440
24525
  }, async ({ path }) => {
24441
- const parent = await inTree(dirname32(path) === "" ? "." : dirname32(path));
24442
- const abs = join54(parent, basename15(path));
24526
+ const parent = await inTree(dirname33(path) === "" ? "." : dirname33(path));
24527
+ const abs = join55(parent, basename15(path));
24443
24528
  if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
24444
24529
  const st = await lstat(abs).catch(() => void 0);
24445
24530
  if (!st) return text(`${path} does not exist`, true);
@@ -24560,7 +24645,7 @@ function findPeriscope(sidecars, segment) {
24560
24645
  }
24561
24646
 
24562
24647
  // src/cli.ts
24563
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname33(process.execPath) : resolve7(dirname33(fileURLToPath2(import.meta.url)), "..");
24648
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname34(process.execPath) : resolve7(dirname34(fileURLToPath2(import.meta.url)), "..");
24564
24649
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
24565
24650
  var CYCLE_SECONDS2 = 120;
24566
24651
  var STALE_CYCLES = 3;
@@ -25278,7 +25363,19 @@ if (command === "passenger-sync-daemon") {
25278
25363
  return;
25279
25364
  }
25280
25365
  try {
25281
- for (const r of syncAllPassengerCheckouts(cfg)) {
25366
+ const refusedRepos = /* @__PURE__ */ new Map();
25367
+ const checkouts = syncAllPassengerCheckouts(cfg, (repo, e) => refusedRepos.set(repo, e.message));
25368
+ for (const [repo, detail] of refusedRepos) {
25369
+ if (await raiseShipAttention({ state, emit }, repoContractRefusedItem(repo, detail))) {
25370
+ emit.warn(`passenger sync skips ${repo}: ${detail}`, { step: "passengers", data: { repo } });
25371
+ }
25372
+ }
25373
+ for (const item of state.shipAttention().previous()) {
25374
+ if (item.kind === "repo_contract_refused" && !refusedRepos.has(item.key.slice(repoContractRefusedKey("").length))) {
25375
+ clearShipAttention(state, item.key);
25376
+ }
25377
+ }
25378
+ for (const r of checkouts) {
25282
25379
  if (r.outcome.action === "clone-failed") {
25283
25380
  if (!r.outcome.repeat) {
25284
25381
  emit.warn(
@@ -25501,7 +25598,7 @@ if (command === "passenger-sync-daemon") {
25501
25598
  }
25502
25599
  if (host === "linux" && hasSystemd) {
25503
25600
  const label = labelFor(CREW_HOME, "run");
25504
- const staleTimer = join55(homedir11(), ".config", "systemd", "user", `${label}.timer`);
25601
+ const staleTimer = join56(homedir11(), ".config", "systemd", "user", `${label}.timer`);
25505
25602
  if (existsSync51(staleTimer)) {
25506
25603
  process.stdout.write(
25507
25604
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -25528,7 +25625,9 @@ if (command === "passenger-sync-daemon") {
25528
25625
  }
25529
25626
  const tracker = new Tracker(c, cfg.ship);
25530
25627
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
25531
- for (const r of await resolvedRepos(c)) {
25628
+ const { settled: doctorRepos, errors: refusedRepos } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
25629
+ for (const e of refusedRepos) problems.push(`repo ${e.repo}: ${e.detail}`);
25630
+ for (const r of doctorRepos) {
25532
25631
  if (!existsSync51(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
25533
25632
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
25534
25633
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
@@ -25590,7 +25689,16 @@ ${changed2.map((c) => ` - ${c}
25590
25689
  `).join("")}` : "fix: nothing changed\n");
25591
25690
  }
25592
25691
  async function resolvedRepos(c) {
25593
- return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
25692
+ const { settled, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
25693
+ for (const e of errors) {
25694
+ try {
25695
+ await raiseShipAttention({ state, emit }, repoContractRefusedItem(e.repo, e.detail));
25696
+ } catch {
25697
+ }
25698
+ }
25699
+ const refused = new Set(errors.map((e) => e.repo));
25700
+ for (const t of reposOf(c)) if (!refused.has(t.name)) clearShipAttention(state, repoContractRefusedKey(t.name));
25701
+ return settled;
25594
25702
  }
25595
25703
  async function sandboxReposOf(routes) {
25596
25704
  const out = /* @__PURE__ */ new Map();
@@ -25709,7 +25817,10 @@ ${changed2.map((c) => ` - ${c}
25709
25817
  }
25710
25818
  let boardLock;
25711
25819
  try {
25712
- const repoFile = loadRepoConfig(target.dir);
25820
+ let contractRefusal = null;
25821
+ const repoFile = loadRepoConfigTolerant(target.dir, (e) => {
25822
+ contractRefusal = e;
25823
+ });
25713
25824
  const o = c.repoOverrides[target.name];
25714
25825
  const repo = resolveRepoConfig(repoFile, {
25715
25826
  hooks: { ...c.hooks, ...o?.hooks },
@@ -25824,6 +25935,21 @@ ${changed2.map((c) => ` - ${c}
25824
25935
  releaseModes
25825
25936
  );
25826
25937
  reportUnplaceable(c.route, unplaceable, tracker.contract.statuses.verified, remit);
25938
+ if (contractRefusal) {
25939
+ const refusal = contractRefusal;
25940
+ const baseRef = remoteBranchExists(target.dir, repo.branch.remote, repo.branch.base) ? `${repo.branch.remote}/${repo.branch.base}` : repo.branch.base;
25941
+ const fixers = contractFixers(
25942
+ target.dir,
25943
+ baseRef,
25944
+ tickets.filter((t) => t.status === tracker.contract.statuses.verified),
25945
+ (t) => locateBranchForTicket(target.dir, repo, t)?.branch ?? null
25946
+ );
25947
+ if (fixers.length === 0) throw refusal;
25948
+ remit.emit(
25949
+ `${target.name}: the base contract is refused (${refusal.message}); ${fixers.map((f) => f.issueId).join(", ")} replace${fixers.length === 1 ? "s" : ""} ${fixers[0].file} \u2014 merging first`,
25950
+ { step: "release" }
25951
+ );
25952
+ }
25827
25953
  const blockerInfo = blockerInfoMap(all, await tracker.ticketsByIds(missingBlockerIds(all)));
25828
25954
  const verifiedStatus = tracker.contract.statuses.verified;
25829
25955
  const verifiedTickets = tickets.filter((t) => t.status === verifiedStatus);
@@ -25856,6 +25982,7 @@ ${changed2.map((c) => ` - ${c}
25856
25982
  emit: remit,
25857
25983
  scope,
25858
25984
  verifiedShas,
25985
+ ...contractRefusal ? { contractRefusal: contractRefusal.message } : {},
25859
25986
  state: state.release(scope),
25860
25987
  reviewColumns: repo.release.mode === "external" ? await tracker.reviewColumns() : void 0,
25861
25988
  dryRun,
@@ -26724,7 +26851,7 @@ ${cu.tail.trimEnd()}
26724
26851
  process.exit(1);
26725
26852
  }
26726
26853
  requireArmed("run the daemon loop");
26727
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join55(CREW_HOME, "src"), join55(CREW_HOME, "dist"), join55(CREW_HOME, "bin")];
26854
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join56(CREW_HOME, "src"), join56(CREW_HOME, "dist"), join56(CREW_HOME, "bin")];
26728
26855
  const configPaths = daemonConfigPaths(cfg);
26729
26856
  watchPaths.push(...configPaths);
26730
26857
  noteDaemonStarted(cfg.ship.stateDir);
@@ -27109,7 +27236,7 @@ ${cu.tail.trimEnd()}
27109
27236
  const found = cfg.routes.find((r) => r.route === routeArg);
27110
27237
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
27111
27238
  const named2 = value("route") ? void 0 : positional[3];
27112
- const dir = named2 ? repoDirOf(configText, dirname33(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
27239
+ const dir = named2 ? repoDirOf(configText, dirname34(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
27113
27240
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
27114
27241
  const o = target ? found.repoOverrides[target.name] : void 0;
27115
27242
  runReposCheck(dir, {
@@ -27133,7 +27260,7 @@ ${cu.tail.trimEnd()}
27133
27260
  if (sub === "hooks") {
27134
27261
  try {
27135
27262
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
27136
- const dir = repoDirOf(configText, dirname33(cfg.configFile), a.route, a.name);
27263
+ const dir = repoDirOf(configText, dirname34(cfg.configFile), a.route, a.name);
27137
27264
  if (!existsSync51(dir)) throw new ReposError(`${dir} does not exist on disk`);
27138
27265
  const files = { exists: (r) => existsSync51(resolvePath(dir, r)), list: (r) => {
27139
27266
  try {
@@ -27300,7 +27427,7 @@ ${cu.tail.trimEnd()}
27300
27427
  }
27301
27428
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
27302
27429
  const text2 = configText;
27303
- const base = dirname33(cfg.configFile);
27430
+ const base = dirname34(cfg.configFile);
27304
27431
  const isCheckout = (d) => existsSync51(`${d}/.git`);
27305
27432
  try {
27306
27433
  if (sub === "list") {
@@ -27315,7 +27442,8 @@ ${cu.tail.trimEnd()}
27315
27442
  isCheckout,
27316
27443
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
27317
27444
  try {
27318
- return Object.keys(parseRepoConfig(readFileSync46(resolvePath(d, f), "utf8"), f).hooks).length > 0;
27445
+ const path = resolvePath(d, f);
27446
+ return Object.keys(parseRepoConfig(readFileSync46(path, "utf8"), path).hooks).length > 0;
27319
27447
  } catch {
27320
27448
  return false;
27321
27449
  }
@@ -27978,7 +28106,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
27978
28106
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
27979
28107
  `);
27980
28108
  } else {
27981
- mkdirSync38(dirname33(resolvedPath), { recursive: true });
28109
+ mkdirSync38(dirname34(resolvedPath), { recursive: true });
27982
28110
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
27983
28111
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
27984
28112
  writeFileSync38(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -28027,7 +28155,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28027
28155
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
28028
28156
  `);
28029
28157
  } else {
28030
- mkdirSync38(dirname33(apiKeyFile), { recursive: true });
28158
+ mkdirSync38(dirname34(apiKeyFile), { recursive: true });
28031
28159
  writeFileSync38(apiKeyFile, `${apiKeyVar}=${apiKey}
28032
28160
  `, { mode: 384 });
28033
28161
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -28724,10 +28852,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
28724
28852
  startupSeconds: cfg.ship.intelStartupSeconds,
28725
28853
  prepare: async (path) => {
28726
28854
  for (const f of repoWt.config.worktrees.copy) {
28727
- const src = join55(repoWt.dir, f);
28728
- if (!existsSync51(src) || existsSync51(join55(path, f))) continue;
28729
- mkdirSync38(dirOf2(join55(path, f)), { recursive: true });
28730
- copyFileSync2(src, join55(path, f));
28855
+ const src = join56(repoWt.dir, f);
28856
+ if (!existsSync51(src) || existsSync51(join56(path, f))) continue;
28857
+ mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
28858
+ copyFileSync2(src, join56(path, f));
28731
28859
  }
28732
28860
  const setup = repoWt.config.hooks.setup;
28733
28861
  return setup ? runScript(setup, { cwd: path, shell: shellWt }) : null;
@@ -29043,10 +29171,10 @@ ${where.join("\n")}`);
29043
29171
  },
29044
29172
  prepare: async (path) => {
29045
29173
  for (const f of repoP.config.worktrees.copy) {
29046
- const src = join55(repoP.dir, f);
29174
+ const src = join56(repoP.dir, f);
29047
29175
  if (!existsSync51(src)) continue;
29048
- mkdirSync38(dirOf2(join55(path, f)), { recursive: true });
29049
- copyFileSync2(src, join55(path, f));
29176
+ mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
29177
+ copyFileSync2(src, join56(path, f));
29050
29178
  }
29051
29179
  const setup = repoP.config.hooks.setup;
29052
29180
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -29500,7 +29628,7 @@ ${where.join("\n")}`);
29500
29628
  if (!route2.resolved) fail2(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
29501
29629
  const tracker = new Tracker(route2, cfg.ship);
29502
29630
  const configText = readFileSync46(cfg.configFile, "utf8");
29503
- const cfgBase = dirname33(cfg.configFile);
29631
+ const cfgBase = dirname34(cfg.configFile);
29504
29632
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
29505
29633
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
29506
29634
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -29544,12 +29672,12 @@ ${where.join("\n")}`);
29544
29672
  `);
29545
29673
  }
29546
29674
  }
29547
- if (existsSync51(join55(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
29675
+ if (existsSync51(join56(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
29548
29676
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
29549
29677
  break;
29550
29678
  }
29551
29679
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
29552
- const fresh = !existsSync51(join55(dir, ".git"));
29680
+ const fresh = !existsSync51(join56(dir, ".git"));
29553
29681
  if (existsSync51(dir) && fresh && readdirSync20(dir).length > 0) fail2(`${dir} exists and is not a git checkout \u2014 move it aside`);
29554
29682
  if (dryRun) {
29555
29683
  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`);
@@ -29557,8 +29685,8 @@ ${where.join("\n")}`);
29557
29685
  }
29558
29686
  if (fresh) {
29559
29687
  mkdirSync38(dir, { recursive: true });
29560
- writeFileSync38(join55(dir, "README.md"), SAMPLE_README);
29561
- writeFileSync38(join55(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
29688
+ writeFileSync38(join56(dir, "README.md"), SAMPLE_README);
29689
+ writeFileSync38(join56(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
29562
29690
  const g = (...a) => {
29563
29691
  const r = spawnSync11("git", a, { cwd: dir, encoding: "utf8" });
29564
29692
  if (r.status !== 0) fail2(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -24682,6 +24682,19 @@ var SPEC_VERSION = 1;
24682
24682
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
24683
24683
  var RepoConfigError = class extends Error {
24684
24684
  };
24685
+ function versionFileProblem(name, text) {
24686
+ const remedy = `a version file that is not a JSON object with a string "version" needs hooks.bump instead of release.versionFiles (docs/REPO_SPEC.md)`;
24687
+ let doc;
24688
+ try {
24689
+ doc = JSON.parse(text);
24690
+ } catch {
24691
+ return `release.versionFiles entry "${name}" is not a JSON document \u2014 ${remedy}`;
24692
+ }
24693
+ if (!doc || typeof doc !== "object" || Array.isArray(doc) || typeof doc.version !== "string") {
24694
+ return `release.versionFiles entry "${name}" has no string "version" field \u2014 ${remedy}`;
24695
+ }
24696
+ return null;
24697
+ }
24685
24698
  var HOOK_NAMES = [
24686
24699
  "test",
24687
24700
  "build",
@@ -25015,6 +25028,22 @@ function parseRepoConfig(text, file) {
25015
25028
  if (versionFiles.length === 0) {
25016
25029
  throw new RepoConfigError(`${file}: release.versionFiles cannot be empty`);
25017
25030
  }
25031
+ if (versionFilesGiven) {
25032
+ for (const name of versionFiles) {
25033
+ const path = (0, import_node_path6.join)((0, import_node_path6.dirname)(file), name);
25034
+ if (!(0, import_node_fs5.existsSync)(path)) continue;
25035
+ let text2;
25036
+ try {
25037
+ text2 = (0, import_node_fs5.readFileSync)(path, "utf8");
25038
+ } catch (e) {
25039
+ throw new RepoConfigError(
25040
+ `${file}: release.versionFiles entry "${name}" cannot be read (${e.code ?? "read error"})`
25041
+ );
25042
+ }
25043
+ const problem = versionFileProblem(name, text2);
25044
+ if (problem) throw new RepoConfigError(`${file}: ${problem}`);
25045
+ }
25046
+ }
25018
25047
  const inspectorRaw = raw.periscope?.inspector;
25019
25048
  if (inspectorRaw !== void 0 && inspectorRaw !== "proxy" && inspectorRaw !== "repo") {
25020
25049
  throw new RepoConfigError(`${file}: periscope.inspector must be "proxy" or "repo"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.126.1",
3
+ "version": "0.126.3",
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",