@tablation/crew 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname9, join as join19, basename as basename7 } from "path";
4
+ import { resolve as resolve4, dirname as dirname10, join as join19, basename as basename7 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir5 } from "os";
7
7
  import { spawnSync as spawnSync3 } from "child_process";
@@ -2323,9 +2323,14 @@ function programFromPlist(text) {
2323
2323
  const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
2324
2324
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
2325
2325
  }
2326
- function listLaunchdUnits(dir = launchAgentsDir()) {
2327
- if (!existsSync5(dir)) return [];
2328
- return readdirSync(dir).filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2326
+ function loadedCrewLabels() {
2327
+ const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
2328
+ if (list.status !== 0) return [];
2329
+ return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
2330
+ }
2331
+ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
2332
+ const files = existsSync5(dir) ? readdirSync(dir) : [];
2333
+ const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2329
2334
  const plistPath = join5(dir, f);
2330
2335
  let program = null;
2331
2336
  try {
@@ -2339,10 +2344,16 @@ function listLaunchdUnits(dir = launchAgentsDir()) {
2339
2344
  programExists: program !== null && existsSync5(program)
2340
2345
  };
2341
2346
  });
2347
+ const seen = new Set(units.map((u) => u.label));
2348
+ for (const label of [...loaded].sort()) {
2349
+ if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
2350
+ units.push({ label, plistPath: join5(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
2351
+ }
2352
+ return units;
2342
2353
  }
2343
- function foreignLaunchdUnits(crewHome, dir) {
2354
+ function foreignLaunchdUnits(crewHome, dir, loaded) {
2344
2355
  const own = new Set(["run", "release", "passengers"].map((job) => labelFor(crewHome, job)));
2345
- return listLaunchdUnits(dir).filter((u) => !own.has(u.label));
2356
+ return listLaunchdUnits(dir, loaded).filter((u) => !own.has(u.label));
2346
2357
  }
2347
2358
  function removeLaunchdUnit(unit, dryRun, log) {
2348
2359
  if (dryRun) {
@@ -2354,9 +2365,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
2354
2365
  if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
2355
2366
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
2356
2367
  }
2357
- function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir) {
2358
- const foreign = foreignLaunchdUnits(crewHome, dir);
2359
- const blocking = foreign.filter((u) => u.program === null || u.programExists);
2368
+ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
2369
+ const foreign = foreignLaunchdUnits(crewHome, dir, loaded);
2370
+ const blocking = foreign.filter((u) => !u.orphan && (u.program === null || u.programExists));
2360
2371
  if (blocking.length > 0 && !replace) {
2361
2372
  log.warn(
2362
2373
  `two crew installs target this machine: ${blocking.map((u) => `${u.label} runs ${u.program ?? "an unreadable ProgramArguments"}`).join("; ")}. Run \`crew uninstall\` from the other one, or pass --replace.`
@@ -2364,14 +2375,14 @@ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir) {
2364
2375
  return false;
2365
2376
  }
2366
2377
  for (const unit of foreign) {
2367
- const why = unit.program === null ? "replacing a unit with no readable ProgramArguments" : unit.programExists ? "replacing a live other install" : "removing a stale unit whose program is gone";
2378
+ const why = unit.orphan ? "removing a loaded job whose plist is gone" : unit.program === null ? "replacing a unit with no readable ProgramArguments" : unit.programExists ? "replacing a live other install" : "removing a stale unit whose program is gone";
2368
2379
  log.emit(`${why}: ${unit.label}`);
2369
2380
  removeLaunchdUnit(unit, dryRun, log);
2370
2381
  }
2371
2382
  return true;
2372
2383
  }
2373
- function removeAllLaunchdUnits(dryRun, log, dir) {
2374
- for (const unit of listLaunchdUnits(dir)) removeLaunchdUnit(unit, dryRun, log);
2384
+ function removeAllLaunchdUnits(dryRun, log, dir, loaded) {
2385
+ for (const unit of listLaunchdUnits(dir, loaded)) removeLaunchdUnit(unit, dryRun, log);
2375
2386
  }
2376
2387
  function parseLaunchctlList(text) {
2377
2388
  const jobs = /* @__PURE__ */ new Map();
@@ -2383,13 +2394,13 @@ function parseLaunchctlList(text) {
2383
2394
  return jobs;
2384
2395
  }
2385
2396
  function describeForeignLaunchdUnits(crewHome, dir) {
2386
- const foreign = foreignLaunchdUnits(crewHome, dir);
2387
- if (foreign.length === 0) return [];
2388
2397
  const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
2389
2398
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
2399
+ const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
2390
2400
  return foreign.map((u) => {
2391
2401
  const status2 = jobs.get(u.label);
2392
2402
  const loaded = status2 ? `loaded, last exit ${status2.lastExit}` : "not loaded";
2403
+ if (u.orphan) return ` ${u.label}: ${loaded}; plist gone \u2014 stale: \`crew install\` removes it`;
2393
2404
  const program = u.program ? `${u.program} (${u.programExists ? "present" : "GONE"})` : "no readable ProgramArguments";
2394
2405
  const stale = status2 && !u.programExists ? " \u2014 stale: `crew install` removes it" : "";
2395
2406
  return ` ${u.label}: ${loaded}; runs ${program}${stale}`;
@@ -3980,16 +3991,20 @@ function refreshBaseBranch(cwd, remote, base, dryRun = false) {
3980
3991
  const [behindStr, aheadStr] = counts.split(/\s+/);
3981
3992
  const behind = Number.parseInt(behindStr ?? "0", 10);
3982
3993
  const ahead = Number.parseInt(aheadStr ?? "0", 10);
3994
+ const named = () => {
3995
+ const subjects = (gitOk(cwd, ["log", "--format=%h %s", "-5", `${upstream}..${base}`]) ?? "").split("\n").filter(Boolean);
3996
+ return subjects.length ? `: ${subjects.join("; ")}${ahead > subjects.length ? "; \u2026" : ""}` : "";
3997
+ };
3983
3998
  if (ahead > 0 && behind > 0) {
3984
3999
  return {
3985
4000
  action: "diverged",
3986
4001
  ahead,
3987
4002
  behind,
3988
- detail: `${base} has diverged from ${upstream} (${ahead} ahead, ${behind} behind)`
4003
+ detail: `${base} has diverged from ${upstream} (${ahead} ahead, ${behind} behind)${named()}`
3989
4004
  };
3990
4005
  }
3991
4006
  if (behind === 0 && ahead > 0) {
3992
- return { action: "ahead", ahead, detail: `${base} has ${ahead} commit(s) ${upstream} does not` };
4007
+ return { action: "ahead", ahead, detail: `${base} has ${ahead} commit(s) ${upstream} does not${named()}` };
3993
4008
  }
3994
4009
  if (behind === 0) return { action: "level", detail: `${base} is level with ${upstream}` };
3995
4010
  if (dryRun) {
@@ -4004,8 +4019,22 @@ function refreshBaseBranch(cwd, remote, base, dryRun = false) {
4004
4019
  detail: `fast-forwarded ${base} to ${upstream} (${behind} commit(s) from elsewhere)`
4005
4020
  };
4006
4021
  }
4007
- function pushBranch(cwd, remote, branch) {
4008
- git(cwd, ["push", remote, branch]);
4022
+ function recoverStrandedReleaseCheckouts(cwd) {
4023
+ gitOk(cwd, ["worktree", "prune"]);
4024
+ const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
4025
+ let path = "";
4026
+ for (const line of out.split("\n")) {
4027
+ if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4028
+ else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4029
+ gitOk(cwd, ["worktree", "remove", "--force", path]);
4030
+ }
4031
+ }
4032
+ const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
4033
+ for (const b of stranded) gitOk(cwd, ["branch", "-q", "-D", b]);
4034
+ return stranded;
4035
+ }
4036
+ function pushBranch(cwd, remote, branch, dst = branch) {
4037
+ git(cwd, ["push", remote, `${branch}:${dst}`]);
4009
4038
  }
4010
4039
  function worktrees(cwd) {
4011
4040
  const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
@@ -5442,8 +5471,9 @@ Wrote ${o.configPath}.
5442
5471
  }
5443
5472
 
5444
5473
  // src/release-run.ts
5445
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
5446
- import { join as join9 } from "path";
5474
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync2 } from "fs";
5475
+ import { dirname as dirname7, join as join9 } from "path";
5476
+ import { tmpdir as tmpdir2 } from "os";
5447
5477
 
5448
5478
  // src/release.ts
5449
5479
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -6018,6 +6048,31 @@ async function confirmLastRelease(o, decision) {
6018
6048
  if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
6019
6049
  return { confirmed, version };
6020
6050
  }
6051
+ function openReleaseCheckout(o) {
6052
+ const { remote, base } = o.repo.branch;
6053
+ const branch = `crew/release-${Date.now()}`;
6054
+ const root = mkdtempSync(join9(tmpdir2(), "crew-release-"));
6055
+ const dir = join9(root, "checkout");
6056
+ const close = () => {
6057
+ gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
6058
+ rmSync2(root, { recursive: true, force: true });
6059
+ gitOk(o.cwd, ["worktree", "prune"]);
6060
+ gitOk(o.cwd, ["branch", "-D", branch]);
6061
+ };
6062
+ try {
6063
+ git(o.cwd, ["worktree", "add", "-q", "-b", branch, dir, `${remote}/${base}`]);
6064
+ } catch (e) {
6065
+ close();
6066
+ throw e;
6067
+ }
6068
+ for (const f of o.repo.worktrees.copy) {
6069
+ const src = join9(o.cwd, f);
6070
+ if (!existsSync9(src)) continue;
6071
+ mkdirSync7(dirname7(join9(dir, f)), { recursive: true });
6072
+ copyFileSync(src, join9(dir, f));
6073
+ }
6074
+ return { dir, branch, close };
6075
+ }
6021
6076
  async function runRelease(o) {
6022
6077
  const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
6023
6078
  const tagPattern = o.repo.release.tagPattern ?? "v*";
@@ -6064,6 +6119,33 @@ async function runRelease(o) {
6064
6119
  const externalClosures = await confirmExternalReleased(o, landed);
6065
6120
  return { merged: [], deployed: false, stopped: "external", decision, externalClosures };
6066
6121
  }
6122
+ const { remote, base } = o.repo.branch;
6123
+ if (o.dryRun || o.mergeOnly || !remoteBranchExists(o.cwd, remote, base)) {
6124
+ return mergeAndRelease(o, decision, fresh);
6125
+ }
6126
+ let checkout;
6127
+ const stranded = recoverStrandedReleaseCheckouts(o.cwd);
6128
+ if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
6129
+ try {
6130
+ checkout = openReleaseCheckout(o);
6131
+ } catch (e) {
6132
+ o.emit.error(scoped(`could not cut a release checkout from ${remote}/${base} \u2014 ${e.message}`));
6133
+ return { merged: [], deployed: false, stopped: "release checkout failed", decision };
6134
+ }
6135
+ try {
6136
+ return await mergeAndRelease({ ...o, cwd: checkout.dir }, decision, fresh, checkout.branch);
6137
+ } finally {
6138
+ checkout.close();
6139
+ const refreshed = refreshBaseBranch(o.cwd, remote, base);
6140
+ if (refreshed.action === "fast-forwarded" && o.repo.hooks.build) {
6141
+ const r = await hook(o, "build");
6142
+ if (r && r.code !== 0) {
6143
+ o.emit.warn(scoped(`rebuilding the primary checkout after the release failed (exit ${r.code}) \u2014 its dist/ is stale until \`${o.repo.hooks.build}\` is run there`));
6144
+ }
6145
+ }
6146
+ }
6147
+ }
6148
+ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
6067
6149
  o.emit.enter("merge");
6068
6150
  const merged = [];
6069
6151
  const conflicts = [];
@@ -6225,6 +6307,16 @@ async function runRelease(o) {
6225
6307
  }
6226
6308
  }
6227
6309
  }
6310
+ if (releaseBranch && !o.dryRun) {
6311
+ const { remote, base } = o.repo.branch;
6312
+ if (fetchRemote(o.cwd, remote) && remoteBranchExists(o.cwd, remote, base)) {
6313
+ const moved = resolve2(o.cwd, `${remote}/${base}`);
6314
+ if (moved && !isAncestor(o.cwd, moved, "HEAD")) {
6315
+ o.emit.warn(`${remote}/${base} moved during the release \u2014 discarding it; the tickets stay verified and the next cycle retries`);
6316
+ return { merged, conflicts, unbuildable, version, deployed: false, decision, stopped: "base branch moved during release" };
6317
+ }
6318
+ }
6319
+ }
6228
6320
  let deployed = false;
6229
6321
  let integrated = false;
6230
6322
  if (o.repo.release.mode === "integrate") {
@@ -6254,7 +6346,7 @@ async function runRelease(o) {
6254
6346
  o.emit.emit(`would push ${o.repo.branch.base} to ${o.repo.branch.remote}`);
6255
6347
  } else {
6256
6348
  try {
6257
- pushBranch(o.cwd, o.repo.branch.remote, o.repo.branch.base);
6349
+ pushBranch(o.cwd, o.repo.branch.remote, releaseBranch ?? o.repo.branch.base, o.repo.branch.base);
6258
6350
  o.emit.emit(`pushed ${o.repo.branch.base} to ${o.repo.branch.remote}`);
6259
6351
  } catch (e) {
6260
6352
  o.emit.error(
@@ -8009,7 +8101,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
8009
8101
  }
8010
8102
 
8011
8103
  // src/stream-sweep.ts
8012
- import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync2, statSync as statSync6 } from "fs";
8104
+ import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync6 } from "fs";
8013
8105
  import { join as join14 } from "path";
8014
8106
  var DAY_MS = 24 * 60 * 60 * 1e3;
8015
8107
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
@@ -8032,7 +8124,7 @@ function applyStreamSweep(paths, dryRun, log) {
8032
8124
  continue;
8033
8125
  }
8034
8126
  try {
8035
- rmSync2(path, { force: true });
8127
+ rmSync3(path, { force: true });
8036
8128
  removed++;
8037
8129
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
8038
8130
  } catch (e) {
@@ -8043,14 +8135,14 @@ function applyStreamSweep(paths, dryRun, log) {
8043
8135
  }
8044
8136
 
8045
8137
  // src/cli.ts
8046
- import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
8138
+ import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync } from "fs";
8047
8139
  import { createInterface } from "readline/promises";
8048
8140
  import { dirname as dirOf, resolve as resolvePath } from "path";
8049
8141
 
8050
8142
  // src/ssh-keys.ts
8051
8143
  import { execFileSync as execFileSync3 } from "child_process";
8052
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, chmodSync } from "fs";
8053
- import { dirname as dirname8, join as join15 } from "path";
8144
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync11, chmodSync } from "fs";
8145
+ import { dirname as dirname9, join as join15 } from "path";
8054
8146
  var SshKeyError = class extends Error {
8055
8147
  };
8056
8148
  function shipSshKeyPathFor(stateDir) {
@@ -8060,7 +8152,7 @@ function ensureShipSshKeypair(stateDir) {
8060
8152
  const privateKeyPath = shipSshKeyPathFor(stateDir);
8061
8153
  const publicKeyPath = `${privateKeyPath}.pub`;
8062
8154
  if (!existsSync16(privateKeyPath)) {
8063
- mkdirSync7(dirname8(privateKeyPath), { recursive: true });
8155
+ mkdirSync8(dirname9(privateKeyPath), { recursive: true });
8064
8156
  try {
8065
8157
  execFileSync3("ssh-keygen", [
8066
8158
  "-t",
@@ -8297,7 +8389,7 @@ function syncAllPassengerCheckouts(cfg) {
8297
8389
 
8298
8390
  // src/tunnel.ts
8299
8391
  import { spawn as spawn3 } from "child_process";
8300
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
8392
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
8301
8393
  import { randomInt } from "crypto";
8302
8394
  import { join as join16 } from "path";
8303
8395
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -8379,7 +8471,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
8379
8471
  }
8380
8472
  function writePersistedTunnel(stateDir, state) {
8381
8473
  const path = tunnelStatePath(stateDir, state.workspaceId);
8382
- mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
8474
+ mkdirSync9(join16(stateDir, "passengers"), { recursive: true });
8383
8475
  writeFileSync7(path, `${JSON.stringify(state, null, 2)}
8384
8476
  `);
8385
8477
  }
@@ -8599,7 +8691,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
8599
8691
  import { createServer as createHttpServer } from "http";
8600
8692
  import { spawn as spawn4 } from "child_process";
8601
8693
  import { randomBytes } from "crypto";
8602
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
8694
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
8603
8695
  import { join as join17 } from "path";
8604
8696
 
8605
8697
  // src/secret-compare.ts
@@ -8702,7 +8794,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
8702
8794
  }
8703
8795
  function writePersistedSyncDaemon(stateDir, state) {
8704
8796
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
8705
- mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
8797
+ mkdirSync10(join17(stateDir, "passengers"), { recursive: true });
8706
8798
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
8707
8799
  `);
8708
8800
  }
@@ -9066,7 +9158,7 @@ async function buildEnvironment(route, ship, ticket) {
9066
9158
  }
9067
9159
 
9068
9160
  // src/cli.ts
9069
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname9(process.execPath) : resolve4(dirname9(fileURLToPath(import.meta.url)), "..");
9161
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname10(process.execPath) : resolve4(dirname10(fileURLToPath(import.meta.url)), "..");
9070
9162
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
9071
9163
  var CYCLE_SECONDS = 120;
9072
9164
  var STALE_CYCLES = 3;
@@ -9516,7 +9608,7 @@ if (command === "passenger-sync-daemon") {
9516
9608
  `),
9517
9609
  warn: (m) => process.stderr.write(`crew install: ${m}
9518
9610
  `)
9519
- });
9611
+ }, void 0, loadedCrewLabels());
9520
9612
  if (!ok) return false;
9521
9613
  }
9522
9614
  const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
@@ -10518,7 +10610,7 @@ ${changed2.map((c) => ` - ${c}
10518
10610
  const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10519
10611
  raw.agentPersonas = personaResult.agentPersonas;
10520
10612
  raw.agentSkills = skillResult.agentSkills;
10521
- mkdirSync10(dirOf(resolvedPath), { recursive: true });
10613
+ mkdirSync11(dirOf(resolvedPath), { recursive: true });
10522
10614
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
10523
10615
  `);
10524
10616
  }
@@ -10566,7 +10658,7 @@ ${changed2.map((c) => ` - ${c}
10566
10658
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10567
10659
  const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10568
10660
  raw.agentSkills = result.agentSkills;
10569
- mkdirSync10(dirOf(resolvedPath), { recursive: true });
10661
+ mkdirSync11(dirOf(resolvedPath), { recursive: true });
10570
10662
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
10571
10663
  `);
10572
10664
  }
@@ -10610,7 +10702,7 @@ ${changed2.map((c) => ` - ${c}
10610
10702
  }
10611
10703
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
10612
10704
  const text = configText;
10613
- const base = dirname9(cfg.configFile);
10705
+ const base = dirname10(cfg.configFile);
10614
10706
  try {
10615
10707
  if (sub === "list") {
10616
10708
  const repos = configuredRepos(text, routeName, base);
@@ -10971,8 +11063,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10971
11063
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
10972
11064
  `);
10973
11065
  } else {
10974
- mkdirSync10(dirname9(resolvedPath), { recursive: true });
10975
- if (hadPreviousFile) copyFileSync(resolvedPath, `${resolvedPath}.bak`);
11066
+ mkdirSync11(dirname10(resolvedPath), { recursive: true });
11067
+ if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
10976
11068
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
10977
11069
  writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
10978
11070
  `);
@@ -10988,7 +11080,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10988
11080
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
10989
11081
  `);
10990
11082
  } else {
10991
- mkdirSync10(dirname9(apiKeyFile), { recursive: true });
11083
+ mkdirSync11(dirname10(apiKeyFile), { recursive: true });
10992
11084
  writeFileSync9(apiKeyFile, `${apiKeyVar}=${apiKey}
10993
11085
  `, { mode: 384 });
10994
11086
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -11444,7 +11536,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11444
11536
  `),
11445
11537
  warn: (m) => process.stderr.write(`crew uninstall: ${m}
11446
11538
  `)
11447
- });
11539
+ }, void 0, loadedCrewLabels());
11448
11540
  }
11449
11541
  break;
11450
11542
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -353,9 +353,12 @@ if `report_type` is either of those.
353
353
  reconcile it yourself. Post a comment on the ticket naming the repo and the
354
354
  counts `crew sync` printed, leave the ticket as you found it, and end the
355
355
  run — the operator reconciles the checkout. Otherwise, from
356
- the primary checkout, `git worktree add` a sibling worktree for this
357
- ticket, on a new branch cut from the base branch's current HEAD. The
358
- digest's `worktree` and `branch` columns give this ticket's own directory
356
+ the primary checkout, `git worktree add <dir> -b <branch> <remote>/<base>`
357
+ a sibling worktree for this ticket, on a new branch cut from the base
358
+ branch's REMOTE-TRACKING ref (`origin/main`, say), never from the primary
359
+ checkout's own local base — that copy is a cache of the remote, and a
360
+ local commit on it would be carried into every ticket cut from it
361
+ (CREW-1383). The digest's `worktree` and `branch` columns give this ticket's own directory
359
362
  and branch name — use them verbatim. The Environment section names the
360
363
  base branch and describes the general pattern, but its own worked example
361
364
  is illustrative only (rendered before any ticket is chosen, so it cannot
@@ -334,8 +334,12 @@ if `report_type` is either of those.
334
334
  reconcile it yourself. Post a comment on the ticket naming the repo and the
335
335
  counts `crew sync` printed, leave the ticket as you found it, and end the
336
336
  run — the operator reconciles the checkout. Otherwise, from
337
- the primary checkout, `git worktree add` a sibling worktree for this
338
- ticket, on a new branch cut from the base branch's current HEAD. The
337
+ the primary checkout, `git worktree add <dir> -b <branch> <remote>/<base>`
338
+ a sibling worktree for this ticket, on a new branch cut from the base
339
+ branch's REMOTE-TRACKING ref (`origin/main`, say), never from the primary
340
+ checkout's own local base — that copy is a cache of the remote, and a
341
+ local commit on it would be carried into every ticket cut from it
342
+ (CREW-1383). The
339
343
  digest's `worktree` and `branch` columns give this ticket's own directory
340
344
  and branch name — use them verbatim. The Environment section names the
341
345
  base branch and describes the general pattern, but its own worked example