@archme/cli 0.4.2 → 0.4.4

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 +2625 -322
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1020,15 +1020,15 @@ var require_windows = __commonJS({
1020
1020
  }
1021
1021
  return false;
1022
1022
  }
1023
- function checkStat(stat10, path7, options) {
1024
- if (!stat10.isSymbolicLink() && !stat10.isFile()) {
1023
+ function checkStat(stat11, path7, options) {
1024
+ if (!stat11.isSymbolicLink() && !stat11.isFile()) {
1025
1025
  return false;
1026
1026
  }
1027
1027
  return checkPathExt(path7, options);
1028
1028
  }
1029
1029
  function isexe(path7, options, cb) {
1030
- fs.stat(path7, function(er, stat10) {
1031
- cb(er, er ? false : checkStat(stat10, path7, options));
1030
+ fs.stat(path7, function(er, stat11) {
1031
+ cb(er, er ? false : checkStat(stat11, path7, options));
1032
1032
  });
1033
1033
  }
1034
1034
  function sync(path7, options) {
@@ -1044,20 +1044,20 @@ var require_mode = __commonJS({
1044
1044
  isexe.sync = sync;
1045
1045
  var fs = __require("fs");
1046
1046
  function isexe(path7, options, cb) {
1047
- fs.stat(path7, function(er, stat10) {
1048
- cb(er, er ? false : checkStat(stat10, options));
1047
+ fs.stat(path7, function(er, stat11) {
1048
+ cb(er, er ? false : checkStat(stat11, options));
1049
1049
  });
1050
1050
  }
1051
1051
  function sync(path7, options) {
1052
1052
  return checkStat(fs.statSync(path7), options);
1053
1053
  }
1054
- function checkStat(stat10, options) {
1055
- return stat10.isFile() && checkMode(stat10, options);
1054
+ function checkStat(stat11, options) {
1055
+ return stat11.isFile() && checkMode(stat11, options);
1056
1056
  }
1057
- function checkMode(stat10, options) {
1058
- var mod = stat10.mode;
1059
- var uid = stat10.uid;
1060
- var gid = stat10.gid;
1057
+ function checkMode(stat11, options) {
1058
+ var mod = stat11.mode;
1059
+ var uid = stat11.uid;
1060
+ var gid = stat11.gid;
1061
1061
  var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
1062
1062
  var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
1063
1063
  var u2 = parseInt("100", 8);
@@ -1091,12 +1091,12 @@ var require_isexe = __commonJS({
1091
1091
  if (typeof Promise !== "function") {
1092
1092
  throw new TypeError("callback not provided");
1093
1093
  }
1094
- return new Promise(function(resolve4, reject) {
1094
+ return new Promise(function(resolve5, reject) {
1095
1095
  isexe(path7, options || {}, function(er, is) {
1096
1096
  if (er) {
1097
1097
  reject(er);
1098
1098
  } else {
1099
- resolve4(is);
1099
+ resolve5(is);
1100
1100
  }
1101
1101
  });
1102
1102
  });
@@ -1162,27 +1162,27 @@ var require_which = __commonJS({
1162
1162
  opt = {};
1163
1163
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
1164
1164
  const found = [];
1165
- const step = (i2) => new Promise((resolve4, reject) => {
1165
+ const step = (i2) => new Promise((resolve5, reject) => {
1166
1166
  if (i2 === pathEnv.length)
1167
- return opt.all && found.length ? resolve4(found) : reject(getNotFoundError(cmd));
1167
+ return opt.all && found.length ? resolve5(found) : reject(getNotFoundError(cmd));
1168
1168
  const ppRaw = pathEnv[i2];
1169
1169
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
1170
1170
  const pCmd = path7.join(pathPart, cmd);
1171
1171
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
1172
- resolve4(subStep(p, i2, 0));
1172
+ resolve5(subStep(p, i2, 0));
1173
1173
  });
1174
- const subStep = (p, i2, ii) => new Promise((resolve4, reject) => {
1174
+ const subStep = (p, i2, ii) => new Promise((resolve5, reject) => {
1175
1175
  if (ii === pathExt.length)
1176
- return resolve4(step(i2 + 1));
1176
+ return resolve5(step(i2 + 1));
1177
1177
  const ext2 = pathExt[ii];
1178
1178
  isexe(p + ext2, { pathExt: pathExtExe }, (er, is) => {
1179
1179
  if (!er && is) {
1180
1180
  if (opt.all)
1181
1181
  found.push(p + ext2);
1182
1182
  else
1183
- return resolve4(p + ext2);
1183
+ return resolve5(p + ext2);
1184
1184
  }
1185
- return resolve4(subStep(p, i2, ii + 1));
1185
+ return resolve5(subStep(p, i2, ii + 1));
1186
1186
  });
1187
1187
  });
1188
1188
  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
@@ -1253,12 +1253,12 @@ var require_resolveCommand = __commonJS({
1253
1253
  if (shouldSwitchCwd) {
1254
1254
  try {
1255
1255
  process.chdir(parsed.options.cwd);
1256
- } catch (err) {
1256
+ } catch (err2) {
1257
1257
  }
1258
1258
  }
1259
- let resolved;
1259
+ let resolved2;
1260
1260
  try {
1261
- resolved = which.sync(parsed.command, {
1261
+ resolved2 = which.sync(parsed.command, {
1262
1262
  path: env[getPathKey({ env })],
1263
1263
  pathExt: withoutPathExt ? path7.delimiter : void 0
1264
1264
  });
@@ -1268,10 +1268,10 @@ var require_resolveCommand = __commonJS({
1268
1268
  process.chdir(cwd);
1269
1269
  }
1270
1270
  }
1271
- if (resolved) {
1272
- resolved = path7.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
1271
+ if (resolved2) {
1272
+ resolved2 = path7.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved2);
1273
1273
  }
1274
- return resolved;
1274
+ return resolved2;
1275
1275
  }
1276
1276
  function resolveCommand(parsed) {
1277
1277
  return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true);
@@ -1394,7 +1394,7 @@ var require_parse = __commonJS({
1394
1394
  }
1395
1395
  return parsed;
1396
1396
  }
1397
- function parse(command, args, options) {
1397
+ function parse2(command, args, options) {
1398
1398
  if (args && !Array.isArray(args)) {
1399
1399
  options = args;
1400
1400
  args = null;
@@ -1413,7 +1413,7 @@ var require_parse = __commonJS({
1413
1413
  };
1414
1414
  return options.shell ? parsed : parseNonShell(parsed);
1415
1415
  }
1416
- module.exports = parse;
1416
+ module.exports = parse2;
1417
1417
  }
1418
1418
  });
1419
1419
 
@@ -1438,9 +1438,9 @@ var require_enoent = __commonJS({
1438
1438
  const originalEmit = cp.emit;
1439
1439
  cp.emit = function(name, arg1) {
1440
1440
  if (name === "exit") {
1441
- const err = verifyENOENT(arg1, parsed);
1442
- if (err) {
1443
- return originalEmit.call(cp, "error", err);
1441
+ const err2 = verifyENOENT(arg1, parsed);
1442
+ if (err2) {
1443
+ return originalEmit.call(cp, "error", err2);
1444
1444
  }
1445
1445
  }
1446
1446
  return originalEmit.apply(cp, arguments);
@@ -1472,16 +1472,16 @@ var require_cross_spawn = __commonJS({
1472
1472
  "node_modules/cross-spawn/index.js"(exports, module) {
1473
1473
  "use strict";
1474
1474
  var cp = __require("child_process");
1475
- var parse = require_parse();
1475
+ var parse2 = require_parse();
1476
1476
  var enoent = require_enoent();
1477
1477
  function spawn2(command, args, options) {
1478
- const parsed = parse(command, args, options);
1478
+ const parsed = parse2(command, args, options);
1479
1479
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
1480
1480
  enoent.hookChildProcess(spawned, parsed);
1481
1481
  return spawned;
1482
1482
  }
1483
1483
  function spawnSync2(command, args, options) {
1484
- const parsed = parse(command, args, options);
1484
+ const parsed = parse2(command, args, options);
1485
1485
  const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
1486
1486
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
1487
1487
  return result;
@@ -1489,7 +1489,7 @@ var require_cross_spawn = __commonJS({
1489
1489
  module.exports = spawn2;
1490
1490
  module.exports.spawn = spawn2;
1491
1491
  module.exports.sync = spawnSync2;
1492
- module.exports._parse = parse;
1492
+ module.exports._parse = parse2;
1493
1493
  module.exports._enoent = enoent;
1494
1494
  }
1495
1495
  });
@@ -2262,8 +2262,8 @@ var init_deferred = __esm({
2262
2262
  "node_modules/execa/lib/utils/deferred.js"() {
2263
2263
  createDeferred = () => {
2264
2264
  const methods = {};
2265
- const promise = new Promise((resolve4, reject) => {
2266
- Object.assign(methods, { resolve: resolve4, reject });
2265
+ const promise = new Promise((resolve5, reject) => {
2266
+ Object.assign(methods, { resolve: resolve5, reject });
2267
2267
  });
2268
2268
  return Object.assign(promise, methods);
2269
2269
  };
@@ -4057,8 +4057,8 @@ function prettyMilliseconds(milliseconds, options) {
4057
4057
  const flooredValue = Math.round(flooredInterimValue) / 10 ** decimalDigits;
4058
4058
  return flooredValue.toFixed(decimalDigits);
4059
4059
  };
4060
- const add = (value, long, short, valueString) => {
4061
- if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short === "m")) {
4060
+ const add = (value, long, short4, valueString) => {
4061
+ if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short4 === "m")) {
4062
4062
  return;
4063
4063
  }
4064
4064
  valueString ??= String(value);
@@ -4067,7 +4067,7 @@ function prettyMilliseconds(milliseconds, options) {
4067
4067
  const minLength = result.length > 0 ? 2 : 1;
4068
4068
  valueString = "0".repeat(Math.max(0, minLength - wholeDigits)) + valueString;
4069
4069
  } else {
4070
- valueString += options.verbose ? " " + pluralize(long, value) : short;
4070
+ valueString += options.verbose ? " " + pluralize(long, value) : short4;
4071
4071
  }
4072
4072
  result.push(valueString);
4073
4073
  };
@@ -7560,11 +7560,11 @@ var init_concurrent = __esm({
7560
7560
  const promises = weakMap.get(stream);
7561
7561
  const promise = createDeferred();
7562
7562
  promises.push(promise);
7563
- const resolve4 = promise.resolve.bind(promise);
7564
- return { resolve: resolve4, promises };
7563
+ const resolve5 = promise.resolve.bind(promise);
7564
+ return { resolve: resolve5, promises };
7565
7565
  };
7566
- waitForConcurrentStreams = async ({ resolve: resolve4, promises }, subprocess) => {
7567
- resolve4();
7566
+ waitForConcurrentStreams = async ({ resolve: resolve5, promises }, subprocess) => {
7567
+ resolve5();
7568
7568
  const [isSubprocessExit] = await Promise.race([
7569
7569
  Promise.allSettled([true, subprocess]),
7570
7570
  Promise.all([false, ...promises])
@@ -8376,9 +8376,9 @@ async function saveCredentials(credentials) {
8376
8376
  next2.push(merged2);
8377
8377
  try {
8378
8378
  await keytar.setPassword(SERVICE_NAME, credentials.supabase_url, serializeStore(next2));
8379
- } catch (err) {
8380
- warnCredentialPersistFailure("macOS Keychain", err);
8381
- throw err;
8379
+ } catch (err2) {
8380
+ warnCredentialPersistFailure("macOS Keychain", err2);
8381
+ throw err2;
8382
8382
  }
8383
8383
  return { credentials: merged2, source: "keychain" };
8384
8384
  }
@@ -8391,8 +8391,8 @@ async function saveCredentials(credentials) {
8391
8391
  return { credentials: merged, source: "file", path: fallbackPath() };
8392
8392
  });
8393
8393
  }
8394
- function warnCredentialPersistFailure(target, err) {
8395
- const detail = err instanceof Error ? err.message : String(err);
8394
+ function warnCredentialPersistFailure(target, err2) {
8395
+ const detail = err2 instanceof Error ? err2.message : String(err2);
8396
8396
  console.error(
8397
8397
  `auth: CRITICAL \u2014 failed to persist credentials to ${target}: ${detail}. The rotated refresh token was NOT saved; the daemon keeps working on its in-memory token until it restarts, then may fail with "Refresh Token Not Found" because it reloads the previous, already-used refresh token. If a keychain password prompt was dismissed, grant access (or re-run \`archme login\`) before restarting.`
8398
8398
  );
@@ -8452,9 +8452,9 @@ async function writeFileStore(all) {
8452
8452
  await writeFile(fallbackPath(), serializeStore(all), { mode: 384 });
8453
8453
  await chmod(fallbackPath(), 384).catch(() => {
8454
8454
  });
8455
- } catch (err) {
8456
- warnCredentialPersistFailure(`file fallback (${fallbackPath()})`, err);
8457
- throw err;
8455
+ } catch (err2) {
8456
+ warnCredentialPersistFailure(`file fallback (${fallbackPath()})`, err2);
8457
+ throw err2;
8458
8458
  }
8459
8459
  }
8460
8460
  function describeCredentialSource(result) {
@@ -8518,8 +8518,8 @@ function resolveConfig() {
8518
8518
  if (!archmeDaemonClientId) missing.push("ARCHME_DAEMON_CLIENT_ID");
8519
8519
  if (!archmeMcpBaseUrl) missing.push("ARCHME_MCP_BASE_URL");
8520
8520
  if (missing.length > 0) {
8521
- const err = new MissingConfigError({ archmeEnv, missing });
8522
- throw err;
8521
+ const err2 = new MissingConfigError({ archmeEnv, missing });
8522
+ throw err2;
8523
8523
  }
8524
8524
  return {
8525
8525
  archmeEnv,
@@ -8596,12 +8596,12 @@ async function runLogin() {
8596
8596
  let SUPABASE_URL2;
8597
8597
  try {
8598
8598
  SUPABASE_URL2 = resolveConfig().supabaseUrl;
8599
- } catch (err) {
8600
- if (err instanceof MissingConfigError) {
8601
- console.error(err.message);
8599
+ } catch (err2) {
8600
+ if (err2 instanceof MissingConfigError) {
8601
+ console.error(err2.message);
8602
8602
  process.exit(1);
8603
8603
  }
8604
- throw err;
8604
+ throw err2;
8605
8605
  }
8606
8606
  function functionsBaseUrl() {
8607
8607
  return `${SUPABASE_URL2.replace(/\/$/, "")}/functions/v1/agents-device`;
@@ -8636,7 +8636,7 @@ async function runLogin() {
8636
8636
  const expiresAt = Date.now() + data.expires_in * 1e3;
8637
8637
  let intervalMs = Math.max(data.interval, 1) * 1e3;
8638
8638
  while (Date.now() < expiresAt) {
8639
- await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
8639
+ await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
8640
8640
  const poll = await postJson("/poll", {
8641
8641
  device_code: data.device_code
8642
8642
  });
@@ -8667,8 +8667,8 @@ var init_enroll = __esm({
8667
8667
  init_credentials();
8668
8668
  init_defaults();
8669
8669
  if (import.meta.url === `file://${process.argv[1]}`) {
8670
- runLogin().catch((err) => {
8671
- console.error(err instanceof Error ? err.message : err);
8670
+ runLogin().catch((err2) => {
8671
+ console.error(err2 instanceof Error ? err2.message : err2);
8672
8672
  process.exit(1);
8673
8673
  });
8674
8674
  }
@@ -8852,7 +8852,7 @@ var init_launchctl = __esm({
8852
8852
  if (r.exitCode === 0 || r.exitCode === 17) return;
8853
8853
  if (r.exitCode === 5) {
8854
8854
  await this.opts.spawner.run("launchctl", ["bootout", SERVICE_TARGET(this.opts.uid)]);
8855
- await new Promise((resolve4) => setTimeout(resolve4, 500));
8855
+ await new Promise((resolve5) => setTimeout(resolve5, 500));
8856
8856
  r = await bootstrap();
8857
8857
  if (r.exitCode === 0 || r.exitCode === 17) return;
8858
8858
  }
@@ -9222,16 +9222,16 @@ async function tailLog(opts = {}) {
9222
9222
  await handle.close();
9223
9223
  }
9224
9224
  offset = st.size;
9225
- } catch (err) {
9226
- if (err.code === "ENOENT") {
9225
+ } catch (err2) {
9226
+ if (err2.code === "ENOENT") {
9227
9227
  out.write(`(no log file yet at ${path7})
9228
9228
  `);
9229
9229
  } else {
9230
- throw err;
9230
+ throw err2;
9231
9231
  }
9232
9232
  }
9233
9233
  if (!opts.follow) return;
9234
- await new Promise((resolve4, reject) => {
9234
+ await new Promise((resolve5, reject) => {
9235
9235
  let fileWatcher = null;
9236
9236
  let dirWatcher = null;
9237
9237
  const dir = dirname6(path7);
@@ -9249,9 +9249,9 @@ async function tailLog(opts = {}) {
9249
9249
  });
9250
9250
  offset = st.size;
9251
9251
  }
9252
- } catch (err) {
9253
- if (err.code === "ENOENT") return;
9254
- reject(err);
9252
+ } catch (err2) {
9253
+ if (err2.code === "ENOENT") return;
9254
+ reject(err2);
9255
9255
  }
9256
9256
  };
9257
9257
  const attachFileWatcher = () => {
@@ -9259,9 +9259,9 @@ async function tailLog(opts = {}) {
9259
9259
  fileWatcher = watch(path7, (event) => {
9260
9260
  if (event === "change") void onChange();
9261
9261
  });
9262
- } catch (err) {
9263
- if (err.code !== "ENOENT") {
9264
- reject(err);
9262
+ } catch (err2) {
9263
+ if (err2.code !== "ENOENT") {
9264
+ reject(err2);
9265
9265
  return;
9266
9266
  }
9267
9267
  attachDirWatcher();
@@ -9278,24 +9278,24 @@ async function tailLog(opts = {}) {
9278
9278
  });
9279
9279
  }
9280
9280
  });
9281
- } catch (err) {
9282
- reject(err);
9281
+ } catch (err2) {
9282
+ reject(err2);
9283
9283
  }
9284
9284
  };
9285
9285
  stat(path7).then(
9286
9286
  () => attachFileWatcher(),
9287
- (err) => {
9288
- if (err.code === "ENOENT") {
9287
+ (err2) => {
9288
+ if (err2.code === "ENOENT") {
9289
9289
  attachDirWatcher();
9290
9290
  } else {
9291
- reject(err);
9291
+ reject(err2);
9292
9292
  }
9293
9293
  }
9294
9294
  );
9295
9295
  const handleSigint = () => {
9296
9296
  fileWatcher?.close();
9297
9297
  dirWatcher?.close();
9298
- resolve4();
9298
+ resolve5();
9299
9299
  };
9300
9300
  process.once("SIGINT", handleSigint);
9301
9301
  });
@@ -9495,6 +9495,83 @@ var init_env_scrub = __esm({
9495
9495
  }
9496
9496
  });
9497
9497
 
9498
+ // src/preflight.ts
9499
+ import { accessSync, constants as constants4 } from "node:fs";
9500
+ import { delimiter as pathDelimiter, join as join6 } from "node:path";
9501
+ function defaultIsExecutable(path7) {
9502
+ try {
9503
+ accessSync(path7, constants4.X_OK);
9504
+ return true;
9505
+ } catch {
9506
+ return false;
9507
+ }
9508
+ }
9509
+ function findOnPath(name, opts = {}) {
9510
+ const env = opts.env ?? process.env;
9511
+ const isExecutable = opts.isExecutable ?? defaultIsExecutable;
9512
+ const delim = opts.delimiter ?? pathDelimiter;
9513
+ const dirs = (env.PATH ?? "").split(delim).filter((d) => d.length > 0);
9514
+ if (dirs.length === 0) return null;
9515
+ const exts = env.PATHEXT ? ["", ...env.PATHEXT.split(";").filter(Boolean)] : [""];
9516
+ for (const dir of dirs) {
9517
+ for (const ext2 of exts) {
9518
+ const candidate = join6(dir, `${name}${ext2}`);
9519
+ if (isExecutable(candidate)) return candidate;
9520
+ }
9521
+ }
9522
+ return null;
9523
+ }
9524
+ function missingBinaryMessage(binary) {
9525
+ const spec = REQUIRED_BINARIES[binary];
9526
+ return [
9527
+ `\`${spec.name}\` was not found on your PATH.`,
9528
+ "",
9529
+ spec.why,
9530
+ "",
9531
+ "Install it, then re-run:",
9532
+ ` ${spec.install}`,
9533
+ "",
9534
+ `If \`${spec.name}\` is installed somewhere unusual, add its directory to PATH.`,
9535
+ "Note: the daemon service captures PATH at `archme daemon start`, so after",
9536
+ "changing PATH you must run `archme daemon restart` for it to take effect."
9537
+ ].join("\n");
9538
+ }
9539
+ function requireBinary(binary, opts = {}) {
9540
+ const cached = resolved.get(binary);
9541
+ if (cached) return cached;
9542
+ const found = findOnPath(binary, opts);
9543
+ if (!found) throw new MissingBinaryError(binary, missingBinaryMessage(binary));
9544
+ resolved.set(binary, found);
9545
+ return found;
9546
+ }
9547
+ var REQUIRED_BINARIES, MissingBinaryError, resolved;
9548
+ var init_preflight = __esm({
9549
+ "src/preflight.ts"() {
9550
+ "use strict";
9551
+ REQUIRED_BINARIES = {
9552
+ claude: {
9553
+ name: "claude",
9554
+ why: "archme runs Claude Code locally to read your diff and update the architecture.",
9555
+ install: "npm i -g @anthropic-ai/claude-code"
9556
+ },
9557
+ git: {
9558
+ name: "git",
9559
+ why: "archme clones and diffs repositories with git.",
9560
+ install: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
9561
+ }
9562
+ };
9563
+ MissingBinaryError = class extends Error {
9564
+ binary;
9565
+ constructor(binary, message) {
9566
+ super(message);
9567
+ this.name = "MissingBinaryError";
9568
+ this.binary = binary;
9569
+ }
9570
+ };
9571
+ resolved = /* @__PURE__ */ new Map();
9572
+ }
9573
+ });
9574
+
9498
9575
  // ../shared/daemonModel.ts
9499
9576
  var DEFAULT_CLAUDE_MODEL;
9500
9577
  var init_daemonModel = __esm({
@@ -9504,6 +9581,9 @@ var init_daemonModel = __esm({
9504
9581
  });
9505
9582
 
9506
9583
  // src/scan/spawn-claude.ts
9584
+ function stripNulBytes(arg) {
9585
+ return arg.includes("\0") ? arg.replaceAll("\0", "\uFFFD") : arg;
9586
+ }
9507
9587
  function buildClaudeArgs(opts) {
9508
9588
  const tools = (opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS).join(",");
9509
9589
  const args = [
@@ -9530,9 +9610,10 @@ function buildClaudeArgs(opts) {
9530
9610
  if (opts.model) {
9531
9611
  args.push("--model", opts.model);
9532
9612
  }
9533
- return args;
9613
+ return args.map(stripNulBytes);
9534
9614
  }
9535
9615
  function spawnClaude(opts) {
9616
+ requireBinary("claude");
9536
9617
  const env = scrubPrivilegedEnv(process.env);
9537
9618
  const args = buildClaudeArgs({ ...opts, model: opts.model || DEFAULT_CLAUDE_MODEL });
9538
9619
  if (opts.streamJson && !opts.interactive) {
@@ -9571,6 +9652,7 @@ var init_spawn_claude = __esm({
9571
9652
  init_execa();
9572
9653
  init_env_scrub();
9573
9654
  init_mcp_config();
9655
+ init_preflight();
9574
9656
  init_daemonModel();
9575
9657
  init_daemonModel();
9576
9658
  mcpTool = (name) => `mcp__${MCP_SERVER_NAME}__${name}`;
@@ -9743,7 +9825,7 @@ var init_digestPrompt = __esm({
9743
9825
  // src/daemon/digest.ts
9744
9826
  import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
9745
9827
  import { tmpdir as tmpdir2 } from "node:os";
9746
- import { join as join6 } from "node:path";
9828
+ import { join as join7 } from "node:path";
9747
9829
  function isCleanDigestExit(res) {
9748
9830
  return !res.timedOut && res.signal == null && res.exitCode === 0;
9749
9831
  }
@@ -9777,12 +9859,12 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9777
9859
  }
9778
9860
  let dir;
9779
9861
  try {
9780
- dir = await mkdtemp2(join6(tmpdir2(), "archme-digest-"));
9862
+ dir = await mkdtemp2(join7(tmpdir2(), "archme-digest-"));
9781
9863
  let mcpConfigPath;
9782
9864
  if (mcp) {
9783
9865
  mcpConfigPath = mcp.configPath;
9784
9866
  } else {
9785
- mcpConfigPath = join6(dir, "mcp.json");
9867
+ mcpConfigPath = join7(dir, "mcp.json");
9786
9868
  await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
9787
9869
  }
9788
9870
  const model = await resolveModelPin(ctx.supabase);
@@ -9813,8 +9895,8 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9813
9895
  }
9814
9896
  await completeDigest(ctx, job.id, deviceId, narrative);
9815
9897
  console.log(`[${job.id}] digest synthesized (${narrative.length} chars) for ${payload.projectName}`);
9816
- } catch (err) {
9817
- await failDigest(ctx, job.id, deviceId, err instanceof Error ? err.message : String(err));
9898
+ } catch (err2) {
9899
+ await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
9818
9900
  } finally {
9819
9901
  inFlight2?.delete(job.id);
9820
9902
  if (dir) await rm5(dir, { recursive: true, force: true }).catch(() => {
@@ -11774,8 +11856,8 @@ function extractParticipantsFromSequence(sequence) {
11774
11856
  return [...participants];
11775
11857
  }
11776
11858
  function componentScopeSegments(componentPath) {
11777
- const resolved = resolveWaypoint(componentPath, []);
11778
- return resolved.leafId ? [...resolved.scopePath, resolved.leafId] : resolved.scopePath;
11859
+ const resolved2 = resolveWaypoint(componentPath, []);
11860
+ return resolved2.leafId ? [...resolved2.scopePath, resolved2.leafId] : resolved2.scopePath;
11779
11861
  }
11780
11862
  function isPrefix(prefix, full) {
11781
11863
  return full.length >= prefix.length && prefix.every((s, i2) => full[i2] === s);
@@ -12270,10 +12352,10 @@ async function ensureBugsFolder(client, projectId) {
12270
12352
  const createdId = created?.id ?? null;
12271
12353
  if (createdId) cache.set(projectId, createdId);
12272
12354
  return createdId;
12273
- } catch (err) {
12355
+ } catch (err2) {
12274
12356
  console.warn(
12275
12357
  `[bugs-folder] could not resolve/create the "Bugs" folder for project ${projectId}; bug filing falls back to agent find-or-create:`,
12276
- err instanceof Error ? err.message : err
12358
+ err2 instanceof Error ? err2.message : err2
12277
12359
  );
12278
12360
  return null;
12279
12361
  }
@@ -12334,17 +12416,17 @@ var init_lang_detect = __esm({
12334
12416
 
12335
12417
  // src/analyze/tree-sitter/cache.ts
12336
12418
  import { mkdir as mkdir5, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
12337
- import { dirname as dirname7, join as join7 } from "node:path";
12419
+ import { dirname as dirname7, join as join8 } from "node:path";
12338
12420
  import { homedir as homedir3 } from "node:os";
12339
12421
  import { createHash } from "node:crypto";
12340
12422
  function defaultCacheRoot() {
12341
- return process.env.ARCHME_TOOLS_DIR ?? join7(homedir3(), ".archme", "tools");
12423
+ return process.env.ARCHME_TOOLS_DIR ?? join8(homedir3(), ".archme", "tools");
12342
12424
  }
12343
12425
  function grammarCacheDir(cacheRoot) {
12344
- return join7(cacheRoot, "grammars");
12426
+ return join8(cacheRoot, "grammars");
12345
12427
  }
12346
12428
  function cachedGrammarPath(cacheRoot, lang, version) {
12347
- return join7(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
12429
+ return join8(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
12348
12430
  }
12349
12431
  async function fileExists(path7) {
12350
12432
  try {
@@ -12354,6 +12436,12 @@ async function fileExists(path7) {
12354
12436
  return false;
12355
12437
  }
12356
12438
  }
12439
+ function describeFetchFailure(err2) {
12440
+ const top = err2 instanceof Error ? err2.message : String(err2);
12441
+ const cause = err2?.cause;
12442
+ const inner = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
12443
+ return inner && inner !== top ? `${top} (${inner})` : top;
12444
+ }
12357
12445
  async function ensureGrammar(opts) {
12358
12446
  const target = cachedGrammarPath(opts.cacheDir, opts.lang, opts.version);
12359
12447
  if (await fileExists(target)) {
@@ -12363,11 +12451,25 @@ async function ensureGrammar(opts) {
12363
12451
  const url = `${opts.urlBase}/${opts.filename}`;
12364
12452
  process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
12365
12453
  `);
12366
- const res = await fetch(url);
12454
+ let res;
12455
+ try {
12456
+ res = await fetch(url);
12457
+ } catch (err2) {
12458
+ throw new GrammarFetchError({
12459
+ lang: opts.lang,
12460
+ version: opts.version,
12461
+ url,
12462
+ reason: describeFetchFailure(err2),
12463
+ cause: err2
12464
+ });
12465
+ }
12367
12466
  if (!res.ok) {
12368
- throw new Error(
12369
- `failed to fetch grammar ${opts.lang}@${opts.version}: HTTP ${res.status}`
12370
- );
12467
+ throw new GrammarFetchError({
12468
+ lang: opts.lang,
12469
+ version: opts.version,
12470
+ url,
12471
+ reason: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
12472
+ });
12371
12473
  }
12372
12474
  const buf = Buffer.from(await res.arrayBuffer());
12373
12475
  const actualHash = createHash("sha256").update(buf).digest("hex");
@@ -12379,9 +12481,41 @@ async function ensureGrammar(opts) {
12379
12481
  await writeFile6(target, buf);
12380
12482
  return { path: target, downloaded: true };
12381
12483
  }
12484
+ var GrammarFetchError;
12382
12485
  var init_cache = __esm({
12383
12486
  "src/analyze/tree-sitter/cache.ts"() {
12384
12487
  "use strict";
12488
+ GrammarFetchError = class extends Error {
12489
+ lang;
12490
+ url;
12491
+ constructor(opts) {
12492
+ super(
12493
+ [
12494
+ `Failed to download the ${opts.lang} tree-sitter grammar (${opts.lang}@${opts.version}).`,
12495
+ ` URL: ${opts.url}`,
12496
+ ` Reason: ${opts.reason}`,
12497
+ "",
12498
+ "archme fetches language grammars on first use and caches them under",
12499
+ "~/.archme/tools/grammars (override with ARCHME_TOOLS_DIR). This host could",
12500
+ "not reach the CDN.",
12501
+ "",
12502
+ "If you're behind a proxy, export it before re-running:",
12503
+ " export HTTPS_PROXY=http://proxy.example.com:8080",
12504
+ " export HTTP_PROXY=$HTTPS_PROXY",
12505
+ "",
12506
+ "Then warm the cache once (it is reused by every later run):",
12507
+ " archme analyze prefetch --all",
12508
+ "",
12509
+ "Offline/air-gapped: copy a populated ~/.archme/tools/grammars directory",
12510
+ "from a machine that has network access, or point ARCHME_TOOLS_DIR at one."
12511
+ ].join("\n"),
12512
+ opts.cause === void 0 ? void 0 : { cause: opts.cause }
12513
+ );
12514
+ this.name = "GrammarFetchError";
12515
+ this.lang = opts.lang;
12516
+ this.url = opts.url;
12517
+ }
12518
+ };
12385
12519
  }
12386
12520
  });
12387
12521
 
@@ -12864,13 +12998,13 @@ async function outlineSource(source, file) {
12864
12998
  confidence: 0.8,
12865
12999
  data: { file, symbols }
12866
13000
  });
12867
- } catch (err) {
13001
+ } catch (err2) {
12868
13002
  return envelope({
12869
13003
  tool: "file",
12870
13004
  tier: 0,
12871
13005
  confidence: 0.2,
12872
13006
  data: { file, symbols: [] },
12873
- warnings: [`parse failed, falling back to file tier: ${err.message}`]
13007
+ warnings: [`parse failed, falling back to file tier: ${err2.message}`]
12874
13008
  });
12875
13009
  }
12876
13010
  }
@@ -12887,13 +13021,13 @@ async function runOutline(file) {
12887
13021
  let source;
12888
13022
  try {
12889
13023
  source = await readFile6(file, "utf8");
12890
- } catch (err) {
13024
+ } catch (err2) {
12891
13025
  return envelope({
12892
13026
  tool: "file",
12893
13027
  tier: 0,
12894
13028
  confidence: 0,
12895
13029
  data: { file, symbols: [] },
12896
- errors: [`cannot read ${file}: ${err.message}`]
13030
+ errors: [`cannot read ${file}: ${err2.message}`]
12897
13031
  });
12898
13032
  }
12899
13033
  return outlineSource(source, file);
@@ -13496,11 +13630,11 @@ var require_ignore = __commonJS({
13496
13630
 
13497
13631
  // src/analyze/repo-walk.ts
13498
13632
  import { readdir, readFile as readFile7, stat as stat4 } from "node:fs/promises";
13499
- import { join as join8, relative } from "node:path";
13633
+ import { join as join9, relative } from "node:path";
13500
13634
  async function loadGitignore(root) {
13501
13635
  const ig = (0, import_ignore.default)();
13502
13636
  try {
13503
- const text = await readFile7(join8(root, ".gitignore"), "utf8");
13637
+ const text = await readFile7(join9(root, ".gitignore"), "utf8");
13504
13638
  ig.add(text);
13505
13639
  } catch {
13506
13640
  ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
@@ -13514,7 +13648,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
13514
13648
  const entries = await readdir(dir, { withFileTypes: true });
13515
13649
  for (const e of entries) {
13516
13650
  if (ALWAYS_SKIP.has(e.name)) continue;
13517
- const full = join8(dir, e.name);
13651
+ const full = join9(dir, e.name);
13518
13652
  const rel = relative(root, full);
13519
13653
  if (e.isDirectory()) {
13520
13654
  if (ig.ignores(rel + "/")) continue;
@@ -13538,7 +13672,7 @@ async function collectRepoFiles(root, filter2 = {}) {
13538
13672
  }
13539
13673
  async function gitListFiles(root) {
13540
13674
  try {
13541
- await stat4(join8(root, ".git"));
13675
+ await stat4(join9(root, ".git"));
13542
13676
  } catch {
13543
13677
  return null;
13544
13678
  }
@@ -13579,11 +13713,11 @@ var init_repo_walk = __esm({
13579
13713
  });
13580
13714
 
13581
13715
  // src/analyze/commands/repo.ts
13582
- import { join as join9 } from "node:path";
13716
+ import { join as join10 } from "node:path";
13583
13717
  async function* walkRepo(root, opts = {}) {
13584
13718
  const files = await collectRepoFilesWithGit(root, opts);
13585
13719
  for (const rel of files) {
13586
- const env = await runOutline(join9(root, rel));
13720
+ const env = await runOutline(join10(root, rel));
13587
13721
  yield env;
13588
13722
  }
13589
13723
  }
@@ -13597,9 +13731,9 @@ var init_repo = __esm({
13597
13731
 
13598
13732
  // src/analyze/matchers/file.ts
13599
13733
  import { stat as stat5 } from "node:fs/promises";
13600
- import { join as join10 } from "node:path";
13734
+ import { join as join11 } from "node:path";
13601
13735
  async function matchFileAnchor(anchor, repoRoot) {
13602
- const full = join10(repoRoot, anchor.path);
13736
+ const full = join11(repoRoot, anchor.path);
13603
13737
  try {
13604
13738
  const s = await stat5(full);
13605
13739
  if (!s.isFile()) {
@@ -13630,9 +13764,9 @@ var init_file = __esm({
13630
13764
 
13631
13765
  // src/analyze/matchers/directory.ts
13632
13766
  import { stat as stat6 } from "node:fs/promises";
13633
- import { join as join11 } from "node:path";
13767
+ import { join as join12 } from "node:path";
13634
13768
  async function matchDirectoryAnchor(anchor, repoRoot) {
13635
- const full = join11(repoRoot, anchor.path);
13769
+ const full = join12(repoRoot, anchor.path);
13636
13770
  try {
13637
13771
  const s = await stat6(full);
13638
13772
  if (!s.isDirectory()) {
@@ -13701,9 +13835,9 @@ var init_findSymbol = __esm({
13701
13835
 
13702
13836
  // src/analyze/matchers/symbol.ts
13703
13837
  import { stat as stat7 } from "node:fs/promises";
13704
- import { join as join12 } from "node:path";
13838
+ import { join as join13 } from "node:path";
13705
13839
  async function matchSymbolAnchor(anchor, repoRoot) {
13706
- const full = join12(repoRoot, anchor.path);
13840
+ const full = join13(repoRoot, anchor.path);
13707
13841
  try {
13708
13842
  await stat7(full);
13709
13843
  } catch {
@@ -13805,8 +13939,8 @@ async function runPrefetch(langs) {
13805
13939
  } else {
13806
13940
  result.skipped.push(lang);
13807
13941
  }
13808
- } catch (err) {
13809
- result.errors.push({ lang, message: err.message });
13942
+ } catch (err2) {
13943
+ result.errors.push({ lang, message: err2.message });
13810
13944
  }
13811
13945
  }
13812
13946
  return result;
@@ -14027,8 +14161,8 @@ async function acquireSingleInstanceLock(pidfile) {
14027
14161
  await mkdir6(dirname8(pidfile), { recursive: true });
14028
14162
  try {
14029
14163
  await writeFile7(pidfile, String(process.pid), { flag: "wx" });
14030
- } catch (err) {
14031
- if (err.code !== "EEXIST") throw err;
14164
+ } catch (err2) {
14165
+ if (err2.code !== "EEXIST") throw err2;
14032
14166
  }
14033
14167
  let release;
14034
14168
  try {
@@ -14097,7 +14231,7 @@ var init_semaphore = __esm({
14097
14231
  acquire() {
14098
14232
  const slot = this.tryAcquire();
14099
14233
  if (slot) return Promise.resolve(slot);
14100
- return new Promise((resolve4) => this.waiters.push(resolve4));
14234
+ return new Promise((resolve5) => this.waiters.push(resolve5));
14101
14235
  }
14102
14236
  makeRelease() {
14103
14237
  let released = false;
@@ -14144,11 +14278,11 @@ async function refreshOAuthCredentialsWith(refreshToken, cfg) {
14144
14278
  client_id: cfg.clientId
14145
14279
  })
14146
14280
  });
14147
- } catch (err) {
14281
+ } catch (err2) {
14148
14282
  return {
14149
14283
  ok: false,
14150
14284
  permanent: false,
14151
- error: `network error: ${err instanceof Error ? err.message : String(err)}`
14285
+ error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
14152
14286
  };
14153
14287
  }
14154
14288
  const body = await parseJsonResponse(response);
@@ -14182,11 +14316,11 @@ async function getOAuthUserInfoWith(accessToken, cfg) {
14182
14316
  function resolveUserId(userInfo2, storedUserId) {
14183
14317
  return userInfo2.sub ?? storedUserId ?? null;
14184
14318
  }
14185
- function isAuthError(err) {
14186
- if (!err || typeof err !== "object") return false;
14187
- const msg = ("message" in err ? String(err.message) : "").toLowerCase();
14188
- const code = ("code" in err ? String(err.code) : "").toLowerCase();
14189
- const status = "status" in err ? Number(err.status) : 0;
14319
+ function isAuthError(err2) {
14320
+ if (!err2 || typeof err2 !== "object") return false;
14321
+ const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
14322
+ const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
14323
+ const status = "status" in err2 ? Number(err2.status) : 0;
14190
14324
  return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
14191
14325
  }
14192
14326
  async function withAuthRetry(label, fn, reLogin) {
@@ -14195,8 +14329,8 @@ async function withAuthRetry(label, fn, reLogin) {
14195
14329
  console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
14196
14330
  try {
14197
14331
  await reLogin();
14198
- } catch (err) {
14199
- console.error(`${label}: re-login failed:`, err);
14332
+ } catch (err2) {
14333
+ console.error(`${label}: re-login failed:`, err2);
14200
14334
  return first;
14201
14335
  }
14202
14336
  return await fn();
@@ -14207,8 +14341,8 @@ async function fetchWithAuthRetry(label, doFetch, reLogin) {
14207
14341
  console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
14208
14342
  try {
14209
14343
  await reLogin();
14210
- } catch (err) {
14211
- console.error(`${label}: re-login failed:`, err);
14344
+ } catch (err2) {
14345
+ console.error(`${label}: re-login failed:`, err2);
14212
14346
  return first;
14213
14347
  }
14214
14348
  return await doFetch();
@@ -14315,8 +14449,8 @@ var init_sessionAuth = __esm({
14315
14449
  for (const l of this.listeners) {
14316
14450
  try {
14317
14451
  l(token);
14318
- } catch (err) {
14319
- console.error("sessionAuth: listener error:", err);
14452
+ } catch (err2) {
14453
+ console.error("sessionAuth: listener error:", err2);
14320
14454
  }
14321
14455
  }
14322
14456
  }
@@ -14362,7 +14496,7 @@ var init_sessionAuth = __esm({
14362
14496
  armRefreshTimer(delayMs) {
14363
14497
  if (this.refreshTimer) clearTimeout(this.refreshTimer);
14364
14498
  this.refreshTimer = setTimeout(() => {
14365
- this.runScheduledRefresh().catch((err) => console.error("auth: token refresh failed:", err));
14499
+ this.runScheduledRefresh().catch((err2) => console.error("auth: token refresh failed:", err2));
14366
14500
  }, delayMs);
14367
14501
  }
14368
14502
  // Translate a token lifetime (seconds) into the timer delay: fire 5 min before
@@ -14404,9 +14538,9 @@ var init_sessionAuth = __esm({
14404
14538
  try {
14405
14539
  await this.coalescedRefresh("token refresh");
14406
14540
  this.rearmFromLiveExpiry();
14407
- } catch (err) {
14408
- const detail = err instanceof Error ? err.message : String(err);
14409
- const permanent = err instanceof LoadStoredAndRefreshError && err.permanent;
14541
+ } catch (err2) {
14542
+ const detail = err2 instanceof Error ? err2.message : String(err2);
14543
+ const permanent = err2 instanceof LoadStoredAndRefreshError && err2.permanent;
14410
14544
  if (permanent) {
14411
14545
  console.error(`auth: token refresh failed permanently; auto-refresh stopped: ${detail}`);
14412
14546
  return;
@@ -14750,9 +14884,13 @@ function rateLimitDeferPlan(text, nowMs = Date.now(), defaultTimeZone = localTim
14750
14884
  }
14751
14885
  return null;
14752
14886
  }
14887
+ function stripPromptEcho(haystack, prompt) {
14888
+ if (!prompt || prompt.length < 40) return haystack;
14889
+ return haystack.split(prompt).join(" ");
14890
+ }
14753
14891
  function rateLimitDeferPlanForExit(channels, nowMs = Date.now(), defaultTimeZone = localTimeZone()) {
14754
- const haystack = `${channels.message}
14755
- ${channels.stderr}`;
14892
+ const haystack = stripPromptEcho(`${channels.message}
14893
+ ${channels.stderr}`, channels.prompt);
14756
14894
  return rateLimitDeferPlan(haystack, nowMs, defaultTimeZone);
14757
14895
  }
14758
14896
  var RATE_LIMIT_RE, MONTHS, RATE_LIMIT_TENTATIVE_PAUSE_MS, DATED_RESET_ROLLOVER_GRACE_MS, RATE_LIMIT_PAUSE_PADDING_MS, RATE_LIMIT_SHAPED_RE;
@@ -14781,15 +14919,50 @@ var init_rateLimit = __esm({
14781
14919
  }
14782
14920
  });
14783
14921
 
14922
+ // src/packageRoot.ts
14923
+ import { readFileSync as readFileSync4 } from "node:fs";
14924
+ import { dirname as dirname9, join as join14, parse } from "node:path";
14925
+ function defaultReadFile(path7) {
14926
+ return readFileSync4(path7, "utf8");
14927
+ }
14928
+ function findPackageRoot2(startDir, opts = {}) {
14929
+ const readFile14 = opts.readFile ?? defaultReadFile;
14930
+ const stopAt = parse(startDir).root;
14931
+ let dir = startDir;
14932
+ for (; ; ) {
14933
+ try {
14934
+ const parsed = JSON.parse(readFile14(join14(dir, "package.json")));
14935
+ if (parsed.name === PACKAGE_NAME) return dir;
14936
+ } catch {
14937
+ }
14938
+ if (dir === stopAt) return null;
14939
+ const parent = dirname9(dir);
14940
+ if (parent === dir) return null;
14941
+ dir = parent;
14942
+ }
14943
+ }
14944
+ var PACKAGE_NAME;
14945
+ var init_packageRoot = __esm({
14946
+ "src/packageRoot.ts"() {
14947
+ "use strict";
14948
+ PACKAGE_NAME = "@archme/cli";
14949
+ }
14950
+ });
14951
+
14784
14952
  // src/daemon/version.ts
14953
+ var version_exports = {};
14954
+ __export(version_exports, {
14955
+ readDaemonVersion: () => readDaemonVersion
14956
+ });
14785
14957
  import { readFile as readFile10 } from "node:fs/promises";
14786
- import { dirname as dirname9, join as join13 } from "node:path";
14958
+ import { dirname as dirname10, join as join15 } from "node:path";
14787
14959
  import { fileURLToPath as fileURLToPath4 } from "node:url";
14788
14960
  async function readDaemonVersion() {
14789
14961
  try {
14790
- const here = dirname9(fileURLToPath4(import.meta.url));
14791
- const pkgPath = join13(here, "..", "..", "package.json");
14792
- const raw = await readFile10(pkgPath, "utf8");
14962
+ const here = dirname10(fileURLToPath4(import.meta.url));
14963
+ const root = findPackageRoot2(here);
14964
+ if (!root) return null;
14965
+ const raw = await readFile10(join15(root, "package.json"), "utf8");
14793
14966
  const parsed = JSON.parse(raw);
14794
14967
  return typeof parsed.version === "string" ? parsed.version : null;
14795
14968
  } catch {
@@ -14799,12 +14972,13 @@ async function readDaemonVersion() {
14799
14972
  var init_version = __esm({
14800
14973
  "src/daemon/version.ts"() {
14801
14974
  "use strict";
14975
+ init_packageRoot();
14802
14976
  }
14803
14977
  });
14804
14978
 
14805
14979
  // src/daemon/materializeDiff.ts
14806
14980
  import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
14807
- import { join as join14 } from "node:path";
14981
+ import { join as join16 } from "node:path";
14808
14982
  import { createHash as createHash2 } from "node:crypto";
14809
14983
  function splitPatchByFile(patch) {
14810
14984
  if (!patch.trim()) return [];
@@ -14889,18 +15063,18 @@ function buildManifest(files, rangeLabel) {
14889
15063
  }
14890
15064
  async function materializeDiff(worktreeDir, patch, rangeLabel) {
14891
15065
  const files = splitPatchByFile(patch);
14892
- const dir = join14(worktreeDir, CHANGES_DIR);
14893
- const byFileDir = join14(dir, "by-file");
15066
+ const dir = join16(worktreeDir, CHANGES_DIR);
15067
+ const byFileDir = join16(dir, "by-file");
14894
15068
  await mkdir7(byFileDir, { recursive: true });
14895
- await writeFile8(join14(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
15069
+ await writeFile8(join16(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
14896
15070
  `, "utf8");
14897
15071
  for (const f of files) {
14898
15072
  const rel = `by-file/${hashPath(f.path)}.diff`;
14899
15073
  f.diffPath = rel;
14900
- await writeFile8(join14(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
15074
+ await writeFile8(join16(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
14901
15075
  `, "utf8");
14902
15076
  }
14903
- await writeFile8(join14(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
15077
+ await writeFile8(join16(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
14904
15078
  return { dir, fileCount: files.length };
14905
15079
  }
14906
15080
  var CHANGES_DIR, STATUS_LABEL;
@@ -14912,6 +15086,583 @@ var init_materializeDiff = __esm({
14912
15086
  }
14913
15087
  });
14914
15088
 
15089
+ // src/daemon/branchMarker.ts
15090
+ async function readBranchMarker(client, projectId, archmeBranch) {
15091
+ const { data, error } = await client.from("branches").select("last_source_sha, last_source_repo").eq("project_id", projectId).eq("name", archmeBranch).is("deleted_at", null).maybeSingle();
15092
+ if (error || !data) return { sha: null, repo: null };
15093
+ return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
15094
+ }
15095
+ var init_branchMarker = __esm({
15096
+ "src/daemon/branchMarker.ts"() {
15097
+ "use strict";
15098
+ }
15099
+ });
15100
+
15101
+ // src/daemon/localRepos.ts
15102
+ import { chmod as chmod2, mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
15103
+ import { homedir as homedir4 } from "node:os";
15104
+ import { dirname as dirname11, join as join17 } from "node:path";
15105
+ function localReposPath(env = process.env) {
15106
+ return env.ARCHME_LOCAL_REPOS_PATH ?? join17(homedir4(), ".archme", "repos.json");
15107
+ }
15108
+ function isEntry(v) {
15109
+ if (!v || typeof v !== "object") return false;
15110
+ const o2 = v;
15111
+ return REQUIRED_KEYS.every((k) => typeof o2[k] === "string" && o2[k].length > 0);
15112
+ }
15113
+ function parseLocalRepos(raw) {
15114
+ let parsed;
15115
+ try {
15116
+ parsed = JSON.parse(raw);
15117
+ } catch {
15118
+ return [];
15119
+ }
15120
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
15121
+ const env = parsed;
15122
+ if (env.v !== LOCAL_REPOS_VERSION) return [];
15123
+ if (!Array.isArray(env.repos)) return [];
15124
+ return env.repos.filter(isEntry).map((e) => {
15125
+ const entry = {
15126
+ watchedRepoId: e.watchedRepoId,
15127
+ mirrorPath: e.mirrorPath,
15128
+ sourcePath: e.sourcePath,
15129
+ repoOwner: e.repoOwner,
15130
+ repoName: e.repoName
15131
+ };
15132
+ const raw2 = e;
15133
+ if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
15134
+ if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
15135
+ if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
15136
+ return entry;
15137
+ });
15138
+ }
15139
+ function serializeLocalRepos(repos) {
15140
+ return JSON.stringify({ v: LOCAL_REPOS_VERSION, repos }, null, 2);
15141
+ }
15142
+ function keyOf(e) {
15143
+ return e.watchedRepoId;
15144
+ }
15145
+ function upsertLocalRepo(repos, entry) {
15146
+ const k = keyOf(entry);
15147
+ const next = repos.filter((r) => keyOf(r) !== k);
15148
+ next.push(entry);
15149
+ return next;
15150
+ }
15151
+ function slugOf(e) {
15152
+ return `${e.repoOwner}/${e.repoName}`;
15153
+ }
15154
+ function findLocalReposBySlug(repos, slug) {
15155
+ const want = slug.toLowerCase();
15156
+ return repos.filter((r) => slugOf(r).toLowerCase() === want);
15157
+ }
15158
+ function removeLocalReposBySlug(repos, slug) {
15159
+ const want = slug.toLowerCase();
15160
+ return repos.filter((r) => slugOf(r).toLowerCase() !== want);
15161
+ }
15162
+ function markHalted(repos, watchedRepoId, reason, at, code) {
15163
+ return repos.map(
15164
+ (r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
15165
+ );
15166
+ }
15167
+ function clearHalt(repos, watchedRepoId) {
15168
+ return repos.map((r) => {
15169
+ if (r.watchedRepoId !== watchedRepoId) return r;
15170
+ const { haltedAt: _at, haltReason: _reason, haltCode: _code, ...rest } = r;
15171
+ return rest;
15172
+ });
15173
+ }
15174
+ function findLocalRepoForJob(repos, job) {
15175
+ if (!job.watched_repo_id) return null;
15176
+ const match2 = repos.find((r) => r.watchedRepoId === job.watched_repo_id);
15177
+ if (!match2) return null;
15178
+ const sameRepo = match2.repoOwner.toLowerCase() === job.repo_owner.toLowerCase() && match2.repoName.toLowerCase() === job.repo_name.toLowerCase();
15179
+ return sameRepo ? match2 : null;
15180
+ }
15181
+ async function loadLocalRepos(env = process.env) {
15182
+ try {
15183
+ return parseLocalRepos(await readFile11(localReposPath(env), "utf8"));
15184
+ } catch {
15185
+ return [];
15186
+ }
15187
+ }
15188
+ async function saveLocalRepos(repos, env = process.env) {
15189
+ const path7 = localReposPath(env);
15190
+ await mkdir8(dirname11(path7), { recursive: true, mode: 448 });
15191
+ await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
15192
+ await chmod2(path7, 384).catch(() => {
15193
+ });
15194
+ }
15195
+ async function registerLocalRepo(entry, env = process.env) {
15196
+ await saveLocalRepos(upsertLocalRepo(await loadLocalRepos(env), entry), env);
15197
+ }
15198
+ var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
15199
+ var init_localRepos = __esm({
15200
+ "src/daemon/localRepos.ts"() {
15201
+ "use strict";
15202
+ LOCAL_REPOS_VERSION = 1;
15203
+ REQUIRED_KEYS = [
15204
+ "watchedRepoId",
15205
+ "mirrorPath",
15206
+ "sourcePath",
15207
+ "repoOwner",
15208
+ "repoName"
15209
+ ];
15210
+ }
15211
+ });
15212
+
15213
+ // ../shared/commitAdvance.ts
15214
+ function planAdvance(input) {
15215
+ const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
15216
+ if (!marker) return { kind: "bootstrap-needed" };
15217
+ if (marker === upstreamTip) return { kind: "up-to-date" };
15218
+ if (markerIsAncestorOfTip) {
15219
+ return firstParentShas.length === 0 ? { kind: "force-push", tip: upstreamTip } : { kind: "fast-forward", shas: firstParentShas };
15220
+ }
15221
+ if (tipIsAncestorOfMarker) return { kind: "upstream-behind", tip: upstreamTip };
15222
+ return { kind: "force-push", tip: upstreamTip };
15223
+ }
15224
+ var init_commitAdvance = __esm({
15225
+ "../shared/commitAdvance.ts"() {
15226
+ }
15227
+ });
15228
+
15229
+ // ../shared/commitGate.ts
15230
+ function decideGate(input) {
15231
+ const { targetSha, markerSha, job, elapsedMs, timeoutMs } = input;
15232
+ const pushedNewRef = input.pushedNewRef ?? true;
15233
+ const noopGraceMs = input.noopGraceMs ?? 5 * 60 * 1e3;
15234
+ if (markerSha && markerSha === targetSha) {
15235
+ return {
15236
+ action: "advance",
15237
+ code: "marker-advanced",
15238
+ reason: "marker advanced to target (clean completion)"
15239
+ };
15240
+ }
15241
+ if (job && (job.status === "failed" || job.status === "cancelled")) {
15242
+ return {
15243
+ action: "stop",
15244
+ code: "job-terminal",
15245
+ reason: `job ${job.status} after ${job.attemptCount} attempt(s); marker did not advance \u2014 re-push commit ${short(targetSha)} (fresh delivery) or admin-requeue, do not fold forward`
15246
+ };
15247
+ }
15248
+ if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
15249
+ return {
15250
+ action: "stop",
15251
+ code: "noop-push-no-job",
15252
+ reason: `no webhook fired for ${short(targetSha)} (fork ref already there) and no job appeared in ${Math.round(noopGraceMs / 1e3)}s \u2014 likely a lost webhook or rolled-back job; re-push with a fresh delivery (ref-bounce) or admin-requeue`
15253
+ };
15254
+ }
15255
+ if (elapsedMs >= timeoutMs) {
15256
+ return {
15257
+ action: "stop",
15258
+ code: "timeout",
15259
+ reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short(targetSha)} (job: ${job ? job.status : "not enqueued"})`
15260
+ };
15261
+ }
15262
+ return {
15263
+ action: "wait",
15264
+ code: "waiting",
15265
+ reason: job ? `job ${job.status}` : "job not enqueued yet"
15266
+ };
15267
+ }
15268
+ function short(sha) {
15269
+ return sha.slice(0, 7);
15270
+ }
15271
+ var init_commitGate = __esm({
15272
+ "../shared/commitGate.ts"() {
15273
+ }
15274
+ });
15275
+
15276
+ // src/daemon/localPoll.ts
15277
+ function planPollTick(input) {
15278
+ const { marker, repo } = input;
15279
+ if (!marker.sha) {
15280
+ return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
15281
+ }
15282
+ if (marker.repo !== repo) {
15283
+ return {
15284
+ kind: "halt",
15285
+ code: "job-terminal",
15286
+ reason: `marker repo ${marker.repo ?? "n/a"} does not match ${repo} \u2014 local mode requires a github.com origin (re-run \`archme init\` if the remote changed)`
15287
+ };
15288
+ }
15289
+ const plan = planAdvance({
15290
+ marker: marker.sha,
15291
+ upstreamTip: input.upstreamTip,
15292
+ markerIsAncestorOfTip: input.markerIsAncestorOfTip,
15293
+ tipIsAncestorOfMarker: input.tipIsAncestorOfMarker,
15294
+ firstParentShas: input.firstParentShas
15295
+ });
15296
+ let target;
15297
+ switch (plan.kind) {
15298
+ case "bootstrap-needed":
15299
+ return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
15300
+ case "up-to-date":
15301
+ return { kind: "idle", reason: "up to date" };
15302
+ case "upstream-behind":
15303
+ return {
15304
+ kind: "idle",
15305
+ reason: `upstream is behind the marker (reset to ${short2(plan.tip)}); nothing to do`
15306
+ };
15307
+ case "force-push":
15308
+ target = plan.tip;
15309
+ break;
15310
+ case "fast-forward":
15311
+ target = plan.shas[0];
15312
+ break;
15313
+ }
15314
+ if (!input.jobForTarget) {
15315
+ return { kind: "enqueue", beforeSha: marker.sha, afterSha: target };
15316
+ }
15317
+ const decision = decideGate({
15318
+ targetSha: target,
15319
+ markerSha: marker.sha,
15320
+ job: { status: input.jobForTarget.status, attemptCount: input.jobForTarget.attemptCount },
15321
+ elapsedMs: input.nowMs - input.jobForTarget.createdAtMs,
15322
+ timeoutMs: input.gateTimeoutMs,
15323
+ // The poller enqueues directly, so the "no-op push fired no webhook" case
15324
+ // decideGate guards for cannot arise here.
15325
+ pushedNewRef: true
15326
+ });
15327
+ switch (decision.action) {
15328
+ case "stop":
15329
+ return {
15330
+ kind: "halt",
15331
+ code: decision.code,
15332
+ reason: localHaltReason(decision, input.jobForTarget, target)
15333
+ };
15334
+ case "advance":
15335
+ return { kind: "idle", reason: decision.reason };
15336
+ case "wait":
15337
+ return { kind: "idle", reason: `${short2(target)}: ${decision.reason}` };
15338
+ }
15339
+ }
15340
+ function localHaltReason(decision, job, targetSha) {
15341
+ if (decision.code !== "job-terminal" || !job) return decision.reason;
15342
+ return `commit ${short2(targetSha)} could not be processed (job ${job.status} after ${job.attemptCount} attempt(s)). Polling has stopped here rather than skipping it, because skipping would fold its changes into the next update.
15343
+ This one needs us \u2014 the failure is recorded against your account and visible to the ArchMe team. Get in touch if it is blocking you.`;
15344
+ }
15345
+ function short2(sha) {
15346
+ return sha.slice(0, 7);
15347
+ }
15348
+ var init_localPoll = __esm({
15349
+ "src/daemon/localPoll.ts"() {
15350
+ "use strict";
15351
+ init_commitAdvance();
15352
+ init_commitGate();
15353
+ }
15354
+ });
15355
+
15356
+ // src/daemon/defaultBranch.ts
15357
+ function pickDefaultBranch(input) {
15358
+ const onOrigin = new Set(input.remoteBranches);
15359
+ if (input.symbolicRef) {
15360
+ const slash = input.symbolicRef.indexOf("/");
15361
+ const named = slash >= 0 ? input.symbolicRef.slice(slash + 1) : null;
15362
+ if (named && onOrigin.has(named)) return named;
15363
+ }
15364
+ for (const candidate of CONVENTIONAL) {
15365
+ if (onOrigin.has(candidate)) return candidate;
15366
+ }
15367
+ if (input.currentBranch && onOrigin.has(input.currentBranch)) return input.currentBranch;
15368
+ return null;
15369
+ }
15370
+ var CONVENTIONAL;
15371
+ var init_defaultBranch = __esm({
15372
+ "src/daemon/defaultBranch.ts"() {
15373
+ "use strict";
15374
+ CONVENTIONAL = ["main", "master"];
15375
+ }
15376
+ });
15377
+
15378
+ // src/daemon/localGit.ts
15379
+ var localGit_exports = {};
15380
+ __export(localGit_exports, {
15381
+ createMirrorClone: () => createMirrorClone,
15382
+ detectDefaultBranch: () => detectDefaultBranch,
15383
+ fetchOrigin: () => fetchOrigin,
15384
+ firstParentRange: () => firstParentRange,
15385
+ isAncestor: () => isAncestor,
15386
+ readCommitMeta: () => readCommitMeta,
15387
+ readOriginUrl: () => readOriginUrl,
15388
+ revParse: () => revParse,
15389
+ setOriginUrl: () => setOriginUrl
15390
+ });
15391
+ import { mkdir as mkdir9 } from "node:fs/promises";
15392
+ import { dirname as dirname12 } from "node:path";
15393
+ async function git(dir, args, opts = {}) {
15394
+ requireBinary("git");
15395
+ const res = await execa("git", args, {
15396
+ cwd: dir ?? void 0,
15397
+ reject: false,
15398
+ timeout: GIT_TIMEOUT_MS,
15399
+ // Kill the whole process group: a hung `git fetch` has spawned helper
15400
+ // processes (ssh, credential helpers) that keep the pipe open, so killing
15401
+ // git alone can leave the await pending on a dead parent.
15402
+ killSignal: "SIGKILL",
15403
+ // A large `rev-list` on a busy repo can be megabytes.
15404
+ maxBuffer: 64 * 1024 * 1024
15405
+ });
15406
+ const exitCode = res.exitCode ?? 1;
15407
+ if (!opts.allowFail && exitCode !== 0) {
15408
+ throw new Error(`git ${args.join(" ")} failed (${exitCode}): ${(res.stderr || "").trim()}`);
15409
+ }
15410
+ return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
15411
+ }
15412
+ async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
15413
+ await mkdir9(dirname12(mirrorPath), { recursive: true });
15414
+ await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
15415
+ await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
15416
+ }
15417
+ async function setOriginUrl(dir, originUrl) {
15418
+ await git(dir, ["remote", "set-url", "origin", originUrl]);
15419
+ }
15420
+ async function readOriginUrl(dir) {
15421
+ const { exitCode, stdout } = await git(dir, ["remote", "get-url", "origin"], { allowFail: true });
15422
+ if (exitCode !== 0) return null;
15423
+ return stdout.trim() || null;
15424
+ }
15425
+ async function fetchOrigin(dir, branch) {
15426
+ await git(dir, ["fetch", "--quiet", "--no-tags", "origin", branch]);
15427
+ }
15428
+ async function revParse(dir, ref) {
15429
+ return (await git(dir, ["rev-parse", ref])).stdout.trim();
15430
+ }
15431
+ async function isAncestor(dir, a2, b) {
15432
+ return (await git(dir, ["merge-base", "--is-ancestor", a2, b], { allowFail: true })).exitCode === 0;
15433
+ }
15434
+ async function firstParentRange(dir, marker, tip) {
15435
+ const { stdout } = await git(dir, [
15436
+ "rev-list",
15437
+ "--reverse",
15438
+ "--first-parent",
15439
+ `${marker}..${tip}`
15440
+ ]);
15441
+ return stdout.split("\n").map((s) => s.trim()).filter(Boolean);
15442
+ }
15443
+ async function readCommitMeta(dir, sha) {
15444
+ const { exitCode, stdout } = await git(
15445
+ dir,
15446
+ ["show", "-s", "--format=%s%n%an%n%cI", sha],
15447
+ { allowFail: true }
15448
+ );
15449
+ if (exitCode !== 0) return { message: "", authorName: null, committedAt: null };
15450
+ const [message = "", authorName = "", committedAt = ""] = stdout.split("\n");
15451
+ return {
15452
+ message: message.trim(),
15453
+ authorName: authorName.trim() || null,
15454
+ committedAt: committedAt.trim() || null
15455
+ };
15456
+ }
15457
+ async function detectDefaultBranch(dir) {
15458
+ const symbolic = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
15459
+ allowFail: true
15460
+ });
15461
+ const remotes = await git(
15462
+ dir,
15463
+ ["for-each-ref", "--format=%(refname:strip=3)", "refs/remotes/origin"],
15464
+ { allowFail: true }
15465
+ );
15466
+ const current = await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"], { allowFail: true });
15467
+ return pickDefaultBranch({
15468
+ symbolicRef: symbolic.exitCode === 0 ? symbolic.stdout.trim() : null,
15469
+ remoteBranches: remotes.exitCode === 0 ? remotes.stdout.split("\n").map((l) => l.trim()).filter((l) => l && l !== "HEAD") : [],
15470
+ // Detached HEAD reports the literal "HEAD", which is not a branch name.
15471
+ currentBranch: current.exitCode === 0 && current.stdout.trim() !== "HEAD" ? current.stdout.trim() : null
15472
+ });
15473
+ }
15474
+ var GIT_TIMEOUT_MS;
15475
+ var init_localGit = __esm({
15476
+ "src/daemon/localGit.ts"() {
15477
+ "use strict";
15478
+ init_execa();
15479
+ init_preflight();
15480
+ init_defaultBranch();
15481
+ GIT_TIMEOUT_MS = 10 * 60 * 1e3;
15482
+ }
15483
+ });
15484
+
15485
+ // src/daemon/localPoller.ts
15486
+ async function pollRepoOnce(entry, deps) {
15487
+ const repo = `${entry.repoOwner}/${entry.repoName}`;
15488
+ const gateTimeoutMs = deps.gateTimeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;
15489
+ const row = await deps.readWatchedRepo(entry.watchedRepoId);
15490
+ if (!row) {
15491
+ return {
15492
+ kind: "halt",
15493
+ // Not a gate outcome — the repo is gone from under us. Reuses the
15494
+ // terminal code because the consequence is the same: this will not fix
15495
+ // itself, and `--resume` cannot clear it.
15496
+ code: "job-terminal",
15497
+ reason: `${repo} is no longer registered on the server (the watched repo was removed). Stop watching it here with \`archme local-watch --stop ${repo}\`.`
15498
+ };
15499
+ }
15500
+ if (row.sourceKind !== "local") {
15501
+ return {
15502
+ kind: "halt",
15503
+ code: "job-terminal",
15504
+ reason: `${repo} is now connected via the GitHub App, which produces its own updates. Local polling is no longer needed \u2014 \`archme local-watch --stop ${repo}\`.`
15505
+ };
15506
+ }
15507
+ if (!row.enabled) {
15508
+ return { kind: "idle", reason: `${repo}: watching is disabled on the server` };
15509
+ }
15510
+ await deps.git.fetchOrigin(entry.mirrorPath, row.repoBranch);
15511
+ const marker = await deps.readMarker(row.projectId, row.archmeBranch);
15512
+ const upstreamTip = await deps.git.revParse(entry.mirrorPath, `origin/${row.repoBranch}`);
15513
+ const base = {
15514
+ repo,
15515
+ marker,
15516
+ upstreamTip,
15517
+ nowMs: deps.now(),
15518
+ gateTimeoutMs
15519
+ };
15520
+ if (!marker.sha) {
15521
+ return planPollTick({
15522
+ ...base,
15523
+ markerIsAncestorOfTip: false,
15524
+ tipIsAncestorOfMarker: false,
15525
+ firstParentShas: [],
15526
+ jobForTarget: null
15527
+ });
15528
+ }
15529
+ const markerIsAncestorOfTip = await deps.git.isAncestor(entry.mirrorPath, marker.sha, upstreamTip);
15530
+ const tipIsAncestorOfMarker = await deps.git.isAncestor(entry.mirrorPath, upstreamTip, marker.sha);
15531
+ const firstParentShas = markerIsAncestorOfTip ? await deps.git.firstParentRange(entry.mirrorPath, marker.sha, upstreamTip) : [];
15532
+ const ancestry = {
15533
+ ...base,
15534
+ markerIsAncestorOfTip,
15535
+ tipIsAncestorOfMarker,
15536
+ firstParentShas
15537
+ };
15538
+ const first = planPollTick({ ...ancestry, jobForTarget: null });
15539
+ if (first.kind !== "enqueue") return first;
15540
+ const jobForTarget = await deps.readJobForSha(row.projectId, row.archmeBranch, first.afterSha);
15541
+ const observedAtMs = deps.firstObservedAt(
15542
+ `${entry.watchedRepoId}:${first.afterSha}`,
15543
+ deps.now()
15544
+ );
15545
+ const decision = planPollTick({
15546
+ ...ancestry,
15547
+ jobForTarget: jobForTarget ? { ...jobForTarget, createdAtMs: observedAtMs } : null
15548
+ });
15549
+ if (decision.kind !== "enqueue") return decision;
15550
+ const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
15551
+ if (result.dedup) {
15552
+ deps.log(` ${repo}: ${short3(decision.afterSha)} already enqueued elsewhere`);
15553
+ } else {
15554
+ deps.log(` ${repo}: enqueued ${short3(decision.afterSha)} (job ${result.jobId ?? "?"})`);
15555
+ }
15556
+ return decision;
15557
+ }
15558
+ async function pollAllRepos(repos, deps) {
15559
+ for (const entry of repos) {
15560
+ const key = `${entry.repoOwner}/${entry.repoName} (${entry.watchedRepoId.slice(0, 8)})`;
15561
+ if (entry.haltedAt) continue;
15562
+ try {
15563
+ const action = await pollRepoOnce(entry, deps);
15564
+ if (action.kind === "halt") {
15565
+ await deps.persistHalt(entry, action.reason, action.code);
15566
+ deps.log(
15567
+ `
15568
+ \u26A0 local poll HALTED for ${key}: ${action.reason}
15569
+ No further commits will be processed for this repo.
15570
+ Advancing past a failed commit would fold it into the next diff and break the
15571
+ one-ArchMe-commit-per-git-commit guarantee, so this stops instead.
15572
+ When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
15573
+ `
15574
+ );
15575
+ }
15576
+ } catch (err2) {
15577
+ deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
15578
+ }
15579
+ }
15580
+ }
15581
+ function makePollerDeps(client, log, gateTimeoutMs) {
15582
+ const firstSeen = /* @__PURE__ */ new Map();
15583
+ return {
15584
+ git: localGit_exports,
15585
+ log,
15586
+ now: () => Date.now(),
15587
+ gateTimeoutMs,
15588
+ firstObservedAt(key, nowMs) {
15589
+ const existing = firstSeen.get(key);
15590
+ if (existing !== void 0) return existing;
15591
+ firstSeen.set(key, nowMs);
15592
+ return nowMs;
15593
+ },
15594
+ async persistHalt(entry, reason, code) {
15595
+ const repos = await loadLocalRepos();
15596
+ await saveLocalRepos(
15597
+ markHalted(repos, entry.watchedRepoId, reason, (/* @__PURE__ */ new Date()).toISOString(), code)
15598
+ );
15599
+ },
15600
+ async readWatchedRepo(watchedRepoId) {
15601
+ const { data, error } = await client.from("watched_repos").select("project_id, branch, commit_update_archme_branch, source_kind, enabled").eq("id", watchedRepoId).is("deleted_at", null).maybeSingle();
15602
+ if (error || !data) return null;
15603
+ const r = data;
15604
+ return {
15605
+ projectId: r.project_id,
15606
+ repoBranch: r.branch,
15607
+ // Mirrors the webhook's own fallback (`commit_update_archme_branch ??
15608
+ // branch`), so both producers resolve the same lane for the same row.
15609
+ archmeBranch: r.commit_update_archme_branch ?? r.branch,
15610
+ sourceKind: r.source_kind,
15611
+ enabled: r.enabled
15612
+ };
15613
+ },
15614
+ // Shared with the job runner and local-watch's readiness check: three
15615
+ // readers of the same marker drifting apart is how one of them ends up
15616
+ // deciding against a value the others don't see.
15617
+ readMarker: (projectId, archmeBranch) => readBranchMarker(client, projectId, archmeBranch),
15618
+ async readJobForSha(projectId, archmeBranch, sha) {
15619
+ const { data, error } = await client.from("archme_jobs").select("status, attempt_count, created_at").eq("project_id", projectId).eq("archme_branch", archmeBranch).eq("after_sha", sha).order("created_at", { ascending: false }).limit(1);
15620
+ if (error || !data || data.length === 0) return null;
15621
+ const row = data[0];
15622
+ return {
15623
+ status: row.status,
15624
+ attemptCount: row.attempt_count ?? 0,
15625
+ createdAtMs: new Date(row.created_at).getTime()
15626
+ };
15627
+ },
15628
+ async enqueue(entry, row, beforeSha, afterSha) {
15629
+ const meta = await readCommitMeta(entry.mirrorPath, afterSha);
15630
+ const { data, error } = await client.rpc("enqueue_local_archme_job", {
15631
+ p_watched_repo_id: entry.watchedRepoId,
15632
+ // From the ROW, not the registry: these decide which lane the job lands
15633
+ // in, and a stale copy would enqueue against the wrong branch.
15634
+ p_repo_branch: row.repoBranch,
15635
+ p_archme_branch: row.archmeBranch,
15636
+ p_before_sha: beforeSha,
15637
+ p_after_sha: afterSha,
15638
+ p_commit_message: meta.message || null,
15639
+ p_head_commit_timestamp: meta.committedAt,
15640
+ p_author_name: meta.authorName,
15641
+ // `author_login` is a GitHub concept with no git equivalent. The webhook
15642
+ // path fills it from the push payload; local mode leaves it null.
15643
+ p_author_login: null
15644
+ });
15645
+ if (error) throw new Error(`enqueue_local_archme_job: ${error.message}`);
15646
+ return data ?? {};
15647
+ }
15648
+ };
15649
+ }
15650
+ function short3(sha) {
15651
+ return sha.slice(0, 7);
15652
+ }
15653
+ var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
15654
+ var init_localPoller = __esm({
15655
+ "src/daemon/localPoller.ts"() {
15656
+ "use strict";
15657
+ init_localPoll();
15658
+ init_localRepos();
15659
+ init_branchMarker();
15660
+ init_localGit();
15661
+ DEFAULT_POLL_INTERVAL_MS = 6e4;
15662
+ DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
15663
+ }
15664
+ });
15665
+
14915
15666
  // src/daemon/run.ts
14916
15667
  var run_exports = {};
14917
15668
  __export(run_exports, {
@@ -14933,11 +15684,11 @@ __export(run_exports, {
14933
15684
  });
14934
15685
  import { createClient } from "@supabase/supabase-js";
14935
15686
  import { randomUUID } from "node:crypto";
14936
- import { mkdtemp as mkdtemp3, mkdir as mkdir8, writeFile as writeFile9, rm as rm6, stat as stat8, chmod as chmod2 } from "node:fs/promises";
15687
+ import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
14937
15688
  import { tmpdir as tmpdir3 } from "node:os";
14938
- import { join as join15 } from "node:path";
15689
+ import { join as join18 } from "node:path";
14939
15690
  import { pathToFileURL } from "node:url";
14940
- import { homedir as homedir4 } from "node:os";
15691
+ import { homedir as homedir5 } from "node:os";
14941
15692
  function attributeKillCause(result, recorded) {
14942
15693
  if (recorded) return recorded;
14943
15694
  if (result.signal === "SIGTERM") return "timeout";
@@ -14968,7 +15719,7 @@ async function lookupProjectSlug(client, projectId) {
14968
15719
  return `${orgSlug}/${data.slug}`;
14969
15720
  }
14970
15721
  function cloneDirFor(baseDir, owner, name) {
14971
- return join15(baseDir, owner, name);
15722
+ return join18(baseDir, owner, name);
14972
15723
  }
14973
15724
  async function exists(p) {
14974
15725
  try {
@@ -15013,9 +15764,9 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
15013
15764
  };
15014
15765
  }
15015
15766
  async function buildGitEnv(credentials) {
15016
- const dir = await mkdtemp3(join15(tmpdir3(), "archme-git-askpass-"));
15017
- const askpassPath = join15(dir, "askpass.sh");
15018
- await writeFile9(
15767
+ const dir = await mkdtemp3(join18(tmpdir3(), "archme-git-askpass-"));
15768
+ const askpassPath = join18(dir, "askpass.sh");
15769
+ await writeFile10(
15019
15770
  askpassPath,
15020
15771
  [
15021
15772
  "#!/bin/sh",
@@ -15028,7 +15779,7 @@ async function buildGitEnv(credentials) {
15028
15779
  ].join("\n"),
15029
15780
  { mode: 448 }
15030
15781
  );
15031
- await chmod2(askpassPath, 448).catch(() => {
15782
+ await chmod3(askpassPath, 448).catch(() => {
15032
15783
  });
15033
15784
  const gitConfigEntries = [
15034
15785
  ["credential.helper", ""],
@@ -15053,16 +15804,38 @@ async function buildGitEnv(credentials) {
15053
15804
  return { env, cleanup: () => rm6(dir, { recursive: true, force: true }).catch(() => {
15054
15805
  }) };
15055
15806
  }
15807
+ async function ensureLocalCloneAndFetch(job, entry) {
15808
+ const dir = entry.mirrorPath;
15809
+ await cloneMutex.runExclusive(dir, async () => {
15810
+ if (!await exists(join18(dir, ".git"))) {
15811
+ throw new Error(
15812
+ `local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
15813
+ );
15814
+ }
15815
+ await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
15816
+ const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
15817
+ if (afterCheck.exitCode !== 0) {
15818
+ await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
15819
+ }
15820
+ }).catch((err2) => {
15821
+ const msg = err2 instanceof Error ? err2.message : String(err2);
15822
+ throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
15823
+ });
15824
+ return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
15825
+ } };
15826
+ }
15056
15827
  async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
15828
+ const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
15829
+ if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
15057
15830
  const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
15058
15831
  if (!deviceId) throw new Error("daemon is not signed in with a registered device");
15059
15832
  const credentials = await getRepoCredentials(ctx, job.id, deviceId);
15060
- await mkdir8(ctx.cloneBaseDir, { recursive: true });
15833
+ await mkdir10(ctx.cloneBaseDir, { recursive: true });
15061
15834
  const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
15062
15835
  const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
15063
15836
  await cloneMutex.runExclusive(dir, async () => {
15064
- if (!await exists(join15(dir, ".git"))) {
15065
- await mkdir8(join15(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
15837
+ if (!await exists(join18(dir, ".git"))) {
15838
+ await mkdir10(join18(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
15066
15839
  console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
15067
15840
  await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
15068
15841
  } else {
@@ -15083,22 +15856,22 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
15083
15856
  await execa("git", ["fetch", "origin", job.before_sha], { cwd: dir, env, reject: false });
15084
15857
  }
15085
15858
  }
15086
- }).catch(async (err) => {
15859
+ }).catch(async (err2) => {
15087
15860
  await cleanupGitEnv();
15088
- const msg = err instanceof Error ? err.message : String(err);
15861
+ const msg = err2 instanceof Error ? err2.message : String(err2);
15089
15862
  throw new Error(`GitHub App cannot access ${job.repo_owner}/${job.repo_name}: ${msg}`);
15090
15863
  });
15091
15864
  return { dir, gitEnv: env, cleanupGitEnv };
15092
15865
  }
15093
15866
  async function createJobWorktree(repoDir, job) {
15094
- const parentDir = await mkdtemp3(join15(tmpdir3(), "archme-job-worktree-"));
15095
- const worktreeDir = join15(parentDir, "repo");
15867
+ const parentDir = await mkdtemp3(join18(tmpdir3(), "archme-job-worktree-"));
15868
+ const worktreeDir = join18(parentDir, "repo");
15096
15869
  try {
15097
15870
  await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
15098
- } catch (err) {
15871
+ } catch (err2) {
15099
15872
  await rm6(parentDir, { recursive: true, force: true }).catch(() => {
15100
15873
  });
15101
- throw err;
15874
+ throw err2;
15102
15875
  }
15103
15876
  return {
15104
15877
  worktreeDir,
@@ -15117,7 +15890,7 @@ function noUsableBaseError(branch, markerSha, detail) {
15117
15890
  );
15118
15891
  }
15119
15892
  async function isShaReachable(repoDir, sha) {
15120
- const { exitCode } = await execa("git", ["cat-file", "-e", sha], { cwd: repoDir, reject: false });
15893
+ const { exitCode } = await execa("git", ["cat-file", "-e", `${sha}^{commit}`], { cwd: repoDir, reject: false });
15121
15894
  return exitCode === 0;
15122
15895
  }
15123
15896
  async function buildDiffPayload(repoDir, before, after) {
@@ -15140,7 +15913,16 @@ async function forcePushProvenance(repoDir, base, after) {
15140
15913
  const forkNote = fork.trim() ? ` (history diverged at \`${fork.trim().slice(0, 8)}\`)` : "";
15141
15914
  return `Force push: the branch tip was rewritten${forkNote}. The diff below is the exact net delta from the last reflected commit \`${base.slice(0, 8)}\` to the new tip \`${after.slice(0, 8)}\`, computed tree-to-tree \u2014 the rewrite does not affect it.`;
15142
15915
  }
15916
+ function isGitSha(v) {
15917
+ return typeof v === "string" && GIT_SHA_RE.test(v);
15918
+ }
15919
+ function assertGitSha(v, field) {
15920
+ if (!isGitSha(v)) {
15921
+ throw new Error(`${field} ${v ?? "(null)"} is not a 40-character git SHA`);
15922
+ }
15923
+ }
15143
15924
  async function computeDiff(job, repoDir, marker, gitEnv) {
15925
+ assertGitSha(job.after_sha, "after_sha");
15144
15926
  if (!await isShaReachable(repoDir, job.after_sha)) {
15145
15927
  throw new Error(`after_sha ${job.after_sha} is not reachable after fetch`);
15146
15928
  }
@@ -15163,6 +15945,9 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
15163
15945
  if (marker.repo !== repo) {
15164
15946
  throw noUsableBaseError(job.archme_branch, marker.sha, `marker repo ${marker.repo ?? "n/a"} does not match job repo ${repo}`);
15165
15947
  }
15948
+ if (!isGitSha(marker.sha)) {
15949
+ throw noUsableBaseError(job.archme_branch, marker.sha, `marker SHA ${marker.sha} is not a 40-character git SHA`);
15950
+ }
15166
15951
  const base = marker.sha;
15167
15952
  if (base === after) return "skip";
15168
15953
  if (!await isShaReachable(repoDir, base)) {
@@ -15177,11 +15962,6 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
15177
15962
  if (job.forced) payload.provenance = await forcePushProvenance(repoDir, base, after);
15178
15963
  return payload;
15179
15964
  }
15180
- async function readBranchMarker(client, projectId, archmeBranch) {
15181
- const { data, error } = await client.from("branches").select("last_source_sha, last_source_repo").eq("project_id", projectId).eq("name", archmeBranch).maybeSingle();
15182
- if (error || !data) return { sha: null, repo: null };
15183
- return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
15184
- }
15185
15965
  function buildDiffPrompt(job, projectSlug, diff, checkoutDir, affectedSeed, bugsFolderId) {
15186
15966
  const sentinel = `UNTRUSTED-DIFF-${randomUUID()}`;
15187
15967
  const seedBlock = affectedSeed ? `
@@ -15515,9 +16295,9 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
15515
16295
  };
15516
16296
  }
15517
16297
  lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
15518
- } catch (err) {
16298
+ } catch (err2) {
15519
16299
  lastStatus = null;
15520
- lastDetail = `network error reaching MCP: ${err instanceof Error ? err.message : String(err)}`;
16300
+ lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
15521
16301
  }
15522
16302
  }
15523
16303
  if (lastStatus === 401) {
@@ -15655,6 +16435,7 @@ async function runJob(ctx, job, claimDeviceId) {
15655
16435
  };
15656
16436
  let metrics = { durations_ms: timings };
15657
16437
  let logs = { stdout: "", stderr: "" };
16438
+ let sentPrompt;
15658
16439
  const cleanupFns = [];
15659
16440
  const heartbeat = setInterval(() => {
15660
16441
  rpcHeartbeat(ctx, job.id, deviceId);
@@ -15765,7 +16546,7 @@ async function runJob(ctx, job, claimDeviceId) {
15765
16546
  const affected = resolveAffected(seedArch, diff.changedFiles, changedSymbolsByFile);
15766
16547
  affectedForComplete = affected;
15767
16548
  const symbolsInFile = async (file) => {
15768
- const outline = await runOutline(join15(worktreeDir, file));
16549
+ const outline = await runOutline(join18(worktreeDir, file));
15769
16550
  if (outline.tier !== 2) return null;
15770
16551
  return outline.data.symbols.map((s) => s.name);
15771
16552
  };
@@ -15788,6 +16569,7 @@ async function runJob(ctx, job, claimDeviceId) {
15788
16569
  }
15789
16570
  const bugsFolderId = await ensureBugsFolder(ctx.supabase, job.project_id);
15790
16571
  const prompt = buildDiffPrompt(job, projectSlug, diff, worktreeDir, affectedSeed, bugsFolderId);
16572
+ sentPrompt = prompt;
15791
16573
  if (cancelled.has(job.id)) {
15792
16574
  log(`cancelled before claude spawn, aborting`);
15793
16575
  return;
@@ -15893,9 +16675,9 @@ async function runJob(ctx, job, claimDeviceId) {
15893
16675
  true,
15894
16676
  affectedFieldForComplete(job.source_mode, affectedForComplete)
15895
16677
  );
15896
- } catch (err) {
16678
+ } catch (err2) {
15897
16679
  if (cancelled.has(job.id)) {
15898
- console.log(`[${job.id}] cancelled, swallowing error: ${err instanceof Error ? err.message : err}`);
16680
+ console.log(`[${job.id}] cancelled, swallowing error: ${err2 instanceof Error ? err2.message : err2}`);
15899
16681
  await rpcRollback(ctx, job.id, deviceId);
15900
16682
  return;
15901
16683
  }
@@ -15903,10 +16685,15 @@ async function runJob(ctx, job, claimDeviceId) {
15903
16685
  console.log(`[${job.id}] shutdown \u2014 routed to retry by shutdown handler`);
15904
16686
  return;
15905
16687
  }
15906
- const msg = err instanceof Error ? err.message : String(err);
15907
- const haystack = `${msg}
15908
- ${logs.stderr}`;
15909
- const plan = rateLimitDeferPlanForExit({ message: msg, stderr: logs.stderr, stdout: logs.stdout });
16688
+ const msg = err2 instanceof Error ? err2.message : String(err2);
16689
+ const haystack = stripPromptEcho(`${msg}
16690
+ ${logs.stderr}`, sentPrompt);
16691
+ const plan = rateLimitDeferPlanForExit({
16692
+ message: msg,
16693
+ stderr: logs.stderr,
16694
+ stdout: logs.stdout,
16695
+ prompt: sentPrompt
16696
+ });
15910
16697
  if (plan) {
15911
16698
  const { parsed, pauseUntil, info } = plan;
15912
16699
  if (!parsed) {
@@ -15975,7 +16762,7 @@ async function dispatch(sched) {
15975
16762
  }
15976
16763
  ctxByJob.set(claimed.job.id, claimed.ctx);
15977
16764
  deviceByJob.set(claimed.job.id, claimed.deviceId);
15978
- const p = sched.run(claimed.ctx, claimed.job, claimed.deviceId).catch((err) => console.error(`[${claimed.job.id}] run error:`, err instanceof Error ? err.message : err)).finally(() => {
16765
+ const p = sched.run(claimed.ctx, claimed.job, claimed.deviceId).catch((err2) => console.error(`[${claimed.job.id}] run error:`, err2 instanceof Error ? err2.message : err2)).finally(() => {
15979
16766
  ctxByJob.delete(claimed.job.id);
15980
16767
  deviceByJob.delete(claimed.job.id);
15981
16768
  slot();
@@ -16011,7 +16798,7 @@ function subscribe(client, sched) {
16011
16798
  "postgres_changes",
16012
16799
  { event: "INSERT", schema: "public", table: "archme_jobs" },
16013
16800
  (payload) => {
16014
- if (payload.new.status === "pending") dispatch(sched).catch((err) => console.error("dispatch:", err));
16801
+ if (payload.new.status === "pending") dispatch(sched).catch((err2) => console.error("dispatch:", err2));
16015
16802
  }
16016
16803
  ).on(
16017
16804
  "postgres_changes",
@@ -16028,12 +16815,12 @@ function subscribe(client, sched) {
16028
16815
  console.log(`[${payload.new.id}] received cancellation (pre-spawn), will abort`);
16029
16816
  }
16030
16817
  } else if (payload.new.status === "pending") {
16031
- dispatch(sched).catch((err) => console.error("dispatch:", err));
16818
+ dispatch(sched).catch((err2) => console.error("dispatch:", err2));
16032
16819
  }
16033
16820
  }
16034
16821
  ).subscribe((status) => {
16035
16822
  console.log(`realtime channel: ${status}`);
16036
- if (status === "SUBSCRIBED") dispatch(sched).catch((err) => console.error("startup dispatch:", err));
16823
+ if (status === "SUBSCRIBED") dispatch(sched).catch((err2) => console.error("startup dispatch:", err2));
16037
16824
  });
16038
16825
  return () => {
16039
16826
  client.removeChannel(channel);
@@ -16074,9 +16861,9 @@ async function signInAccount(creds, makeAuth) {
16074
16861
  active = { auth, cred, result };
16075
16862
  usedFallback = attempts > 1;
16076
16863
  break;
16077
- } catch (err) {
16864
+ } catch (err2) {
16078
16865
  auth.clearRefreshTimer();
16079
- lastErr = err;
16866
+ lastErr = err2;
16080
16867
  }
16081
16868
  }
16082
16869
  if (active) {
@@ -16093,19 +16880,19 @@ async function runDaemon() {
16093
16880
  let lockHandle = null;
16094
16881
  try {
16095
16882
  lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
16096
- } catch (err) {
16097
- if (err instanceof SingleInstanceLockedError) {
16098
- console.error(String(err.message));
16883
+ } catch (err2) {
16884
+ if (err2 instanceof SingleInstanceLockedError) {
16885
+ console.error(String(err2.message));
16099
16886
  process.exit(64);
16100
16887
  }
16101
- throw err;
16888
+ throw err2;
16102
16889
  }
16103
16890
  let cfg;
16104
16891
  try {
16105
16892
  cfg = resolveConfig();
16106
- } catch (err) {
16107
- if (err instanceof MissingConfigError) {
16108
- console.error(err.message);
16893
+ } catch (err2) {
16894
+ if (err2 instanceof MissingConfigError) {
16895
+ console.error(err2.message);
16109
16896
  if (lockHandle) {
16110
16897
  try {
16111
16898
  await lockHandle.release();
@@ -16114,7 +16901,7 @@ async function runDaemon() {
16114
16901
  }
16115
16902
  process.exit(1);
16116
16903
  }
16117
- throw err;
16904
+ throw err2;
16118
16905
  }
16119
16906
  SUPABASE_URL = cfg.supabaseUrl;
16120
16907
  SUPABASE_ANON_KEY = cfg.supabaseAnonKey;
@@ -16148,9 +16935,9 @@ async function runDaemon() {
16148
16935
  let credentials;
16149
16936
  try {
16150
16937
  credentials = assertSingleAccount(loaded);
16151
- } catch (err) {
16152
- if (err instanceof MultipleAccountsError) {
16153
- console.error(err.message);
16938
+ } catch (err2) {
16939
+ if (err2 instanceof MultipleAccountsError) {
16940
+ console.error(err2.message);
16154
16941
  if (lockHandle) {
16155
16942
  try {
16156
16943
  await lockHandle.release();
@@ -16159,7 +16946,7 @@ async function runDaemon() {
16159
16946
  }
16160
16947
  process.exit(78);
16161
16948
  }
16162
- throw err;
16949
+ throw err2;
16163
16950
  }
16164
16951
  const devices = buildDevices(credentials);
16165
16952
  console.log(` booting 1 session across ${devices.length} org(s)`);
@@ -16251,16 +17038,49 @@ async function runDaemon() {
16251
17038
  console.warn(`daemon_pauses lookup failed (proceeding unpaused): ${e.message}`);
16252
17039
  }
16253
17040
  const sweepTimer = setInterval(
16254
- () => dispatch(sched).catch((err) => console.error("safety sweep:", err)),
17041
+ () => dispatch(sched).catch((err2) => console.error("safety sweep:", err2)),
16255
17042
  SAFETY_SWEEP_MS
16256
17043
  );
16257
- const shutdown = async (signal) => {
16258
- if (shuttingDown) return;
16259
- shuttingDown = true;
16260
- console.log(`
16261
- ${signal} received, shutting down`);
17044
+ const localPollDeps = makePollerDeps(client, (m) => console.log(m));
17045
+ const localPollIntervalMs = Number.parseInt(process.env.ARCHME_LOCAL_POLL_INTERVAL_MS ?? "", 10);
17046
+ const pollEveryMs = Number.isFinite(localPollIntervalMs) && localPollIntervalMs > 0 ? localPollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
17047
+ let localPollInFlight = false;
17048
+ const runLocalPoll = async () => {
17049
+ if (localPollInFlight) return;
17050
+ localPollInFlight = true;
17051
+ try {
17052
+ const repos = await loadLocalRepos();
17053
+ if (repos.length === 0) return;
17054
+ await pollAllRepos(repos, localPollDeps);
17055
+ } finally {
17056
+ localPollInFlight = false;
17057
+ }
17058
+ };
17059
+ const initialLocalRepos = await loadLocalRepos();
17060
+ if (initialLocalRepos.length > 0) {
17061
+ const halted = initialLocalRepos.filter((r) => r.haltedAt);
17062
+ console.log(
17063
+ ` local-poll: ${initialLocalRepos.length} repo(s) every ${Math.round(pollEveryMs / 1e3)}s`
17064
+ );
17065
+ for (const r of halted) {
17066
+ console.log(
17067
+ ` \u26A0 ${r.repoOwner}/${r.repoName} is HALTED (${r.haltReason ?? "unknown reason"}) \u2014 resume with \`archme local-watch --resume ${r.repoOwner}/${r.repoName}\``
17068
+ );
17069
+ }
17070
+ }
17071
+ const localPollTimer = setInterval(
17072
+ () => void runLocalPoll().catch((err2) => console.error("local poll:", err2)),
17073
+ pollEveryMs
17074
+ );
17075
+ void runLocalPoll().catch((err2) => console.error("local poll:", err2));
17076
+ const shutdown = async (signal) => {
17077
+ if (shuttingDown) return;
17078
+ shuttingDown = true;
17079
+ console.log(`
17080
+ ${signal} received, shutting down`);
16262
17081
  for (const ctx of sessions) ctx.auth.clearRefreshTimer();
16263
17082
  clearInterval(sweepTimer);
17083
+ clearInterval(localPollTimer);
16264
17084
  for (const u2 of unsubscribers) u2();
16265
17085
  const SHUTDOWN_DEADLINE_MS = 3e4;
16266
17086
  const deadline = setTimeout(() => {
@@ -16300,7 +17120,7 @@ ${signal} received, shutting down`);
16300
17120
  process.on("SIGINT", () => shutdown("SIGINT"));
16301
17121
  process.on("SIGTERM", () => shutdown("SIGTERM"));
16302
17122
  }
16303
- var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, CURATOR_PREAMBLE, CORE_PRINCIPLES, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
17123
+ var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
16304
17124
  var init_run = __esm({
16305
17125
  "src/daemon/run.ts"() {
16306
17126
  "use strict";
@@ -16331,6 +17151,10 @@ var init_run = __esm({
16331
17151
  init_rateLimit();
16332
17152
  init_version();
16333
17153
  init_materializeDiff();
17154
+ init_branchMarker();
17155
+ init_branchMarker();
17156
+ init_localRepos();
17157
+ init_localPoller();
16334
17158
  HEARTBEAT_INTERVAL_MS = 6e4;
16335
17159
  CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
16336
17160
  CLAUDE_LARGE_DIFF_TIMEOUT_MS = 45 * 60 * 1e3;
@@ -16351,8 +17175,9 @@ var init_run = __esm({
16351
17175
  MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
16352
17176
  ctxByJob = /* @__PURE__ */ new Map();
16353
17177
  deviceByJob = /* @__PURE__ */ new Map();
16354
- DEFAULT_CLONE_BASE_DIR = `${homedir4()}/.archme-job-runner/clones`;
17178
+ DEFAULT_CLONE_BASE_DIR = `${homedir5()}/.archme-job-runner/clones`;
16355
17179
  INLINE_PATCH_BYTE_CAP = 2e5;
17180
+ GIT_SHA_RE = /^[0-9a-f]{40}$/;
16356
17181
  CURATOR_PREAMBLE = `## Your role
16357
17182
 
16358
17183
  You are curating the architecture diagram, not just transcribing the change. The diagram is an abstraction read by engineers and technical leaders to understand the codebase at a higher level than the code itself. Two things have to stay true: (A) it reflects the reality of the code, and (B) it is a useful abstraction of it. Use judgement on the altitude \u2014 pick the level of detail that helps a reader build a mental model, not one that mirrors every function and file.`;
@@ -16517,8 +17342,8 @@ ${ANCHOR_VERIFICATION}`;
16517
17342
  }
16518
17343
  };
16519
17344
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
16520
- runDaemon().catch((err) => {
16521
- console.error("fatal:", err);
17345
+ runDaemon().catch((err2) => {
17346
+ console.error("fatal:", err2);
16522
17347
  process.exit(1);
16523
17348
  });
16524
17349
  }
@@ -16557,12 +17382,12 @@ async function dispatchDaemon(route2) {
16557
17382
  console.log(`Logs: ${out.logPath}`);
16558
17383
  console.log(`State: ${out.result.actions.join(" \u2192 ")}`);
16559
17384
  return 0;
16560
- } catch (err) {
16561
- if (err instanceof NotSignedInError) {
16562
- console.error(String(err.message));
17385
+ } catch (err2) {
17386
+ if (err2 instanceof NotSignedInError) {
17387
+ console.error(String(err2.message));
16563
17388
  return 75;
16564
17389
  }
16565
- console.error(String(err));
17390
+ console.error(String(err2));
16566
17391
  return 1;
16567
17392
  }
16568
17393
  }
@@ -16575,13 +17400,13 @@ async function dispatchDaemon(route2) {
16575
17400
  case "restart": {
16576
17401
  try {
16577
17402
  await ensureStartPrerequisites({ noLogin: route2.noLogin });
16578
- } catch (err) {
16579
- if (err instanceof NotSignedInError) {
16580
- console.error(String(err.message));
17403
+ } catch (err2) {
17404
+ if (err2 instanceof NotSignedInError) {
17405
+ console.error(String(err2.message));
16581
17406
  console.error("(daemon left running unchanged)");
16582
17407
  return 75;
16583
17408
  }
16584
- console.error(String(err));
17409
+ console.error(String(err2));
16585
17410
  console.error("(daemon left running unchanged)");
16586
17411
  return 1;
16587
17412
  }
@@ -16669,9 +17494,9 @@ async function defaultLoadSupabaseState(_deviceId) {
16669
17494
  async function defaultLoadUptime(pid) {
16670
17495
  if (process.platform === "linux") {
16671
17496
  try {
16672
- const { readFile: readFile12 } = await import("node:fs/promises");
16673
- const stat10 = await readFile12(`/proc/${pid}/stat`, "utf8");
16674
- const parts = stat10.split(") ").pop()?.split(" ") ?? [];
17497
+ const { readFile: readFile14 } = await import("node:fs/promises");
17498
+ const stat11 = await readFile14(`/proc/${pid}/stat`, "utf8");
17499
+ const parts = stat11.split(") ").pop()?.split(" ") ?? [];
16675
17500
  const ticks = Number(parts[19]);
16676
17501
  if (!Number.isFinite(ticks)) return null;
16677
17502
  const { readFile: rf } = await import("node:fs/promises");
@@ -16738,8 +17563,8 @@ async function createAuthedClient(opts) {
16738
17563
  let loaded;
16739
17564
  try {
16740
17565
  loaded = await loadStoredAndRefresh();
16741
- } catch (err) {
16742
- const msg = err instanceof Error ? err.message : String(err);
17566
+ } catch (err2) {
17567
+ const msg = err2 instanceof Error ? err2.message : String(err2);
16743
17568
  throw new Error(`${msg}
16744
17569
  \u2192 run \`archme login\` first`);
16745
17570
  }
@@ -16770,9 +17595,9 @@ function deriveWebOrigin(apiUrl, onMalformed = "throw") {
16770
17595
  let parsed;
16771
17596
  try {
16772
17597
  parsed = new URL(apiUrl);
16773
- } catch (err) {
17598
+ } catch (err2) {
16774
17599
  if (onMalformed === "throw") {
16775
- throw new Error(`could not parse API URL \`${apiUrl}\`: ${err instanceof Error ? err.message : String(err)}`);
17600
+ throw new Error(`could not parse API URL \`${apiUrl}\`: ${err2 instanceof Error ? err2.message : String(err2)}`);
16776
17601
  }
16777
17602
  return "http://localhost:5173";
16778
17603
  }
@@ -16981,8 +17806,8 @@ var init_transcript_signals = __esm({
16981
17806
  });
16982
17807
 
16983
17808
  // src/scan/report.ts
16984
- import { mkdir as mkdir9, writeFile as writeFile10 } from "node:fs/promises";
16985
- import { dirname as dirname10 } from "node:path";
17809
+ import { mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
17810
+ import { dirname as dirname13 } from "node:path";
16986
17811
  async function buildReport(input) {
16987
17812
  return {
16988
17813
  schema_version: REPORT_SCHEMA_VERSION,
@@ -17016,10 +17841,10 @@ async function buildReport(input) {
17016
17841
  };
17017
17842
  }
17018
17843
  async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
17019
- await mkdir9(dirname10(reportPath), { recursive: true });
17020
- await writeFile10(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
17021
- await mkdir9(dirname10(orphansTxtPath), { recursive: true });
17022
- await writeFile10(
17844
+ await mkdir11(dirname13(reportPath), { recursive: true });
17845
+ await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
17846
+ await mkdir11(dirname13(orphansTxtPath), { recursive: true });
17847
+ await writeFile11(
17023
17848
  orphansTxtPath,
17024
17849
  orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
17025
17850
  "utf8"
@@ -17044,10 +17869,26 @@ var init_github_remote = __esm({
17044
17869
  }
17045
17870
  });
17046
17871
 
17872
+ // src/scan/claimDefaultProject.ts
17873
+ function isClaimableDefaultProject(c3) {
17874
+ if (c3.slug !== SIGNUP_DEFAULT_SLUG) return false;
17875
+ if (c3.name.trim().toLowerCase() !== SIGNUP_DEFAULT_NAME) return false;
17876
+ if (c3.fileCount === null || c3.commitCount === null) return false;
17877
+ return c3.fileCount === 0 && c3.commitCount === 0;
17878
+ }
17879
+ var SIGNUP_DEFAULT_SLUG, SIGNUP_DEFAULT_NAME;
17880
+ var init_claimDefaultProject = __esm({
17881
+ "src/scan/claimDefaultProject.ts"() {
17882
+ "use strict";
17883
+ SIGNUP_DEFAULT_SLUG = "default";
17884
+ SIGNUP_DEFAULT_NAME = "default";
17885
+ }
17886
+ });
17887
+
17047
17888
  // src/scan/run-scan.ts
17048
17889
  import { createWriteStream as createWriteStream2 } from "node:fs";
17049
- import { mkdir as mkdir10, readFile as readFile11 } from "node:fs/promises";
17050
- import { basename as basename2, join as join16 } from "node:path";
17890
+ import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
17891
+ import { basename as basename2, join as join19 } from "node:path";
17051
17892
  function buildBootstrapPrompt(opts = {}) {
17052
17893
  const { promptVariant, interactive, briefing } = opts;
17053
17894
  let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
@@ -17132,6 +17973,8 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
17132
17973
  if (!membership) {
17133
17974
  throw new Error(`you are not a member of org \`${orgSlug}\`; cannot create project there`);
17134
17975
  }
17976
+ const claimed = await claimSignupDefaultProject(supabase, org, projectSlug);
17977
+ if (claimed) return claimed;
17135
17978
  const { data: created, error: cErr } = await supabase.from("projects").insert({ name: projectSlug, org_id: org.id, slug: projectSlug }).select("id").single();
17136
17979
  if (cErr) {
17137
17980
  if (cErr.code === "23505" || /duplicate key/i.test(cErr.message ?? "")) {
@@ -17164,6 +18007,43 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
17164
18007
  created: true
17165
18008
  };
17166
18009
  }
18010
+ async function claimSignupDefaultProject(supabase, org, projectSlug) {
18011
+ if (projectSlug === SIGNUP_DEFAULT_SLUG) return null;
18012
+ try {
18013
+ const { data: candidate } = await supabase.from("projects").select("id, slug, name").eq("org_id", org.id).eq("slug", SIGNUP_DEFAULT_SLUG).is("deleted_at", null).maybeSingle();
18014
+ if (!candidate) return null;
18015
+ const row = candidate;
18016
+ const [{ count: fileCount }, { count: commitCount }] = await Promise.all([
18017
+ supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", row.id),
18018
+ supabase.from("commits").select("id", { count: "exact", head: true }).eq("project_id", row.id)
18019
+ ]);
18020
+ if (!isClaimableDefaultProject({
18021
+ slug: row.slug,
18022
+ name: row.name,
18023
+ fileCount: fileCount ?? null,
18024
+ commitCount: commitCount ?? null
18025
+ })) {
18026
+ return null;
18027
+ }
18028
+ const { data: branches } = await supabase.from("branches").select("name, is_default").eq("project_id", row.id).is("deleted_at", null);
18029
+ const existing = branches ?? [];
18030
+ if (existing.length === 0) {
18031
+ const { error: bErr } = await supabase.from("branches").insert({ project_id: row.id, name: "main", is_default: true });
18032
+ if (bErr) return null;
18033
+ }
18034
+ const { error: renameErr } = await supabase.from("projects").update({ name: projectSlug, slug: projectSlug }).eq("id", row.id);
18035
+ if (renameErr) return null;
18036
+ return {
18037
+ orgSlug: org.slug,
18038
+ projectSlug,
18039
+ projectId: row.id,
18040
+ defaultBranch: existing.find((b) => b.is_default)?.name ?? "main",
18041
+ created: true
18042
+ };
18043
+ } catch {
18044
+ return null;
18045
+ }
18046
+ }
17167
18047
  async function fetchExistingProject(supabase, org, projectSlug) {
17168
18048
  const project = await mustQuery(
17169
18049
  supabase.from("projects").select("id, slug").eq("org_id", org.id).eq("slug", projectSlug).is("deleted_at", null).maybeSingle(),
@@ -17269,8 +18149,8 @@ async function runScan(opts, deps = {}) {
17269
18149
  let headSha;
17270
18150
  try {
17271
18151
  headSha = await repoHeadSha(opts.repoPath);
17272
- } catch (err) {
17273
- const msg = err instanceof Error ? err.message : String(err);
18152
+ } catch (err2) {
18153
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17274
18154
  throw new Error(`could not read git HEAD at \`${opts.repoPath}\` (is this a git repo?): ${msg}`);
17275
18155
  }
17276
18156
  let fullSlug;
@@ -17287,19 +18167,19 @@ async function runScan(opts, deps = {}) {
17287
18167
  }
17288
18168
  fullSlug = defaultProjectSlug(orgSlug, opts.repoPath);
17289
18169
  }
17290
- const resolved = await resolveOrCreateProject(supabase, userId, fullSlug);
17291
- if (resolved.created) {
17292
- log(` project: ${resolved.orgSlug}/${resolved.projectSlug} (created)`);
18170
+ const resolved2 = await resolveOrCreateProject(supabase, userId, fullSlug);
18171
+ if (resolved2.created) {
18172
+ log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (created)`);
17293
18173
  } else {
17294
- log(` project: ${resolved.orgSlug}/${resolved.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
18174
+ log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
17295
18175
  }
17296
- log(` default branch: ${resolved.defaultBranch}`);
18176
+ log(` default branch: ${resolved2.defaultBranch}`);
17297
18177
  const branchName = opts.branch ? normalizeScanBranchName(opts.branch) : defaultBranchName(headSha);
17298
18178
  log(` scan branch: ${branchName}`);
17299
18179
  const bearer = await getMcpAccessToken();
17300
18180
  const { configPath, cleanup: cleanupMcp } = await writeMcpConfig({
17301
18181
  baseUrl: mcpUrl,
17302
- projectSlug: `${resolved.orgSlug}/${resolved.projectSlug}`,
18182
+ projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
17303
18183
  branch: branchName,
17304
18184
  bearer,
17305
18185
  // No source-SHA block (no job id for a local run), but DO tag these as scan
@@ -17334,9 +18214,9 @@ async function runScan(opts, deps = {}) {
17334
18214
  // Incompatible with an interactive TTY session.
17335
18215
  streamJson: !interactive
17336
18216
  });
17337
- const transcriptDir = join16(opts.repoPath, ".archme", "scan");
17338
- const transcriptPath = join16(transcriptDir, "transcript.log");
17339
- await mkdir10(transcriptDir, { recursive: true });
18217
+ const transcriptDir = join19(opts.repoPath, ".archme", "scan");
18218
+ const transcriptPath = join19(transcriptDir, "transcript.log");
18219
+ await mkdir12(transcriptDir, { recursive: true });
17340
18220
  const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
17341
18221
  let finalResultText = "";
17342
18222
  if (proc.stdout) {
@@ -17370,19 +18250,19 @@ async function runScan(opts, deps = {}) {
17370
18250
  } else {
17371
18251
  log(`claude exited ${exitCode}`);
17372
18252
  }
17373
- const counts = await countArchitecture(supabase, resolved.projectId, branchName);
18253
+ const counts = await countArchitecture(supabase, resolved2.projectId, branchName);
17374
18254
  log(` components: ${counts.components}`);
17375
18255
  log(` stories: ${counts.stories}`);
17376
18256
  let signals2 = null;
17377
18257
  try {
17378
- await new Promise((resolve4, reject) => {
17379
- transcriptStream.end((err) => err ? reject(err) : resolve4());
18258
+ await new Promise((resolve5, reject) => {
18259
+ transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
17380
18260
  });
17381
- const reportPath = join16(transcriptDir, "report.json");
17382
- const orphansTxtPath = join16(transcriptDir, "orphans.txt");
17383
- const transcript = await readFile11(transcriptPath, "utf8").catch(() => "");
18261
+ const reportPath = join19(transcriptDir, "report.json");
18262
+ const orphansTxtPath = join19(transcriptDir, "orphans.txt");
18263
+ const transcript = await readFile12(transcriptPath, "utf8").catch(() => "");
17384
18264
  signals2 = extractTranscriptSignals(transcript);
17385
- const architecture = await fetchArchitectureDetail(supabase, resolved.projectId, branchName);
18265
+ const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
17386
18266
  const orphans = await classifyOrphans({
17387
18267
  repoRoot: opts.repoPath,
17388
18268
  fileAnchors: architecture.fileAnchors,
@@ -17391,7 +18271,7 @@ async function runScan(opts, deps = {}) {
17391
18271
  excludeGlobs: DEFAULT_EXCLUDE_GLOBS
17392
18272
  });
17393
18273
  const report = await buildReport({
17394
- projectSlug: `${resolved.orgSlug}/${resolved.projectSlug}`,
18274
+ projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
17395
18275
  branch: branchName,
17396
18276
  transcriptPath,
17397
18277
  reportPath,
@@ -17407,8 +18287,8 @@ async function runScan(opts, deps = {}) {
17407
18287
  log(` coverage: ${report.repo.anchor_coverage_ratio} orphans: ${orphans.orphan_files_full.length}`);
17408
18288
  log(` report: ${reportPath}`);
17409
18289
  log(` transcript: ${transcriptPath}`);
17410
- } catch (err) {
17411
- const msg = err instanceof Error ? err.message : String(err);
18290
+ } catch (err2) {
18291
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17412
18292
  log(`warning: failed to build scan report: ${msg}`);
17413
18293
  }
17414
18294
  if (signals2 !== null && shouldStampMarker(signals2, exitCode, signal)) {
@@ -17418,15 +18298,15 @@ async function runScan(opts, deps = {}) {
17418
18298
  repo = stdout ? parseGitHubRepo(stdout) : null;
17419
18299
  } catch {
17420
18300
  }
17421
- const { error: markerErr } = await supabase.from("branches").update({ last_source_sha: headSha, last_source_repo: repo }).eq("project_id", resolved.projectId).eq("name", branchName);
18301
+ const { error: markerErr } = await supabase.from("branches").update({ last_source_sha: headSha, last_source_repo: repo }).eq("project_id", resolved2.projectId).eq("name", branchName);
17422
18302
  if (markerErr) log(` marker advance failed (next daemon job has no usable base): ${markerErr.message}`);
17423
18303
  else log(` marker advanced: ${branchName} \u2192 ${headSha.slice(0, 8)}${repo ? ` (${repo})` : ""}`);
17424
18304
  }
17425
- const diffBase = resolved.defaultBranch;
18305
+ const diffBase = resolved2.defaultBranch;
17426
18306
  const webUrl = buildWebUrl({
17427
18307
  apiUrl: mcpUrl,
17428
- orgSlug: resolved.orgSlug,
17429
- projectSlug: resolved.projectSlug,
18308
+ orgSlug: resolved2.orgSlug,
18309
+ projectSlug: resolved2.projectSlug,
17430
18310
  branchName,
17431
18311
  defaultBranch: diffBase,
17432
18312
  // CLI: throw on malformed mcpUrl so misconfiguration is loud.
@@ -17439,8 +18319,8 @@ async function runScan(opts, deps = {}) {
17439
18319
  components: counts.components,
17440
18320
  stories: counts.stories,
17441
18321
  branchName,
17442
- orgSlug: resolved.orgSlug,
17443
- projectSlug: resolved.projectSlug,
18322
+ orgSlug: resolved2.orgSlug,
18323
+ projectSlug: resolved2.projectSlug,
17444
18324
  webUrl,
17445
18325
  summary
17446
18326
  };
@@ -17466,6 +18346,7 @@ var init_run_scan = __esm({
17466
18346
  init_transcript_signals();
17467
18347
  init_report();
17468
18348
  init_github_remote();
18349
+ init_claimDefaultProject();
17469
18350
  BOOTSTRAP_HEADLESS = `Run the \`scan\` MCP prompt now and follow it to completion. This is a non-interactive automated run \u2014 skip any steps that pause for user confirmation (e.g. "does this hierarchy look right?"). Proceed with your best-judgement decisions rather than waiting for human input.`;
17470
18351
  BOOTSTRAP_INTERACTIVE = `Run the \`scan\` MCP prompt now and follow it to completion. The user is at an interactive terminal \u2014 ask them when the prompt says to, and wait for their answer.
17471
18352
 
@@ -17475,6 +18356,62 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
17475
18356
  }
17476
18357
  });
17477
18358
 
18359
+ // src/scan/scanOutcome.ts
18360
+ function scanSucceeded(r) {
18361
+ return r.exitCode === 0 && !r.signal && r.components > 0;
18362
+ }
18363
+ function nextSteps() {
18364
+ return [
18365
+ "",
18366
+ "This is a snapshot of your architecture as of now. It will not follow your",
18367
+ "code unless something keeps it up to date:",
18368
+ "",
18369
+ " archme init walks you through the options",
18370
+ "",
18371
+ " \xB7 GitHub App \u2014 a couple of minutes, PR-aware, works for your whole team.",
18372
+ " Needs someone who can grant the app read access to the repo.",
18373
+ " \xB7 archme local-watch \u2014 no GitHub App and no approval, but updates only",
18374
+ " happen while your daemon is running on a machine that stays on."
18375
+ ];
18376
+ }
18377
+ function formatScanOutcome(r) {
18378
+ const lines = [""];
18379
+ if (!scanSucceeded(r)) {
18380
+ if (r.signal || r.exitCode !== 0) {
18381
+ lines.push("scan did not complete");
18382
+ lines.push(r.signal ? ` killed by ${r.signal}` : ` exit ${r.exitCode}`);
18383
+ } else {
18384
+ lines.push("scan finished but wrote no components");
18385
+ }
18386
+ lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
18387
+ lines.push(` components: ${r.components}`);
18388
+ lines.push(` stories: ${r.stories}`);
18389
+ lines.push("");
18390
+ lines.push("If Claude Code is installed but not signed in, run `claude` once and");
18391
+ lines.push("authenticate, then re-run this command.");
18392
+ } else {
18393
+ lines.push("scan complete");
18394
+ lines.push(` components: ${r.components}`);
18395
+ lines.push(` stories: ${r.stories}`);
18396
+ lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
18397
+ lines.push(` web UI: ${r.webUrl}`);
18398
+ }
18399
+ if (r.summary) {
18400
+ const tail = r.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${r.summary.slice(-SUMMARY_TAIL_CHARS)}` : r.summary;
18401
+ lines.push("", `Claude summary:
18402
+ ${tail}`);
18403
+ }
18404
+ if (scanSucceeded(r)) lines.push(...nextSteps());
18405
+ return lines.join("\n");
18406
+ }
18407
+ var SUMMARY_TAIL_CHARS;
18408
+ var init_scanOutcome = __esm({
18409
+ "src/scan/scanOutcome.ts"() {
18410
+ "use strict";
18411
+ SUMMARY_TAIL_CHARS = 2e3;
18412
+ }
18413
+ });
18414
+
17478
18415
  // src/scan/dispatch.ts
17479
18416
  var dispatch_exports3 = {};
17480
18417
  __export(dispatch_exports3, {
@@ -17546,22 +18483,10 @@ async function dispatchScan(args) {
17546
18483
  opts.repoPath = resolve2(repoPath);
17547
18484
  try {
17548
18485
  const result = await runScan(opts);
17549
- console.log("");
17550
- console.log(`scan complete`);
17551
- console.log(` exit: ${result.exitCode}${result.signal ? ` (signal: ${result.signal})` : ""}`);
17552
- console.log(` components: ${result.components}`);
17553
- console.log(` stories: ${result.stories}`);
17554
- console.log(` branch: ${result.orgSlug}/${result.projectSlug}@${result.branchName}`);
17555
- console.log(` web UI: ${result.webUrl}`);
17556
- if (result.summary) {
17557
- const tail = result.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${result.summary.slice(-SUMMARY_TAIL_CHARS)}` : result.summary;
17558
- console.log(`
17559
- Claude summary:
17560
- ${tail}`);
17561
- }
18486
+ console.log(formatScanOutcome(result));
17562
18487
  return dispatchExitCode(result);
17563
- } catch (err) {
17564
- const msg = err instanceof Error ? err.message : String(err);
18488
+ } catch (err2) {
18489
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17565
18490
  console.error(`archme scan: ${msg}`);
17566
18491
  return EX_INFRA;
17567
18492
  }
@@ -17573,7 +18498,7 @@ Usage:
17573
18498
  archme scan <repo-path> [flags]
17574
18499
 
17575
18500
  Flags:
17576
- --project <org/slug> Target project. Defaults to e2e/<basename>.
18501
+ --project <org/slug> Target project. Defaults to <your-org>/<basename>.
17577
18502
  --branch <name> Target scan branch. Defaults to <date>-<sha7>-scan.
17578
18503
  --no-interactive Run headless (no questions). For CI/automation.
17579
18504
  --prompt-variant <id> Scan prompt variant. Defaults to latest.
@@ -17582,23 +18507,23 @@ Flags:
17582
18507
  Reads OAuth credentials from the local keychain (run \`archme login\` first).
17583
18508
  Outputs component / story counts and the web-UI URL on completion.`);
17584
18509
  }
17585
- var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, SUMMARY_TAIL_CHARS, PROJECT_SLUG_RE;
18510
+ var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, PROJECT_SLUG_RE;
17586
18511
  var init_dispatch3 = __esm({
17587
18512
  "src/scan/dispatch.ts"() {
17588
18513
  "use strict";
17589
18514
  init_run_scan();
18515
+ init_scanOutcome();
17590
18516
  EX_OK = 0;
17591
18517
  EX_SIGNAL = 2;
17592
18518
  EX_INFRA = 3;
17593
18519
  EX_USAGE = 78;
17594
- SUMMARY_TAIL_CHARS = 2e3;
17595
18520
  PROJECT_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
17596
18521
  }
17597
18522
  });
17598
18523
 
17599
18524
  // src/init/repoSize.ts
17600
18525
  import { stat as stat9 } from "node:fs/promises";
17601
- import { extname, join as join17 } from "node:path";
18526
+ import { extname, join as join20 } from "node:path";
17602
18527
  async function measureRepo(repoPath) {
17603
18528
  const files = await gitListFiles(repoPath);
17604
18529
  if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
@@ -17607,7 +18532,7 @@ async function measureRepo(repoPath) {
17607
18532
  for (const rel of files) {
17608
18533
  if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
17609
18534
  try {
17610
- const s = await stat9(join17(repoPath, rel));
18535
+ const s = await stat9(join20(repoPath, rel));
17611
18536
  if (!s.isFile()) continue;
17612
18537
  sourceFiles++;
17613
18538
  sourceBytes += s.size;
@@ -17726,7 +18651,7 @@ async function watchRepoForProject(input) {
17726
18651
  reason: "the ArchMe GitHub App is not installed on this org yet \u2014 connect it in the app to get automatic updates on every push."
17727
18652
  };
17728
18653
  }
17729
- const { error } = await supabase.rpc("watch_repo", {
18654
+ const { data: watchedRepoId, error } = await supabase.rpc("watch_repo", {
17730
18655
  p_org_id: orgId,
17731
18656
  p_project_id: projectId,
17732
18657
  p_repo_owner: owner,
@@ -17737,6 +18662,17 @@ async function watchRepoForProject(input) {
17737
18662
  if (error) {
17738
18663
  return { watched: false, reason: `could not watch the repo (${error.message}).` };
17739
18664
  }
18665
+ const { error: enableErr } = await supabase.from("watched_repos").update({
18666
+ commit_update_enabled: true,
18667
+ commit_update_archme_branch: input.archmeBranch,
18668
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
18669
+ }).eq("id", watchedRepoId);
18670
+ if (enableErr) {
18671
+ return {
18672
+ watched: false,
18673
+ reason: `the repo is linked but commit-driven updates could not be switched on (${enableErr.message}). Pushes will not reach your daemon until they are.`
18674
+ };
18675
+ }
17740
18676
  return { watched: true };
17741
18677
  }
17742
18678
  var init_watchRepo = __esm({
@@ -17746,21 +18682,21 @@ var init_watchRepo = __esm({
17746
18682
  });
17747
18683
 
17748
18684
  // src/init/gitContext.ts
17749
- async function git(cwd, args) {
18685
+ async function git2(cwd, args) {
17750
18686
  const { stdout } = await execa("git", args, { cwd });
17751
18687
  return stdout.trim();
17752
18688
  }
17753
18689
  async function resolveGitContext(repoPath) {
17754
18690
  let root;
17755
18691
  try {
17756
- root = await git(repoPath, ["rev-parse", "--show-toplevel"]);
18692
+ root = await git2(repoPath, ["rev-parse", "--show-toplevel"]);
17757
18693
  } catch {
17758
18694
  throw new Error(
17759
18695
  `\`${repoPath}\` is not inside a git repository \u2014 run \`archme init\` from a repo, or pass its path`
17760
18696
  );
17761
18697
  }
17762
18698
  try {
17763
- await git(root, ["rev-parse", "HEAD"]);
18699
+ await git2(root, ["rev-parse", "HEAD"]);
17764
18700
  } catch {
17765
18701
  throw new Error(
17766
18702
  `\`${root}\` is a git repository with no commits yet \u2014 make a commit first, then run \`archme init\``
@@ -17768,26 +18704,29 @@ async function resolveGitContext(repoPath) {
17768
18704
  }
17769
18705
  let branch = "";
17770
18706
  try {
17771
- branch = await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
18707
+ branch = await git2(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
17772
18708
  } catch {
17773
18709
  branch = "";
17774
18710
  }
17775
18711
  if (!branch || branch === "HEAD") {
17776
18712
  try {
17777
- const ref = await git(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
18713
+ const ref = await git2(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
17778
18714
  branch = ref.replace(/^refs\/remotes\/origin\//, "");
17779
18715
  } catch {
17780
18716
  branch = "main";
17781
18717
  }
17782
18718
  }
17783
18719
  let repoSlug = null;
18720
+ let originUrl = null;
17784
18721
  try {
17785
- const url = await git(root, ["remote", "get-url", "origin"]);
18722
+ const url = await git2(root, ["remote", "get-url", "origin"]);
18723
+ originUrl = url || null;
17786
18724
  repoSlug = url ? parseGitHubRepo(url) : null;
17787
18725
  } catch {
17788
18726
  repoSlug = null;
18727
+ originUrl = null;
17789
18728
  }
17790
- return { root, branch, repoSlug };
18729
+ return { root, branch, repoSlug, originUrl };
17791
18730
  }
17792
18731
  var init_gitContext = __esm({
17793
18732
  "src/init/gitContext.ts"() {
@@ -17797,6 +18736,661 @@ var init_gitContext = __esm({
17797
18736
  }
17798
18737
  });
17799
18738
 
18739
+ // src/init/keepCurrent.ts
18740
+ async function chooseKeepCurrent(deps) {
18741
+ if (deps.yes) {
18742
+ return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
18743
+ }
18744
+ const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
18745
+ "How should ArchMe detect new commits?",
18746
+ DETECTION_OPTIONS
18747
+ );
18748
+ const runner = await deps.choose(
18749
+ "Something has to process each update. Who runs it?",
18750
+ RUNNER_OPTIONS
18751
+ );
18752
+ return { detection, runner };
18753
+ }
18754
+ function formatKeepCurrentPlan(plan, ctx) {
18755
+ const steps = [];
18756
+ if (plan.detection === "local-watch" && !ctx.localWatchRegistered) {
18757
+ steps.push({
18758
+ title: "Register this repo so your machine watches it:",
18759
+ command: "archme local-watch"
18760
+ });
18761
+ }
18762
+ if (plan.runner === "self") {
18763
+ steps.push({
18764
+ title: "Start the daemon \u2014 nothing processes the queue until you do:",
18765
+ command: "archme daemon start",
18766
+ notes: [
18767
+ "Keep it running on a machine that stays on. Each update spends tokens",
18768
+ "from your Claude subscription, the same way this scan did. Your code is",
18769
+ "not sent to our servers \u2014 only the resulting architecture."
18770
+ ]
18771
+ });
18772
+ } else {
18773
+ steps.push({
18774
+ title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
18775
+ notes: [
18776
+ ctx.repoSlug ? `Mention ${ctx.repoSlug} so we know which repo you mean.` : "",
18777
+ // Deliberately not "we'll be in touch": running this command notifies
18778
+ // nobody. Promising contact that depends on the user sending the mail
18779
+ // is how someone ends up waiting on a team that never heard.
18780
+ "Until then this architecture stays as it is \u2014 it won't track new commits."
18781
+ ].filter(Boolean)
18782
+ });
18783
+ }
18784
+ const lines = ["", "Keeping it current:"];
18785
+ if (ctx.localWatchRegistered) {
18786
+ lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
18787
+ }
18788
+ const numbered = steps.length > 1;
18789
+ steps.forEach((step, i2) => {
18790
+ const bullet = numbered ? ` ${i2 + 1}. ` : " ";
18791
+ const indent = " ".repeat(bullet.length);
18792
+ lines.push("", `${bullet}${step.title}`);
18793
+ if (step.command) lines.push("", `${indent} ${step.command}`);
18794
+ if (step.notes?.length) {
18795
+ lines.push("");
18796
+ for (const note of step.notes) lines.push(`${indent}${note}`);
18797
+ }
18798
+ });
18799
+ if (ctx.boardUrl) lines.push("", ` Your board: ${ctx.boardUrl}`);
18800
+ return lines.join("\n");
18801
+ }
18802
+ var CONTACT_EMAIL, DETECTION_OPTIONS, RUNNER_OPTIONS;
18803
+ var init_keepCurrent = __esm({
18804
+ "src/init/keepCurrent.ts"() {
18805
+ "use strict";
18806
+ CONTACT_EMAIL = "signup@archme.io";
18807
+ DETECTION_OPTIONS = [
18808
+ {
18809
+ // First, and named as recommended: it is less work per-commit, survives
18810
+ // this laptop being closed, and is the only path that sees pull requests.
18811
+ label: "GitHub App (recommended) \u2014 a couple of minutes to set up. Sees pushes and pull requests, works for your whole team, nothing to keep running on your side. Someone has to grant it read access to the repo.",
18812
+ value: "github-app"
18813
+ },
18814
+ {
18815
+ label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. No pull-request awareness.",
18816
+ value: "local-watch"
18817
+ }
18818
+ ];
18819
+ RUNNER_OPTIONS = [
18820
+ {
18821
+ label: "I'll run it (recommended) \u2014 `archme daemon start` on a machine that stays on. Updates use your own Claude subscription, the same way this scan did, and your code never leaves your machine.",
18822
+ value: "self"
18823
+ },
18824
+ {
18825
+ label: "ArchMe runs it \u2014 if you don't have a machine that's always on, or a Claude subscription to spend on it.",
18826
+ value: "archme"
18827
+ }
18828
+ ];
18829
+ }
18830
+ });
18831
+
18832
+ // src/init/keepCurrentFlow.ts
18833
+ async function applyKeepCurrentPlan(plan, deps, alreadyWatched) {
18834
+ const base = { detection: plan.detection, runner: plan.runner };
18835
+ if (plan.detection === "github-app") {
18836
+ if (alreadyWatched) {
18837
+ return { ...base, wired: true, localWatchRegistered: false };
18838
+ }
18839
+ const url = await deps.startGitHubAppInstall();
18840
+ deps.log("");
18841
+ deps.log(" Install the ArchMe GitHub App here, then come back \u2014 I'll wait:");
18842
+ deps.log("");
18843
+ deps.log(` ${url}`);
18844
+ deps.log("");
18845
+ const installed = await deps.waitForInstallation();
18846
+ if (!installed) {
18847
+ return {
18848
+ ...base,
18849
+ wired: false,
18850
+ localWatchRegistered: false,
18851
+ reason: "the GitHub App install hasn't come through yet. If your org needs an owner to approve it, that can take a while \u2014 re-run `archme init` once it's approved."
18852
+ };
18853
+ }
18854
+ const watched = await deps.watchRepo();
18855
+ if (!watched.watched) {
18856
+ return {
18857
+ ...base,
18858
+ wired: false,
18859
+ localWatchRegistered: false,
18860
+ reason: watched.reason ?? "the repo could not be linked to this project."
18861
+ };
18862
+ }
18863
+ deps.log(" \u2713 GitHub App installed and the repo is linked.");
18864
+ return { ...base, wired: true, localWatchRegistered: false };
18865
+ }
18866
+ const registered = await deps.registerLocalWatch();
18867
+ if (!registered.ok) {
18868
+ return {
18869
+ ...base,
18870
+ wired: false,
18871
+ localWatchRegistered: false,
18872
+ reason: registered.reason ?? "the repo could not be registered for local polling."
18873
+ };
18874
+ }
18875
+ return { ...base, wired: true, localWatchRegistered: true };
18876
+ }
18877
+ var init_keepCurrentFlow = __esm({
18878
+ "src/init/keepCurrentFlow.ts"() {
18879
+ "use strict";
18880
+ }
18881
+ });
18882
+
18883
+ // src/init/githubAppSetup.ts
18884
+ async function waitForInstallation(deps) {
18885
+ const deadline = deps.now() + INSTALL_WAIT_TIMEOUT_MS;
18886
+ let announced = false;
18887
+ for (; ; ) {
18888
+ try {
18889
+ if (await deps.hasInstallation()) return true;
18890
+ } catch {
18891
+ }
18892
+ if (deps.now() >= deadline) return false;
18893
+ if (!announced) {
18894
+ deps.log(" waiting for the installation to come through\u2026");
18895
+ announced = true;
18896
+ }
18897
+ await deps.sleep(INSTALL_POLL_INTERVAL_MS);
18898
+ }
18899
+ }
18900
+ async function buildInstallUrl(opts) {
18901
+ const doFetch = opts.fetchImpl ?? fetch;
18902
+ const res = await doFetch(`${opts.supabaseUrl}/functions/v1/github/install-state`, {
18903
+ method: "POST",
18904
+ headers: {
18905
+ "Content-Type": "application/json",
18906
+ Authorization: `Bearer ${opts.accessToken}`
18907
+ },
18908
+ body: JSON.stringify({ orgId: opts.orgId, env: opts.env ?? "prod" })
18909
+ });
18910
+ if (!res.ok) {
18911
+ const body = await res.json().catch(() => ({}));
18912
+ throw new Error(body.error ?? `could not start the GitHub App install (${res.status})`);
18913
+ }
18914
+ const { token, url } = await res.json();
18915
+ if (url) return url;
18916
+ if (!token) throw new Error("install-state returned neither a url nor a token");
18917
+ const slug = process.env.ARCHME_GITHUB_APP_SLUG || "architectme";
18918
+ return `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(token)}`;
18919
+ }
18920
+ async function orgHasInstallation(supabase, orgId) {
18921
+ const { data, error } = await supabase.from("org_github_installations").select("installation_id").eq("org_id", orgId).limit(1);
18922
+ if (error) throw new Error(error.message);
18923
+ return (data?.length ?? 0) > 0;
18924
+ }
18925
+ var INSTALL_WAIT_TIMEOUT_MS, INSTALL_POLL_INTERVAL_MS;
18926
+ var init_githubAppSetup = __esm({
18927
+ "src/init/githubAppSetup.ts"() {
18928
+ "use strict";
18929
+ INSTALL_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
18930
+ INSTALL_POLL_INTERVAL_MS = 3e3;
18931
+ }
18932
+ });
18933
+
18934
+ // src/daemon/prompt.ts
18935
+ import { createInterface } from "node:readline/promises";
18936
+ async function withReadline(fn) {
18937
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18938
+ try {
18939
+ return await fn(rl);
18940
+ } finally {
18941
+ rl.close();
18942
+ }
18943
+ }
18944
+ async function promptLine(question) {
18945
+ return await withReadline(async (rl) => {
18946
+ try {
18947
+ return await rl.question(`${question}: `);
18948
+ } catch {
18949
+ return "";
18950
+ }
18951
+ });
18952
+ }
18953
+ async function promptChoice(question, options) {
18954
+ return await withReadline(async (rl) => {
18955
+ console.log(`
18956
+ ${question}
18957
+ `);
18958
+ options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
18959
+ `));
18960
+ for (let attempt = 0; attempt < 3; attempt++) {
18961
+ let answer;
18962
+ try {
18963
+ answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
18964
+ } catch {
18965
+ return options[0].value;
18966
+ }
18967
+ if (answer === "") return options[0].value;
18968
+ const n2 = Number.parseInt(answer, 10);
18969
+ if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
18970
+ console.log(` "${answer}" isn't one of the options.`);
18971
+ }
18972
+ return options[0].value;
18973
+ });
18974
+ }
18975
+ async function promptYesNo(question) {
18976
+ return await withReadline(async (rl) => {
18977
+ try {
18978
+ const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
18979
+ return answer === "" || answer === "y" || answer === "yes";
18980
+ } catch {
18981
+ return false;
18982
+ }
18983
+ });
18984
+ }
18985
+ var init_prompt = __esm({
18986
+ "src/daemon/prompt.ts"() {
18987
+ "use strict";
18988
+ }
18989
+ });
18990
+
18991
+ // src/daemon/watchReadiness.ts
18992
+ function evaluateWatchReadiness(input) {
18993
+ const { marker, repo, archmeBranch } = input;
18994
+ if (!marker.sha) {
18995
+ return {
18996
+ ok: false,
18997
+ reason: `${repo} has no architecture on \`${archmeBranch}\` yet, so there is nothing to keep up to date and no commit to diff against.
18998
+ Run \`archme init\` in this repo first \u2014 it creates the architecture, then comes back here.`
18999
+ };
19000
+ }
19001
+ if (!GIT_SHA_RE2.test(marker.sha)) {
19002
+ return {
19003
+ ok: false,
19004
+ reason: `the recorded commit for \`${archmeBranch}\` is "${marker.sha}", which is not a git commit id. Re-run \`archme init --force\` to re-establish it.`
19005
+ };
19006
+ }
19007
+ if (!marker.repo) {
19008
+ return {
19009
+ ok: false,
19010
+ reason: `the architecture on \`${archmeBranch}\` has no source repo recorded, so updates cannot be matched to ${repo}.
19011
+ That usually means the scan ran against a non-github.com remote. Re-run \`archme init --force\` from a clone whose \`origin\` is on github.com.`
19012
+ };
19013
+ }
19014
+ if (marker.repo !== repo) {
19015
+ return {
19016
+ ok: false,
19017
+ reason: `\`${archmeBranch}\` is already tracking ${marker.repo}, not ${repo}. Watching it from here would make every update fail on the mismatch.
19018
+ Use \`--project <org>/<slug>\` to pick a different project, or a different branch.`
19019
+ };
19020
+ }
19021
+ return { ok: true };
19022
+ }
19023
+ var GIT_SHA_RE2;
19024
+ var init_watchReadiness = __esm({
19025
+ "src/daemon/watchReadiness.ts"() {
19026
+ "use strict";
19027
+ GIT_SHA_RE2 = /^[0-9a-f]{40}$/;
19028
+ }
19029
+ });
19030
+
19031
+ // src/daemon/localWatchList.ts
19032
+ function formatWatchList(repos) {
19033
+ if (repos.length === 0) {
19034
+ return [
19035
+ "Not watching any repos on this machine.",
19036
+ "",
19037
+ " archme local-watch register the repo you're in"
19038
+ ].join("\n");
19039
+ }
19040
+ const lines = [
19041
+ `Watching ${repos.length} repo${repos.length === 1 ? "" : "s"} on this machine:`,
19042
+ ""
19043
+ ];
19044
+ for (const r of repos) {
19045
+ const slug = slugOf(r);
19046
+ lines.push(` ${slug}`);
19047
+ lines.push(` mirror: ${r.mirrorPath}`);
19048
+ if (r.haltedAt) {
19049
+ lines.push(` HALTED at ${r.haltedAt}: ${r.haltReason ?? "unknown reason"}`);
19050
+ lines.push(` no new commits are being picked up. Resume with:`);
19051
+ lines.push(` archme local-watch --resume ${slug}`);
19052
+ }
19053
+ lines.push("");
19054
+ }
19055
+ if (repos.every((r) => !r.haltedAt)) {
19056
+ lines.push("Updates only happen while the daemon is running: archme daemon status");
19057
+ }
19058
+ return lines.join("\n").trimEnd();
19059
+ }
19060
+ var init_localWatchList = __esm({
19061
+ "src/daemon/localWatchList.ts"() {
19062
+ "use strict";
19063
+ init_localRepos();
19064
+ }
19065
+ });
19066
+
19067
+ // src/daemon/localWatchResolve.ts
19068
+ function confirmationFor(target, repoPath) {
19069
+ return `Watch ${target.repo} (branch ${target.repoBranch}) for architecture updates?
19070
+ checkout: ${repoPath}`;
19071
+ }
19072
+ async function evaluate(path7, deps) {
19073
+ const inspected = await deps.inspect(path7);
19074
+ if (!inspected.ok) return { ok: false, reason: inspected.error };
19075
+ const resolved2 = resolveInitTarget({
19076
+ originUrl: inspected.originUrl,
19077
+ branchOverride: deps.branch,
19078
+ detectedDefaultBranch: inspected.detectedDefaultBranch
19079
+ });
19080
+ if (!resolved2.ok) return { ok: false, reason: resolved2.error };
19081
+ return {
19082
+ ok: true,
19083
+ // The ROOT, never the cwd — registering a subdirectory would mirror a
19084
+ // partial tree and record a source path that isn't the repo.
19085
+ repoPath: inspected.root,
19086
+ originUrl: inspected.originUrl,
19087
+ target: resolved2.target
19088
+ };
19089
+ }
19090
+ async function resolveRepoInteractive(deps) {
19091
+ const first = deps.explicitPath ?? deps.cwd;
19092
+ let evaluated = await evaluate(first, deps);
19093
+ if (deps.yes) {
19094
+ if (!evaluated.ok) return { ok: false, message: evaluated.reason };
19095
+ return evaluated;
19096
+ }
19097
+ if (evaluated.ok) {
19098
+ if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
19099
+ }
19100
+ let reason = evaluated.ok ? "Which repo should ArchMe watch? Enter its path" : `${evaluated.reason}
19101
+ Which repo should ArchMe watch? Enter its path`;
19102
+ for (let attempt = 0; attempt < MAX_REPO_PROMPTS; attempt++) {
19103
+ const answer = (await deps.ask(reason)).trim();
19104
+ if (!answer) return { ok: false, message: "cancelled \u2014 nothing was registered" };
19105
+ evaluated = await evaluate(answer, deps);
19106
+ if (evaluated.ok) {
19107
+ if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
19108
+ reason = "Which repo should ArchMe watch? Enter its path";
19109
+ continue;
19110
+ }
19111
+ reason = `${evaluated.reason}
19112
+ Which repo should ArchMe watch? Enter its path`;
19113
+ }
19114
+ return {
19115
+ ok: false,
19116
+ message: "could not find a repo to watch. `archme local-watch` needs a git clone with a github.com origin \u2014 run it from inside one, or pass the path directly."
19117
+ };
19118
+ }
19119
+ var MAX_REPO_PROMPTS;
19120
+ var init_localWatchResolve = __esm({
19121
+ "src/daemon/localWatchResolve.ts"() {
19122
+ "use strict";
19123
+ init_localWatch();
19124
+ MAX_REPO_PROMPTS = 3;
19125
+ }
19126
+ });
19127
+
19128
+ // src/daemon/localWatch.ts
19129
+ var localWatch_exports = {};
19130
+ __export(localWatch_exports, {
19131
+ DEFAULT_CLONE_BASE_DIR: () => DEFAULT_CLONE_BASE_DIR2,
19132
+ inspectRepoPath: () => inspectRepoPath,
19133
+ mirrorPathFor: () => mirrorPathFor,
19134
+ registerLocalWatch: () => registerLocalWatch,
19135
+ resolveInitTarget: () => resolveInitTarget,
19136
+ runLocalWatch: () => runLocalWatch,
19137
+ runLocalWatchList: () => runLocalWatchList,
19138
+ runLocalWatchResume: () => runLocalWatchResume,
19139
+ runLocalWatchStop: () => runLocalWatchStop
19140
+ });
19141
+ import { homedir as homedir6 } from "node:os";
19142
+ import { join as join21, resolve as resolve3 } from "node:path";
19143
+ import { stat as stat10 } from "node:fs/promises";
19144
+ function mirrorPathFor(cloneBaseDir, owner, name) {
19145
+ return join21(cloneBaseDir, owner, name);
19146
+ }
19147
+ function resolveInitTarget(input) {
19148
+ if (!input.originUrl) {
19149
+ return {
19150
+ ok: false,
19151
+ error: "no `origin` remote found \u2014 archme local-watch needs a clone with a GitHub origin"
19152
+ };
19153
+ }
19154
+ const repo = parseGitHubRepo(input.originUrl);
19155
+ if (!repo) {
19156
+ return {
19157
+ ok: false,
19158
+ error: `origin ${input.originUrl} is not a github.com remote.
19159
+ Local mode identifies repos the same way the GitHub App does, so that a team can later install the App without re-scanning or losing history. That identity only exists for github.com remotes.`
19160
+ };
19161
+ }
19162
+ const repoBranch = input.branchOverride ?? input.detectedDefaultBranch;
19163
+ if (!repoBranch) {
19164
+ return {
19165
+ ok: false,
19166
+ error: "could not determine the default branch \u2014 pass --branch <name>"
19167
+ };
19168
+ }
19169
+ const [repoOwner, repoName] = repo.split("/");
19170
+ return {
19171
+ ok: true,
19172
+ target: {
19173
+ repo,
19174
+ repoOwner,
19175
+ repoName,
19176
+ repoBranch,
19177
+ // Scanned projects live on a `<default>-scan` companion branch; the poller
19178
+ // must enqueue against the SAME branch the scan stamped the marker on.
19179
+ archmeBranch: input.archmeBranchOverride ?? normalizeScanBranchName(repoBranch)
19180
+ }
19181
+ };
19182
+ }
19183
+ async function inspectRepoPath(path7) {
19184
+ const abs = resolve3(path7);
19185
+ const st = await stat10(abs).catch(() => null);
19186
+ if (!st?.isDirectory()) return { ok: false, error: `${abs} is not a directory` };
19187
+ const toplevel = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: abs, reject: false });
19188
+ if (toplevel.exitCode !== 0) return { ok: false, error: `${abs} is not a git repository` };
19189
+ const root = toplevel.stdout.trim() || abs;
19190
+ return {
19191
+ ok: true,
19192
+ root,
19193
+ originUrl: await readOriginUrl(root),
19194
+ detectedDefaultBranch: await detectDefaultBranch(root)
19195
+ };
19196
+ }
19197
+ async function runLocalWatch(opts) {
19198
+ const log = opts.log ?? ((m) => console.log(m));
19199
+ const cloneBaseDir = opts.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
19200
+ const resolved2 = await resolveRepoInteractive({
19201
+ cwd: process.cwd(),
19202
+ explicitPath: opts.repoPath,
19203
+ branch: opts.branch,
19204
+ yes: opts.yes ?? false,
19205
+ inspect: inspectRepoPath,
19206
+ ask: promptLine,
19207
+ confirm: promptYesNo,
19208
+ log
19209
+ });
19210
+ if (!resolved2.ok) {
19211
+ console.error(`archme local-watch: ${resolved2.message}`);
19212
+ return 78;
19213
+ }
19214
+ const { repoPath, originUrl, target } = resolved2;
19215
+ log(` repo: ${target.repo}`);
19216
+ log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
19217
+ const cfg = resolveConfig();
19218
+ let authed;
19219
+ try {
19220
+ authed = await createAuthedClient({
19221
+ supabaseUrl: cfg.supabaseUrl,
19222
+ supabaseAnonKey: cfg.supabaseAnonKey,
19223
+ clientId: cfg.archmeDaemonClientId,
19224
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
19225
+ });
19226
+ } catch (err2) {
19227
+ console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
19228
+ return 3;
19229
+ }
19230
+ const { supabase, userId } = authed;
19231
+ const orgId = authed.orgId;
19232
+ if (!orgId) {
19233
+ console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
19234
+ return 3;
19235
+ }
19236
+ const orgSlug = await resolveCurrentOrgSlug(supabase, orgId);
19237
+ if (!opts.project && !orgSlug) {
19238
+ console.error(
19239
+ "archme local-watch: could not resolve your organization slug, so the project name can't be derived \u2014 pass --project <org/slug>"
19240
+ );
19241
+ return 3;
19242
+ }
19243
+ const fullSlug = opts.project ?? defaultProjectSlug(orgSlug, repoPath);
19244
+ const project = await resolveOrCreateProject(supabase, userId, fullSlug);
19245
+ log(` project: ${project.orgSlug}/${project.projectSlug}${project.created ? " (created)" : ""}`);
19246
+ const registered = await registerLocalWatch({
19247
+ supabase,
19248
+ orgId,
19249
+ projectId: project.projectId,
19250
+ repoPath,
19251
+ originUrl,
19252
+ target,
19253
+ cloneBaseDir,
19254
+ log
19255
+ });
19256
+ if (!registered.ok) {
19257
+ console.error(`archme local-watch: ${registered.reason}`);
19258
+ return registered.exitCode;
19259
+ }
19260
+ log(`
19261
+ \u2713 ${target.repo} is now watched locally.`);
19262
+ log(`
19263
+ Start the daemon if it isn't running: archme daemon start`);
19264
+ log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
19265
+ return 0;
19266
+ }
19267
+ async function registerLocalWatch(args) {
19268
+ const log = args.log ?? (() => {
19269
+ });
19270
+ const { target, supabase, orgId, projectId, repoPath, originUrl } = args;
19271
+ const cloneBaseDir = args.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
19272
+ const marker = await readBranchMarker(supabase, projectId, target.archmeBranch);
19273
+ const readiness = evaluateWatchReadiness({
19274
+ repo: target.repo,
19275
+ archmeBranch: target.archmeBranch,
19276
+ marker
19277
+ });
19278
+ if (!readiness.ok) return { ok: false, reason: readiness.reason, exitCode: 78 };
19279
+ const { data: watchedRepoId, error: watchErr } = await supabase.rpc("watch_local_repo", {
19280
+ p_org_id: orgId,
19281
+ p_project_id: projectId,
19282
+ p_repo_owner: target.repoOwner,
19283
+ p_repo_name: target.repoName,
19284
+ p_branch: target.repoBranch,
19285
+ // The ArchMe branch has to be RECORDED, not re-derived by each reader.
19286
+ // The poller resolves it from the row (`commit_update_archme_branch ??
19287
+ // branch`, mirroring the webhook); with the column unset it fell back to
19288
+ // the git branch, so registration checked `main-scan` while polling read
19289
+ // `main`, found no marker, and idled forever with everything looking
19290
+ // healthy. Migration 111.
19291
+ p_archme_branch: target.archmeBranch
19292
+ });
19293
+ if (watchErr || typeof watchedRepoId !== "string") {
19294
+ return {
19295
+ ok: false,
19296
+ reason: `could not register the repo: ${watchErr?.message ?? "no id returned"}`,
19297
+ exitCode: 3
19298
+ };
19299
+ }
19300
+ const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
19301
+ const mirrorExists = await stat10(join21(mirrorPath, ".git")).then(() => true).catch(() => false);
19302
+ if (mirrorExists) {
19303
+ log(` mirror: ${mirrorPath} (reusing)`);
19304
+ await setOriginUrl(mirrorPath, originUrl);
19305
+ } else {
19306
+ log(` mirror: ${mirrorPath}`);
19307
+ await createMirrorClone(repoPath, mirrorPath, originUrl);
19308
+ }
19309
+ await execa("git", ["fetch", "--no-tags", "origin", target.repoBranch], {
19310
+ cwd: mirrorPath,
19311
+ reject: true
19312
+ });
19313
+ const entry = {
19314
+ watchedRepoId,
19315
+ mirrorPath,
19316
+ sourcePath: repoPath,
19317
+ repoOwner: target.repoOwner,
19318
+ repoName: target.repoName
19319
+ };
19320
+ await registerLocalRepo(entry);
19321
+ return { ok: true, entry };
19322
+ }
19323
+ async function runLocalWatchList() {
19324
+ console.log(formatWatchList(await loadLocalRepos()));
19325
+ return 0;
19326
+ }
19327
+ async function runLocalWatchStop(slug) {
19328
+ const repos = await loadLocalRepos();
19329
+ const matches = findLocalReposBySlug(repos, slug);
19330
+ if (matches.length === 0) {
19331
+ console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
19332
+ console.error("Run `archme local-watch --list` to see what is.");
19333
+ return 78;
19334
+ }
19335
+ await saveLocalRepos(removeLocalReposBySlug(repos, slug));
19336
+ for (const m of matches) {
19337
+ console.log(`Stopped watching ${slugOf(m)}.`);
19338
+ }
19339
+ console.log(
19340
+ `
19341
+ The architecture already on the board is untouched \u2014 this only stops new commits
19342
+ from being picked up. Re-watch with: archme local-watch ${matches[0].sourcePath}`
19343
+ );
19344
+ return 0;
19345
+ }
19346
+ async function runLocalWatchResume(slug) {
19347
+ const repos = await loadLocalRepos();
19348
+ const matches = findLocalReposBySlug(repos, slug);
19349
+ if (matches.length === 0) {
19350
+ console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
19351
+ return 78;
19352
+ }
19353
+ const halted = matches.filter((m) => m.haltedAt);
19354
+ if (halted.length === 0) {
19355
+ console.log(`${slug} is not halted \u2014 nothing to resume.`);
19356
+ return 0;
19357
+ }
19358
+ let next = repos;
19359
+ for (const m of halted) next = clearHalt(next, m.watchedRepoId);
19360
+ await saveLocalRepos(next);
19361
+ for (const m of halted) {
19362
+ console.log(`Resumed ${slugOf(m)} \u2014 was: ${m.haltReason ?? "unknown reason"}`);
19363
+ }
19364
+ const stillTerminal = halted.some((m) => m.haltCode === "job-terminal");
19365
+ if (stillTerminal) {
19366
+ console.log(
19367
+ "\nNote: at least one of these stopped because a job failed outright. Resuming\ndoes not re-run a failed job \u2014 polling will stop again on the next tick.\nThat failure is visible to the ArchMe team; get in touch if it is blocking you."
19368
+ );
19369
+ } else {
19370
+ console.log("\nPolling resumes from the recorded commit on the next tick.");
19371
+ }
19372
+ return 0;
19373
+ }
19374
+ var DEFAULT_CLONE_BASE_DIR2;
19375
+ var init_localWatch = __esm({
19376
+ "src/daemon/localWatch.ts"() {
19377
+ "use strict";
19378
+ init_execa();
19379
+ init_defaults();
19380
+ init_github_remote();
19381
+ init_run_scan();
19382
+ init_authedClient();
19383
+ init_watchReadiness();
19384
+ init_localGit();
19385
+ init_localRepos();
19386
+ init_branchMarker();
19387
+ init_localWatchList();
19388
+ init_localWatchResolve();
19389
+ init_prompt();
19390
+ DEFAULT_CLONE_BASE_DIR2 = join21(homedir6(), ".archme-job-runner", "clones");
19391
+ }
19392
+ });
19393
+
17800
19394
  // src/init/plan.ts
17801
19395
  function shouldRunScan(d) {
17802
19396
  if (d.skipScan) return false;
@@ -17826,9 +19420,9 @@ var init_plan = __esm({
17826
19420
  });
17827
19421
 
17828
19422
  // src/init/run-init.ts
17829
- import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile11 } from "node:fs/promises";
19423
+ import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile12 } from "node:fs/promises";
17830
19424
  import { tmpdir as tmpdir4 } from "node:os";
17831
- import { join as join18 } from "node:path";
19425
+ import { join as join22 } from "node:path";
17832
19426
  function initScanBranch(defaultBranch) {
17833
19427
  return normalizeScanBranchName(defaultBranch);
17834
19428
  }
@@ -17840,10 +19434,10 @@ async function defaultFetchInstallScript(url) {
17840
19434
  return await resp.text();
17841
19435
  }
17842
19436
  async function defaultRunInstallScript(script, cwd) {
17843
- const dir = await mkdtemp4(join18(tmpdir4(), "archme-init-"));
17844
- const scriptPath = join18(dir, "install-agent.sh");
19437
+ const dir = await mkdtemp4(join22(tmpdir4(), "archme-init-"));
19438
+ const scriptPath = join22(dir, "install-agent.sh");
17845
19439
  try {
17846
- await writeFile11(scriptPath, script, { mode: 448 });
19440
+ await writeFile12(scriptPath, script, { mode: 448 });
17847
19441
  await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
17848
19442
  } finally {
17849
19443
  await rm8(dir, { recursive: true, force: true });
@@ -17864,8 +19458,8 @@ async function runInit(opts, deps = {}) {
17864
19458
  const measure = deps.measure ?? measureRepo;
17865
19459
  const watchRepo = deps.watchRepo ?? watchRepoForProject;
17866
19460
  const cfg = resolveConfig();
17867
- const git2 = await gitContext(opts.repoPath);
17868
- const repoPath = git2.root;
19461
+ const git4 = await gitContext(opts.repoPath);
19462
+ const repoPath = git4.root;
17869
19463
  const { credentials } = await loadCreds(cfg.supabaseUrl);
17870
19464
  const signedIn = credentials.length > 0;
17871
19465
  if (!signedIn) {
@@ -17924,8 +19518,9 @@ async function runInit(opts, deps = {}) {
17924
19518
  supabase: authed.supabase,
17925
19519
  orgId: authed.orgId,
17926
19520
  projectId: project.projectId,
17927
- repoSlug: git2.repoSlug,
17928
- branch: git2.branch,
19521
+ repoSlug: git4.repoSlug,
19522
+ branch: git4.branch,
19523
+ archmeBranch: scanBranch,
17929
19524
  log
17930
19525
  });
17931
19526
  if (!watched.watched && watched.reason) {
@@ -17975,28 +19570,79 @@ Your board: ${webUrl}`);
17975
19570
  log(`
17976
19571
  \u2713 Done. Your architecture: ${finalUrl}`);
17977
19572
  log(" Restart Claude Code in this repo to pick up the archme MCP server.");
17978
- if (watched.watched) {
17979
- log("");
17980
- log("Keeping it up to date \u2014 one more step:");
17981
- log(" Pushes to this repo now queue an architecture update, but");
17982
- log(" something has to process that queue, and nothing does yet.");
17983
- log("");
17984
- log(" Run the agent yourself, on a machine that stays on:");
17985
- log("");
17986
- log(" archme daemon start");
17987
- log("");
17988
- log(" Each update counts tokens against your Claude account, the same");
17989
- log(" way this scan did. Your code is not sent to our servers \u2014 only");
17990
- log(" the resulting architecture.");
17991
- log("");
17992
- log(" Or get in touch at archme.io and we'll run it for you.");
17993
- } else {
19573
+ const plan = await chooseKeepCurrent({
19574
+ alreadyWatched: watched.watched,
19575
+ yes: Boolean(opts.yes),
19576
+ repoSlug: git4.repoSlug,
19577
+ choose: deps.choose ?? promptChoice,
19578
+ log
19579
+ });
19580
+ const outcome = await applyKeepCurrentPlan(
19581
+ plan,
19582
+ {
19583
+ startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
19584
+ supabaseUrl: cfg.supabaseUrl,
19585
+ accessToken: getCurrentAccessToken() ?? "",
19586
+ orgId: authed.orgId
19587
+ }),
19588
+ waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
19589
+ hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
19590
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
19591
+ now: () => Date.now(),
19592
+ log
19593
+ }),
19594
+ watchRepo: async () => {
19595
+ const w = await watchRepo({
19596
+ supabase: authed.supabase,
19597
+ orgId: authed.orgId,
19598
+ projectId: project.projectId,
19599
+ repoSlug: git4.repoSlug,
19600
+ branch: git4.branch,
19601
+ archmeBranch: scanBranch,
19602
+ log
19603
+ });
19604
+ return { watched: w.watched, reason: w.reason };
19605
+ },
19606
+ registerLocalWatch: async () => {
19607
+ const target = resolveInitTarget({
19608
+ originUrl: git4.originUrl,
19609
+ detectedDefaultBranch: git4.branch,
19610
+ // The branch this run actually scanned. Deriving it from the git
19611
+ // branch instead would point the poller at `<checkout>-scan` while
19612
+ // the marker sits on `<default>-scan` — so registering would fail
19613
+ // on a feature-branch checkout, immediately after a scan that
19614
+ // succeeded.
19615
+ archmeBranchOverride: scanBranch
19616
+ });
19617
+ if (!target.ok) return { ok: false, reason: target.error };
19618
+ const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
19619
+ supabase: authed.supabase,
19620
+ orgId: authed.orgId,
19621
+ projectId: project.projectId,
19622
+ repoPath: git4.root,
19623
+ originUrl: git4.originUrl,
19624
+ target: target.target,
19625
+ log
19626
+ });
19627
+ return res.ok ? { ok: true } : { ok: false, reason: res.reason };
19628
+ },
19629
+ log
19630
+ },
19631
+ watched.watched
19632
+ );
19633
+ if (!outcome.wired && outcome.reason) {
17994
19634
  log("");
17995
19635
  log("Not yet set up for automatic updates:");
17996
- log(` ${watched.reason ?? "the repo could not be linked to this project."}`);
17997
- log(" Until that is resolved, this architecture will not track new");
17998
- log(" commits \u2014 re-run `archme init` once it is.");
19636
+ log(` ${outcome.reason}`);
19637
+ log(" Until that is resolved, this architecture will not track new commits.");
17999
19638
  }
19639
+ log(
19640
+ formatKeepCurrentPlan(plan, {
19641
+ repoSlug: git4.repoSlug,
19642
+ boardUrl: finalUrl,
19643
+ localWatchRegistered: outcome.localWatchRegistered
19644
+ })
19645
+ );
18000
19646
  }
18001
19647
  return {
18002
19648
  exitCode: result.exitCode,
@@ -18020,6 +19666,12 @@ var init_run_init = __esm({
18020
19666
  init_watchRepo();
18021
19667
  init_gitContext();
18022
19668
  init_web_url();
19669
+ init_bearer();
19670
+ init_keepCurrent();
19671
+ init_keepCurrentFlow();
19672
+ init_githubAppSetup();
19673
+ init_prompt();
19674
+ init_localWatch();
18023
19675
  init_plan();
18024
19676
  }
18025
19677
  });
@@ -18111,7 +19763,7 @@ __export(dispatch_exports4, {
18111
19763
  dispatchInit: () => dispatchInit,
18112
19764
  printInitHelp: () => printInitHelp
18113
19765
  });
18114
- import { resolve as resolve3 } from "node:path";
19766
+ import { resolve as resolve4 } from "node:path";
18115
19767
  function printInitHelp() {
18116
19768
  console.log(`archme init \u2014 guided first-time setup for this repo
18117
19769
 
@@ -18149,7 +19801,7 @@ async function dispatchInit(argv) {
18149
19801
  }
18150
19802
  try {
18151
19803
  const result = await runInit({
18152
- repoPath: resolve3(parsed.args.repoPath ?? process.cwd()),
19804
+ repoPath: resolve4(parsed.args.repoPath ?? process.cwd()),
18153
19805
  project: parsed.args.project,
18154
19806
  appOrigin: parsed.args.appOrigin,
18155
19807
  force: parsed.args.force,
@@ -18157,8 +19809,8 @@ async function dispatchInit(argv) {
18157
19809
  yes: parsed.args.yes || parsed.args.noInteractive
18158
19810
  });
18159
19811
  return result.exitCode;
18160
- } catch (err) {
18161
- console.error(`archme init: ${err instanceof Error ? err.message : String(err)}`);
19812
+ } catch (err2) {
19813
+ console.error(`archme init: ${err2 instanceof Error ? err2.message : String(err2)}`);
18162
19814
  return EX_INFRA2;
18163
19815
  }
18164
19816
  }
@@ -18174,6 +19826,620 @@ var init_dispatch4 = __esm({
18174
19826
  }
18175
19827
  });
18176
19828
 
19829
+ // src/daemon/localWatchDispatch.ts
19830
+ var localWatchDispatch_exports = {};
19831
+ __export(localWatchDispatch_exports, {
19832
+ EX_INFRA: () => EX_INFRA3,
19833
+ EX_OK: () => EX_OK3,
19834
+ EX_USAGE: () => EX_USAGE3,
19835
+ dispatchLocalWatch: () => dispatchLocalWatch,
19836
+ parseLocalWatchArgs: () => parseLocalWatchArgs,
19837
+ printLocalWatchHelp: () => printLocalWatchHelp
19838
+ });
19839
+ function err(message) {
19840
+ return { kind: "error", message: `archme local-watch: ${message}` };
19841
+ }
19842
+ function parseLocalWatchArgs(argv) {
19843
+ if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
19844
+ let repoPath;
19845
+ let project;
19846
+ let branch;
19847
+ let yes = false;
19848
+ let list = false;
19849
+ let stop;
19850
+ let resume;
19851
+ for (let i2 = 0; i2 < argv.length; i2++) {
19852
+ const arg = argv[i2];
19853
+ const next = argv[i2 + 1];
19854
+ if (arg === "--project") {
19855
+ if (!next) return err("--project needs an org/slug");
19856
+ if (!PROJECT_SLUG_RE3.test(next)) {
19857
+ return err(
19858
+ `--project '${next}' must be \`org/slug\` (exactly two non-empty segments separated by /)`
19859
+ );
19860
+ }
19861
+ project = next;
19862
+ i2++;
19863
+ } else if (arg === "--branch") {
19864
+ if (!next) return err("--branch needs a branch name");
19865
+ branch = next;
19866
+ i2++;
19867
+ } else if (arg === "--yes" || arg === "--no-interactive") {
19868
+ yes = true;
19869
+ } else if (arg === "--list") {
19870
+ list = true;
19871
+ } else if (arg === "--stop" || arg === "--resume") {
19872
+ if (!next || next.startsWith("-")) return err(`${arg} needs a repo (owner/name)`);
19873
+ if (!REPO_SLUG_RE.test(next)) {
19874
+ return err(`${arg} '${next}' must be \`owner/name\` \u2014 that is how repos are registered`);
19875
+ }
19876
+ if (arg === "--stop") stop = next;
19877
+ else resume = next;
19878
+ i2++;
19879
+ } else if (arg && arg.startsWith("-")) {
19880
+ return err(`unknown flag '${arg}'`);
19881
+ } else if (arg && repoPath === void 0) {
19882
+ repoPath = arg;
19883
+ } else if (arg) {
19884
+ return err(`unexpected argument '${arg}'`);
19885
+ }
19886
+ }
19887
+ const modes = [list && "--list", stop && "--stop", resume && "--resume"].filter(Boolean);
19888
+ if (modes.length > 1) {
19889
+ return err(`${modes.join(" and ")} do different things \u2014 pass one of them at once, not both`);
19890
+ }
19891
+ if (list) return { kind: "list" };
19892
+ if (stop) return { kind: "stop", repo: stop };
19893
+ if (resume) return { kind: "resume", repo: resume };
19894
+ return { kind: "register", repoPath, project, branch, yes };
19895
+ }
19896
+ function printLocalWatchHelp() {
19897
+ console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
19898
+
19899
+ Registers a repo so this machine polls \`origin\` and feeds new commits into
19900
+ ArchMe, one commit at a time. No GitHub App, no admin approval \u2014 but the
19901
+ updates only happen while your daemon is running. Requires a github.com origin.
19902
+
19903
+ Your working checkout is read exactly once (to find its origin URL and seed a
19904
+ mirror); everything afterwards runs against a daemon-owned copy, so ArchMe
19905
+ never touches your working tree.
19906
+
19907
+ Usage:
19908
+ archme local-watch [repo-path] Register a repo (defaults to the one you're in)
19909
+ archme local-watch --list Show what this machine is watching
19910
+ archme local-watch --stop <owner/name>
19911
+ archme local-watch --resume <owner/name>
19912
+
19913
+ Options:
19914
+ --project <org/slug> Project to write to (default: derived from the repo name)
19915
+ --branch <name> Branch to track (default: the repo's default branch)
19916
+ --yes Don't ask for confirmation (needs an explicit repo-path)
19917
+
19918
+ Watching queues work; the daemon does it. After registering: archme daemon start`);
19919
+ }
19920
+ async function dispatchLocalWatch(argv) {
19921
+ const parsed = parseLocalWatchArgs(argv);
19922
+ if (parsed.kind === "help") {
19923
+ printLocalWatchHelp();
19924
+ return EX_OK3;
19925
+ }
19926
+ if (parsed.kind === "error") {
19927
+ console.error(parsed.message);
19928
+ printLocalWatchHelp();
19929
+ return EX_USAGE3;
19930
+ }
19931
+ const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
19932
+ switch (parsed.kind) {
19933
+ case "list":
19934
+ return await mod.runLocalWatchList();
19935
+ case "stop":
19936
+ return await mod.runLocalWatchStop(parsed.repo);
19937
+ case "resume":
19938
+ return await mod.runLocalWatchResume(parsed.repo);
19939
+ case "register":
19940
+ return await mod.runLocalWatch(parsed);
19941
+ }
19942
+ }
19943
+ var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
19944
+ var init_localWatchDispatch = __esm({
19945
+ "src/daemon/localWatchDispatch.ts"() {
19946
+ "use strict";
19947
+ EX_OK3 = 0;
19948
+ EX_INFRA3 = 3;
19949
+ EX_USAGE3 = 78;
19950
+ PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
19951
+ REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
19952
+ }
19953
+ });
19954
+
19955
+ // src/doctor/collect.ts
19956
+ import { readFile as readFile13 } from "node:fs/promises";
19957
+ import { join as join23 } from "node:path";
19958
+ function binaryPresent(name) {
19959
+ try {
19960
+ requireBinary(name);
19961
+ return true;
19962
+ } catch {
19963
+ return false;
19964
+ }
19965
+ }
19966
+ async function git3(cwd, args) {
19967
+ const r = await execa("git", args, { cwd, reject: false });
19968
+ return r.exitCode === 0 ? r.stdout.trim() || null : null;
19969
+ }
19970
+ async function readMcpPin(repoRoot) {
19971
+ const empty = { installed: false, pin: { project: null, branch: null } };
19972
+ try {
19973
+ const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
19974
+ const parsed = JSON.parse(raw);
19975
+ const servers = parsed.mcpServers ?? {};
19976
+ const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
19977
+ if (!entry) return empty;
19978
+ const url = entry[1]?.url;
19979
+ if (!url) return { installed: true, pin: { project: null, branch: null } };
19980
+ const params = new URL(url).searchParams;
19981
+ return {
19982
+ installed: true,
19983
+ pin: { project: params.get("project"), branch: params.get("branch") }
19984
+ };
19985
+ } catch {
19986
+ return empty;
19987
+ }
19988
+ }
19989
+ async function isDaemonRunning() {
19990
+ const r = await execa("pgrep", ["-f", "archme.*daemon"], { reject: false });
19991
+ return r.exitCode === 0 && Boolean(r.stdout.trim());
19992
+ }
19993
+ async function collectFacts(cwd) {
19994
+ const facts = {
19995
+ cliVersion: await readDaemonVersion(),
19996
+ // `requireBinary` throws when absent; doctor's whole job is to report that
19997
+ // calmly rather than die on it.
19998
+ claudeInstalled: binaryPresent("claude"),
19999
+ gitInstalled: binaryPresent("git"),
20000
+ signedIn: false,
20001
+ identity: null,
20002
+ orgId: null,
20003
+ inRepo: false,
20004
+ repoRoot: null,
20005
+ repoSlug: null,
20006
+ gitBranch: null,
20007
+ agentConfigInstalled: false,
20008
+ pinnedProject: null,
20009
+ pinnedBranch: null,
20010
+ pinnedOrgVisible: null,
20011
+ projectExists: null,
20012
+ componentCount: null,
20013
+ markerSha: null,
20014
+ watchedBy: "unknown",
20015
+ localRepos: [],
20016
+ daemonRunning: await isDaemonRunning()
20017
+ };
20018
+ const root = facts.gitInstalled ? await git3(cwd, ["rev-parse", "--show-toplevel"]) : null;
20019
+ if (root) {
20020
+ facts.inRepo = true;
20021
+ facts.repoRoot = root;
20022
+ facts.gitBranch = await git3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
20023
+ const originUrl = await git3(root, ["remote", "get-url", "origin"]);
20024
+ facts.repoSlug = originUrl ? parseGitHubRepo(originUrl) : null;
20025
+ const mcp = await readMcpPin(root);
20026
+ facts.agentConfigInstalled = mcp.installed;
20027
+ facts.pinnedProject = mcp.pin.project;
20028
+ facts.pinnedBranch = mcp.pin.branch;
20029
+ }
20030
+ const registry = await loadLocalRepos();
20031
+ const forThisRepo = facts.repoSlug ? registry.filter((r) => slugOf(r).toLowerCase() === facts.repoSlug.toLowerCase()) : [];
20032
+ facts.localRepos = forThisRepo.map(
20033
+ (r) => ({
20034
+ slug: slugOf(r),
20035
+ halted: Boolean(r.haltedAt),
20036
+ haltReason: r.haltReason
20037
+ })
20038
+ );
20039
+ try {
20040
+ const cfg = resolveConfig();
20041
+ const authed = await createAuthedClient({
20042
+ supabaseUrl: cfg.supabaseUrl,
20043
+ supabaseAnonKey: cfg.supabaseAnonKey,
20044
+ clientId: cfg.archmeDaemonClientId,
20045
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20046
+ });
20047
+ if (!authed.orgId) return facts;
20048
+ facts.signedIn = true;
20049
+ facts.orgId = authed.orgId;
20050
+ facts.identity = authed.identity;
20051
+ const supabase = authed.supabase;
20052
+ const slug = facts.pinnedProject;
20053
+ if (slug) {
20054
+ const [orgSlug, projectSlug] = slug.split("/");
20055
+ const { data: org } = await supabase.from("organizations").select("id").eq("slug", orgSlug ?? "").maybeSingle();
20056
+ facts.pinnedOrgVisible = Boolean(org);
20057
+ const { data: project } = org ? await supabase.from("projects").select("id").eq("org_id", org.id).eq("slug", projectSlug ?? "").is("deleted_at", null).maybeSingle() : { data: null };
20058
+ if (!org) {
20059
+ facts.projectExists = null;
20060
+ } else if (!project) {
20061
+ facts.projectExists = false;
20062
+ } else {
20063
+ facts.projectExists = true;
20064
+ const branch = facts.pinnedBranch ?? "main";
20065
+ const { count: count2 } = await supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("branch", branch);
20066
+ facts.componentCount = count2 ?? null;
20067
+ const marker = await readBranchMarker(supabase, project.id, branch);
20068
+ facts.markerSha = marker.sha;
20069
+ }
20070
+ }
20071
+ if (facts.localRepos.length > 0) {
20072
+ facts.watchedBy = "local";
20073
+ } else if (facts.repoSlug) {
20074
+ const [owner, name] = facts.repoSlug.split("/");
20075
+ const { data: watched } = await supabase.from("watched_repos").select("source_kind, enabled, commit_update_enabled").eq("repo_owner", owner ?? "").eq("repo_name", name ?? "").is("deleted_at", null);
20076
+ const rows = watched ?? [];
20077
+ const live = rows.find((r) => r.commit_update_enabled);
20078
+ facts.watchedBy = live ? live.source_kind === "local" ? "local" : "github" : "none";
20079
+ }
20080
+ } catch {
20081
+ }
20082
+ return facts;
20083
+ }
20084
+ var init_collect = __esm({
20085
+ "src/doctor/collect.ts"() {
20086
+ "use strict";
20087
+ init_execa();
20088
+ init_defaults();
20089
+ init_version();
20090
+ init_preflight();
20091
+ init_branchMarker();
20092
+ init_localRepos();
20093
+ init_github_remote();
20094
+ init_authedClient();
20095
+ }
20096
+ });
20097
+
20098
+ // src/doctor/facts.ts
20099
+ function diagnose(f) {
20100
+ const issues = [];
20101
+ if (!f.claudeInstalled) {
20102
+ issues.push({
20103
+ severity: "error",
20104
+ title: "Claude Code is not installed",
20105
+ detail: "archme runs Claude Code locally to read your diff and update the architecture. Without it, scans and every daemon job fail at the spawn.",
20106
+ fix: "npm i -g @anthropic-ai/claude-code"
20107
+ });
20108
+ }
20109
+ if (!f.gitInstalled) {
20110
+ issues.push({
20111
+ severity: "error",
20112
+ title: "git is not installed",
20113
+ detail: "archme clones and diffs repositories with git.",
20114
+ fix: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
20115
+ });
20116
+ }
20117
+ if (f.cliVersion === null) {
20118
+ issues.push({
20119
+ severity: "error",
20120
+ title: "this install cannot read its own version",
20121
+ detail: "The daemon reports its version on every job claim, and the server treats an unknown version as below any minimum. If a floor is ever set, this install stops claiming jobs silently \u2014 no error, just an idle daemon.",
20122
+ fix: "npm i -g @archme/cli"
20123
+ });
20124
+ }
20125
+ if (!f.signedIn) {
20126
+ issues.push({
20127
+ severity: "error",
20128
+ title: "not signed in",
20129
+ detail: "Your project, architecture and watch status can't be checked until this machine is paired with an ArchMe account.",
20130
+ fix: "archme login"
20131
+ });
20132
+ return issues;
20133
+ }
20134
+ if (!f.inRepo) {
20135
+ issues.push({
20136
+ severity: "warn",
20137
+ title: "not inside a git repository",
20138
+ detail: "Run this from the repo you want ArchMe to track, so it can check that repo's setup.",
20139
+ fix: "cd <your repo> && archme doctor"
20140
+ });
20141
+ return issues;
20142
+ }
20143
+ if (f.pinnedOrgVisible === false) {
20144
+ issues.push({
20145
+ severity: "error",
20146
+ title: "this repo is pinned to a project your account cannot see",
20147
+ detail: `.mcp.json pins \`${f.pinnedProject}\`, but this machine is signed in to an account that is not a member of that organisation. Nothing here can read or update that project. The architecture may well exist \u2014 you just cannot reach it from this login.`,
20148
+ fix: "archme login (choose the org that owns the project), or re-run `archme init` to pin one you can reach"
20149
+ });
20150
+ return issues;
20151
+ }
20152
+ if (f.projectExists === false || f.componentCount === 0) {
20153
+ issues.push({
20154
+ severity: "error",
20155
+ title: "this repo has no architecture yet",
20156
+ detail: "Nothing has been scanned, so there is nothing for updates to keep current.",
20157
+ fix: "archme init"
20158
+ });
20159
+ return issues;
20160
+ }
20161
+ if (!f.agentConfigInstalled) {
20162
+ issues.push({
20163
+ severity: "warn",
20164
+ title: "agent config is missing from this repo",
20165
+ detail: "Without `.mcp.json`, Claude Code in this repo can't read or write the architecture while you work. Updates from commits still happen.",
20166
+ fix: "archme init"
20167
+ });
20168
+ }
20169
+ if (f.projectExists === true && f.markerSha === null) {
20170
+ issues.push({
20171
+ severity: "error",
20172
+ title: "the last scan left no resume point",
20173
+ detail: 'Updates diff from the commit the architecture reflects, and that commit was never recorded. Every job will fail with "no usable base" until a scan completes cleanly.',
20174
+ fix: "archme init --force"
20175
+ });
20176
+ }
20177
+ if (f.watchedBy === "none") {
20178
+ issues.push({
20179
+ severity: "error",
20180
+ title: "nothing is watching this repo",
20181
+ detail: "The architecture exists but will not track new commits \u2014 the board stays as it is while the code moves on, which looks current and isn't.",
20182
+ fix: "archme init"
20183
+ });
20184
+ }
20185
+ const halted = f.localRepos.filter((r) => r.halted);
20186
+ for (const r of halted) {
20187
+ issues.push({
20188
+ severity: "error",
20189
+ title: `local polling is halted for ${r.slug}`,
20190
+ detail: `Stopped after: ${r.haltReason ?? "an unrecorded failure"}. No new commits are being picked up. It stops rather than skipping the failed commit, which would fold it into the next diff.`,
20191
+ fix: `archme local-watch --resume ${r.slug}`
20192
+ });
20193
+ }
20194
+ if (!f.daemonRunning && f.watchedBy !== "none" && f.watchedBy !== "unknown") {
20195
+ issues.push({
20196
+ severity: "error",
20197
+ title: "the daemon is not running",
20198
+ detail: "Commits are queueing up, but nothing is processing them. Updates only happen while the daemon runs, on a machine that stays on.",
20199
+ fix: "archme daemon start"
20200
+ });
20201
+ }
20202
+ return issues;
20203
+ }
20204
+ function yesNo(v) {
20205
+ return v ? "yes" : "no";
20206
+ }
20207
+ function formatFacts(f) {
20208
+ const lines = ["ArchMe setup"];
20209
+ lines.push("", " This machine");
20210
+ lines.push(` archme version: ${f.cliVersion ?? "UNKNOWN (cannot read its own version)"}`);
20211
+ lines.push(` claude installed: ${yesNo(f.claudeInstalled)}`);
20212
+ lines.push(` git installed: ${yesNo(f.gitInstalled)}`);
20213
+ lines.push(` signed in: ${f.signedIn ? f.identity ?? "yes" : "no"}`);
20214
+ lines.push(` daemon running: ${yesNo(f.daemonRunning)}`);
20215
+ lines.push("", " This repo");
20216
+ if (!f.inRepo) {
20217
+ lines.push(" not inside a git repository");
20218
+ } else {
20219
+ lines.push(` path: ${f.repoRoot}`);
20220
+ lines.push(` repo: ${f.repoSlug ?? "no github.com remote"}`);
20221
+ lines.push(` branch: ${f.gitBranch ?? "unknown"}`);
20222
+ lines.push(` .mcp.json: ${f.agentConfigInstalled ? "installed" : "missing"}`);
20223
+ if (f.pinnedProject) lines.push(` pinned to: ${f.pinnedProject} @ ${f.pinnedBranch ?? "?"}`);
20224
+ }
20225
+ lines.push("", " Architecture");
20226
+ if (!f.signedIn) {
20227
+ lines.push(" unknown \u2014 not signed in");
20228
+ } else {
20229
+ if (f.pinnedOrgVisible === false) {
20230
+ lines.push(` NOT VISIBLE \u2014 ${f.pinnedProject} belongs to another organisation`);
20231
+ }
20232
+ lines.push(` project exists: ${f.projectExists === null ? "unknown" : yesNo(f.projectExists)}`);
20233
+ lines.push(` components: ${f.componentCount === null ? "unknown" : f.componentCount}`);
20234
+ lines.push(
20235
+ ` last scanned at: ${f.markerSha ? f.markerSha.slice(0, 7) : "never recorded"}`
20236
+ );
20237
+ lines.push(` updates from: ${describeWatch(f)}`);
20238
+ }
20239
+ for (const r of f.localRepos) {
20240
+ lines.push(
20241
+ ` \xB7 ${r.slug}${r.halted ? ` HALTED: ${r.haltReason ?? "unknown"}` : ""}`
20242
+ );
20243
+ }
20244
+ return lines.join("\n");
20245
+ }
20246
+ function describeWatch(f) {
20247
+ switch (f.watchedBy) {
20248
+ case "github":
20249
+ return "GitHub App (pushes and pull requests)";
20250
+ case "local":
20251
+ return "local polling on this machine";
20252
+ case "none":
20253
+ return "NOTHING \u2014 this architecture will not track new commits";
20254
+ default:
20255
+ return "unknown";
20256
+ }
20257
+ }
20258
+ var init_facts = __esm({
20259
+ "src/doctor/facts.ts"() {
20260
+ "use strict";
20261
+ }
20262
+ });
20263
+
20264
+ // src/doctor/prompt.ts
20265
+ function buildDoctorPrompt(facts, issues) {
20266
+ const problems = issues.length === 0 ? "No problems were found." : issues.map((i2) => `- [${i2.severity}] ${i2.title}
20267
+ ${i2.detail}
20268
+ Fix: ${i2.fix}`).join("\n");
20269
+ return `You are helping someone understand their ArchMe setup. This is the
20270
+ \`archme doctor\` command.
20271
+
20272
+ ${HOW_IT_WORKS}
20273
+
20274
+ ## What was measured on this machine
20275
+
20276
+ These facts were gathered by the CLI before you started. They are accurate.
20277
+ Do NOT re-derive them, and do not go looking for them in the filesystem \u2014 you
20278
+ have everything already.
20279
+
20280
+ \`\`\`
20281
+ ${formatFacts(facts)}
20282
+ \`\`\`
20283
+
20284
+ ## Problems found
20285
+
20286
+ ${problems}
20287
+
20288
+ ## Your job
20289
+
20290
+ The report above has ALREADY been printed to the user's terminal. Do not repeat
20291
+ it back to them.
20292
+
20293
+ Open with two or three sentences: where they are in the flow above, and the
20294
+ single most important thing to do next (or that nothing needs doing). Then stop
20295
+ and let them ask.
20296
+
20297
+ Rules:
20298
+ - You are READ-ONLY. You cannot fix anything, and you must not offer to. Give
20299
+ them the command to run.
20300
+ - Do not invent state. If they ask something the facts above do not answer, say
20301
+ so and tell them which command would show it.
20302
+ - Never tell them their setup is fine if a problem was listed.
20303
+ - Be brief. They ran a diagnostic, not a tutorial. Depth on request.`;
20304
+ }
20305
+ var HOW_IT_WORKS;
20306
+ var init_prompt2 = __esm({
20307
+ "src/doctor/prompt.ts"() {
20308
+ "use strict";
20309
+ init_facts();
20310
+ HOW_IT_WORKS = `## How ArchMe is meant to work
20311
+
20312
+ 1. **Scan** \u2014 \`archme init\` maps the repo into an architecture on the board.
20313
+ This is a one-time snapshot. It runs Claude Code locally, on the user's own
20314
+ Claude subscription. Their code is never sent to ArchMe's servers; only the
20315
+ resulting architecture is.
20316
+
20317
+ 2. **Detection** \u2014 something has to notice new commits. Two options:
20318
+ - **GitHub App**: webhooks. Sees pushes AND pull requests, works for the
20319
+ whole team, nothing to keep running. Needs someone to grant repo read
20320
+ access.
20321
+ - **Local polling** (\`archme local-watch\`): this machine fetches the repo on
20322
+ a timer. No GitHub App, no approvals, but no pull-request awareness.
20323
+
20324
+ 3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
20325
+ step people miss. Detection only queues work; the queue does nothing on its
20326
+ own. Either the user runs \`archme daemon start\` on a machine that stays on,
20327
+ or they email signup@archme.io and ArchMe runs it for them.
20328
+
20329
+ The failure mode that matters most: an architecture that exists but is not
20330
+ being updated. The board still looks current, so nobody notices it is stale.
20331
+ Detection without execution produces exactly that, which is why step 3 is not
20332
+ optional.`;
20333
+ }
20334
+ });
20335
+
20336
+ // src/doctor/dispatch.ts
20337
+ var dispatch_exports5 = {};
20338
+ __export(dispatch_exports5, {
20339
+ EX_INFRA: () => EX_INFRA4,
20340
+ EX_OK: () => EX_OK4,
20341
+ EX_USAGE: () => EX_USAGE4,
20342
+ dispatchDoctor: () => dispatchDoctor,
20343
+ parseDoctorArgs: () => parseDoctorArgs,
20344
+ printDoctorHelp: () => printDoctorHelp
20345
+ });
20346
+ import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
20347
+ import { tmpdir as tmpdir5 } from "node:os";
20348
+ import { join as join24 } from "node:path";
20349
+ function parseDoctorArgs(argv) {
20350
+ const args = { plain: false, help: false };
20351
+ for (const a2 of argv) {
20352
+ if (a2 === "--help" || a2 === "-h" || a2 === "help") args.help = true;
20353
+ else if (a2 === "--plain" || a2 === "--no-agent") args.plain = true;
20354
+ else return { ok: false, message: `archme doctor: unknown flag '${a2}'` };
20355
+ }
20356
+ return { ok: true, args };
20357
+ }
20358
+ function printDoctorHelp() {
20359
+ console.log(`archme doctor \u2014 check this machine's ArchMe setup and explain it
20360
+
20361
+ Reports what is installed, signed in, scanned, watched and running, then hands
20362
+ the results to Claude Code so you can ask about them.
20363
+
20364
+ Read-only: it never changes anything. Anything that needs doing is printed as a
20365
+ command for you to run.
20366
+
20367
+ Usage:
20368
+ archme doctor [--plain]
20369
+
20370
+ Options:
20371
+ --plain Just print the report \u2014 don't start Claude. Use this when Claude
20372
+ Code itself is what's broken.
20373
+ -h, --help`);
20374
+ }
20375
+ async function dispatchDoctor(argv) {
20376
+ const parsed = parseDoctorArgs(argv);
20377
+ if (!parsed.ok) {
20378
+ console.error(parsed.message);
20379
+ printDoctorHelp();
20380
+ return EX_USAGE4;
20381
+ }
20382
+ if (parsed.args.help) {
20383
+ printDoctorHelp();
20384
+ return EX_OK4;
20385
+ }
20386
+ const facts = await collectFacts(process.cwd());
20387
+ const issues = diagnose(facts);
20388
+ console.log(formatFacts(facts));
20389
+ if (issues.length === 0) {
20390
+ console.log("\n\u2713 Everything checks out.");
20391
+ } else {
20392
+ console.log("");
20393
+ for (const issue of issues) {
20394
+ console.log(`${issue.severity === "error" ? "\u2717" : "!"} ${issue.title}`);
20395
+ console.log(` ${issue.detail}`);
20396
+ console.log(` \u2192 ${issue.fix}`);
20397
+ console.log("");
20398
+ }
20399
+ }
20400
+ if (parsed.args.plain) return EX_OK4;
20401
+ try {
20402
+ requireBinary("claude");
20403
+ } catch {
20404
+ console.log(
20405
+ "Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
20406
+ );
20407
+ return EX_OK4;
20408
+ }
20409
+ const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
20410
+ const mcpConfigPath = join24(dir, "mcp.json");
20411
+ await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
20412
+ try {
20413
+ const result = await spawnClaude({
20414
+ prompt: buildDoctorPrompt(facts, issues),
20415
+ cwd: facts.repoRoot ?? process.cwd(),
20416
+ mcpConfigPath,
20417
+ interactive: true,
20418
+ // Read-only, and no MCP: doctor diagnoses, it does not repair. An LLM
20419
+ // with write or shell access on a machine whose setup is already broken
20420
+ // is a much larger surface than the problem justifies.
20421
+ allowedTools: ["Read", "Glob", "Grep"]
20422
+ });
20423
+ return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
20424
+ } finally {
20425
+ await rm9(dir, { recursive: true, force: true });
20426
+ }
20427
+ }
20428
+ var EX_OK4, EX_INFRA4, EX_USAGE4;
20429
+ var init_dispatch5 = __esm({
20430
+ "src/doctor/dispatch.ts"() {
20431
+ "use strict";
20432
+ init_collect();
20433
+ init_facts();
20434
+ init_prompt2();
20435
+ init_spawn_claude();
20436
+ init_preflight();
20437
+ EX_OK4 = 0;
20438
+ EX_INFRA4 = 3;
20439
+ EX_USAGE4 = 78;
20440
+ }
20441
+ });
20442
+
18177
20443
  // src/auth/logout.ts
18178
20444
  var logout_exports = {};
18179
20445
  __export(logout_exports, {
@@ -18187,12 +20453,12 @@ async function runLogout(args) {
18187
20453
  let url;
18188
20454
  try {
18189
20455
  url = resolveConfig().supabaseUrl;
18190
- } catch (err) {
18191
- if (err instanceof MissingConfigError) {
18192
- console.error(err.message);
20456
+ } catch (err2) {
20457
+ if (err2 instanceof MissingConfigError) {
20458
+ console.error(err2.message);
18193
20459
  return 1;
18194
20460
  }
18195
- throw err;
20461
+ throw err2;
18196
20462
  }
18197
20463
  const target = args.find((a2) => !a2.startsWith("-"));
18198
20464
  if (!target || args.includes("--all")) {
@@ -18220,10 +20486,22 @@ var init_logout = __esm({
18220
20486
 
18221
20487
  // src/cli.ts
18222
20488
  init_enroll();
18223
- var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["daemon", "analyze", "login", "logout", "keys", "scan", "init"]);
20489
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
20490
+ "daemon",
20491
+ "analyze",
20492
+ "login",
20493
+ "logout",
20494
+ "keys",
20495
+ "scan",
20496
+ "init",
20497
+ "local-watch",
20498
+ "doctor"
20499
+ ]);
20500
+ var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
18224
20501
  function route(argv) {
18225
20502
  if (argv.length === 0) return { name: "help", args: [] };
18226
20503
  const [first, ...rest] = argv;
20504
+ if (first && VERSION_FLAGS.has(first)) return { name: "version", args: [] };
18227
20505
  if (!first || !KNOWN_COMMANDS.has(first)) {
18228
20506
  return { name: "help", args: [] };
18229
20507
  }
@@ -18287,6 +20565,18 @@ async function main(argv) {
18287
20565
  const { dispatchDaemon: dispatchDaemon2 } = await Promise.resolve().then(() => (init_dispatch2(), dispatch_exports2));
18288
20566
  return await dispatchDaemon2(sub);
18289
20567
  }
20568
+ case "version": {
20569
+ const { readDaemonVersion: readDaemonVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
20570
+ const v = await readDaemonVersion2();
20571
+ if (!v) {
20572
+ console.error(
20573
+ "archme: could not read its own version \u2014 the install looks incomplete.\nThe daemon reports this as no version at all, and a configured minimum-version\nfloor will refuse its job claims. Reinstall with `npm i -g @archme/cli`."
20574
+ );
20575
+ return 1;
20576
+ }
20577
+ console.log(v);
20578
+ return 0;
20579
+ }
18290
20580
  case "login":
18291
20581
  await runLogin();
18292
20582
  return 0;
@@ -18302,6 +20592,14 @@ async function main(argv) {
18302
20592
  const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
18303
20593
  return await dispatchInit2(r.args);
18304
20594
  }
20595
+ case "local-watch": {
20596
+ const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
20597
+ return await dispatchLocalWatch2(r.args);
20598
+ }
20599
+ case "doctor": {
20600
+ const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
20601
+ return await dispatchDoctor2(r.args);
20602
+ }
18305
20603
  case "logout": {
18306
20604
  const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
18307
20605
  return await runLogout2(r.args);
@@ -18324,7 +20622,12 @@ Other commands:
18324
20622
  disable|install|uninstall|run)
18325
20623
  archme analyze <subcommand> Code analysis (outline|repo|diff|match|prefetch)
18326
20624
  archme scan <repo-path> Single-pass scan into an explicit project/branch
20625
+ archme local-watch [repo-path] Watch a repo without a GitHub App
20626
+ (--list|--stop|--resume)
20627
+ archme doctor Check this machine's setup and explain it
18327
20628
  archme login | logout Authenticate with archme
20629
+ archme keys list|create|revoke Manage MCP API keys
20630
+ archme --version Print the installed version
18328
20631
 
18329
20632
  Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
18330
20633
  'archme scan --help' for subcommand help.`);