@tablation/crew 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +118 -36
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10616,7 +10616,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10616
10616
  }
10617
10617
 
10618
10618
  // src/cli.ts
10619
- import { readFileSync as readFileSync15, existsSync as existsSync22, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10619
+ import { readFileSync as readFileSync16, existsSync as existsSync23, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10620
10620
  import { createInterface } from "readline/promises";
10621
10621
  import { dirname as dirOf, resolve as resolvePath } from "path";
10622
10622
 
@@ -11354,7 +11354,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
11354
11354
  }
11355
11355
 
11356
11356
  // src/daemon.ts
11357
- import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
11357
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
11358
11358
  import { join as join21 } from "path";
11359
11359
  var DEFAULT_FLOOR_MS = 5e3;
11360
11360
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -11401,9 +11401,38 @@ function latestMtime(path) {
11401
11401
  function computeTreeSignature(paths) {
11402
11402
  return paths.map((p) => String(latestMtime(p))).join(":");
11403
11403
  }
11404
- function makeStaleChecker(paths) {
11404
+ function makeStaleChecker(paths, opts = {}) {
11405
11405
  const initial = computeTreeSignature(paths);
11406
- return () => computeTreeSignature(paths) !== initial;
11406
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync22(opts.reloadSentinel);
11407
+ }
11408
+ function reloadSentinelFor(stateDir) {
11409
+ return join21(stateDir, "reload-requested");
11410
+ }
11411
+ function daemonStampFor(stateDir) {
11412
+ return join21(stateDir, "daemon.started");
11413
+ }
11414
+ function noteDaemonStarted(stateDir, now = Date.now()) {
11415
+ mkdirSync12(stateDir, { recursive: true });
11416
+ rmSync8(reloadSentinelFor(stateDir), { force: true });
11417
+ writeFileSync12(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
11418
+ `);
11419
+ }
11420
+ function readDaemonStarted(stateDir) {
11421
+ try {
11422
+ const v = JSON.parse(readFileSync15(daemonStampFor(stateDir), "utf8")).startedAt;
11423
+ return typeof v === "number" ? v : null;
11424
+ } catch {
11425
+ return null;
11426
+ }
11427
+ }
11428
+ function configChangedAfter(files, startedAt) {
11429
+ for (const f of files) {
11430
+ try {
11431
+ if (statSync8(f).mtimeMs > startedAt) return f;
11432
+ } catch {
11433
+ }
11434
+ }
11435
+ return null;
11407
11436
  }
11408
11437
  var SPAWN_PARK_AFTER = 3;
11409
11438
  var SpawnBreaker = class {
@@ -11804,6 +11833,7 @@ function usage() {
11804
11833
  crew watch [route] live view of what the crew is doing
11805
11834
  crew status [route] [--json] paused/running state; --json for a machine reader
11806
11835
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
11836
+ crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
11807
11837
  crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
11808
11838
  crew ports [route] which checkout owns which ports, and what is up
11809
11839
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
@@ -11858,7 +11888,12 @@ var value = (name) => {
11858
11888
  var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
11859
11889
  var command = positional[0];
11860
11890
  if (!command) usage();
11861
- var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
11891
+ function daemonConfigPaths(c) {
11892
+ const files = [c.configFile];
11893
+ for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
11894
+ return files;
11895
+ }
11896
+ var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
11862
11897
  var STATE_WRITING_SUBCOMMANDS = {
11863
11898
  agents: /* @__PURE__ */ new Set(["sync"]),
11864
11899
  skills: /* @__PURE__ */ new Set(["sync"])
@@ -11968,7 +12003,7 @@ if (command === "passenger-sync-daemon") {
11968
12003
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
11969
12004
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
11970
12005
  const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
11971
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
12006
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
11972
12007
  let route;
11973
12008
  try {
11974
12009
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -12175,7 +12210,7 @@ if (command === "passenger-sync-daemon") {
12175
12210
  if (host === "linux" && hasSystemd) {
12176
12211
  const label = labelFor(CREW_HOME, "run");
12177
12212
  const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
12178
- if (existsSync22(staleTimer)) {
12213
+ if (existsSync23(staleTimer)) {
12179
12214
  process.stdout.write(
12180
12215
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
12181
12216
  `
@@ -12201,7 +12236,7 @@ if (command === "passenger-sync-daemon") {
12201
12236
  const tracker = new Tracker(c, cfg.ship);
12202
12237
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
12203
12238
  for (const r of await resolvedRepos(c)) {
12204
- if (!existsSync22(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12239
+ if (!existsSync23(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12205
12240
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
12206
12241
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
12207
12242
  }
@@ -12219,7 +12254,7 @@ if (command === "passenger-sync-daemon") {
12219
12254
  }
12220
12255
  async function doctorFix(o) {
12221
12256
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
12222
- const unitExists = (p) => existsSync22(p);
12257
+ const unitExists = (p) => existsSync23(p);
12223
12258
  const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12224
12259
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
12225
12260
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -12236,7 +12271,7 @@ if (command === "passenger-sync-daemon") {
12236
12271
  for (const a of plan.actions) {
12237
12272
  if (a.kind === "enable") {
12238
12273
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
12239
- writeFileSync12(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
12274
+ writeFileSync13(cfg.configFile, planEnable(readFileSync16(cfg.configFile, "utf8"), a.route));
12240
12275
  enabledSound.add(a.route);
12241
12276
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
12242
12277
  } else {
@@ -13141,6 +13176,9 @@ ${changed2.map((c) => ` - ${c}
13141
13176
  }
13142
13177
  requireArmed("run the daemon loop");
13143
13178
  const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
13179
+ const configPaths = daemonConfigPaths(cfg);
13180
+ watchPaths.push(...configPaths);
13181
+ noteDaemonStarted(cfg.ship.stateDir);
13144
13182
  const spawnBreaker = new SpawnBreaker();
13145
13183
  const stopReason = await runDaemonLoop({
13146
13184
  runPass: () => runOnePass({
@@ -13158,14 +13196,16 @@ ${changed2.map((c) => ` - ${c}
13158
13196
  `)
13159
13197
  })
13160
13198
  }),
13161
- checkStale: makeStaleChecker(watchPaths),
13199
+ checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
13162
13200
  onPass: (result) => {
13163
13201
  if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
13164
13202
  for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
13165
13203
  }
13166
13204
  });
13167
13205
  if (stopReason === "stale") {
13168
- emit.emit("crew daemon: installed tree changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update");
13206
+ emit.emit(
13207
+ "crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
13208
+ );
13169
13209
  process.exit(0);
13170
13210
  }
13171
13211
  break;
@@ -13289,11 +13329,11 @@ ${changed2.map((c) => ` - ${c}
13289
13329
  `);
13290
13330
  if (!dryRun) {
13291
13331
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13292
- const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13332
+ const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13293
13333
  raw.agentPersonas = personaResult.agentPersonas;
13294
13334
  raw.agentSkills = skillResult.agentSkills;
13295
- mkdirSync12(dirOf(resolvedPath), { recursive: true });
13296
- writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
13335
+ mkdirSync13(dirOf(resolvedPath), { recursive: true });
13336
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
13297
13337
  `);
13298
13338
  }
13299
13339
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -13338,10 +13378,10 @@ ${changed2.map((c) => ` - ${c}
13338
13378
  `);
13339
13379
  if (!dryRun) {
13340
13380
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13341
- const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13381
+ const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13342
13382
  raw.agentSkills = result.agentSkills;
13343
- mkdirSync12(dirOf(resolvedPath), { recursive: true });
13344
- writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
13383
+ mkdirSync13(dirOf(resolvedPath), { recursive: true });
13384
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
13345
13385
  `);
13346
13386
  }
13347
13387
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -13354,14 +13394,14 @@ ${changed2.map((c) => ` - ${c}
13354
13394
  process.exit(2);
13355
13395
  };
13356
13396
  if (sub !== "add" && sub !== "list") usage2();
13357
- const configText = readFileSync15(cfg.configFile, "utf8");
13397
+ const configText = readFileSync16(cfg.configFile, "utf8");
13358
13398
  let routeName = positional[2];
13359
13399
  let addPath = ".";
13360
13400
  let addName;
13361
13401
  if (sub === "list" && !routeName) usage2();
13362
13402
  if (sub === "add") {
13363
13403
  try {
13364
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync22(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
13404
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync23(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
13365
13405
  routeName = a.route;
13366
13406
  addPath = a.path;
13367
13407
  addName = a.name;
@@ -13376,9 +13416,9 @@ ${changed2.map((c) => ` - ${c}
13376
13416
  }
13377
13417
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
13378
13418
  let resolvedRemotes;
13379
- if (existsSync22(resolvedFile)) {
13419
+ if (existsSync23(resolvedFile)) {
13380
13420
  try {
13381
- resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13421
+ resolvedRemotes = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13382
13422
  } catch {
13383
13423
  }
13384
13424
  }
@@ -13393,14 +13433,14 @@ ${changed2.map((c) => ` - ${c}
13393
13433
  for (const r of repos) {
13394
13434
  const origin2 = originOf(r.dir);
13395
13435
  const row = matchRepoName(origin2, resolvedRemotes);
13396
- const note = !existsSync22(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13436
+ const note = !existsSync23(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13397
13437
  process.stdout.write(`${r.name} ${r.dir} ${note}
13398
13438
  `);
13399
13439
  }
13400
13440
  break;
13401
13441
  }
13402
13442
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
13403
- const top = existsSync22(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13443
+ const top = existsSync23(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13404
13444
  if (!top) throw new ReposError(`${target} is not a git checkout`);
13405
13445
  const origin = originOf(top);
13406
13446
  const plan = planRepoAdd({
@@ -13449,15 +13489,15 @@ ${changed2.map((c) => ` - ${c}
13449
13489
  const created = await rowTracker.createRepoRow(rowPayload);
13450
13490
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
13451
13491
  `);
13452
- if (existsSync22(resolvedFile)) {
13453
- const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13492
+ if (existsSync23(resolvedFile)) {
13493
+ const raw = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13454
13494
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
13455
13495
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
13456
- writeFileSync12(resolvedFile, `${JSON.stringify(raw, null, 2)}
13496
+ writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
13457
13497
  `);
13458
13498
  }
13459
13499
  }
13460
- writeFileSync12(cfg.configFile, plan.text);
13500
+ writeFileSync13(cfg.configFile, plan.text);
13461
13501
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
13462
13502
  `);
13463
13503
  } catch (e) {
@@ -13696,10 +13736,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13696
13736
  operator = await pickOperator(found.holds);
13697
13737
  }
13698
13738
  let previous;
13699
- const hadPreviousFile = existsSync22(resolvedPath);
13739
+ const hadPreviousFile = existsSync23(resolvedPath);
13700
13740
  if (hadPreviousFile) {
13701
13741
  try {
13702
- previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
13742
+ previous = JSON.parse(readFileSync16(resolvedPath, "utf8"));
13703
13743
  } catch {
13704
13744
  }
13705
13745
  }
@@ -13760,10 +13800,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13760
13800
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
13761
13801
  `);
13762
13802
  } else {
13763
- mkdirSync12(dirname12(resolvedPath), { recursive: true });
13803
+ mkdirSync13(dirname12(resolvedPath), { recursive: true });
13764
13804
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
13765
13805
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
13766
- writeFileSync12(tmpPath, `${JSON.stringify(resolved, null, 2)}
13806
+ writeFileSync13(tmpPath, `${JSON.stringify(resolved, null, 2)}
13767
13807
  `);
13768
13808
  renameSync2(tmpPath, resolvedPath);
13769
13809
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -13777,8 +13817,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13777
13817
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
13778
13818
  `);
13779
13819
  } else {
13780
- mkdirSync12(dirname12(apiKeyFile), { recursive: true });
13781
- writeFileSync12(apiKeyFile, `${apiKeyVar}=${apiKey}
13820
+ mkdirSync13(dirname12(apiKeyFile), { recursive: true });
13821
+ writeFileSync13(apiKeyFile, `${apiKeyVar}=${apiKey}
13782
13822
  `, { mode: 384 });
13783
13823
  process.stderr.write(`API key written to ${apiKeyFile}
13784
13824
  `);
@@ -14285,6 +14325,38 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14285
14325
  if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
14286
14326
  break;
14287
14327
  }
14328
+ case "reload": {
14329
+ const sentinel = reloadSentinelFor(cfg.ship.stateDir);
14330
+ if (dryRun) {
14331
+ process.stdout.write(`would write ${sentinel}
14332
+ `);
14333
+ break;
14334
+ }
14335
+ const host = hostPlatform();
14336
+ const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
14337
+ const unitStatus = control?.statusCommand ? spawnSync5(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
14338
+ const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
14339
+ if (!unitRunning) {
14340
+ process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
14341
+ break;
14342
+ }
14343
+ mkdirSync13(cfg.ship.stateDir, { recursive: true });
14344
+ writeFileSync13(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
14345
+ `);
14346
+ process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
14347
+ if (flag("now") && control?.stopCommand && control.startCommand) {
14348
+ for (const cmd of [control.stopCommand, control.startCommand]) {
14349
+ const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
14350
+ if (cmd === control.startCommand && (res.error || res.status !== 0)) {
14351
+ process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
14352
+ `);
14353
+ process.exitCode = 1;
14354
+ }
14355
+ }
14356
+ process.stdout.write("crew reload: restarted the daemon unit now\n");
14357
+ }
14358
+ break;
14359
+ }
14288
14360
  case "uninstall": {
14289
14361
  const host = hostPlatform();
14290
14362
  if (host === "windows") {
@@ -14320,7 +14392,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14320
14392
  }
14321
14393
  case "log": {
14322
14394
  try {
14323
- const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
14395
+ const text = readFileSync16(cfg.ship.logFile, "utf8").split("\n");
14324
14396
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
14325
14397
  `);
14326
14398
  } catch {
@@ -14530,13 +14602,23 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
14530
14602
  );
14531
14603
  try {
14532
14604
  const evFile = eventFileFor(cfg.ship.stateDir);
14533
- const evSize = existsSync22(evFile) ? statSync9(evFile).size : 0;
14605
+ const evSize = existsSync23(evFile) ? statSync9(evFile).size : 0;
14534
14606
  process.stdout.write(
14535
14607
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
14536
14608
  ` : "events log: ok\n"
14537
14609
  );
14538
14610
  } catch {
14539
14611
  }
14612
+ const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
14613
+ if (daemonStartedAt !== null) {
14614
+ const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
14615
+ if (changed2) {
14616
+ process.stdout.write(
14617
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
14618
+ `
14619
+ );
14620
+ }
14621
+ }
14540
14622
  const tracker = new Tracker(route, cfg.ship);
14541
14623
  const contractProblems = validateContract(tracker.contract);
14542
14624
  process.stdout.write(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.36.0",
3
+ "version": "0.37.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",