@archme/cli 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +2601 -319
  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({
@@ -9533,6 +9610,7 @@ function buildClaudeArgs(opts) {
9533
9610
  return args;
9534
9611
  }
9535
9612
  function spawnClaude(opts) {
9613
+ requireBinary("claude");
9536
9614
  const env = scrubPrivilegedEnv(process.env);
9537
9615
  const args = buildClaudeArgs({ ...opts, model: opts.model || DEFAULT_CLAUDE_MODEL });
9538
9616
  if (opts.streamJson && !opts.interactive) {
@@ -9571,6 +9649,7 @@ var init_spawn_claude = __esm({
9571
9649
  init_execa();
9572
9650
  init_env_scrub();
9573
9651
  init_mcp_config();
9652
+ init_preflight();
9574
9653
  init_daemonModel();
9575
9654
  init_daemonModel();
9576
9655
  mcpTool = (name) => `mcp__${MCP_SERVER_NAME}__${name}`;
@@ -9743,7 +9822,7 @@ var init_digestPrompt = __esm({
9743
9822
  // src/daemon/digest.ts
9744
9823
  import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
9745
9824
  import { tmpdir as tmpdir2 } from "node:os";
9746
- import { join as join6 } from "node:path";
9825
+ import { join as join7 } from "node:path";
9747
9826
  function isCleanDigestExit(res) {
9748
9827
  return !res.timedOut && res.signal == null && res.exitCode === 0;
9749
9828
  }
@@ -9777,12 +9856,12 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9777
9856
  }
9778
9857
  let dir;
9779
9858
  try {
9780
- dir = await mkdtemp2(join6(tmpdir2(), "archme-digest-"));
9859
+ dir = await mkdtemp2(join7(tmpdir2(), "archme-digest-"));
9781
9860
  let mcpConfigPath;
9782
9861
  if (mcp) {
9783
9862
  mcpConfigPath = mcp.configPath;
9784
9863
  } else {
9785
- mcpConfigPath = join6(dir, "mcp.json");
9864
+ mcpConfigPath = join7(dir, "mcp.json");
9786
9865
  await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
9787
9866
  }
9788
9867
  const model = await resolveModelPin(ctx.supabase);
@@ -9813,8 +9892,8 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9813
9892
  }
9814
9893
  await completeDigest(ctx, job.id, deviceId, narrative);
9815
9894
  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));
9895
+ } catch (err2) {
9896
+ await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
9818
9897
  } finally {
9819
9898
  inFlight2?.delete(job.id);
9820
9899
  if (dir) await rm5(dir, { recursive: true, force: true }).catch(() => {
@@ -11774,8 +11853,8 @@ function extractParticipantsFromSequence(sequence) {
11774
11853
  return [...participants];
11775
11854
  }
11776
11855
  function componentScopeSegments(componentPath) {
11777
- const resolved = resolveWaypoint(componentPath, []);
11778
- return resolved.leafId ? [...resolved.scopePath, resolved.leafId] : resolved.scopePath;
11856
+ const resolved2 = resolveWaypoint(componentPath, []);
11857
+ return resolved2.leafId ? [...resolved2.scopePath, resolved2.leafId] : resolved2.scopePath;
11779
11858
  }
11780
11859
  function isPrefix(prefix, full) {
11781
11860
  return full.length >= prefix.length && prefix.every((s, i2) => full[i2] === s);
@@ -12270,10 +12349,10 @@ async function ensureBugsFolder(client, projectId) {
12270
12349
  const createdId = created?.id ?? null;
12271
12350
  if (createdId) cache.set(projectId, createdId);
12272
12351
  return createdId;
12273
- } catch (err) {
12352
+ } catch (err2) {
12274
12353
  console.warn(
12275
12354
  `[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
12355
+ err2 instanceof Error ? err2.message : err2
12277
12356
  );
12278
12357
  return null;
12279
12358
  }
@@ -12334,17 +12413,17 @@ var init_lang_detect = __esm({
12334
12413
 
12335
12414
  // src/analyze/tree-sitter/cache.ts
12336
12415
  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";
12416
+ import { dirname as dirname7, join as join8 } from "node:path";
12338
12417
  import { homedir as homedir3 } from "node:os";
12339
12418
  import { createHash } from "node:crypto";
12340
12419
  function defaultCacheRoot() {
12341
- return process.env.ARCHME_TOOLS_DIR ?? join7(homedir3(), ".archme", "tools");
12420
+ return process.env.ARCHME_TOOLS_DIR ?? join8(homedir3(), ".archme", "tools");
12342
12421
  }
12343
12422
  function grammarCacheDir(cacheRoot) {
12344
- return join7(cacheRoot, "grammars");
12423
+ return join8(cacheRoot, "grammars");
12345
12424
  }
12346
12425
  function cachedGrammarPath(cacheRoot, lang, version) {
12347
- return join7(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
12426
+ return join8(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
12348
12427
  }
12349
12428
  async function fileExists(path7) {
12350
12429
  try {
@@ -12354,6 +12433,12 @@ async function fileExists(path7) {
12354
12433
  return false;
12355
12434
  }
12356
12435
  }
12436
+ function describeFetchFailure(err2) {
12437
+ const top = err2 instanceof Error ? err2.message : String(err2);
12438
+ const cause = err2?.cause;
12439
+ const inner = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
12440
+ return inner && inner !== top ? `${top} (${inner})` : top;
12441
+ }
12357
12442
  async function ensureGrammar(opts) {
12358
12443
  const target = cachedGrammarPath(opts.cacheDir, opts.lang, opts.version);
12359
12444
  if (await fileExists(target)) {
@@ -12363,11 +12448,25 @@ async function ensureGrammar(opts) {
12363
12448
  const url = `${opts.urlBase}/${opts.filename}`;
12364
12449
  process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
12365
12450
  `);
12366
- const res = await fetch(url);
12451
+ let res;
12452
+ try {
12453
+ res = await fetch(url);
12454
+ } catch (err2) {
12455
+ throw new GrammarFetchError({
12456
+ lang: opts.lang,
12457
+ version: opts.version,
12458
+ url,
12459
+ reason: describeFetchFailure(err2),
12460
+ cause: err2
12461
+ });
12462
+ }
12367
12463
  if (!res.ok) {
12368
- throw new Error(
12369
- `failed to fetch grammar ${opts.lang}@${opts.version}: HTTP ${res.status}`
12370
- );
12464
+ throw new GrammarFetchError({
12465
+ lang: opts.lang,
12466
+ version: opts.version,
12467
+ url,
12468
+ reason: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
12469
+ });
12371
12470
  }
12372
12471
  const buf = Buffer.from(await res.arrayBuffer());
12373
12472
  const actualHash = createHash("sha256").update(buf).digest("hex");
@@ -12379,9 +12478,41 @@ async function ensureGrammar(opts) {
12379
12478
  await writeFile6(target, buf);
12380
12479
  return { path: target, downloaded: true };
12381
12480
  }
12481
+ var GrammarFetchError;
12382
12482
  var init_cache = __esm({
12383
12483
  "src/analyze/tree-sitter/cache.ts"() {
12384
12484
  "use strict";
12485
+ GrammarFetchError = class extends Error {
12486
+ lang;
12487
+ url;
12488
+ constructor(opts) {
12489
+ super(
12490
+ [
12491
+ `Failed to download the ${opts.lang} tree-sitter grammar (${opts.lang}@${opts.version}).`,
12492
+ ` URL: ${opts.url}`,
12493
+ ` Reason: ${opts.reason}`,
12494
+ "",
12495
+ "archme fetches language grammars on first use and caches them under",
12496
+ "~/.archme/tools/grammars (override with ARCHME_TOOLS_DIR). This host could",
12497
+ "not reach the CDN.",
12498
+ "",
12499
+ "If you're behind a proxy, export it before re-running:",
12500
+ " export HTTPS_PROXY=http://proxy.example.com:8080",
12501
+ " export HTTP_PROXY=$HTTPS_PROXY",
12502
+ "",
12503
+ "Then warm the cache once (it is reused by every later run):",
12504
+ " archme analyze prefetch --all",
12505
+ "",
12506
+ "Offline/air-gapped: copy a populated ~/.archme/tools/grammars directory",
12507
+ "from a machine that has network access, or point ARCHME_TOOLS_DIR at one."
12508
+ ].join("\n"),
12509
+ opts.cause === void 0 ? void 0 : { cause: opts.cause }
12510
+ );
12511
+ this.name = "GrammarFetchError";
12512
+ this.lang = opts.lang;
12513
+ this.url = opts.url;
12514
+ }
12515
+ };
12385
12516
  }
12386
12517
  });
12387
12518
 
@@ -12864,13 +12995,13 @@ async function outlineSource(source, file) {
12864
12995
  confidence: 0.8,
12865
12996
  data: { file, symbols }
12866
12997
  });
12867
- } catch (err) {
12998
+ } catch (err2) {
12868
12999
  return envelope({
12869
13000
  tool: "file",
12870
13001
  tier: 0,
12871
13002
  confidence: 0.2,
12872
13003
  data: { file, symbols: [] },
12873
- warnings: [`parse failed, falling back to file tier: ${err.message}`]
13004
+ warnings: [`parse failed, falling back to file tier: ${err2.message}`]
12874
13005
  });
12875
13006
  }
12876
13007
  }
@@ -12887,13 +13018,13 @@ async function runOutline(file) {
12887
13018
  let source;
12888
13019
  try {
12889
13020
  source = await readFile6(file, "utf8");
12890
- } catch (err) {
13021
+ } catch (err2) {
12891
13022
  return envelope({
12892
13023
  tool: "file",
12893
13024
  tier: 0,
12894
13025
  confidence: 0,
12895
13026
  data: { file, symbols: [] },
12896
- errors: [`cannot read ${file}: ${err.message}`]
13027
+ errors: [`cannot read ${file}: ${err2.message}`]
12897
13028
  });
12898
13029
  }
12899
13030
  return outlineSource(source, file);
@@ -13496,11 +13627,11 @@ var require_ignore = __commonJS({
13496
13627
 
13497
13628
  // src/analyze/repo-walk.ts
13498
13629
  import { readdir, readFile as readFile7, stat as stat4 } from "node:fs/promises";
13499
- import { join as join8, relative } from "node:path";
13630
+ import { join as join9, relative } from "node:path";
13500
13631
  async function loadGitignore(root) {
13501
13632
  const ig = (0, import_ignore.default)();
13502
13633
  try {
13503
- const text = await readFile7(join8(root, ".gitignore"), "utf8");
13634
+ const text = await readFile7(join9(root, ".gitignore"), "utf8");
13504
13635
  ig.add(text);
13505
13636
  } catch {
13506
13637
  ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
@@ -13514,7 +13645,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
13514
13645
  const entries = await readdir(dir, { withFileTypes: true });
13515
13646
  for (const e of entries) {
13516
13647
  if (ALWAYS_SKIP.has(e.name)) continue;
13517
- const full = join8(dir, e.name);
13648
+ const full = join9(dir, e.name);
13518
13649
  const rel = relative(root, full);
13519
13650
  if (e.isDirectory()) {
13520
13651
  if (ig.ignores(rel + "/")) continue;
@@ -13538,7 +13669,7 @@ async function collectRepoFiles(root, filter2 = {}) {
13538
13669
  }
13539
13670
  async function gitListFiles(root) {
13540
13671
  try {
13541
- await stat4(join8(root, ".git"));
13672
+ await stat4(join9(root, ".git"));
13542
13673
  } catch {
13543
13674
  return null;
13544
13675
  }
@@ -13579,11 +13710,11 @@ var init_repo_walk = __esm({
13579
13710
  });
13580
13711
 
13581
13712
  // src/analyze/commands/repo.ts
13582
- import { join as join9 } from "node:path";
13713
+ import { join as join10 } from "node:path";
13583
13714
  async function* walkRepo(root, opts = {}) {
13584
13715
  const files = await collectRepoFilesWithGit(root, opts);
13585
13716
  for (const rel of files) {
13586
- const env = await runOutline(join9(root, rel));
13717
+ const env = await runOutline(join10(root, rel));
13587
13718
  yield env;
13588
13719
  }
13589
13720
  }
@@ -13597,9 +13728,9 @@ var init_repo = __esm({
13597
13728
 
13598
13729
  // src/analyze/matchers/file.ts
13599
13730
  import { stat as stat5 } from "node:fs/promises";
13600
- import { join as join10 } from "node:path";
13731
+ import { join as join11 } from "node:path";
13601
13732
  async function matchFileAnchor(anchor, repoRoot) {
13602
- const full = join10(repoRoot, anchor.path);
13733
+ const full = join11(repoRoot, anchor.path);
13603
13734
  try {
13604
13735
  const s = await stat5(full);
13605
13736
  if (!s.isFile()) {
@@ -13630,9 +13761,9 @@ var init_file = __esm({
13630
13761
 
13631
13762
  // src/analyze/matchers/directory.ts
13632
13763
  import { stat as stat6 } from "node:fs/promises";
13633
- import { join as join11 } from "node:path";
13764
+ import { join as join12 } from "node:path";
13634
13765
  async function matchDirectoryAnchor(anchor, repoRoot) {
13635
- const full = join11(repoRoot, anchor.path);
13766
+ const full = join12(repoRoot, anchor.path);
13636
13767
  try {
13637
13768
  const s = await stat6(full);
13638
13769
  if (!s.isDirectory()) {
@@ -13701,9 +13832,9 @@ var init_findSymbol = __esm({
13701
13832
 
13702
13833
  // src/analyze/matchers/symbol.ts
13703
13834
  import { stat as stat7 } from "node:fs/promises";
13704
- import { join as join12 } from "node:path";
13835
+ import { join as join13 } from "node:path";
13705
13836
  async function matchSymbolAnchor(anchor, repoRoot) {
13706
- const full = join12(repoRoot, anchor.path);
13837
+ const full = join13(repoRoot, anchor.path);
13707
13838
  try {
13708
13839
  await stat7(full);
13709
13840
  } catch {
@@ -13805,8 +13936,8 @@ async function runPrefetch(langs) {
13805
13936
  } else {
13806
13937
  result.skipped.push(lang);
13807
13938
  }
13808
- } catch (err) {
13809
- result.errors.push({ lang, message: err.message });
13939
+ } catch (err2) {
13940
+ result.errors.push({ lang, message: err2.message });
13810
13941
  }
13811
13942
  }
13812
13943
  return result;
@@ -14027,8 +14158,8 @@ async function acquireSingleInstanceLock(pidfile) {
14027
14158
  await mkdir6(dirname8(pidfile), { recursive: true });
14028
14159
  try {
14029
14160
  await writeFile7(pidfile, String(process.pid), { flag: "wx" });
14030
- } catch (err) {
14031
- if (err.code !== "EEXIST") throw err;
14161
+ } catch (err2) {
14162
+ if (err2.code !== "EEXIST") throw err2;
14032
14163
  }
14033
14164
  let release;
14034
14165
  try {
@@ -14097,7 +14228,7 @@ var init_semaphore = __esm({
14097
14228
  acquire() {
14098
14229
  const slot = this.tryAcquire();
14099
14230
  if (slot) return Promise.resolve(slot);
14100
- return new Promise((resolve4) => this.waiters.push(resolve4));
14231
+ return new Promise((resolve5) => this.waiters.push(resolve5));
14101
14232
  }
14102
14233
  makeRelease() {
14103
14234
  let released = false;
@@ -14144,11 +14275,11 @@ async function refreshOAuthCredentialsWith(refreshToken, cfg) {
14144
14275
  client_id: cfg.clientId
14145
14276
  })
14146
14277
  });
14147
- } catch (err) {
14278
+ } catch (err2) {
14148
14279
  return {
14149
14280
  ok: false,
14150
14281
  permanent: false,
14151
- error: `network error: ${err instanceof Error ? err.message : String(err)}`
14282
+ error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
14152
14283
  };
14153
14284
  }
14154
14285
  const body = await parseJsonResponse(response);
@@ -14182,11 +14313,11 @@ async function getOAuthUserInfoWith(accessToken, cfg) {
14182
14313
  function resolveUserId(userInfo2, storedUserId) {
14183
14314
  return userInfo2.sub ?? storedUserId ?? null;
14184
14315
  }
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;
14316
+ function isAuthError(err2) {
14317
+ if (!err2 || typeof err2 !== "object") return false;
14318
+ const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
14319
+ const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
14320
+ const status = "status" in err2 ? Number(err2.status) : 0;
14190
14321
  return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
14191
14322
  }
14192
14323
  async function withAuthRetry(label, fn, reLogin) {
@@ -14195,8 +14326,8 @@ async function withAuthRetry(label, fn, reLogin) {
14195
14326
  console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
14196
14327
  try {
14197
14328
  await reLogin();
14198
- } catch (err) {
14199
- console.error(`${label}: re-login failed:`, err);
14329
+ } catch (err2) {
14330
+ console.error(`${label}: re-login failed:`, err2);
14200
14331
  return first;
14201
14332
  }
14202
14333
  return await fn();
@@ -14207,8 +14338,8 @@ async function fetchWithAuthRetry(label, doFetch, reLogin) {
14207
14338
  console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
14208
14339
  try {
14209
14340
  await reLogin();
14210
- } catch (err) {
14211
- console.error(`${label}: re-login failed:`, err);
14341
+ } catch (err2) {
14342
+ console.error(`${label}: re-login failed:`, err2);
14212
14343
  return first;
14213
14344
  }
14214
14345
  return await doFetch();
@@ -14315,8 +14446,8 @@ var init_sessionAuth = __esm({
14315
14446
  for (const l of this.listeners) {
14316
14447
  try {
14317
14448
  l(token);
14318
- } catch (err) {
14319
- console.error("sessionAuth: listener error:", err);
14449
+ } catch (err2) {
14450
+ console.error("sessionAuth: listener error:", err2);
14320
14451
  }
14321
14452
  }
14322
14453
  }
@@ -14362,7 +14493,7 @@ var init_sessionAuth = __esm({
14362
14493
  armRefreshTimer(delayMs) {
14363
14494
  if (this.refreshTimer) clearTimeout(this.refreshTimer);
14364
14495
  this.refreshTimer = setTimeout(() => {
14365
- this.runScheduledRefresh().catch((err) => console.error("auth: token refresh failed:", err));
14496
+ this.runScheduledRefresh().catch((err2) => console.error("auth: token refresh failed:", err2));
14366
14497
  }, delayMs);
14367
14498
  }
14368
14499
  // Translate a token lifetime (seconds) into the timer delay: fire 5 min before
@@ -14404,9 +14535,9 @@ var init_sessionAuth = __esm({
14404
14535
  try {
14405
14536
  await this.coalescedRefresh("token refresh");
14406
14537
  this.rearmFromLiveExpiry();
14407
- } catch (err) {
14408
- const detail = err instanceof Error ? err.message : String(err);
14409
- const permanent = err instanceof LoadStoredAndRefreshError && err.permanent;
14538
+ } catch (err2) {
14539
+ const detail = err2 instanceof Error ? err2.message : String(err2);
14540
+ const permanent = err2 instanceof LoadStoredAndRefreshError && err2.permanent;
14410
14541
  if (permanent) {
14411
14542
  console.error(`auth: token refresh failed permanently; auto-refresh stopped: ${detail}`);
14412
14543
  return;
@@ -14781,15 +14912,50 @@ var init_rateLimit = __esm({
14781
14912
  }
14782
14913
  });
14783
14914
 
14915
+ // src/packageRoot.ts
14916
+ import { readFileSync as readFileSync4 } from "node:fs";
14917
+ import { dirname as dirname9, join as join14, parse } from "node:path";
14918
+ function defaultReadFile(path7) {
14919
+ return readFileSync4(path7, "utf8");
14920
+ }
14921
+ function findPackageRoot2(startDir, opts = {}) {
14922
+ const readFile14 = opts.readFile ?? defaultReadFile;
14923
+ const stopAt = parse(startDir).root;
14924
+ let dir = startDir;
14925
+ for (; ; ) {
14926
+ try {
14927
+ const parsed = JSON.parse(readFile14(join14(dir, "package.json")));
14928
+ if (parsed.name === PACKAGE_NAME) return dir;
14929
+ } catch {
14930
+ }
14931
+ if (dir === stopAt) return null;
14932
+ const parent = dirname9(dir);
14933
+ if (parent === dir) return null;
14934
+ dir = parent;
14935
+ }
14936
+ }
14937
+ var PACKAGE_NAME;
14938
+ var init_packageRoot = __esm({
14939
+ "src/packageRoot.ts"() {
14940
+ "use strict";
14941
+ PACKAGE_NAME = "@archme/cli";
14942
+ }
14943
+ });
14944
+
14784
14945
  // src/daemon/version.ts
14946
+ var version_exports = {};
14947
+ __export(version_exports, {
14948
+ readDaemonVersion: () => readDaemonVersion
14949
+ });
14785
14950
  import { readFile as readFile10 } from "node:fs/promises";
14786
- import { dirname as dirname9, join as join13 } from "node:path";
14951
+ import { dirname as dirname10, join as join15 } from "node:path";
14787
14952
  import { fileURLToPath as fileURLToPath4 } from "node:url";
14788
14953
  async function readDaemonVersion() {
14789
14954
  try {
14790
- const here = dirname9(fileURLToPath4(import.meta.url));
14791
- const pkgPath = join13(here, "..", "..", "package.json");
14792
- const raw = await readFile10(pkgPath, "utf8");
14955
+ const here = dirname10(fileURLToPath4(import.meta.url));
14956
+ const root = findPackageRoot2(here);
14957
+ if (!root) return null;
14958
+ const raw = await readFile10(join15(root, "package.json"), "utf8");
14793
14959
  const parsed = JSON.parse(raw);
14794
14960
  return typeof parsed.version === "string" ? parsed.version : null;
14795
14961
  } catch {
@@ -14799,12 +14965,13 @@ async function readDaemonVersion() {
14799
14965
  var init_version = __esm({
14800
14966
  "src/daemon/version.ts"() {
14801
14967
  "use strict";
14968
+ init_packageRoot();
14802
14969
  }
14803
14970
  });
14804
14971
 
14805
14972
  // src/daemon/materializeDiff.ts
14806
14973
  import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
14807
- import { join as join14 } from "node:path";
14974
+ import { join as join16 } from "node:path";
14808
14975
  import { createHash as createHash2 } from "node:crypto";
14809
14976
  function splitPatchByFile(patch) {
14810
14977
  if (!patch.trim()) return [];
@@ -14889,18 +15056,18 @@ function buildManifest(files, rangeLabel) {
14889
15056
  }
14890
15057
  async function materializeDiff(worktreeDir, patch, rangeLabel) {
14891
15058
  const files = splitPatchByFile(patch);
14892
- const dir = join14(worktreeDir, CHANGES_DIR);
14893
- const byFileDir = join14(dir, "by-file");
15059
+ const dir = join16(worktreeDir, CHANGES_DIR);
15060
+ const byFileDir = join16(dir, "by-file");
14894
15061
  await mkdir7(byFileDir, { recursive: true });
14895
- await writeFile8(join14(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
15062
+ await writeFile8(join16(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
14896
15063
  `, "utf8");
14897
15064
  for (const f of files) {
14898
15065
  const rel = `by-file/${hashPath(f.path)}.diff`;
14899
15066
  f.diffPath = rel;
14900
- await writeFile8(join14(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
15067
+ await writeFile8(join16(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
14901
15068
  `, "utf8");
14902
15069
  }
14903
- await writeFile8(join14(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
15070
+ await writeFile8(join16(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
14904
15071
  return { dir, fileCount: files.length };
14905
15072
  }
14906
15073
  var CHANGES_DIR, STATUS_LABEL;
@@ -14912,6 +15079,583 @@ var init_materializeDiff = __esm({
14912
15079
  }
14913
15080
  });
14914
15081
 
15082
+ // src/daemon/branchMarker.ts
15083
+ async function readBranchMarker(client, projectId, archmeBranch) {
15084
+ 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();
15085
+ if (error || !data) return { sha: null, repo: null };
15086
+ return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
15087
+ }
15088
+ var init_branchMarker = __esm({
15089
+ "src/daemon/branchMarker.ts"() {
15090
+ "use strict";
15091
+ }
15092
+ });
15093
+
15094
+ // src/daemon/localRepos.ts
15095
+ import { chmod as chmod2, mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
15096
+ import { homedir as homedir4 } from "node:os";
15097
+ import { dirname as dirname11, join as join17 } from "node:path";
15098
+ function localReposPath(env = process.env) {
15099
+ return env.ARCHME_LOCAL_REPOS_PATH ?? join17(homedir4(), ".archme", "repos.json");
15100
+ }
15101
+ function isEntry(v) {
15102
+ if (!v || typeof v !== "object") return false;
15103
+ const o2 = v;
15104
+ return REQUIRED_KEYS.every((k) => typeof o2[k] === "string" && o2[k].length > 0);
15105
+ }
15106
+ function parseLocalRepos(raw) {
15107
+ let parsed;
15108
+ try {
15109
+ parsed = JSON.parse(raw);
15110
+ } catch {
15111
+ return [];
15112
+ }
15113
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
15114
+ const env = parsed;
15115
+ if (env.v !== LOCAL_REPOS_VERSION) return [];
15116
+ if (!Array.isArray(env.repos)) return [];
15117
+ return env.repos.filter(isEntry).map((e) => {
15118
+ const entry = {
15119
+ watchedRepoId: e.watchedRepoId,
15120
+ mirrorPath: e.mirrorPath,
15121
+ sourcePath: e.sourcePath,
15122
+ repoOwner: e.repoOwner,
15123
+ repoName: e.repoName
15124
+ };
15125
+ const raw2 = e;
15126
+ if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
15127
+ if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
15128
+ if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
15129
+ return entry;
15130
+ });
15131
+ }
15132
+ function serializeLocalRepos(repos) {
15133
+ return JSON.stringify({ v: LOCAL_REPOS_VERSION, repos }, null, 2);
15134
+ }
15135
+ function keyOf(e) {
15136
+ return e.watchedRepoId;
15137
+ }
15138
+ function upsertLocalRepo(repos, entry) {
15139
+ const k = keyOf(entry);
15140
+ const next = repos.filter((r) => keyOf(r) !== k);
15141
+ next.push(entry);
15142
+ return next;
15143
+ }
15144
+ function slugOf(e) {
15145
+ return `${e.repoOwner}/${e.repoName}`;
15146
+ }
15147
+ function findLocalReposBySlug(repos, slug) {
15148
+ const want = slug.toLowerCase();
15149
+ return repos.filter((r) => slugOf(r).toLowerCase() === want);
15150
+ }
15151
+ function removeLocalReposBySlug(repos, slug) {
15152
+ const want = slug.toLowerCase();
15153
+ return repos.filter((r) => slugOf(r).toLowerCase() !== want);
15154
+ }
15155
+ function markHalted(repos, watchedRepoId, reason, at, code) {
15156
+ return repos.map(
15157
+ (r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
15158
+ );
15159
+ }
15160
+ function clearHalt(repos, watchedRepoId) {
15161
+ return repos.map((r) => {
15162
+ if (r.watchedRepoId !== watchedRepoId) return r;
15163
+ const { haltedAt: _at, haltReason: _reason, haltCode: _code, ...rest } = r;
15164
+ return rest;
15165
+ });
15166
+ }
15167
+ function findLocalRepoForJob(repos, job) {
15168
+ if (!job.watched_repo_id) return null;
15169
+ const match2 = repos.find((r) => r.watchedRepoId === job.watched_repo_id);
15170
+ if (!match2) return null;
15171
+ const sameRepo = match2.repoOwner.toLowerCase() === job.repo_owner.toLowerCase() && match2.repoName.toLowerCase() === job.repo_name.toLowerCase();
15172
+ return sameRepo ? match2 : null;
15173
+ }
15174
+ async function loadLocalRepos(env = process.env) {
15175
+ try {
15176
+ return parseLocalRepos(await readFile11(localReposPath(env), "utf8"));
15177
+ } catch {
15178
+ return [];
15179
+ }
15180
+ }
15181
+ async function saveLocalRepos(repos, env = process.env) {
15182
+ const path7 = localReposPath(env);
15183
+ await mkdir8(dirname11(path7), { recursive: true, mode: 448 });
15184
+ await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
15185
+ await chmod2(path7, 384).catch(() => {
15186
+ });
15187
+ }
15188
+ async function registerLocalRepo(entry, env = process.env) {
15189
+ await saveLocalRepos(upsertLocalRepo(await loadLocalRepos(env), entry), env);
15190
+ }
15191
+ var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
15192
+ var init_localRepos = __esm({
15193
+ "src/daemon/localRepos.ts"() {
15194
+ "use strict";
15195
+ LOCAL_REPOS_VERSION = 1;
15196
+ REQUIRED_KEYS = [
15197
+ "watchedRepoId",
15198
+ "mirrorPath",
15199
+ "sourcePath",
15200
+ "repoOwner",
15201
+ "repoName"
15202
+ ];
15203
+ }
15204
+ });
15205
+
15206
+ // ../shared/commitAdvance.ts
15207
+ function planAdvance(input) {
15208
+ const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
15209
+ if (!marker) return { kind: "bootstrap-needed" };
15210
+ if (marker === upstreamTip) return { kind: "up-to-date" };
15211
+ if (markerIsAncestorOfTip) {
15212
+ return firstParentShas.length === 0 ? { kind: "force-push", tip: upstreamTip } : { kind: "fast-forward", shas: firstParentShas };
15213
+ }
15214
+ if (tipIsAncestorOfMarker) return { kind: "upstream-behind", tip: upstreamTip };
15215
+ return { kind: "force-push", tip: upstreamTip };
15216
+ }
15217
+ var init_commitAdvance = __esm({
15218
+ "../shared/commitAdvance.ts"() {
15219
+ }
15220
+ });
15221
+
15222
+ // ../shared/commitGate.ts
15223
+ function decideGate(input) {
15224
+ const { targetSha, markerSha, job, elapsedMs, timeoutMs } = input;
15225
+ const pushedNewRef = input.pushedNewRef ?? true;
15226
+ const noopGraceMs = input.noopGraceMs ?? 5 * 60 * 1e3;
15227
+ if (markerSha && markerSha === targetSha) {
15228
+ return {
15229
+ action: "advance",
15230
+ code: "marker-advanced",
15231
+ reason: "marker advanced to target (clean completion)"
15232
+ };
15233
+ }
15234
+ if (job && (job.status === "failed" || job.status === "cancelled")) {
15235
+ return {
15236
+ action: "stop",
15237
+ code: "job-terminal",
15238
+ 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`
15239
+ };
15240
+ }
15241
+ if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
15242
+ return {
15243
+ action: "stop",
15244
+ code: "noop-push-no-job",
15245
+ 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`
15246
+ };
15247
+ }
15248
+ if (elapsedMs >= timeoutMs) {
15249
+ return {
15250
+ action: "stop",
15251
+ code: "timeout",
15252
+ reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short(targetSha)} (job: ${job ? job.status : "not enqueued"})`
15253
+ };
15254
+ }
15255
+ return {
15256
+ action: "wait",
15257
+ code: "waiting",
15258
+ reason: job ? `job ${job.status}` : "job not enqueued yet"
15259
+ };
15260
+ }
15261
+ function short(sha) {
15262
+ return sha.slice(0, 7);
15263
+ }
15264
+ var init_commitGate = __esm({
15265
+ "../shared/commitGate.ts"() {
15266
+ }
15267
+ });
15268
+
15269
+ // src/daemon/localPoll.ts
15270
+ function planPollTick(input) {
15271
+ const { marker, repo } = input;
15272
+ if (!marker.sha) {
15273
+ return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
15274
+ }
15275
+ if (marker.repo !== repo) {
15276
+ return {
15277
+ kind: "halt",
15278
+ code: "job-terminal",
15279
+ 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)`
15280
+ };
15281
+ }
15282
+ const plan = planAdvance({
15283
+ marker: marker.sha,
15284
+ upstreamTip: input.upstreamTip,
15285
+ markerIsAncestorOfTip: input.markerIsAncestorOfTip,
15286
+ tipIsAncestorOfMarker: input.tipIsAncestorOfMarker,
15287
+ firstParentShas: input.firstParentShas
15288
+ });
15289
+ let target;
15290
+ switch (plan.kind) {
15291
+ case "bootstrap-needed":
15292
+ return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
15293
+ case "up-to-date":
15294
+ return { kind: "idle", reason: "up to date" };
15295
+ case "upstream-behind":
15296
+ return {
15297
+ kind: "idle",
15298
+ reason: `upstream is behind the marker (reset to ${short2(plan.tip)}); nothing to do`
15299
+ };
15300
+ case "force-push":
15301
+ target = plan.tip;
15302
+ break;
15303
+ case "fast-forward":
15304
+ target = plan.shas[0];
15305
+ break;
15306
+ }
15307
+ if (!input.jobForTarget) {
15308
+ return { kind: "enqueue", beforeSha: marker.sha, afterSha: target };
15309
+ }
15310
+ const decision = decideGate({
15311
+ targetSha: target,
15312
+ markerSha: marker.sha,
15313
+ job: { status: input.jobForTarget.status, attemptCount: input.jobForTarget.attemptCount },
15314
+ elapsedMs: input.nowMs - input.jobForTarget.createdAtMs,
15315
+ timeoutMs: input.gateTimeoutMs,
15316
+ // The poller enqueues directly, so the "no-op push fired no webhook" case
15317
+ // decideGate guards for cannot arise here.
15318
+ pushedNewRef: true
15319
+ });
15320
+ switch (decision.action) {
15321
+ case "stop":
15322
+ return {
15323
+ kind: "halt",
15324
+ code: decision.code,
15325
+ reason: localHaltReason(decision, input.jobForTarget, target)
15326
+ };
15327
+ case "advance":
15328
+ return { kind: "idle", reason: decision.reason };
15329
+ case "wait":
15330
+ return { kind: "idle", reason: `${short2(target)}: ${decision.reason}` };
15331
+ }
15332
+ }
15333
+ function localHaltReason(decision, job, targetSha) {
15334
+ if (decision.code !== "job-terminal" || !job) return decision.reason;
15335
+ 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.
15336
+ 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.`;
15337
+ }
15338
+ function short2(sha) {
15339
+ return sha.slice(0, 7);
15340
+ }
15341
+ var init_localPoll = __esm({
15342
+ "src/daemon/localPoll.ts"() {
15343
+ "use strict";
15344
+ init_commitAdvance();
15345
+ init_commitGate();
15346
+ }
15347
+ });
15348
+
15349
+ // src/daemon/defaultBranch.ts
15350
+ function pickDefaultBranch(input) {
15351
+ const onOrigin = new Set(input.remoteBranches);
15352
+ if (input.symbolicRef) {
15353
+ const slash = input.symbolicRef.indexOf("/");
15354
+ const named = slash >= 0 ? input.symbolicRef.slice(slash + 1) : null;
15355
+ if (named && onOrigin.has(named)) return named;
15356
+ }
15357
+ for (const candidate of CONVENTIONAL) {
15358
+ if (onOrigin.has(candidate)) return candidate;
15359
+ }
15360
+ if (input.currentBranch && onOrigin.has(input.currentBranch)) return input.currentBranch;
15361
+ return null;
15362
+ }
15363
+ var CONVENTIONAL;
15364
+ var init_defaultBranch = __esm({
15365
+ "src/daemon/defaultBranch.ts"() {
15366
+ "use strict";
15367
+ CONVENTIONAL = ["main", "master"];
15368
+ }
15369
+ });
15370
+
15371
+ // src/daemon/localGit.ts
15372
+ var localGit_exports = {};
15373
+ __export(localGit_exports, {
15374
+ createMirrorClone: () => createMirrorClone,
15375
+ detectDefaultBranch: () => detectDefaultBranch,
15376
+ fetchOrigin: () => fetchOrigin,
15377
+ firstParentRange: () => firstParentRange,
15378
+ isAncestor: () => isAncestor,
15379
+ readCommitMeta: () => readCommitMeta,
15380
+ readOriginUrl: () => readOriginUrl,
15381
+ revParse: () => revParse,
15382
+ setOriginUrl: () => setOriginUrl
15383
+ });
15384
+ import { mkdir as mkdir9 } from "node:fs/promises";
15385
+ import { dirname as dirname12 } from "node:path";
15386
+ async function git(dir, args, opts = {}) {
15387
+ requireBinary("git");
15388
+ const res = await execa("git", args, {
15389
+ cwd: dir ?? void 0,
15390
+ reject: false,
15391
+ timeout: GIT_TIMEOUT_MS,
15392
+ // Kill the whole process group: a hung `git fetch` has spawned helper
15393
+ // processes (ssh, credential helpers) that keep the pipe open, so killing
15394
+ // git alone can leave the await pending on a dead parent.
15395
+ killSignal: "SIGKILL",
15396
+ // A large `rev-list` on a busy repo can be megabytes.
15397
+ maxBuffer: 64 * 1024 * 1024
15398
+ });
15399
+ const exitCode = res.exitCode ?? 1;
15400
+ if (!opts.allowFail && exitCode !== 0) {
15401
+ throw new Error(`git ${args.join(" ")} failed (${exitCode}): ${(res.stderr || "").trim()}`);
15402
+ }
15403
+ return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
15404
+ }
15405
+ async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
15406
+ await mkdir9(dirname12(mirrorPath), { recursive: true });
15407
+ await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
15408
+ await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
15409
+ }
15410
+ async function setOriginUrl(dir, originUrl) {
15411
+ await git(dir, ["remote", "set-url", "origin", originUrl]);
15412
+ }
15413
+ async function readOriginUrl(dir) {
15414
+ const { exitCode, stdout } = await git(dir, ["remote", "get-url", "origin"], { allowFail: true });
15415
+ if (exitCode !== 0) return null;
15416
+ return stdout.trim() || null;
15417
+ }
15418
+ async function fetchOrigin(dir, branch) {
15419
+ await git(dir, ["fetch", "--quiet", "--no-tags", "origin", branch]);
15420
+ }
15421
+ async function revParse(dir, ref) {
15422
+ return (await git(dir, ["rev-parse", ref])).stdout.trim();
15423
+ }
15424
+ async function isAncestor(dir, a2, b) {
15425
+ return (await git(dir, ["merge-base", "--is-ancestor", a2, b], { allowFail: true })).exitCode === 0;
15426
+ }
15427
+ async function firstParentRange(dir, marker, tip) {
15428
+ const { stdout } = await git(dir, [
15429
+ "rev-list",
15430
+ "--reverse",
15431
+ "--first-parent",
15432
+ `${marker}..${tip}`
15433
+ ]);
15434
+ return stdout.split("\n").map((s) => s.trim()).filter(Boolean);
15435
+ }
15436
+ async function readCommitMeta(dir, sha) {
15437
+ const { exitCode, stdout } = await git(
15438
+ dir,
15439
+ ["show", "-s", "--format=%s%n%an%n%cI", sha],
15440
+ { allowFail: true }
15441
+ );
15442
+ if (exitCode !== 0) return { message: "", authorName: null, committedAt: null };
15443
+ const [message = "", authorName = "", committedAt = ""] = stdout.split("\n");
15444
+ return {
15445
+ message: message.trim(),
15446
+ authorName: authorName.trim() || null,
15447
+ committedAt: committedAt.trim() || null
15448
+ };
15449
+ }
15450
+ async function detectDefaultBranch(dir) {
15451
+ const symbolic = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
15452
+ allowFail: true
15453
+ });
15454
+ const remotes = await git(
15455
+ dir,
15456
+ ["for-each-ref", "--format=%(refname:strip=3)", "refs/remotes/origin"],
15457
+ { allowFail: true }
15458
+ );
15459
+ const current = await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"], { allowFail: true });
15460
+ return pickDefaultBranch({
15461
+ symbolicRef: symbolic.exitCode === 0 ? symbolic.stdout.trim() : null,
15462
+ remoteBranches: remotes.exitCode === 0 ? remotes.stdout.split("\n").map((l) => l.trim()).filter((l) => l && l !== "HEAD") : [],
15463
+ // Detached HEAD reports the literal "HEAD", which is not a branch name.
15464
+ currentBranch: current.exitCode === 0 && current.stdout.trim() !== "HEAD" ? current.stdout.trim() : null
15465
+ });
15466
+ }
15467
+ var GIT_TIMEOUT_MS;
15468
+ var init_localGit = __esm({
15469
+ "src/daemon/localGit.ts"() {
15470
+ "use strict";
15471
+ init_execa();
15472
+ init_preflight();
15473
+ init_defaultBranch();
15474
+ GIT_TIMEOUT_MS = 10 * 60 * 1e3;
15475
+ }
15476
+ });
15477
+
15478
+ // src/daemon/localPoller.ts
15479
+ async function pollRepoOnce(entry, deps) {
15480
+ const repo = `${entry.repoOwner}/${entry.repoName}`;
15481
+ const gateTimeoutMs = deps.gateTimeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;
15482
+ const row = await deps.readWatchedRepo(entry.watchedRepoId);
15483
+ if (!row) {
15484
+ return {
15485
+ kind: "halt",
15486
+ // Not a gate outcome — the repo is gone from under us. Reuses the
15487
+ // terminal code because the consequence is the same: this will not fix
15488
+ // itself, and `--resume` cannot clear it.
15489
+ code: "job-terminal",
15490
+ reason: `${repo} is no longer registered on the server (the watched repo was removed). Stop watching it here with \`archme local-watch --stop ${repo}\`.`
15491
+ };
15492
+ }
15493
+ if (row.sourceKind !== "local") {
15494
+ return {
15495
+ kind: "halt",
15496
+ code: "job-terminal",
15497
+ 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}\`.`
15498
+ };
15499
+ }
15500
+ if (!row.enabled) {
15501
+ return { kind: "idle", reason: `${repo}: watching is disabled on the server` };
15502
+ }
15503
+ await deps.git.fetchOrigin(entry.mirrorPath, row.repoBranch);
15504
+ const marker = await deps.readMarker(row.projectId, row.archmeBranch);
15505
+ const upstreamTip = await deps.git.revParse(entry.mirrorPath, `origin/${row.repoBranch}`);
15506
+ const base = {
15507
+ repo,
15508
+ marker,
15509
+ upstreamTip,
15510
+ nowMs: deps.now(),
15511
+ gateTimeoutMs
15512
+ };
15513
+ if (!marker.sha) {
15514
+ return planPollTick({
15515
+ ...base,
15516
+ markerIsAncestorOfTip: false,
15517
+ tipIsAncestorOfMarker: false,
15518
+ firstParentShas: [],
15519
+ jobForTarget: null
15520
+ });
15521
+ }
15522
+ const markerIsAncestorOfTip = await deps.git.isAncestor(entry.mirrorPath, marker.sha, upstreamTip);
15523
+ const tipIsAncestorOfMarker = await deps.git.isAncestor(entry.mirrorPath, upstreamTip, marker.sha);
15524
+ const firstParentShas = markerIsAncestorOfTip ? await deps.git.firstParentRange(entry.mirrorPath, marker.sha, upstreamTip) : [];
15525
+ const ancestry = {
15526
+ ...base,
15527
+ markerIsAncestorOfTip,
15528
+ tipIsAncestorOfMarker,
15529
+ firstParentShas
15530
+ };
15531
+ const first = planPollTick({ ...ancestry, jobForTarget: null });
15532
+ if (first.kind !== "enqueue") return first;
15533
+ const jobForTarget = await deps.readJobForSha(row.projectId, row.archmeBranch, first.afterSha);
15534
+ const observedAtMs = deps.firstObservedAt(
15535
+ `${entry.watchedRepoId}:${first.afterSha}`,
15536
+ deps.now()
15537
+ );
15538
+ const decision = planPollTick({
15539
+ ...ancestry,
15540
+ jobForTarget: jobForTarget ? { ...jobForTarget, createdAtMs: observedAtMs } : null
15541
+ });
15542
+ if (decision.kind !== "enqueue") return decision;
15543
+ const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
15544
+ if (result.dedup) {
15545
+ deps.log(` ${repo}: ${short3(decision.afterSha)} already enqueued elsewhere`);
15546
+ } else {
15547
+ deps.log(` ${repo}: enqueued ${short3(decision.afterSha)} (job ${result.jobId ?? "?"})`);
15548
+ }
15549
+ return decision;
15550
+ }
15551
+ async function pollAllRepos(repos, deps) {
15552
+ for (const entry of repos) {
15553
+ const key = `${entry.repoOwner}/${entry.repoName} (${entry.watchedRepoId.slice(0, 8)})`;
15554
+ if (entry.haltedAt) continue;
15555
+ try {
15556
+ const action = await pollRepoOnce(entry, deps);
15557
+ if (action.kind === "halt") {
15558
+ await deps.persistHalt(entry, action.reason, action.code);
15559
+ deps.log(
15560
+ `
15561
+ \u26A0 local poll HALTED for ${key}: ${action.reason}
15562
+ No further commits will be processed for this repo.
15563
+ Advancing past a failed commit would fold it into the next diff and break the
15564
+ one-ArchMe-commit-per-git-commit guarantee, so this stops instead.
15565
+ When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
15566
+ `
15567
+ );
15568
+ }
15569
+ } catch (err2) {
15570
+ deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
15571
+ }
15572
+ }
15573
+ }
15574
+ function makePollerDeps(client, log, gateTimeoutMs) {
15575
+ const firstSeen = /* @__PURE__ */ new Map();
15576
+ return {
15577
+ git: localGit_exports,
15578
+ log,
15579
+ now: () => Date.now(),
15580
+ gateTimeoutMs,
15581
+ firstObservedAt(key, nowMs) {
15582
+ const existing = firstSeen.get(key);
15583
+ if (existing !== void 0) return existing;
15584
+ firstSeen.set(key, nowMs);
15585
+ return nowMs;
15586
+ },
15587
+ async persistHalt(entry, reason, code) {
15588
+ const repos = await loadLocalRepos();
15589
+ await saveLocalRepos(
15590
+ markHalted(repos, entry.watchedRepoId, reason, (/* @__PURE__ */ new Date()).toISOString(), code)
15591
+ );
15592
+ },
15593
+ async readWatchedRepo(watchedRepoId) {
15594
+ 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();
15595
+ if (error || !data) return null;
15596
+ const r = data;
15597
+ return {
15598
+ projectId: r.project_id,
15599
+ repoBranch: r.branch,
15600
+ // Mirrors the webhook's own fallback (`commit_update_archme_branch ??
15601
+ // branch`), so both producers resolve the same lane for the same row.
15602
+ archmeBranch: r.commit_update_archme_branch ?? r.branch,
15603
+ sourceKind: r.source_kind,
15604
+ enabled: r.enabled
15605
+ };
15606
+ },
15607
+ // Shared with the job runner and local-watch's readiness check: three
15608
+ // readers of the same marker drifting apart is how one of them ends up
15609
+ // deciding against a value the others don't see.
15610
+ readMarker: (projectId, archmeBranch) => readBranchMarker(client, projectId, archmeBranch),
15611
+ async readJobForSha(projectId, archmeBranch, sha) {
15612
+ 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);
15613
+ if (error || !data || data.length === 0) return null;
15614
+ const row = data[0];
15615
+ return {
15616
+ status: row.status,
15617
+ attemptCount: row.attempt_count ?? 0,
15618
+ createdAtMs: new Date(row.created_at).getTime()
15619
+ };
15620
+ },
15621
+ async enqueue(entry, row, beforeSha, afterSha) {
15622
+ const meta = await readCommitMeta(entry.mirrorPath, afterSha);
15623
+ const { data, error } = await client.rpc("enqueue_local_archme_job", {
15624
+ p_watched_repo_id: entry.watchedRepoId,
15625
+ // From the ROW, not the registry: these decide which lane the job lands
15626
+ // in, and a stale copy would enqueue against the wrong branch.
15627
+ p_repo_branch: row.repoBranch,
15628
+ p_archme_branch: row.archmeBranch,
15629
+ p_before_sha: beforeSha,
15630
+ p_after_sha: afterSha,
15631
+ p_commit_message: meta.message || null,
15632
+ p_head_commit_timestamp: meta.committedAt,
15633
+ p_author_name: meta.authorName,
15634
+ // `author_login` is a GitHub concept with no git equivalent. The webhook
15635
+ // path fills it from the push payload; local mode leaves it null.
15636
+ p_author_login: null
15637
+ });
15638
+ if (error) throw new Error(`enqueue_local_archme_job: ${error.message}`);
15639
+ return data ?? {};
15640
+ }
15641
+ };
15642
+ }
15643
+ function short3(sha) {
15644
+ return sha.slice(0, 7);
15645
+ }
15646
+ var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
15647
+ var init_localPoller = __esm({
15648
+ "src/daemon/localPoller.ts"() {
15649
+ "use strict";
15650
+ init_localPoll();
15651
+ init_localRepos();
15652
+ init_branchMarker();
15653
+ init_localGit();
15654
+ DEFAULT_POLL_INTERVAL_MS = 6e4;
15655
+ DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
15656
+ }
15657
+ });
15658
+
14915
15659
  // src/daemon/run.ts
14916
15660
  var run_exports = {};
14917
15661
  __export(run_exports, {
@@ -14933,11 +15677,11 @@ __export(run_exports, {
14933
15677
  });
14934
15678
  import { createClient } from "@supabase/supabase-js";
14935
15679
  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";
15680
+ import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
14937
15681
  import { tmpdir as tmpdir3 } from "node:os";
14938
- import { join as join15 } from "node:path";
15682
+ import { join as join18 } from "node:path";
14939
15683
  import { pathToFileURL } from "node:url";
14940
- import { homedir as homedir4 } from "node:os";
15684
+ import { homedir as homedir5 } from "node:os";
14941
15685
  function attributeKillCause(result, recorded) {
14942
15686
  if (recorded) return recorded;
14943
15687
  if (result.signal === "SIGTERM") return "timeout";
@@ -14968,7 +15712,7 @@ async function lookupProjectSlug(client, projectId) {
14968
15712
  return `${orgSlug}/${data.slug}`;
14969
15713
  }
14970
15714
  function cloneDirFor(baseDir, owner, name) {
14971
- return join15(baseDir, owner, name);
15715
+ return join18(baseDir, owner, name);
14972
15716
  }
14973
15717
  async function exists(p) {
14974
15718
  try {
@@ -15013,9 +15757,9 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
15013
15757
  };
15014
15758
  }
15015
15759
  async function buildGitEnv(credentials) {
15016
- const dir = await mkdtemp3(join15(tmpdir3(), "archme-git-askpass-"));
15017
- const askpassPath = join15(dir, "askpass.sh");
15018
- await writeFile9(
15760
+ const dir = await mkdtemp3(join18(tmpdir3(), "archme-git-askpass-"));
15761
+ const askpassPath = join18(dir, "askpass.sh");
15762
+ await writeFile10(
15019
15763
  askpassPath,
15020
15764
  [
15021
15765
  "#!/bin/sh",
@@ -15028,7 +15772,7 @@ async function buildGitEnv(credentials) {
15028
15772
  ].join("\n"),
15029
15773
  { mode: 448 }
15030
15774
  );
15031
- await chmod2(askpassPath, 448).catch(() => {
15775
+ await chmod3(askpassPath, 448).catch(() => {
15032
15776
  });
15033
15777
  const gitConfigEntries = [
15034
15778
  ["credential.helper", ""],
@@ -15053,16 +15797,38 @@ async function buildGitEnv(credentials) {
15053
15797
  return { env, cleanup: () => rm6(dir, { recursive: true, force: true }).catch(() => {
15054
15798
  }) };
15055
15799
  }
15800
+ async function ensureLocalCloneAndFetch(job, entry) {
15801
+ const dir = entry.mirrorPath;
15802
+ await cloneMutex.runExclusive(dir, async () => {
15803
+ if (!await exists(join18(dir, ".git"))) {
15804
+ throw new Error(
15805
+ `local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
15806
+ );
15807
+ }
15808
+ await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
15809
+ const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
15810
+ if (afterCheck.exitCode !== 0) {
15811
+ await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
15812
+ }
15813
+ }).catch((err2) => {
15814
+ const msg = err2 instanceof Error ? err2.message : String(err2);
15815
+ throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
15816
+ });
15817
+ return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
15818
+ } };
15819
+ }
15056
15820
  async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
15821
+ const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
15822
+ if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
15057
15823
  const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
15058
15824
  if (!deviceId) throw new Error("daemon is not signed in with a registered device");
15059
15825
  const credentials = await getRepoCredentials(ctx, job.id, deviceId);
15060
- await mkdir8(ctx.cloneBaseDir, { recursive: true });
15826
+ await mkdir10(ctx.cloneBaseDir, { recursive: true });
15061
15827
  const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
15062
15828
  const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
15063
15829
  await cloneMutex.runExclusive(dir, async () => {
15064
- if (!await exists(join15(dir, ".git"))) {
15065
- await mkdir8(join15(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
15830
+ if (!await exists(join18(dir, ".git"))) {
15831
+ await mkdir10(join18(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
15066
15832
  console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
15067
15833
  await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
15068
15834
  } else {
@@ -15083,22 +15849,22 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
15083
15849
  await execa("git", ["fetch", "origin", job.before_sha], { cwd: dir, env, reject: false });
15084
15850
  }
15085
15851
  }
15086
- }).catch(async (err) => {
15852
+ }).catch(async (err2) => {
15087
15853
  await cleanupGitEnv();
15088
- const msg = err instanceof Error ? err.message : String(err);
15854
+ const msg = err2 instanceof Error ? err2.message : String(err2);
15089
15855
  throw new Error(`GitHub App cannot access ${job.repo_owner}/${job.repo_name}: ${msg}`);
15090
15856
  });
15091
15857
  return { dir, gitEnv: env, cleanupGitEnv };
15092
15858
  }
15093
15859
  async function createJobWorktree(repoDir, job) {
15094
- const parentDir = await mkdtemp3(join15(tmpdir3(), "archme-job-worktree-"));
15095
- const worktreeDir = join15(parentDir, "repo");
15860
+ const parentDir = await mkdtemp3(join18(tmpdir3(), "archme-job-worktree-"));
15861
+ const worktreeDir = join18(parentDir, "repo");
15096
15862
  try {
15097
15863
  await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
15098
- } catch (err) {
15864
+ } catch (err2) {
15099
15865
  await rm6(parentDir, { recursive: true, force: true }).catch(() => {
15100
15866
  });
15101
- throw err;
15867
+ throw err2;
15102
15868
  }
15103
15869
  return {
15104
15870
  worktreeDir,
@@ -15117,7 +15883,7 @@ function noUsableBaseError(branch, markerSha, detail) {
15117
15883
  );
15118
15884
  }
15119
15885
  async function isShaReachable(repoDir, sha) {
15120
- const { exitCode } = await execa("git", ["cat-file", "-e", sha], { cwd: repoDir, reject: false });
15886
+ const { exitCode } = await execa("git", ["cat-file", "-e", `${sha}^{commit}`], { cwd: repoDir, reject: false });
15121
15887
  return exitCode === 0;
15122
15888
  }
15123
15889
  async function buildDiffPayload(repoDir, before, after) {
@@ -15140,7 +15906,16 @@ async function forcePushProvenance(repoDir, base, after) {
15140
15906
  const forkNote = fork.trim() ? ` (history diverged at \`${fork.trim().slice(0, 8)}\`)` : "";
15141
15907
  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
15908
  }
15909
+ function isGitSha(v) {
15910
+ return typeof v === "string" && GIT_SHA_RE.test(v);
15911
+ }
15912
+ function assertGitSha(v, field) {
15913
+ if (!isGitSha(v)) {
15914
+ throw new Error(`${field} ${v ?? "(null)"} is not a 40-character git SHA`);
15915
+ }
15916
+ }
15143
15917
  async function computeDiff(job, repoDir, marker, gitEnv) {
15918
+ assertGitSha(job.after_sha, "after_sha");
15144
15919
  if (!await isShaReachable(repoDir, job.after_sha)) {
15145
15920
  throw new Error(`after_sha ${job.after_sha} is not reachable after fetch`);
15146
15921
  }
@@ -15163,6 +15938,9 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
15163
15938
  if (marker.repo !== repo) {
15164
15939
  throw noUsableBaseError(job.archme_branch, marker.sha, `marker repo ${marker.repo ?? "n/a"} does not match job repo ${repo}`);
15165
15940
  }
15941
+ if (!isGitSha(marker.sha)) {
15942
+ throw noUsableBaseError(job.archme_branch, marker.sha, `marker SHA ${marker.sha} is not a 40-character git SHA`);
15943
+ }
15166
15944
  const base = marker.sha;
15167
15945
  if (base === after) return "skip";
15168
15946
  if (!await isShaReachable(repoDir, base)) {
@@ -15177,11 +15955,6 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
15177
15955
  if (job.forced) payload.provenance = await forcePushProvenance(repoDir, base, after);
15178
15956
  return payload;
15179
15957
  }
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
15958
  function buildDiffPrompt(job, projectSlug, diff, checkoutDir, affectedSeed, bugsFolderId) {
15186
15959
  const sentinel = `UNTRUSTED-DIFF-${randomUUID()}`;
15187
15960
  const seedBlock = affectedSeed ? `
@@ -15515,9 +16288,9 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
15515
16288
  };
15516
16289
  }
15517
16290
  lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
15518
- } catch (err) {
16291
+ } catch (err2) {
15519
16292
  lastStatus = null;
15520
- lastDetail = `network error reaching MCP: ${err instanceof Error ? err.message : String(err)}`;
16293
+ lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
15521
16294
  }
15522
16295
  }
15523
16296
  if (lastStatus === 401) {
@@ -15765,7 +16538,7 @@ async function runJob(ctx, job, claimDeviceId) {
15765
16538
  const affected = resolveAffected(seedArch, diff.changedFiles, changedSymbolsByFile);
15766
16539
  affectedForComplete = affected;
15767
16540
  const symbolsInFile = async (file) => {
15768
- const outline = await runOutline(join15(worktreeDir, file));
16541
+ const outline = await runOutline(join18(worktreeDir, file));
15769
16542
  if (outline.tier !== 2) return null;
15770
16543
  return outline.data.symbols.map((s) => s.name);
15771
16544
  };
@@ -15893,9 +16666,9 @@ async function runJob(ctx, job, claimDeviceId) {
15893
16666
  true,
15894
16667
  affectedFieldForComplete(job.source_mode, affectedForComplete)
15895
16668
  );
15896
- } catch (err) {
16669
+ } catch (err2) {
15897
16670
  if (cancelled.has(job.id)) {
15898
- console.log(`[${job.id}] cancelled, swallowing error: ${err instanceof Error ? err.message : err}`);
16671
+ console.log(`[${job.id}] cancelled, swallowing error: ${err2 instanceof Error ? err2.message : err2}`);
15899
16672
  await rpcRollback(ctx, job.id, deviceId);
15900
16673
  return;
15901
16674
  }
@@ -15903,7 +16676,7 @@ async function runJob(ctx, job, claimDeviceId) {
15903
16676
  console.log(`[${job.id}] shutdown \u2014 routed to retry by shutdown handler`);
15904
16677
  return;
15905
16678
  }
15906
- const msg = err instanceof Error ? err.message : String(err);
16679
+ const msg = err2 instanceof Error ? err2.message : String(err2);
15907
16680
  const haystack = `${msg}
15908
16681
  ${logs.stderr}`;
15909
16682
  const plan = rateLimitDeferPlanForExit({ message: msg, stderr: logs.stderr, stdout: logs.stdout });
@@ -15975,7 +16748,7 @@ async function dispatch(sched) {
15975
16748
  }
15976
16749
  ctxByJob.set(claimed.job.id, claimed.ctx);
15977
16750
  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(() => {
16751
+ 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
16752
  ctxByJob.delete(claimed.job.id);
15980
16753
  deviceByJob.delete(claimed.job.id);
15981
16754
  slot();
@@ -16011,7 +16784,7 @@ function subscribe(client, sched) {
16011
16784
  "postgres_changes",
16012
16785
  { event: "INSERT", schema: "public", table: "archme_jobs" },
16013
16786
  (payload) => {
16014
- if (payload.new.status === "pending") dispatch(sched).catch((err) => console.error("dispatch:", err));
16787
+ if (payload.new.status === "pending") dispatch(sched).catch((err2) => console.error("dispatch:", err2));
16015
16788
  }
16016
16789
  ).on(
16017
16790
  "postgres_changes",
@@ -16028,12 +16801,12 @@ function subscribe(client, sched) {
16028
16801
  console.log(`[${payload.new.id}] received cancellation (pre-spawn), will abort`);
16029
16802
  }
16030
16803
  } else if (payload.new.status === "pending") {
16031
- dispatch(sched).catch((err) => console.error("dispatch:", err));
16804
+ dispatch(sched).catch((err2) => console.error("dispatch:", err2));
16032
16805
  }
16033
16806
  }
16034
16807
  ).subscribe((status) => {
16035
16808
  console.log(`realtime channel: ${status}`);
16036
- if (status === "SUBSCRIBED") dispatch(sched).catch((err) => console.error("startup dispatch:", err));
16809
+ if (status === "SUBSCRIBED") dispatch(sched).catch((err2) => console.error("startup dispatch:", err2));
16037
16810
  });
16038
16811
  return () => {
16039
16812
  client.removeChannel(channel);
@@ -16074,9 +16847,9 @@ async function signInAccount(creds, makeAuth) {
16074
16847
  active = { auth, cred, result };
16075
16848
  usedFallback = attempts > 1;
16076
16849
  break;
16077
- } catch (err) {
16850
+ } catch (err2) {
16078
16851
  auth.clearRefreshTimer();
16079
- lastErr = err;
16852
+ lastErr = err2;
16080
16853
  }
16081
16854
  }
16082
16855
  if (active) {
@@ -16093,19 +16866,19 @@ async function runDaemon() {
16093
16866
  let lockHandle = null;
16094
16867
  try {
16095
16868
  lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
16096
- } catch (err) {
16097
- if (err instanceof SingleInstanceLockedError) {
16098
- console.error(String(err.message));
16869
+ } catch (err2) {
16870
+ if (err2 instanceof SingleInstanceLockedError) {
16871
+ console.error(String(err2.message));
16099
16872
  process.exit(64);
16100
16873
  }
16101
- throw err;
16874
+ throw err2;
16102
16875
  }
16103
16876
  let cfg;
16104
16877
  try {
16105
16878
  cfg = resolveConfig();
16106
- } catch (err) {
16107
- if (err instanceof MissingConfigError) {
16108
- console.error(err.message);
16879
+ } catch (err2) {
16880
+ if (err2 instanceof MissingConfigError) {
16881
+ console.error(err2.message);
16109
16882
  if (lockHandle) {
16110
16883
  try {
16111
16884
  await lockHandle.release();
@@ -16114,7 +16887,7 @@ async function runDaemon() {
16114
16887
  }
16115
16888
  process.exit(1);
16116
16889
  }
16117
- throw err;
16890
+ throw err2;
16118
16891
  }
16119
16892
  SUPABASE_URL = cfg.supabaseUrl;
16120
16893
  SUPABASE_ANON_KEY = cfg.supabaseAnonKey;
@@ -16148,9 +16921,9 @@ async function runDaemon() {
16148
16921
  let credentials;
16149
16922
  try {
16150
16923
  credentials = assertSingleAccount(loaded);
16151
- } catch (err) {
16152
- if (err instanceof MultipleAccountsError) {
16153
- console.error(err.message);
16924
+ } catch (err2) {
16925
+ if (err2 instanceof MultipleAccountsError) {
16926
+ console.error(err2.message);
16154
16927
  if (lockHandle) {
16155
16928
  try {
16156
16929
  await lockHandle.release();
@@ -16159,7 +16932,7 @@ async function runDaemon() {
16159
16932
  }
16160
16933
  process.exit(78);
16161
16934
  }
16162
- throw err;
16935
+ throw err2;
16163
16936
  }
16164
16937
  const devices = buildDevices(credentials);
16165
16938
  console.log(` booting 1 session across ${devices.length} org(s)`);
@@ -16251,17 +17024,50 @@ async function runDaemon() {
16251
17024
  console.warn(`daemon_pauses lookup failed (proceeding unpaused): ${e.message}`);
16252
17025
  }
16253
17026
  const sweepTimer = setInterval(
16254
- () => dispatch(sched).catch((err) => console.error("safety sweep:", err)),
17027
+ () => dispatch(sched).catch((err2) => console.error("safety sweep:", err2)),
16255
17028
  SAFETY_SWEEP_MS
16256
17029
  );
16257
- const shutdown = async (signal) => {
16258
- if (shuttingDown) return;
16259
- shuttingDown = true;
16260
- console.log(`
16261
- ${signal} received, shutting down`);
16262
- for (const ctx of sessions) ctx.auth.clearRefreshTimer();
16263
- clearInterval(sweepTimer);
16264
- for (const u2 of unsubscribers) u2();
17030
+ const localPollDeps = makePollerDeps(client, (m) => console.log(m));
17031
+ const localPollIntervalMs = Number.parseInt(process.env.ARCHME_LOCAL_POLL_INTERVAL_MS ?? "", 10);
17032
+ const pollEveryMs = Number.isFinite(localPollIntervalMs) && localPollIntervalMs > 0 ? localPollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
17033
+ let localPollInFlight = false;
17034
+ const runLocalPoll = async () => {
17035
+ if (localPollInFlight) return;
17036
+ localPollInFlight = true;
17037
+ try {
17038
+ const repos = await loadLocalRepos();
17039
+ if (repos.length === 0) return;
17040
+ await pollAllRepos(repos, localPollDeps);
17041
+ } finally {
17042
+ localPollInFlight = false;
17043
+ }
17044
+ };
17045
+ const initialLocalRepos = await loadLocalRepos();
17046
+ if (initialLocalRepos.length > 0) {
17047
+ const halted = initialLocalRepos.filter((r) => r.haltedAt);
17048
+ console.log(
17049
+ ` local-poll: ${initialLocalRepos.length} repo(s) every ${Math.round(pollEveryMs / 1e3)}s`
17050
+ );
17051
+ for (const r of halted) {
17052
+ console.log(
17053
+ ` \u26A0 ${r.repoOwner}/${r.repoName} is HALTED (${r.haltReason ?? "unknown reason"}) \u2014 resume with \`archme local-watch --resume ${r.repoOwner}/${r.repoName}\``
17054
+ );
17055
+ }
17056
+ }
17057
+ const localPollTimer = setInterval(
17058
+ () => void runLocalPoll().catch((err2) => console.error("local poll:", err2)),
17059
+ pollEveryMs
17060
+ );
17061
+ void runLocalPoll().catch((err2) => console.error("local poll:", err2));
17062
+ const shutdown = async (signal) => {
17063
+ if (shuttingDown) return;
17064
+ shuttingDown = true;
17065
+ console.log(`
17066
+ ${signal} received, shutting down`);
17067
+ for (const ctx of sessions) ctx.auth.clearRefreshTimer();
17068
+ clearInterval(sweepTimer);
17069
+ clearInterval(localPollTimer);
17070
+ for (const u2 of unsubscribers) u2();
16265
17071
  const SHUTDOWN_DEADLINE_MS = 3e4;
16266
17072
  const deadline = setTimeout(() => {
16267
17073
  console.error("shutdown deadline exceeded, forcing exit");
@@ -16300,7 +17106,7 @@ ${signal} received, shutting down`);
16300
17106
  process.on("SIGINT", () => shutdown("SIGINT"));
16301
17107
  process.on("SIGTERM", () => shutdown("SIGTERM"));
16302
17108
  }
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;
17109
+ 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
17110
  var init_run = __esm({
16305
17111
  "src/daemon/run.ts"() {
16306
17112
  "use strict";
@@ -16331,6 +17137,10 @@ var init_run = __esm({
16331
17137
  init_rateLimit();
16332
17138
  init_version();
16333
17139
  init_materializeDiff();
17140
+ init_branchMarker();
17141
+ init_branchMarker();
17142
+ init_localRepos();
17143
+ init_localPoller();
16334
17144
  HEARTBEAT_INTERVAL_MS = 6e4;
16335
17145
  CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
16336
17146
  CLAUDE_LARGE_DIFF_TIMEOUT_MS = 45 * 60 * 1e3;
@@ -16351,8 +17161,9 @@ var init_run = __esm({
16351
17161
  MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
16352
17162
  ctxByJob = /* @__PURE__ */ new Map();
16353
17163
  deviceByJob = /* @__PURE__ */ new Map();
16354
- DEFAULT_CLONE_BASE_DIR = `${homedir4()}/.archme-job-runner/clones`;
17164
+ DEFAULT_CLONE_BASE_DIR = `${homedir5()}/.archme-job-runner/clones`;
16355
17165
  INLINE_PATCH_BYTE_CAP = 2e5;
17166
+ GIT_SHA_RE = /^[0-9a-f]{40}$/;
16356
17167
  CURATOR_PREAMBLE = `## Your role
16357
17168
 
16358
17169
  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 +17328,8 @@ ${ANCHOR_VERIFICATION}`;
16517
17328
  }
16518
17329
  };
16519
17330
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
16520
- runDaemon().catch((err) => {
16521
- console.error("fatal:", err);
17331
+ runDaemon().catch((err2) => {
17332
+ console.error("fatal:", err2);
16522
17333
  process.exit(1);
16523
17334
  });
16524
17335
  }
@@ -16557,12 +17368,12 @@ async function dispatchDaemon(route2) {
16557
17368
  console.log(`Logs: ${out.logPath}`);
16558
17369
  console.log(`State: ${out.result.actions.join(" \u2192 ")}`);
16559
17370
  return 0;
16560
- } catch (err) {
16561
- if (err instanceof NotSignedInError) {
16562
- console.error(String(err.message));
17371
+ } catch (err2) {
17372
+ if (err2 instanceof NotSignedInError) {
17373
+ console.error(String(err2.message));
16563
17374
  return 75;
16564
17375
  }
16565
- console.error(String(err));
17376
+ console.error(String(err2));
16566
17377
  return 1;
16567
17378
  }
16568
17379
  }
@@ -16575,13 +17386,13 @@ async function dispatchDaemon(route2) {
16575
17386
  case "restart": {
16576
17387
  try {
16577
17388
  await ensureStartPrerequisites({ noLogin: route2.noLogin });
16578
- } catch (err) {
16579
- if (err instanceof NotSignedInError) {
16580
- console.error(String(err.message));
17389
+ } catch (err2) {
17390
+ if (err2 instanceof NotSignedInError) {
17391
+ console.error(String(err2.message));
16581
17392
  console.error("(daemon left running unchanged)");
16582
17393
  return 75;
16583
17394
  }
16584
- console.error(String(err));
17395
+ console.error(String(err2));
16585
17396
  console.error("(daemon left running unchanged)");
16586
17397
  return 1;
16587
17398
  }
@@ -16669,9 +17480,9 @@ async function defaultLoadSupabaseState(_deviceId) {
16669
17480
  async function defaultLoadUptime(pid) {
16670
17481
  if (process.platform === "linux") {
16671
17482
  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(" ") ?? [];
17483
+ const { readFile: readFile14 } = await import("node:fs/promises");
17484
+ const stat11 = await readFile14(`/proc/${pid}/stat`, "utf8");
17485
+ const parts = stat11.split(") ").pop()?.split(" ") ?? [];
16675
17486
  const ticks = Number(parts[19]);
16676
17487
  if (!Number.isFinite(ticks)) return null;
16677
17488
  const { readFile: rf } = await import("node:fs/promises");
@@ -16738,8 +17549,8 @@ async function createAuthedClient(opts) {
16738
17549
  let loaded;
16739
17550
  try {
16740
17551
  loaded = await loadStoredAndRefresh();
16741
- } catch (err) {
16742
- const msg = err instanceof Error ? err.message : String(err);
17552
+ } catch (err2) {
17553
+ const msg = err2 instanceof Error ? err2.message : String(err2);
16743
17554
  throw new Error(`${msg}
16744
17555
  \u2192 run \`archme login\` first`);
16745
17556
  }
@@ -16770,9 +17581,9 @@ function deriveWebOrigin(apiUrl, onMalformed = "throw") {
16770
17581
  let parsed;
16771
17582
  try {
16772
17583
  parsed = new URL(apiUrl);
16773
- } catch (err) {
17584
+ } catch (err2) {
16774
17585
  if (onMalformed === "throw") {
16775
- throw new Error(`could not parse API URL \`${apiUrl}\`: ${err instanceof Error ? err.message : String(err)}`);
17586
+ throw new Error(`could not parse API URL \`${apiUrl}\`: ${err2 instanceof Error ? err2.message : String(err2)}`);
16776
17587
  }
16777
17588
  return "http://localhost:5173";
16778
17589
  }
@@ -16981,8 +17792,8 @@ var init_transcript_signals = __esm({
16981
17792
  });
16982
17793
 
16983
17794
  // src/scan/report.ts
16984
- import { mkdir as mkdir9, writeFile as writeFile10 } from "node:fs/promises";
16985
- import { dirname as dirname10 } from "node:path";
17795
+ import { mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
17796
+ import { dirname as dirname13 } from "node:path";
16986
17797
  async function buildReport(input) {
16987
17798
  return {
16988
17799
  schema_version: REPORT_SCHEMA_VERSION,
@@ -17016,10 +17827,10 @@ async function buildReport(input) {
17016
17827
  };
17017
17828
  }
17018
17829
  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(
17830
+ await mkdir11(dirname13(reportPath), { recursive: true });
17831
+ await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
17832
+ await mkdir11(dirname13(orphansTxtPath), { recursive: true });
17833
+ await writeFile11(
17023
17834
  orphansTxtPath,
17024
17835
  orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
17025
17836
  "utf8"
@@ -17044,10 +17855,26 @@ var init_github_remote = __esm({
17044
17855
  }
17045
17856
  });
17046
17857
 
17858
+ // src/scan/claimDefaultProject.ts
17859
+ function isClaimableDefaultProject(c3) {
17860
+ if (c3.slug !== SIGNUP_DEFAULT_SLUG) return false;
17861
+ if (c3.name.trim().toLowerCase() !== SIGNUP_DEFAULT_NAME) return false;
17862
+ if (c3.fileCount === null || c3.commitCount === null) return false;
17863
+ return c3.fileCount === 0 && c3.commitCount === 0;
17864
+ }
17865
+ var SIGNUP_DEFAULT_SLUG, SIGNUP_DEFAULT_NAME;
17866
+ var init_claimDefaultProject = __esm({
17867
+ "src/scan/claimDefaultProject.ts"() {
17868
+ "use strict";
17869
+ SIGNUP_DEFAULT_SLUG = "default";
17870
+ SIGNUP_DEFAULT_NAME = "default";
17871
+ }
17872
+ });
17873
+
17047
17874
  // src/scan/run-scan.ts
17048
17875
  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";
17876
+ import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
17877
+ import { basename as basename2, join as join19 } from "node:path";
17051
17878
  function buildBootstrapPrompt(opts = {}) {
17052
17879
  const { promptVariant, interactive, briefing } = opts;
17053
17880
  let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
@@ -17132,6 +17959,8 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
17132
17959
  if (!membership) {
17133
17960
  throw new Error(`you are not a member of org \`${orgSlug}\`; cannot create project there`);
17134
17961
  }
17962
+ const claimed = await claimSignupDefaultProject(supabase, org, projectSlug);
17963
+ if (claimed) return claimed;
17135
17964
  const { data: created, error: cErr } = await supabase.from("projects").insert({ name: projectSlug, org_id: org.id, slug: projectSlug }).select("id").single();
17136
17965
  if (cErr) {
17137
17966
  if (cErr.code === "23505" || /duplicate key/i.test(cErr.message ?? "")) {
@@ -17164,6 +17993,43 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
17164
17993
  created: true
17165
17994
  };
17166
17995
  }
17996
+ async function claimSignupDefaultProject(supabase, org, projectSlug) {
17997
+ if (projectSlug === SIGNUP_DEFAULT_SLUG) return null;
17998
+ try {
17999
+ 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();
18000
+ if (!candidate) return null;
18001
+ const row = candidate;
18002
+ const [{ count: fileCount }, { count: commitCount }] = await Promise.all([
18003
+ supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", row.id),
18004
+ supabase.from("commits").select("id", { count: "exact", head: true }).eq("project_id", row.id)
18005
+ ]);
18006
+ if (!isClaimableDefaultProject({
18007
+ slug: row.slug,
18008
+ name: row.name,
18009
+ fileCount: fileCount ?? null,
18010
+ commitCount: commitCount ?? null
18011
+ })) {
18012
+ return null;
18013
+ }
18014
+ const { data: branches } = await supabase.from("branches").select("name, is_default").eq("project_id", row.id).is("deleted_at", null);
18015
+ const existing = branches ?? [];
18016
+ if (existing.length === 0) {
18017
+ const { error: bErr } = await supabase.from("branches").insert({ project_id: row.id, name: "main", is_default: true });
18018
+ if (bErr) return null;
18019
+ }
18020
+ const { error: renameErr } = await supabase.from("projects").update({ name: projectSlug, slug: projectSlug }).eq("id", row.id);
18021
+ if (renameErr) return null;
18022
+ return {
18023
+ orgSlug: org.slug,
18024
+ projectSlug,
18025
+ projectId: row.id,
18026
+ defaultBranch: existing.find((b) => b.is_default)?.name ?? "main",
18027
+ created: true
18028
+ };
18029
+ } catch {
18030
+ return null;
18031
+ }
18032
+ }
17167
18033
  async function fetchExistingProject(supabase, org, projectSlug) {
17168
18034
  const project = await mustQuery(
17169
18035
  supabase.from("projects").select("id, slug").eq("org_id", org.id).eq("slug", projectSlug).is("deleted_at", null).maybeSingle(),
@@ -17269,8 +18135,8 @@ async function runScan(opts, deps = {}) {
17269
18135
  let headSha;
17270
18136
  try {
17271
18137
  headSha = await repoHeadSha(opts.repoPath);
17272
- } catch (err) {
17273
- const msg = err instanceof Error ? err.message : String(err);
18138
+ } catch (err2) {
18139
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17274
18140
  throw new Error(`could not read git HEAD at \`${opts.repoPath}\` (is this a git repo?): ${msg}`);
17275
18141
  }
17276
18142
  let fullSlug;
@@ -17287,19 +18153,19 @@ async function runScan(opts, deps = {}) {
17287
18153
  }
17288
18154
  fullSlug = defaultProjectSlug(orgSlug, opts.repoPath);
17289
18155
  }
17290
- const resolved = await resolveOrCreateProject(supabase, userId, fullSlug);
17291
- if (resolved.created) {
17292
- log(` project: ${resolved.orgSlug}/${resolved.projectSlug} (created)`);
18156
+ const resolved2 = await resolveOrCreateProject(supabase, userId, fullSlug);
18157
+ if (resolved2.created) {
18158
+ log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (created)`);
17293
18159
  } else {
17294
- log(` project: ${resolved.orgSlug}/${resolved.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
18160
+ log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
17295
18161
  }
17296
- log(` default branch: ${resolved.defaultBranch}`);
18162
+ log(` default branch: ${resolved2.defaultBranch}`);
17297
18163
  const branchName = opts.branch ? normalizeScanBranchName(opts.branch) : defaultBranchName(headSha);
17298
18164
  log(` scan branch: ${branchName}`);
17299
18165
  const bearer = await getMcpAccessToken();
17300
18166
  const { configPath, cleanup: cleanupMcp } = await writeMcpConfig({
17301
18167
  baseUrl: mcpUrl,
17302
- projectSlug: `${resolved.orgSlug}/${resolved.projectSlug}`,
18168
+ projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
17303
18169
  branch: branchName,
17304
18170
  bearer,
17305
18171
  // No source-SHA block (no job id for a local run), but DO tag these as scan
@@ -17334,9 +18200,9 @@ async function runScan(opts, deps = {}) {
17334
18200
  // Incompatible with an interactive TTY session.
17335
18201
  streamJson: !interactive
17336
18202
  });
17337
- const transcriptDir = join16(opts.repoPath, ".archme", "scan");
17338
- const transcriptPath = join16(transcriptDir, "transcript.log");
17339
- await mkdir10(transcriptDir, { recursive: true });
18203
+ const transcriptDir = join19(opts.repoPath, ".archme", "scan");
18204
+ const transcriptPath = join19(transcriptDir, "transcript.log");
18205
+ await mkdir12(transcriptDir, { recursive: true });
17340
18206
  const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
17341
18207
  let finalResultText = "";
17342
18208
  if (proc.stdout) {
@@ -17370,19 +18236,19 @@ async function runScan(opts, deps = {}) {
17370
18236
  } else {
17371
18237
  log(`claude exited ${exitCode}`);
17372
18238
  }
17373
- const counts = await countArchitecture(supabase, resolved.projectId, branchName);
18239
+ const counts = await countArchitecture(supabase, resolved2.projectId, branchName);
17374
18240
  log(` components: ${counts.components}`);
17375
18241
  log(` stories: ${counts.stories}`);
17376
18242
  let signals2 = null;
17377
18243
  try {
17378
- await new Promise((resolve4, reject) => {
17379
- transcriptStream.end((err) => err ? reject(err) : resolve4());
18244
+ await new Promise((resolve5, reject) => {
18245
+ transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
17380
18246
  });
17381
- const reportPath = join16(transcriptDir, "report.json");
17382
- const orphansTxtPath = join16(transcriptDir, "orphans.txt");
17383
- const transcript = await readFile11(transcriptPath, "utf8").catch(() => "");
18247
+ const reportPath = join19(transcriptDir, "report.json");
18248
+ const orphansTxtPath = join19(transcriptDir, "orphans.txt");
18249
+ const transcript = await readFile12(transcriptPath, "utf8").catch(() => "");
17384
18250
  signals2 = extractTranscriptSignals(transcript);
17385
- const architecture = await fetchArchitectureDetail(supabase, resolved.projectId, branchName);
18251
+ const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
17386
18252
  const orphans = await classifyOrphans({
17387
18253
  repoRoot: opts.repoPath,
17388
18254
  fileAnchors: architecture.fileAnchors,
@@ -17391,7 +18257,7 @@ async function runScan(opts, deps = {}) {
17391
18257
  excludeGlobs: DEFAULT_EXCLUDE_GLOBS
17392
18258
  });
17393
18259
  const report = await buildReport({
17394
- projectSlug: `${resolved.orgSlug}/${resolved.projectSlug}`,
18260
+ projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
17395
18261
  branch: branchName,
17396
18262
  transcriptPath,
17397
18263
  reportPath,
@@ -17407,8 +18273,8 @@ async function runScan(opts, deps = {}) {
17407
18273
  log(` coverage: ${report.repo.anchor_coverage_ratio} orphans: ${orphans.orphan_files_full.length}`);
17408
18274
  log(` report: ${reportPath}`);
17409
18275
  log(` transcript: ${transcriptPath}`);
17410
- } catch (err) {
17411
- const msg = err instanceof Error ? err.message : String(err);
18276
+ } catch (err2) {
18277
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17412
18278
  log(`warning: failed to build scan report: ${msg}`);
17413
18279
  }
17414
18280
  if (signals2 !== null && shouldStampMarker(signals2, exitCode, signal)) {
@@ -17418,15 +18284,15 @@ async function runScan(opts, deps = {}) {
17418
18284
  repo = stdout ? parseGitHubRepo(stdout) : null;
17419
18285
  } catch {
17420
18286
  }
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);
18287
+ 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
18288
  if (markerErr) log(` marker advance failed (next daemon job has no usable base): ${markerErr.message}`);
17423
18289
  else log(` marker advanced: ${branchName} \u2192 ${headSha.slice(0, 8)}${repo ? ` (${repo})` : ""}`);
17424
18290
  }
17425
- const diffBase = resolved.defaultBranch;
18291
+ const diffBase = resolved2.defaultBranch;
17426
18292
  const webUrl = buildWebUrl({
17427
18293
  apiUrl: mcpUrl,
17428
- orgSlug: resolved.orgSlug,
17429
- projectSlug: resolved.projectSlug,
18294
+ orgSlug: resolved2.orgSlug,
18295
+ projectSlug: resolved2.projectSlug,
17430
18296
  branchName,
17431
18297
  defaultBranch: diffBase,
17432
18298
  // CLI: throw on malformed mcpUrl so misconfiguration is loud.
@@ -17439,8 +18305,8 @@ async function runScan(opts, deps = {}) {
17439
18305
  components: counts.components,
17440
18306
  stories: counts.stories,
17441
18307
  branchName,
17442
- orgSlug: resolved.orgSlug,
17443
- projectSlug: resolved.projectSlug,
18308
+ orgSlug: resolved2.orgSlug,
18309
+ projectSlug: resolved2.projectSlug,
17444
18310
  webUrl,
17445
18311
  summary
17446
18312
  };
@@ -17466,6 +18332,7 @@ var init_run_scan = __esm({
17466
18332
  init_transcript_signals();
17467
18333
  init_report();
17468
18334
  init_github_remote();
18335
+ init_claimDefaultProject();
17469
18336
  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
18337
  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
18338
 
@@ -17475,6 +18342,62 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
17475
18342
  }
17476
18343
  });
17477
18344
 
18345
+ // src/scan/scanOutcome.ts
18346
+ function scanSucceeded(r) {
18347
+ return r.exitCode === 0 && !r.signal && r.components > 0;
18348
+ }
18349
+ function nextSteps() {
18350
+ return [
18351
+ "",
18352
+ "This is a snapshot of your architecture as of now. It will not follow your",
18353
+ "code unless something keeps it up to date:",
18354
+ "",
18355
+ " archme init walks you through the options",
18356
+ "",
18357
+ " \xB7 GitHub App \u2014 a couple of minutes, PR-aware, works for your whole team.",
18358
+ " Needs someone who can grant the app read access to the repo.",
18359
+ " \xB7 archme local-watch \u2014 no GitHub App and no approval, but updates only",
18360
+ " happen while your daemon is running on a machine that stays on."
18361
+ ];
18362
+ }
18363
+ function formatScanOutcome(r) {
18364
+ const lines = [""];
18365
+ if (!scanSucceeded(r)) {
18366
+ if (r.signal || r.exitCode !== 0) {
18367
+ lines.push("scan did not complete");
18368
+ lines.push(r.signal ? ` killed by ${r.signal}` : ` exit ${r.exitCode}`);
18369
+ } else {
18370
+ lines.push("scan finished but wrote no components");
18371
+ }
18372
+ lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
18373
+ lines.push(` components: ${r.components}`);
18374
+ lines.push(` stories: ${r.stories}`);
18375
+ lines.push("");
18376
+ lines.push("If Claude Code is installed but not signed in, run `claude` once and");
18377
+ lines.push("authenticate, then re-run this command.");
18378
+ } else {
18379
+ lines.push("scan complete");
18380
+ lines.push(` components: ${r.components}`);
18381
+ lines.push(` stories: ${r.stories}`);
18382
+ lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
18383
+ lines.push(` web UI: ${r.webUrl}`);
18384
+ }
18385
+ if (r.summary) {
18386
+ const tail = r.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${r.summary.slice(-SUMMARY_TAIL_CHARS)}` : r.summary;
18387
+ lines.push("", `Claude summary:
18388
+ ${tail}`);
18389
+ }
18390
+ if (scanSucceeded(r)) lines.push(...nextSteps());
18391
+ return lines.join("\n");
18392
+ }
18393
+ var SUMMARY_TAIL_CHARS;
18394
+ var init_scanOutcome = __esm({
18395
+ "src/scan/scanOutcome.ts"() {
18396
+ "use strict";
18397
+ SUMMARY_TAIL_CHARS = 2e3;
18398
+ }
18399
+ });
18400
+
17478
18401
  // src/scan/dispatch.ts
17479
18402
  var dispatch_exports3 = {};
17480
18403
  __export(dispatch_exports3, {
@@ -17546,22 +18469,10 @@ async function dispatchScan(args) {
17546
18469
  opts.repoPath = resolve2(repoPath);
17547
18470
  try {
17548
18471
  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
- }
18472
+ console.log(formatScanOutcome(result));
17562
18473
  return dispatchExitCode(result);
17563
- } catch (err) {
17564
- const msg = err instanceof Error ? err.message : String(err);
18474
+ } catch (err2) {
18475
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17565
18476
  console.error(`archme scan: ${msg}`);
17566
18477
  return EX_INFRA;
17567
18478
  }
@@ -17573,7 +18484,7 @@ Usage:
17573
18484
  archme scan <repo-path> [flags]
17574
18485
 
17575
18486
  Flags:
17576
- --project <org/slug> Target project. Defaults to e2e/<basename>.
18487
+ --project <org/slug> Target project. Defaults to <your-org>/<basename>.
17577
18488
  --branch <name> Target scan branch. Defaults to <date>-<sha7>-scan.
17578
18489
  --no-interactive Run headless (no questions). For CI/automation.
17579
18490
  --prompt-variant <id> Scan prompt variant. Defaults to latest.
@@ -17582,23 +18493,23 @@ Flags:
17582
18493
  Reads OAuth credentials from the local keychain (run \`archme login\` first).
17583
18494
  Outputs component / story counts and the web-UI URL on completion.`);
17584
18495
  }
17585
- var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, SUMMARY_TAIL_CHARS, PROJECT_SLUG_RE;
18496
+ var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, PROJECT_SLUG_RE;
17586
18497
  var init_dispatch3 = __esm({
17587
18498
  "src/scan/dispatch.ts"() {
17588
18499
  "use strict";
17589
18500
  init_run_scan();
18501
+ init_scanOutcome();
17590
18502
  EX_OK = 0;
17591
18503
  EX_SIGNAL = 2;
17592
18504
  EX_INFRA = 3;
17593
18505
  EX_USAGE = 78;
17594
- SUMMARY_TAIL_CHARS = 2e3;
17595
18506
  PROJECT_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
17596
18507
  }
17597
18508
  });
17598
18509
 
17599
18510
  // src/init/repoSize.ts
17600
18511
  import { stat as stat9 } from "node:fs/promises";
17601
- import { extname, join as join17 } from "node:path";
18512
+ import { extname, join as join20 } from "node:path";
17602
18513
  async function measureRepo(repoPath) {
17603
18514
  const files = await gitListFiles(repoPath);
17604
18515
  if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
@@ -17607,7 +18518,7 @@ async function measureRepo(repoPath) {
17607
18518
  for (const rel of files) {
17608
18519
  if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
17609
18520
  try {
17610
- const s = await stat9(join17(repoPath, rel));
18521
+ const s = await stat9(join20(repoPath, rel));
17611
18522
  if (!s.isFile()) continue;
17612
18523
  sourceFiles++;
17613
18524
  sourceBytes += s.size;
@@ -17726,7 +18637,7 @@ async function watchRepoForProject(input) {
17726
18637
  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
18638
  };
17728
18639
  }
17729
- const { error } = await supabase.rpc("watch_repo", {
18640
+ const { data: watchedRepoId, error } = await supabase.rpc("watch_repo", {
17730
18641
  p_org_id: orgId,
17731
18642
  p_project_id: projectId,
17732
18643
  p_repo_owner: owner,
@@ -17737,6 +18648,17 @@ async function watchRepoForProject(input) {
17737
18648
  if (error) {
17738
18649
  return { watched: false, reason: `could not watch the repo (${error.message}).` };
17739
18650
  }
18651
+ const { error: enableErr } = await supabase.from("watched_repos").update({
18652
+ commit_update_enabled: true,
18653
+ commit_update_archme_branch: input.archmeBranch,
18654
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
18655
+ }).eq("id", watchedRepoId);
18656
+ if (enableErr) {
18657
+ return {
18658
+ watched: false,
18659
+ 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.`
18660
+ };
18661
+ }
17740
18662
  return { watched: true };
17741
18663
  }
17742
18664
  var init_watchRepo = __esm({
@@ -17746,21 +18668,21 @@ var init_watchRepo = __esm({
17746
18668
  });
17747
18669
 
17748
18670
  // src/init/gitContext.ts
17749
- async function git(cwd, args) {
18671
+ async function git2(cwd, args) {
17750
18672
  const { stdout } = await execa("git", args, { cwd });
17751
18673
  return stdout.trim();
17752
18674
  }
17753
18675
  async function resolveGitContext(repoPath) {
17754
18676
  let root;
17755
18677
  try {
17756
- root = await git(repoPath, ["rev-parse", "--show-toplevel"]);
18678
+ root = await git2(repoPath, ["rev-parse", "--show-toplevel"]);
17757
18679
  } catch {
17758
18680
  throw new Error(
17759
18681
  `\`${repoPath}\` is not inside a git repository \u2014 run \`archme init\` from a repo, or pass its path`
17760
18682
  );
17761
18683
  }
17762
18684
  try {
17763
- await git(root, ["rev-parse", "HEAD"]);
18685
+ await git2(root, ["rev-parse", "HEAD"]);
17764
18686
  } catch {
17765
18687
  throw new Error(
17766
18688
  `\`${root}\` is a git repository with no commits yet \u2014 make a commit first, then run \`archme init\``
@@ -17768,26 +18690,29 @@ async function resolveGitContext(repoPath) {
17768
18690
  }
17769
18691
  let branch = "";
17770
18692
  try {
17771
- branch = await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
18693
+ branch = await git2(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
17772
18694
  } catch {
17773
18695
  branch = "";
17774
18696
  }
17775
18697
  if (!branch || branch === "HEAD") {
17776
18698
  try {
17777
- const ref = await git(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
18699
+ const ref = await git2(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
17778
18700
  branch = ref.replace(/^refs\/remotes\/origin\//, "");
17779
18701
  } catch {
17780
18702
  branch = "main";
17781
18703
  }
17782
18704
  }
17783
18705
  let repoSlug = null;
18706
+ let originUrl = null;
17784
18707
  try {
17785
- const url = await git(root, ["remote", "get-url", "origin"]);
18708
+ const url = await git2(root, ["remote", "get-url", "origin"]);
18709
+ originUrl = url || null;
17786
18710
  repoSlug = url ? parseGitHubRepo(url) : null;
17787
18711
  } catch {
17788
18712
  repoSlug = null;
18713
+ originUrl = null;
17789
18714
  }
17790
- return { root, branch, repoSlug };
18715
+ return { root, branch, repoSlug, originUrl };
17791
18716
  }
17792
18717
  var init_gitContext = __esm({
17793
18718
  "src/init/gitContext.ts"() {
@@ -17797,6 +18722,654 @@ var init_gitContext = __esm({
17797
18722
  }
17798
18723
  });
17799
18724
 
18725
+ // src/init/keepCurrent.ts
18726
+ async function chooseKeepCurrent(deps) {
18727
+ if (deps.yes) {
18728
+ return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
18729
+ }
18730
+ const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
18731
+ "How should ArchMe detect new commits?",
18732
+ DETECTION_OPTIONS
18733
+ );
18734
+ const runner = await deps.choose(
18735
+ "Something has to process each update. Who runs it?",
18736
+ RUNNER_OPTIONS
18737
+ );
18738
+ return { detection, runner };
18739
+ }
18740
+ function formatKeepCurrentPlan(plan, ctx) {
18741
+ const steps = [];
18742
+ if (plan.detection === "local-watch" && !ctx.localWatchRegistered) {
18743
+ steps.push({
18744
+ title: "Register this repo so your machine watches it:",
18745
+ command: "archme local-watch"
18746
+ });
18747
+ }
18748
+ if (plan.runner === "self") {
18749
+ steps.push({
18750
+ title: "Start the daemon \u2014 nothing processes the queue until you do:",
18751
+ command: "archme daemon start",
18752
+ notes: [
18753
+ "Keep it running on a machine that stays on. Each update spends tokens",
18754
+ "from your Claude subscription, the same way this scan did. Your code is",
18755
+ "not sent to our servers \u2014 only the resulting architecture."
18756
+ ]
18757
+ });
18758
+ } else {
18759
+ steps.push({
18760
+ title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
18761
+ notes: [
18762
+ ctx.repoSlug ? `Mention ${ctx.repoSlug} so we know which repo you mean.` : "",
18763
+ // Deliberately not "we'll be in touch": running this command notifies
18764
+ // nobody. Promising contact that depends on the user sending the mail
18765
+ // is how someone ends up waiting on a team that never heard.
18766
+ "Until then this architecture stays as it is \u2014 it won't track new commits."
18767
+ ].filter(Boolean)
18768
+ });
18769
+ }
18770
+ const lines = ["", "Keeping it current:"];
18771
+ if (ctx.localWatchRegistered) {
18772
+ lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
18773
+ }
18774
+ const numbered = steps.length > 1;
18775
+ steps.forEach((step, i2) => {
18776
+ const bullet = numbered ? ` ${i2 + 1}. ` : " ";
18777
+ const indent = " ".repeat(bullet.length);
18778
+ lines.push("", `${bullet}${step.title}`);
18779
+ if (step.command) lines.push("", `${indent} ${step.command}`);
18780
+ if (step.notes?.length) {
18781
+ lines.push("");
18782
+ for (const note of step.notes) lines.push(`${indent}${note}`);
18783
+ }
18784
+ });
18785
+ if (ctx.boardUrl) lines.push("", ` Your board: ${ctx.boardUrl}`);
18786
+ return lines.join("\n");
18787
+ }
18788
+ var CONTACT_EMAIL, DETECTION_OPTIONS, RUNNER_OPTIONS;
18789
+ var init_keepCurrent = __esm({
18790
+ "src/init/keepCurrent.ts"() {
18791
+ "use strict";
18792
+ CONTACT_EMAIL = "signup@archme.io";
18793
+ DETECTION_OPTIONS = [
18794
+ {
18795
+ // First, and named as recommended: it is less work per-commit, survives
18796
+ // this laptop being closed, and is the only path that sees pull requests.
18797
+ 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.",
18798
+ value: "github-app"
18799
+ },
18800
+ {
18801
+ label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. No pull-request awareness.",
18802
+ value: "local-watch"
18803
+ }
18804
+ ];
18805
+ RUNNER_OPTIONS = [
18806
+ {
18807
+ 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.",
18808
+ value: "self"
18809
+ },
18810
+ {
18811
+ label: "ArchMe runs it \u2014 if you don't have a machine that's always on, or a Claude subscription to spend on it.",
18812
+ value: "archme"
18813
+ }
18814
+ ];
18815
+ }
18816
+ });
18817
+
18818
+ // src/init/keepCurrentFlow.ts
18819
+ async function applyKeepCurrentPlan(plan, deps, alreadyWatched) {
18820
+ const base = { detection: plan.detection, runner: plan.runner };
18821
+ if (plan.detection === "github-app") {
18822
+ if (alreadyWatched) {
18823
+ return { ...base, wired: true, localWatchRegistered: false };
18824
+ }
18825
+ const url = await deps.startGitHubAppInstall();
18826
+ deps.log("");
18827
+ deps.log(" Install the ArchMe GitHub App here, then come back \u2014 I'll wait:");
18828
+ deps.log("");
18829
+ deps.log(` ${url}`);
18830
+ deps.log("");
18831
+ const installed = await deps.waitForInstallation();
18832
+ if (!installed) {
18833
+ return {
18834
+ ...base,
18835
+ wired: false,
18836
+ localWatchRegistered: false,
18837
+ 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."
18838
+ };
18839
+ }
18840
+ const watched = await deps.watchRepo();
18841
+ if (!watched.watched) {
18842
+ return {
18843
+ ...base,
18844
+ wired: false,
18845
+ localWatchRegistered: false,
18846
+ reason: watched.reason ?? "the repo could not be linked to this project."
18847
+ };
18848
+ }
18849
+ deps.log(" \u2713 GitHub App installed and the repo is linked.");
18850
+ return { ...base, wired: true, localWatchRegistered: false };
18851
+ }
18852
+ const registered = await deps.registerLocalWatch();
18853
+ if (!registered.ok) {
18854
+ return {
18855
+ ...base,
18856
+ wired: false,
18857
+ localWatchRegistered: false,
18858
+ reason: registered.reason ?? "the repo could not be registered for local polling."
18859
+ };
18860
+ }
18861
+ return { ...base, wired: true, localWatchRegistered: true };
18862
+ }
18863
+ var init_keepCurrentFlow = __esm({
18864
+ "src/init/keepCurrentFlow.ts"() {
18865
+ "use strict";
18866
+ }
18867
+ });
18868
+
18869
+ // src/init/githubAppSetup.ts
18870
+ async function waitForInstallation(deps) {
18871
+ const deadline = deps.now() + INSTALL_WAIT_TIMEOUT_MS;
18872
+ let announced = false;
18873
+ for (; ; ) {
18874
+ try {
18875
+ if (await deps.hasInstallation()) return true;
18876
+ } catch {
18877
+ }
18878
+ if (deps.now() >= deadline) return false;
18879
+ if (!announced) {
18880
+ deps.log(" waiting for the installation to come through\u2026");
18881
+ announced = true;
18882
+ }
18883
+ await deps.sleep(INSTALL_POLL_INTERVAL_MS);
18884
+ }
18885
+ }
18886
+ async function buildInstallUrl(opts) {
18887
+ const doFetch = opts.fetchImpl ?? fetch;
18888
+ const res = await doFetch(`${opts.supabaseUrl}/functions/v1/github/install-state`, {
18889
+ method: "POST",
18890
+ headers: {
18891
+ "Content-Type": "application/json",
18892
+ Authorization: `Bearer ${opts.accessToken}`
18893
+ },
18894
+ body: JSON.stringify({ orgId: opts.orgId, env: opts.env ?? "prod" })
18895
+ });
18896
+ if (!res.ok) {
18897
+ const body = await res.json().catch(() => ({}));
18898
+ throw new Error(body.error ?? `could not start the GitHub App install (${res.status})`);
18899
+ }
18900
+ const { token, url } = await res.json();
18901
+ if (url) return url;
18902
+ if (!token) throw new Error("install-state returned neither a url nor a token");
18903
+ const slug = process.env.ARCHME_GITHUB_APP_SLUG || "architectme";
18904
+ return `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(token)}`;
18905
+ }
18906
+ async function orgHasInstallation(supabase, orgId) {
18907
+ const { data, error } = await supabase.from("org_github_installations").select("installation_id").eq("org_id", orgId).limit(1);
18908
+ if (error) throw new Error(error.message);
18909
+ return (data?.length ?? 0) > 0;
18910
+ }
18911
+ var INSTALL_WAIT_TIMEOUT_MS, INSTALL_POLL_INTERVAL_MS;
18912
+ var init_githubAppSetup = __esm({
18913
+ "src/init/githubAppSetup.ts"() {
18914
+ "use strict";
18915
+ INSTALL_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
18916
+ INSTALL_POLL_INTERVAL_MS = 3e3;
18917
+ }
18918
+ });
18919
+
18920
+ // src/daemon/prompt.ts
18921
+ import { createInterface } from "node:readline/promises";
18922
+ async function withReadline(fn) {
18923
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18924
+ try {
18925
+ return await fn(rl);
18926
+ } finally {
18927
+ rl.close();
18928
+ }
18929
+ }
18930
+ async function promptLine(question) {
18931
+ return await withReadline(async (rl) => {
18932
+ try {
18933
+ return await rl.question(`${question}: `);
18934
+ } catch {
18935
+ return "";
18936
+ }
18937
+ });
18938
+ }
18939
+ async function promptChoice(question, options) {
18940
+ return await withReadline(async (rl) => {
18941
+ console.log(`
18942
+ ${question}
18943
+ `);
18944
+ options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
18945
+ `));
18946
+ for (let attempt = 0; attempt < 3; attempt++) {
18947
+ let answer;
18948
+ try {
18949
+ answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
18950
+ } catch {
18951
+ return options[0].value;
18952
+ }
18953
+ if (answer === "") return options[0].value;
18954
+ const n2 = Number.parseInt(answer, 10);
18955
+ if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
18956
+ console.log(` "${answer}" isn't one of the options.`);
18957
+ }
18958
+ return options[0].value;
18959
+ });
18960
+ }
18961
+ async function promptYesNo(question) {
18962
+ return await withReadline(async (rl) => {
18963
+ try {
18964
+ const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
18965
+ return answer === "" || answer === "y" || answer === "yes";
18966
+ } catch {
18967
+ return false;
18968
+ }
18969
+ });
18970
+ }
18971
+ var init_prompt = __esm({
18972
+ "src/daemon/prompt.ts"() {
18973
+ "use strict";
18974
+ }
18975
+ });
18976
+
18977
+ // src/daemon/watchReadiness.ts
18978
+ function evaluateWatchReadiness(input) {
18979
+ const { marker, repo, archmeBranch } = input;
18980
+ if (!marker.sha) {
18981
+ return {
18982
+ ok: false,
18983
+ reason: `${repo} has no architecture on \`${archmeBranch}\` yet, so there is nothing to keep up to date and no commit to diff against.
18984
+ Run \`archme init\` in this repo first \u2014 it creates the architecture, then comes back here.`
18985
+ };
18986
+ }
18987
+ if (!GIT_SHA_RE2.test(marker.sha)) {
18988
+ return {
18989
+ ok: false,
18990
+ 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.`
18991
+ };
18992
+ }
18993
+ if (!marker.repo) {
18994
+ return {
18995
+ ok: false,
18996
+ reason: `the architecture on \`${archmeBranch}\` has no source repo recorded, so updates cannot be matched to ${repo}.
18997
+ 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.`
18998
+ };
18999
+ }
19000
+ if (marker.repo !== repo) {
19001
+ return {
19002
+ ok: false,
19003
+ reason: `\`${archmeBranch}\` is already tracking ${marker.repo}, not ${repo}. Watching it from here would make every update fail on the mismatch.
19004
+ Use \`--project <org>/<slug>\` to pick a different project, or a different branch.`
19005
+ };
19006
+ }
19007
+ return { ok: true };
19008
+ }
19009
+ var GIT_SHA_RE2;
19010
+ var init_watchReadiness = __esm({
19011
+ "src/daemon/watchReadiness.ts"() {
19012
+ "use strict";
19013
+ GIT_SHA_RE2 = /^[0-9a-f]{40}$/;
19014
+ }
19015
+ });
19016
+
19017
+ // src/daemon/localWatchList.ts
19018
+ function formatWatchList(repos) {
19019
+ if (repos.length === 0) {
19020
+ return [
19021
+ "Not watching any repos on this machine.",
19022
+ "",
19023
+ " archme local-watch register the repo you're in"
19024
+ ].join("\n");
19025
+ }
19026
+ const lines = [
19027
+ `Watching ${repos.length} repo${repos.length === 1 ? "" : "s"} on this machine:`,
19028
+ ""
19029
+ ];
19030
+ for (const r of repos) {
19031
+ const slug = slugOf(r);
19032
+ lines.push(` ${slug}`);
19033
+ lines.push(` mirror: ${r.mirrorPath}`);
19034
+ if (r.haltedAt) {
19035
+ lines.push(` HALTED at ${r.haltedAt}: ${r.haltReason ?? "unknown reason"}`);
19036
+ lines.push(` no new commits are being picked up. Resume with:`);
19037
+ lines.push(` archme local-watch --resume ${slug}`);
19038
+ }
19039
+ lines.push("");
19040
+ }
19041
+ if (repos.every((r) => !r.haltedAt)) {
19042
+ lines.push("Updates only happen while the daemon is running: archme daemon status");
19043
+ }
19044
+ return lines.join("\n").trimEnd();
19045
+ }
19046
+ var init_localWatchList = __esm({
19047
+ "src/daemon/localWatchList.ts"() {
19048
+ "use strict";
19049
+ init_localRepos();
19050
+ }
19051
+ });
19052
+
19053
+ // src/daemon/localWatchResolve.ts
19054
+ function confirmationFor(target, repoPath) {
19055
+ return `Watch ${target.repo} (branch ${target.repoBranch}) for architecture updates?
19056
+ checkout: ${repoPath}`;
19057
+ }
19058
+ async function evaluate(path7, deps) {
19059
+ const inspected = await deps.inspect(path7);
19060
+ if (!inspected.ok) return { ok: false, reason: inspected.error };
19061
+ const resolved2 = resolveInitTarget({
19062
+ originUrl: inspected.originUrl,
19063
+ branchOverride: deps.branch,
19064
+ detectedDefaultBranch: inspected.detectedDefaultBranch
19065
+ });
19066
+ if (!resolved2.ok) return { ok: false, reason: resolved2.error };
19067
+ return {
19068
+ ok: true,
19069
+ // The ROOT, never the cwd — registering a subdirectory would mirror a
19070
+ // partial tree and record a source path that isn't the repo.
19071
+ repoPath: inspected.root,
19072
+ originUrl: inspected.originUrl,
19073
+ target: resolved2.target
19074
+ };
19075
+ }
19076
+ async function resolveRepoInteractive(deps) {
19077
+ const first = deps.explicitPath ?? deps.cwd;
19078
+ let evaluated = await evaluate(first, deps);
19079
+ if (deps.yes) {
19080
+ if (!evaluated.ok) return { ok: false, message: evaluated.reason };
19081
+ return evaluated;
19082
+ }
19083
+ if (evaluated.ok) {
19084
+ if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
19085
+ }
19086
+ let reason = evaluated.ok ? "Which repo should ArchMe watch? Enter its path" : `${evaluated.reason}
19087
+ Which repo should ArchMe watch? Enter its path`;
19088
+ for (let attempt = 0; attempt < MAX_REPO_PROMPTS; attempt++) {
19089
+ const answer = (await deps.ask(reason)).trim();
19090
+ if (!answer) return { ok: false, message: "cancelled \u2014 nothing was registered" };
19091
+ evaluated = await evaluate(answer, deps);
19092
+ if (evaluated.ok) {
19093
+ if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
19094
+ reason = "Which repo should ArchMe watch? Enter its path";
19095
+ continue;
19096
+ }
19097
+ reason = `${evaluated.reason}
19098
+ Which repo should ArchMe watch? Enter its path`;
19099
+ }
19100
+ return {
19101
+ ok: false,
19102
+ 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."
19103
+ };
19104
+ }
19105
+ var MAX_REPO_PROMPTS;
19106
+ var init_localWatchResolve = __esm({
19107
+ "src/daemon/localWatchResolve.ts"() {
19108
+ "use strict";
19109
+ init_localWatch();
19110
+ MAX_REPO_PROMPTS = 3;
19111
+ }
19112
+ });
19113
+
19114
+ // src/daemon/localWatch.ts
19115
+ var localWatch_exports = {};
19116
+ __export(localWatch_exports, {
19117
+ DEFAULT_CLONE_BASE_DIR: () => DEFAULT_CLONE_BASE_DIR2,
19118
+ inspectRepoPath: () => inspectRepoPath,
19119
+ mirrorPathFor: () => mirrorPathFor,
19120
+ registerLocalWatch: () => registerLocalWatch,
19121
+ resolveInitTarget: () => resolveInitTarget,
19122
+ runLocalWatch: () => runLocalWatch,
19123
+ runLocalWatchList: () => runLocalWatchList,
19124
+ runLocalWatchResume: () => runLocalWatchResume,
19125
+ runLocalWatchStop: () => runLocalWatchStop
19126
+ });
19127
+ import { homedir as homedir6 } from "node:os";
19128
+ import { join as join21, resolve as resolve3 } from "node:path";
19129
+ import { stat as stat10 } from "node:fs/promises";
19130
+ function mirrorPathFor(cloneBaseDir, owner, name) {
19131
+ return join21(cloneBaseDir, owner, name);
19132
+ }
19133
+ function resolveInitTarget(input) {
19134
+ if (!input.originUrl) {
19135
+ return {
19136
+ ok: false,
19137
+ error: "no `origin` remote found \u2014 archme local-watch needs a clone with a GitHub origin"
19138
+ };
19139
+ }
19140
+ const repo = parseGitHubRepo(input.originUrl);
19141
+ if (!repo) {
19142
+ return {
19143
+ ok: false,
19144
+ error: `origin ${input.originUrl} is not a github.com remote.
19145
+ 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.`
19146
+ };
19147
+ }
19148
+ const repoBranch = input.branchOverride ?? input.detectedDefaultBranch;
19149
+ if (!repoBranch) {
19150
+ return {
19151
+ ok: false,
19152
+ error: "could not determine the default branch \u2014 pass --branch <name>"
19153
+ };
19154
+ }
19155
+ const [repoOwner, repoName] = repo.split("/");
19156
+ return {
19157
+ ok: true,
19158
+ target: {
19159
+ repo,
19160
+ repoOwner,
19161
+ repoName,
19162
+ repoBranch,
19163
+ // Scanned projects live on a `<default>-scan` companion branch; the poller
19164
+ // must enqueue against the SAME branch the scan stamped the marker on.
19165
+ archmeBranch: input.archmeBranchOverride ?? normalizeScanBranchName(repoBranch)
19166
+ }
19167
+ };
19168
+ }
19169
+ async function inspectRepoPath(path7) {
19170
+ const abs = resolve3(path7);
19171
+ const st = await stat10(abs).catch(() => null);
19172
+ if (!st?.isDirectory()) return { ok: false, error: `${abs} is not a directory` };
19173
+ const toplevel = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: abs, reject: false });
19174
+ if (toplevel.exitCode !== 0) return { ok: false, error: `${abs} is not a git repository` };
19175
+ const root = toplevel.stdout.trim() || abs;
19176
+ return {
19177
+ ok: true,
19178
+ root,
19179
+ originUrl: await readOriginUrl(root),
19180
+ detectedDefaultBranch: await detectDefaultBranch(root)
19181
+ };
19182
+ }
19183
+ async function runLocalWatch(opts) {
19184
+ const log = opts.log ?? ((m) => console.log(m));
19185
+ const cloneBaseDir = opts.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
19186
+ const resolved2 = await resolveRepoInteractive({
19187
+ cwd: process.cwd(),
19188
+ explicitPath: opts.repoPath,
19189
+ branch: opts.branch,
19190
+ yes: opts.yes ?? false,
19191
+ inspect: inspectRepoPath,
19192
+ ask: promptLine,
19193
+ confirm: promptYesNo,
19194
+ log
19195
+ });
19196
+ if (!resolved2.ok) {
19197
+ console.error(`archme local-watch: ${resolved2.message}`);
19198
+ return 78;
19199
+ }
19200
+ const { repoPath, originUrl, target } = resolved2;
19201
+ log(` repo: ${target.repo}`);
19202
+ log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
19203
+ const cfg = resolveConfig();
19204
+ let authed;
19205
+ try {
19206
+ authed = await createAuthedClient({
19207
+ supabaseUrl: cfg.supabaseUrl,
19208
+ supabaseAnonKey: cfg.supabaseAnonKey,
19209
+ clientId: cfg.archmeDaemonClientId,
19210
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
19211
+ });
19212
+ } catch (err2) {
19213
+ console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
19214
+ return 3;
19215
+ }
19216
+ const { supabase, userId } = authed;
19217
+ const orgId = authed.orgId;
19218
+ if (!orgId) {
19219
+ console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
19220
+ return 3;
19221
+ }
19222
+ const orgSlug = await resolveCurrentOrgSlug(supabase, orgId);
19223
+ if (!opts.project && !orgSlug) {
19224
+ console.error(
19225
+ "archme local-watch: could not resolve your organization slug, so the project name can't be derived \u2014 pass --project <org/slug>"
19226
+ );
19227
+ return 3;
19228
+ }
19229
+ const fullSlug = opts.project ?? defaultProjectSlug(orgSlug, repoPath);
19230
+ const project = await resolveOrCreateProject(supabase, userId, fullSlug);
19231
+ log(` project: ${project.orgSlug}/${project.projectSlug}${project.created ? " (created)" : ""}`);
19232
+ const registered = await registerLocalWatch({
19233
+ supabase,
19234
+ orgId,
19235
+ projectId: project.projectId,
19236
+ repoPath,
19237
+ originUrl,
19238
+ target,
19239
+ cloneBaseDir,
19240
+ log
19241
+ });
19242
+ if (!registered.ok) {
19243
+ console.error(`archme local-watch: ${registered.reason}`);
19244
+ return registered.exitCode;
19245
+ }
19246
+ log(`
19247
+ \u2713 ${target.repo} is now watched locally.`);
19248
+ log(`
19249
+ Start the daemon if it isn't running: archme daemon start`);
19250
+ log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
19251
+ return 0;
19252
+ }
19253
+ async function registerLocalWatch(args) {
19254
+ const log = args.log ?? (() => {
19255
+ });
19256
+ const { target, supabase, orgId, projectId, repoPath, originUrl } = args;
19257
+ const cloneBaseDir = args.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
19258
+ const marker = await readBranchMarker(supabase, projectId, target.archmeBranch);
19259
+ const readiness = evaluateWatchReadiness({
19260
+ repo: target.repo,
19261
+ archmeBranch: target.archmeBranch,
19262
+ marker
19263
+ });
19264
+ if (!readiness.ok) return { ok: false, reason: readiness.reason, exitCode: 78 };
19265
+ const { data: watchedRepoId, error: watchErr } = await supabase.rpc("watch_local_repo", {
19266
+ p_org_id: orgId,
19267
+ p_project_id: projectId,
19268
+ p_repo_owner: target.repoOwner,
19269
+ p_repo_name: target.repoName,
19270
+ p_branch: target.repoBranch
19271
+ });
19272
+ if (watchErr || typeof watchedRepoId !== "string") {
19273
+ return {
19274
+ ok: false,
19275
+ reason: `could not register the repo: ${watchErr?.message ?? "no id returned"}`,
19276
+ exitCode: 3
19277
+ };
19278
+ }
19279
+ const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
19280
+ const mirrorExists = await stat10(join21(mirrorPath, ".git")).then(() => true).catch(() => false);
19281
+ if (mirrorExists) {
19282
+ log(` mirror: ${mirrorPath} (reusing)`);
19283
+ await setOriginUrl(mirrorPath, originUrl);
19284
+ } else {
19285
+ log(` mirror: ${mirrorPath}`);
19286
+ await createMirrorClone(repoPath, mirrorPath, originUrl);
19287
+ }
19288
+ await execa("git", ["fetch", "--no-tags", "origin", target.repoBranch], {
19289
+ cwd: mirrorPath,
19290
+ reject: true
19291
+ });
19292
+ const entry = {
19293
+ watchedRepoId,
19294
+ mirrorPath,
19295
+ sourcePath: repoPath,
19296
+ repoOwner: target.repoOwner,
19297
+ repoName: target.repoName
19298
+ };
19299
+ await registerLocalRepo(entry);
19300
+ return { ok: true, entry };
19301
+ }
19302
+ async function runLocalWatchList() {
19303
+ console.log(formatWatchList(await loadLocalRepos()));
19304
+ return 0;
19305
+ }
19306
+ async function runLocalWatchStop(slug) {
19307
+ const repos = await loadLocalRepos();
19308
+ const matches = findLocalReposBySlug(repos, slug);
19309
+ if (matches.length === 0) {
19310
+ console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
19311
+ console.error("Run `archme local-watch --list` to see what is.");
19312
+ return 78;
19313
+ }
19314
+ await saveLocalRepos(removeLocalReposBySlug(repos, slug));
19315
+ for (const m of matches) {
19316
+ console.log(`Stopped watching ${slugOf(m)}.`);
19317
+ }
19318
+ console.log(
19319
+ `
19320
+ The architecture already on the board is untouched \u2014 this only stops new commits
19321
+ from being picked up. Re-watch with: archme local-watch ${matches[0].sourcePath}`
19322
+ );
19323
+ return 0;
19324
+ }
19325
+ async function runLocalWatchResume(slug) {
19326
+ const repos = await loadLocalRepos();
19327
+ const matches = findLocalReposBySlug(repos, slug);
19328
+ if (matches.length === 0) {
19329
+ console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
19330
+ return 78;
19331
+ }
19332
+ const halted = matches.filter((m) => m.haltedAt);
19333
+ if (halted.length === 0) {
19334
+ console.log(`${slug} is not halted \u2014 nothing to resume.`);
19335
+ return 0;
19336
+ }
19337
+ let next = repos;
19338
+ for (const m of halted) next = clearHalt(next, m.watchedRepoId);
19339
+ await saveLocalRepos(next);
19340
+ for (const m of halted) {
19341
+ console.log(`Resumed ${slugOf(m)} \u2014 was: ${m.haltReason ?? "unknown reason"}`);
19342
+ }
19343
+ const stillTerminal = halted.some((m) => m.haltCode === "job-terminal");
19344
+ if (stillTerminal) {
19345
+ console.log(
19346
+ "\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."
19347
+ );
19348
+ } else {
19349
+ console.log("\nPolling resumes from the recorded commit on the next tick.");
19350
+ }
19351
+ return 0;
19352
+ }
19353
+ var DEFAULT_CLONE_BASE_DIR2;
19354
+ var init_localWatch = __esm({
19355
+ "src/daemon/localWatch.ts"() {
19356
+ "use strict";
19357
+ init_execa();
19358
+ init_defaults();
19359
+ init_github_remote();
19360
+ init_run_scan();
19361
+ init_authedClient();
19362
+ init_watchReadiness();
19363
+ init_localGit();
19364
+ init_localRepos();
19365
+ init_branchMarker();
19366
+ init_localWatchList();
19367
+ init_localWatchResolve();
19368
+ init_prompt();
19369
+ DEFAULT_CLONE_BASE_DIR2 = join21(homedir6(), ".archme-job-runner", "clones");
19370
+ }
19371
+ });
19372
+
17800
19373
  // src/init/plan.ts
17801
19374
  function shouldRunScan(d) {
17802
19375
  if (d.skipScan) return false;
@@ -17826,9 +19399,9 @@ var init_plan = __esm({
17826
19399
  });
17827
19400
 
17828
19401
  // src/init/run-init.ts
17829
- import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile11 } from "node:fs/promises";
19402
+ import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile12 } from "node:fs/promises";
17830
19403
  import { tmpdir as tmpdir4 } from "node:os";
17831
- import { join as join18 } from "node:path";
19404
+ import { join as join22 } from "node:path";
17832
19405
  function initScanBranch(defaultBranch) {
17833
19406
  return normalizeScanBranchName(defaultBranch);
17834
19407
  }
@@ -17840,10 +19413,10 @@ async function defaultFetchInstallScript(url) {
17840
19413
  return await resp.text();
17841
19414
  }
17842
19415
  async function defaultRunInstallScript(script, cwd) {
17843
- const dir = await mkdtemp4(join18(tmpdir4(), "archme-init-"));
17844
- const scriptPath = join18(dir, "install-agent.sh");
19416
+ const dir = await mkdtemp4(join22(tmpdir4(), "archme-init-"));
19417
+ const scriptPath = join22(dir, "install-agent.sh");
17845
19418
  try {
17846
- await writeFile11(scriptPath, script, { mode: 448 });
19419
+ await writeFile12(scriptPath, script, { mode: 448 });
17847
19420
  await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
17848
19421
  } finally {
17849
19422
  await rm8(dir, { recursive: true, force: true });
@@ -17864,8 +19437,8 @@ async function runInit(opts, deps = {}) {
17864
19437
  const measure = deps.measure ?? measureRepo;
17865
19438
  const watchRepo = deps.watchRepo ?? watchRepoForProject;
17866
19439
  const cfg = resolveConfig();
17867
- const git2 = await gitContext(opts.repoPath);
17868
- const repoPath = git2.root;
19440
+ const git4 = await gitContext(opts.repoPath);
19441
+ const repoPath = git4.root;
17869
19442
  const { credentials } = await loadCreds(cfg.supabaseUrl);
17870
19443
  const signedIn = credentials.length > 0;
17871
19444
  if (!signedIn) {
@@ -17924,8 +19497,9 @@ async function runInit(opts, deps = {}) {
17924
19497
  supabase: authed.supabase,
17925
19498
  orgId: authed.orgId,
17926
19499
  projectId: project.projectId,
17927
- repoSlug: git2.repoSlug,
17928
- branch: git2.branch,
19500
+ repoSlug: git4.repoSlug,
19501
+ branch: git4.branch,
19502
+ archmeBranch: scanBranch,
17929
19503
  log
17930
19504
  });
17931
19505
  if (!watched.watched && watched.reason) {
@@ -17975,28 +19549,79 @@ Your board: ${webUrl}`);
17975
19549
  log(`
17976
19550
  \u2713 Done. Your architecture: ${finalUrl}`);
17977
19551
  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 {
19552
+ const plan = await chooseKeepCurrent({
19553
+ alreadyWatched: watched.watched,
19554
+ yes: Boolean(opts.yes),
19555
+ repoSlug: git4.repoSlug,
19556
+ choose: deps.choose ?? promptChoice,
19557
+ log
19558
+ });
19559
+ const outcome = await applyKeepCurrentPlan(
19560
+ plan,
19561
+ {
19562
+ startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
19563
+ supabaseUrl: cfg.supabaseUrl,
19564
+ accessToken: getCurrentAccessToken() ?? "",
19565
+ orgId: authed.orgId
19566
+ }),
19567
+ waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
19568
+ hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
19569
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
19570
+ now: () => Date.now(),
19571
+ log
19572
+ }),
19573
+ watchRepo: async () => {
19574
+ const w = await watchRepo({
19575
+ supabase: authed.supabase,
19576
+ orgId: authed.orgId,
19577
+ projectId: project.projectId,
19578
+ repoSlug: git4.repoSlug,
19579
+ branch: git4.branch,
19580
+ archmeBranch: scanBranch,
19581
+ log
19582
+ });
19583
+ return { watched: w.watched, reason: w.reason };
19584
+ },
19585
+ registerLocalWatch: async () => {
19586
+ const target = resolveInitTarget({
19587
+ originUrl: git4.originUrl,
19588
+ detectedDefaultBranch: git4.branch,
19589
+ // The branch this run actually scanned. Deriving it from the git
19590
+ // branch instead would point the poller at `<checkout>-scan` while
19591
+ // the marker sits on `<default>-scan` — so registering would fail
19592
+ // on a feature-branch checkout, immediately after a scan that
19593
+ // succeeded.
19594
+ archmeBranchOverride: scanBranch
19595
+ });
19596
+ if (!target.ok) return { ok: false, reason: target.error };
19597
+ const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
19598
+ supabase: authed.supabase,
19599
+ orgId: authed.orgId,
19600
+ projectId: project.projectId,
19601
+ repoPath: git4.root,
19602
+ originUrl: git4.originUrl,
19603
+ target: target.target,
19604
+ log
19605
+ });
19606
+ return res.ok ? { ok: true } : { ok: false, reason: res.reason };
19607
+ },
19608
+ log
19609
+ },
19610
+ watched.watched
19611
+ );
19612
+ if (!outcome.wired && outcome.reason) {
17994
19613
  log("");
17995
19614
  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.");
19615
+ log(` ${outcome.reason}`);
19616
+ log(" Until that is resolved, this architecture will not track new commits.");
17999
19617
  }
19618
+ log(
19619
+ formatKeepCurrentPlan(plan, {
19620
+ repoSlug: git4.repoSlug,
19621
+ boardUrl: finalUrl,
19622
+ localWatchRegistered: outcome.localWatchRegistered
19623
+ })
19624
+ );
18000
19625
  }
18001
19626
  return {
18002
19627
  exitCode: result.exitCode,
@@ -18020,6 +19645,12 @@ var init_run_init = __esm({
18020
19645
  init_watchRepo();
18021
19646
  init_gitContext();
18022
19647
  init_web_url();
19648
+ init_bearer();
19649
+ init_keepCurrent();
19650
+ init_keepCurrentFlow();
19651
+ init_githubAppSetup();
19652
+ init_prompt();
19653
+ init_localWatch();
18023
19654
  init_plan();
18024
19655
  }
18025
19656
  });
@@ -18111,7 +19742,7 @@ __export(dispatch_exports4, {
18111
19742
  dispatchInit: () => dispatchInit,
18112
19743
  printInitHelp: () => printInitHelp
18113
19744
  });
18114
- import { resolve as resolve3 } from "node:path";
19745
+ import { resolve as resolve4 } from "node:path";
18115
19746
  function printInitHelp() {
18116
19747
  console.log(`archme init \u2014 guided first-time setup for this repo
18117
19748
 
@@ -18149,7 +19780,7 @@ async function dispatchInit(argv) {
18149
19780
  }
18150
19781
  try {
18151
19782
  const result = await runInit({
18152
- repoPath: resolve3(parsed.args.repoPath ?? process.cwd()),
19783
+ repoPath: resolve4(parsed.args.repoPath ?? process.cwd()),
18153
19784
  project: parsed.args.project,
18154
19785
  appOrigin: parsed.args.appOrigin,
18155
19786
  force: parsed.args.force,
@@ -18157,8 +19788,8 @@ async function dispatchInit(argv) {
18157
19788
  yes: parsed.args.yes || parsed.args.noInteractive
18158
19789
  });
18159
19790
  return result.exitCode;
18160
- } catch (err) {
18161
- console.error(`archme init: ${err instanceof Error ? err.message : String(err)}`);
19791
+ } catch (err2) {
19792
+ console.error(`archme init: ${err2 instanceof Error ? err2.message : String(err2)}`);
18162
19793
  return EX_INFRA2;
18163
19794
  }
18164
19795
  }
@@ -18174,6 +19805,620 @@ var init_dispatch4 = __esm({
18174
19805
  }
18175
19806
  });
18176
19807
 
19808
+ // src/daemon/localWatchDispatch.ts
19809
+ var localWatchDispatch_exports = {};
19810
+ __export(localWatchDispatch_exports, {
19811
+ EX_INFRA: () => EX_INFRA3,
19812
+ EX_OK: () => EX_OK3,
19813
+ EX_USAGE: () => EX_USAGE3,
19814
+ dispatchLocalWatch: () => dispatchLocalWatch,
19815
+ parseLocalWatchArgs: () => parseLocalWatchArgs,
19816
+ printLocalWatchHelp: () => printLocalWatchHelp
19817
+ });
19818
+ function err(message) {
19819
+ return { kind: "error", message: `archme local-watch: ${message}` };
19820
+ }
19821
+ function parseLocalWatchArgs(argv) {
19822
+ if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
19823
+ let repoPath;
19824
+ let project;
19825
+ let branch;
19826
+ let yes = false;
19827
+ let list = false;
19828
+ let stop;
19829
+ let resume;
19830
+ for (let i2 = 0; i2 < argv.length; i2++) {
19831
+ const arg = argv[i2];
19832
+ const next = argv[i2 + 1];
19833
+ if (arg === "--project") {
19834
+ if (!next) return err("--project needs an org/slug");
19835
+ if (!PROJECT_SLUG_RE3.test(next)) {
19836
+ return err(
19837
+ `--project '${next}' must be \`org/slug\` (exactly two non-empty segments separated by /)`
19838
+ );
19839
+ }
19840
+ project = next;
19841
+ i2++;
19842
+ } else if (arg === "--branch") {
19843
+ if (!next) return err("--branch needs a branch name");
19844
+ branch = next;
19845
+ i2++;
19846
+ } else if (arg === "--yes" || arg === "--no-interactive") {
19847
+ yes = true;
19848
+ } else if (arg === "--list") {
19849
+ list = true;
19850
+ } else if (arg === "--stop" || arg === "--resume") {
19851
+ if (!next || next.startsWith("-")) return err(`${arg} needs a repo (owner/name)`);
19852
+ if (!REPO_SLUG_RE.test(next)) {
19853
+ return err(`${arg} '${next}' must be \`owner/name\` \u2014 that is how repos are registered`);
19854
+ }
19855
+ if (arg === "--stop") stop = next;
19856
+ else resume = next;
19857
+ i2++;
19858
+ } else if (arg && arg.startsWith("-")) {
19859
+ return err(`unknown flag '${arg}'`);
19860
+ } else if (arg && repoPath === void 0) {
19861
+ repoPath = arg;
19862
+ } else if (arg) {
19863
+ return err(`unexpected argument '${arg}'`);
19864
+ }
19865
+ }
19866
+ const modes = [list && "--list", stop && "--stop", resume && "--resume"].filter(Boolean);
19867
+ if (modes.length > 1) {
19868
+ return err(`${modes.join(" and ")} do different things \u2014 pass one of them at once, not both`);
19869
+ }
19870
+ if (list) return { kind: "list" };
19871
+ if (stop) return { kind: "stop", repo: stop };
19872
+ if (resume) return { kind: "resume", repo: resume };
19873
+ return { kind: "register", repoPath, project, branch, yes };
19874
+ }
19875
+ function printLocalWatchHelp() {
19876
+ console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
19877
+
19878
+ Registers a repo so this machine polls \`origin\` and feeds new commits into
19879
+ ArchMe, one commit at a time. No GitHub App, no admin approval \u2014 but the
19880
+ updates only happen while your daemon is running. Requires a github.com origin.
19881
+
19882
+ Your working checkout is read exactly once (to find its origin URL and seed a
19883
+ mirror); everything afterwards runs against a daemon-owned copy, so ArchMe
19884
+ never touches your working tree.
19885
+
19886
+ Usage:
19887
+ archme local-watch [repo-path] Register a repo (defaults to the one you're in)
19888
+ archme local-watch --list Show what this machine is watching
19889
+ archme local-watch --stop <owner/name>
19890
+ archme local-watch --resume <owner/name>
19891
+
19892
+ Options:
19893
+ --project <org/slug> Project to write to (default: derived from the repo name)
19894
+ --branch <name> Branch to track (default: the repo's default branch)
19895
+ --yes Don't ask for confirmation (needs an explicit repo-path)
19896
+
19897
+ Watching queues work; the daemon does it. After registering: archme daemon start`);
19898
+ }
19899
+ async function dispatchLocalWatch(argv) {
19900
+ const parsed = parseLocalWatchArgs(argv);
19901
+ if (parsed.kind === "help") {
19902
+ printLocalWatchHelp();
19903
+ return EX_OK3;
19904
+ }
19905
+ if (parsed.kind === "error") {
19906
+ console.error(parsed.message);
19907
+ printLocalWatchHelp();
19908
+ return EX_USAGE3;
19909
+ }
19910
+ const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
19911
+ switch (parsed.kind) {
19912
+ case "list":
19913
+ return await mod.runLocalWatchList();
19914
+ case "stop":
19915
+ return await mod.runLocalWatchStop(parsed.repo);
19916
+ case "resume":
19917
+ return await mod.runLocalWatchResume(parsed.repo);
19918
+ case "register":
19919
+ return await mod.runLocalWatch(parsed);
19920
+ }
19921
+ }
19922
+ var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
19923
+ var init_localWatchDispatch = __esm({
19924
+ "src/daemon/localWatchDispatch.ts"() {
19925
+ "use strict";
19926
+ EX_OK3 = 0;
19927
+ EX_INFRA3 = 3;
19928
+ EX_USAGE3 = 78;
19929
+ PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
19930
+ REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
19931
+ }
19932
+ });
19933
+
19934
+ // src/doctor/collect.ts
19935
+ import { readFile as readFile13 } from "node:fs/promises";
19936
+ import { join as join23 } from "node:path";
19937
+ function binaryPresent(name) {
19938
+ try {
19939
+ requireBinary(name);
19940
+ return true;
19941
+ } catch {
19942
+ return false;
19943
+ }
19944
+ }
19945
+ async function git3(cwd, args) {
19946
+ const r = await execa("git", args, { cwd, reject: false });
19947
+ return r.exitCode === 0 ? r.stdout.trim() || null : null;
19948
+ }
19949
+ async function readMcpPin(repoRoot) {
19950
+ const empty = { installed: false, pin: { project: null, branch: null } };
19951
+ try {
19952
+ const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
19953
+ const parsed = JSON.parse(raw);
19954
+ const servers = parsed.mcpServers ?? {};
19955
+ const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
19956
+ if (!entry) return empty;
19957
+ const url = entry[1]?.url;
19958
+ if (!url) return { installed: true, pin: { project: null, branch: null } };
19959
+ const params = new URL(url).searchParams;
19960
+ return {
19961
+ installed: true,
19962
+ pin: { project: params.get("project"), branch: params.get("branch") }
19963
+ };
19964
+ } catch {
19965
+ return empty;
19966
+ }
19967
+ }
19968
+ async function isDaemonRunning() {
19969
+ const r = await execa("pgrep", ["-f", "archme.*daemon"], { reject: false });
19970
+ return r.exitCode === 0 && Boolean(r.stdout.trim());
19971
+ }
19972
+ async function collectFacts(cwd) {
19973
+ const facts = {
19974
+ cliVersion: await readDaemonVersion(),
19975
+ // `requireBinary` throws when absent; doctor's whole job is to report that
19976
+ // calmly rather than die on it.
19977
+ claudeInstalled: binaryPresent("claude"),
19978
+ gitInstalled: binaryPresent("git"),
19979
+ signedIn: false,
19980
+ identity: null,
19981
+ orgId: null,
19982
+ inRepo: false,
19983
+ repoRoot: null,
19984
+ repoSlug: null,
19985
+ gitBranch: null,
19986
+ agentConfigInstalled: false,
19987
+ pinnedProject: null,
19988
+ pinnedBranch: null,
19989
+ pinnedOrgVisible: null,
19990
+ projectExists: null,
19991
+ componentCount: null,
19992
+ markerSha: null,
19993
+ watchedBy: "unknown",
19994
+ localRepos: [],
19995
+ daemonRunning: await isDaemonRunning()
19996
+ };
19997
+ const root = facts.gitInstalled ? await git3(cwd, ["rev-parse", "--show-toplevel"]) : null;
19998
+ if (root) {
19999
+ facts.inRepo = true;
20000
+ facts.repoRoot = root;
20001
+ facts.gitBranch = await git3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
20002
+ const originUrl = await git3(root, ["remote", "get-url", "origin"]);
20003
+ facts.repoSlug = originUrl ? parseGitHubRepo(originUrl) : null;
20004
+ const mcp = await readMcpPin(root);
20005
+ facts.agentConfigInstalled = mcp.installed;
20006
+ facts.pinnedProject = mcp.pin.project;
20007
+ facts.pinnedBranch = mcp.pin.branch;
20008
+ }
20009
+ const registry = await loadLocalRepos();
20010
+ const forThisRepo = facts.repoSlug ? registry.filter((r) => slugOf(r).toLowerCase() === facts.repoSlug.toLowerCase()) : [];
20011
+ facts.localRepos = forThisRepo.map(
20012
+ (r) => ({
20013
+ slug: slugOf(r),
20014
+ halted: Boolean(r.haltedAt),
20015
+ haltReason: r.haltReason
20016
+ })
20017
+ );
20018
+ try {
20019
+ const cfg = resolveConfig();
20020
+ const authed = await createAuthedClient({
20021
+ supabaseUrl: cfg.supabaseUrl,
20022
+ supabaseAnonKey: cfg.supabaseAnonKey,
20023
+ clientId: cfg.archmeDaemonClientId,
20024
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20025
+ });
20026
+ if (!authed.orgId) return facts;
20027
+ facts.signedIn = true;
20028
+ facts.orgId = authed.orgId;
20029
+ facts.identity = authed.identity;
20030
+ const supabase = authed.supabase;
20031
+ const slug = facts.pinnedProject;
20032
+ if (slug) {
20033
+ const [orgSlug, projectSlug] = slug.split("/");
20034
+ const { data: org } = await supabase.from("organizations").select("id").eq("slug", orgSlug ?? "").maybeSingle();
20035
+ facts.pinnedOrgVisible = Boolean(org);
20036
+ 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 };
20037
+ if (!org) {
20038
+ facts.projectExists = null;
20039
+ } else if (!project) {
20040
+ facts.projectExists = false;
20041
+ } else {
20042
+ facts.projectExists = true;
20043
+ const branch = facts.pinnedBranch ?? "main";
20044
+ const { count: count2 } = await supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("branch", branch);
20045
+ facts.componentCount = count2 ?? null;
20046
+ const marker = await readBranchMarker(supabase, project.id, branch);
20047
+ facts.markerSha = marker.sha;
20048
+ }
20049
+ }
20050
+ if (facts.localRepos.length > 0) {
20051
+ facts.watchedBy = "local";
20052
+ } else if (facts.repoSlug) {
20053
+ const [owner, name] = facts.repoSlug.split("/");
20054
+ 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);
20055
+ const rows = watched ?? [];
20056
+ const live = rows.find((r) => r.commit_update_enabled);
20057
+ facts.watchedBy = live ? live.source_kind === "local" ? "local" : "github" : "none";
20058
+ }
20059
+ } catch {
20060
+ }
20061
+ return facts;
20062
+ }
20063
+ var init_collect = __esm({
20064
+ "src/doctor/collect.ts"() {
20065
+ "use strict";
20066
+ init_execa();
20067
+ init_defaults();
20068
+ init_version();
20069
+ init_preflight();
20070
+ init_branchMarker();
20071
+ init_localRepos();
20072
+ init_github_remote();
20073
+ init_authedClient();
20074
+ }
20075
+ });
20076
+
20077
+ // src/doctor/facts.ts
20078
+ function diagnose(f) {
20079
+ const issues = [];
20080
+ if (!f.claudeInstalled) {
20081
+ issues.push({
20082
+ severity: "error",
20083
+ title: "Claude Code is not installed",
20084
+ 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.",
20085
+ fix: "npm i -g @anthropic-ai/claude-code"
20086
+ });
20087
+ }
20088
+ if (!f.gitInstalled) {
20089
+ issues.push({
20090
+ severity: "error",
20091
+ title: "git is not installed",
20092
+ detail: "archme clones and diffs repositories with git.",
20093
+ fix: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
20094
+ });
20095
+ }
20096
+ if (f.cliVersion === null) {
20097
+ issues.push({
20098
+ severity: "error",
20099
+ title: "this install cannot read its own version",
20100
+ 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.",
20101
+ fix: "npm i -g @archme/cli"
20102
+ });
20103
+ }
20104
+ if (!f.signedIn) {
20105
+ issues.push({
20106
+ severity: "error",
20107
+ title: "not signed in",
20108
+ detail: "Your project, architecture and watch status can't be checked until this machine is paired with an ArchMe account.",
20109
+ fix: "archme login"
20110
+ });
20111
+ return issues;
20112
+ }
20113
+ if (!f.inRepo) {
20114
+ issues.push({
20115
+ severity: "warn",
20116
+ title: "not inside a git repository",
20117
+ detail: "Run this from the repo you want ArchMe to track, so it can check that repo's setup.",
20118
+ fix: "cd <your repo> && archme doctor"
20119
+ });
20120
+ return issues;
20121
+ }
20122
+ if (f.pinnedOrgVisible === false) {
20123
+ issues.push({
20124
+ severity: "error",
20125
+ title: "this repo is pinned to a project your account cannot see",
20126
+ 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.`,
20127
+ fix: "archme login (choose the org that owns the project), or re-run `archme init` to pin one you can reach"
20128
+ });
20129
+ return issues;
20130
+ }
20131
+ if (f.projectExists === false || f.componentCount === 0) {
20132
+ issues.push({
20133
+ severity: "error",
20134
+ title: "this repo has no architecture yet",
20135
+ detail: "Nothing has been scanned, so there is nothing for updates to keep current.",
20136
+ fix: "archme init"
20137
+ });
20138
+ return issues;
20139
+ }
20140
+ if (!f.agentConfigInstalled) {
20141
+ issues.push({
20142
+ severity: "warn",
20143
+ title: "agent config is missing from this repo",
20144
+ detail: "Without `.mcp.json`, Claude Code in this repo can't read or write the architecture while you work. Updates from commits still happen.",
20145
+ fix: "archme init"
20146
+ });
20147
+ }
20148
+ if (f.projectExists === true && f.markerSha === null) {
20149
+ issues.push({
20150
+ severity: "error",
20151
+ title: "the last scan left no resume point",
20152
+ 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.',
20153
+ fix: "archme init --force"
20154
+ });
20155
+ }
20156
+ if (f.watchedBy === "none") {
20157
+ issues.push({
20158
+ severity: "error",
20159
+ title: "nothing is watching this repo",
20160
+ 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.",
20161
+ fix: "archme init"
20162
+ });
20163
+ }
20164
+ const halted = f.localRepos.filter((r) => r.halted);
20165
+ for (const r of halted) {
20166
+ issues.push({
20167
+ severity: "error",
20168
+ title: `local polling is halted for ${r.slug}`,
20169
+ 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.`,
20170
+ fix: `archme local-watch --resume ${r.slug}`
20171
+ });
20172
+ }
20173
+ if (!f.daemonRunning && f.watchedBy !== "none" && f.watchedBy !== "unknown") {
20174
+ issues.push({
20175
+ severity: "error",
20176
+ title: "the daemon is not running",
20177
+ detail: "Commits are queueing up, but nothing is processing them. Updates only happen while the daemon runs, on a machine that stays on.",
20178
+ fix: "archme daemon start"
20179
+ });
20180
+ }
20181
+ return issues;
20182
+ }
20183
+ function yesNo(v) {
20184
+ return v ? "yes" : "no";
20185
+ }
20186
+ function formatFacts(f) {
20187
+ const lines = ["ArchMe setup"];
20188
+ lines.push("", " This machine");
20189
+ lines.push(` archme version: ${f.cliVersion ?? "UNKNOWN (cannot read its own version)"}`);
20190
+ lines.push(` claude installed: ${yesNo(f.claudeInstalled)}`);
20191
+ lines.push(` git installed: ${yesNo(f.gitInstalled)}`);
20192
+ lines.push(` signed in: ${f.signedIn ? f.identity ?? "yes" : "no"}`);
20193
+ lines.push(` daemon running: ${yesNo(f.daemonRunning)}`);
20194
+ lines.push("", " This repo");
20195
+ if (!f.inRepo) {
20196
+ lines.push(" not inside a git repository");
20197
+ } else {
20198
+ lines.push(` path: ${f.repoRoot}`);
20199
+ lines.push(` repo: ${f.repoSlug ?? "no github.com remote"}`);
20200
+ lines.push(` branch: ${f.gitBranch ?? "unknown"}`);
20201
+ lines.push(` .mcp.json: ${f.agentConfigInstalled ? "installed" : "missing"}`);
20202
+ if (f.pinnedProject) lines.push(` pinned to: ${f.pinnedProject} @ ${f.pinnedBranch ?? "?"}`);
20203
+ }
20204
+ lines.push("", " Architecture");
20205
+ if (!f.signedIn) {
20206
+ lines.push(" unknown \u2014 not signed in");
20207
+ } else {
20208
+ if (f.pinnedOrgVisible === false) {
20209
+ lines.push(` NOT VISIBLE \u2014 ${f.pinnedProject} belongs to another organisation`);
20210
+ }
20211
+ lines.push(` project exists: ${f.projectExists === null ? "unknown" : yesNo(f.projectExists)}`);
20212
+ lines.push(` components: ${f.componentCount === null ? "unknown" : f.componentCount}`);
20213
+ lines.push(
20214
+ ` last scanned at: ${f.markerSha ? f.markerSha.slice(0, 7) : "never recorded"}`
20215
+ );
20216
+ lines.push(` updates from: ${describeWatch(f)}`);
20217
+ }
20218
+ for (const r of f.localRepos) {
20219
+ lines.push(
20220
+ ` \xB7 ${r.slug}${r.halted ? ` HALTED: ${r.haltReason ?? "unknown"}` : ""}`
20221
+ );
20222
+ }
20223
+ return lines.join("\n");
20224
+ }
20225
+ function describeWatch(f) {
20226
+ switch (f.watchedBy) {
20227
+ case "github":
20228
+ return "GitHub App (pushes and pull requests)";
20229
+ case "local":
20230
+ return "local polling on this machine";
20231
+ case "none":
20232
+ return "NOTHING \u2014 this architecture will not track new commits";
20233
+ default:
20234
+ return "unknown";
20235
+ }
20236
+ }
20237
+ var init_facts = __esm({
20238
+ "src/doctor/facts.ts"() {
20239
+ "use strict";
20240
+ }
20241
+ });
20242
+
20243
+ // src/doctor/prompt.ts
20244
+ function buildDoctorPrompt(facts, issues) {
20245
+ const problems = issues.length === 0 ? "No problems were found." : issues.map((i2) => `- [${i2.severity}] ${i2.title}
20246
+ ${i2.detail}
20247
+ Fix: ${i2.fix}`).join("\n");
20248
+ return `You are helping someone understand their ArchMe setup. This is the
20249
+ \`archme doctor\` command.
20250
+
20251
+ ${HOW_IT_WORKS}
20252
+
20253
+ ## What was measured on this machine
20254
+
20255
+ These facts were gathered by the CLI before you started. They are accurate.
20256
+ Do NOT re-derive them, and do not go looking for them in the filesystem \u2014 you
20257
+ have everything already.
20258
+
20259
+ \`\`\`
20260
+ ${formatFacts(facts)}
20261
+ \`\`\`
20262
+
20263
+ ## Problems found
20264
+
20265
+ ${problems}
20266
+
20267
+ ## Your job
20268
+
20269
+ The report above has ALREADY been printed to the user's terminal. Do not repeat
20270
+ it back to them.
20271
+
20272
+ Open with two or three sentences: where they are in the flow above, and the
20273
+ single most important thing to do next (or that nothing needs doing). Then stop
20274
+ and let them ask.
20275
+
20276
+ Rules:
20277
+ - You are READ-ONLY. You cannot fix anything, and you must not offer to. Give
20278
+ them the command to run.
20279
+ - Do not invent state. If they ask something the facts above do not answer, say
20280
+ so and tell them which command would show it.
20281
+ - Never tell them their setup is fine if a problem was listed.
20282
+ - Be brief. They ran a diagnostic, not a tutorial. Depth on request.`;
20283
+ }
20284
+ var HOW_IT_WORKS;
20285
+ var init_prompt2 = __esm({
20286
+ "src/doctor/prompt.ts"() {
20287
+ "use strict";
20288
+ init_facts();
20289
+ HOW_IT_WORKS = `## How ArchMe is meant to work
20290
+
20291
+ 1. **Scan** \u2014 \`archme init\` maps the repo into an architecture on the board.
20292
+ This is a one-time snapshot. It runs Claude Code locally, on the user's own
20293
+ Claude subscription. Their code is never sent to ArchMe's servers; only the
20294
+ resulting architecture is.
20295
+
20296
+ 2. **Detection** \u2014 something has to notice new commits. Two options:
20297
+ - **GitHub App**: webhooks. Sees pushes AND pull requests, works for the
20298
+ whole team, nothing to keep running. Needs someone to grant repo read
20299
+ access.
20300
+ - **Local polling** (\`archme local-watch\`): this machine fetches the repo on
20301
+ a timer. No GitHub App, no approvals, but no pull-request awareness.
20302
+
20303
+ 3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
20304
+ step people miss. Detection only queues work; the queue does nothing on its
20305
+ own. Either the user runs \`archme daemon start\` on a machine that stays on,
20306
+ or they email signup@archme.io and ArchMe runs it for them.
20307
+
20308
+ The failure mode that matters most: an architecture that exists but is not
20309
+ being updated. The board still looks current, so nobody notices it is stale.
20310
+ Detection without execution produces exactly that, which is why step 3 is not
20311
+ optional.`;
20312
+ }
20313
+ });
20314
+
20315
+ // src/doctor/dispatch.ts
20316
+ var dispatch_exports5 = {};
20317
+ __export(dispatch_exports5, {
20318
+ EX_INFRA: () => EX_INFRA4,
20319
+ EX_OK: () => EX_OK4,
20320
+ EX_USAGE: () => EX_USAGE4,
20321
+ dispatchDoctor: () => dispatchDoctor,
20322
+ parseDoctorArgs: () => parseDoctorArgs,
20323
+ printDoctorHelp: () => printDoctorHelp
20324
+ });
20325
+ import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
20326
+ import { tmpdir as tmpdir5 } from "node:os";
20327
+ import { join as join24 } from "node:path";
20328
+ function parseDoctorArgs(argv) {
20329
+ const args = { plain: false, help: false };
20330
+ for (const a2 of argv) {
20331
+ if (a2 === "--help" || a2 === "-h" || a2 === "help") args.help = true;
20332
+ else if (a2 === "--plain" || a2 === "--no-agent") args.plain = true;
20333
+ else return { ok: false, message: `archme doctor: unknown flag '${a2}'` };
20334
+ }
20335
+ return { ok: true, args };
20336
+ }
20337
+ function printDoctorHelp() {
20338
+ console.log(`archme doctor \u2014 check this machine's ArchMe setup and explain it
20339
+
20340
+ Reports what is installed, signed in, scanned, watched and running, then hands
20341
+ the results to Claude Code so you can ask about them.
20342
+
20343
+ Read-only: it never changes anything. Anything that needs doing is printed as a
20344
+ command for you to run.
20345
+
20346
+ Usage:
20347
+ archme doctor [--plain]
20348
+
20349
+ Options:
20350
+ --plain Just print the report \u2014 don't start Claude. Use this when Claude
20351
+ Code itself is what's broken.
20352
+ -h, --help`);
20353
+ }
20354
+ async function dispatchDoctor(argv) {
20355
+ const parsed = parseDoctorArgs(argv);
20356
+ if (!parsed.ok) {
20357
+ console.error(parsed.message);
20358
+ printDoctorHelp();
20359
+ return EX_USAGE4;
20360
+ }
20361
+ if (parsed.args.help) {
20362
+ printDoctorHelp();
20363
+ return EX_OK4;
20364
+ }
20365
+ const facts = await collectFacts(process.cwd());
20366
+ const issues = diagnose(facts);
20367
+ console.log(formatFacts(facts));
20368
+ if (issues.length === 0) {
20369
+ console.log("\n\u2713 Everything checks out.");
20370
+ } else {
20371
+ console.log("");
20372
+ for (const issue of issues) {
20373
+ console.log(`${issue.severity === "error" ? "\u2717" : "!"} ${issue.title}`);
20374
+ console.log(` ${issue.detail}`);
20375
+ console.log(` \u2192 ${issue.fix}`);
20376
+ console.log("");
20377
+ }
20378
+ }
20379
+ if (parsed.args.plain) return EX_OK4;
20380
+ try {
20381
+ requireBinary("claude");
20382
+ } catch {
20383
+ console.log(
20384
+ "Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
20385
+ );
20386
+ return EX_OK4;
20387
+ }
20388
+ const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
20389
+ const mcpConfigPath = join24(dir, "mcp.json");
20390
+ await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
20391
+ try {
20392
+ const result = await spawnClaude({
20393
+ prompt: buildDoctorPrompt(facts, issues),
20394
+ cwd: facts.repoRoot ?? process.cwd(),
20395
+ mcpConfigPath,
20396
+ interactive: true,
20397
+ // Read-only, and no MCP: doctor diagnoses, it does not repair. An LLM
20398
+ // with write or shell access on a machine whose setup is already broken
20399
+ // is a much larger surface than the problem justifies.
20400
+ allowedTools: ["Read", "Glob", "Grep"]
20401
+ });
20402
+ return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
20403
+ } finally {
20404
+ await rm9(dir, { recursive: true, force: true });
20405
+ }
20406
+ }
20407
+ var EX_OK4, EX_INFRA4, EX_USAGE4;
20408
+ var init_dispatch5 = __esm({
20409
+ "src/doctor/dispatch.ts"() {
20410
+ "use strict";
20411
+ init_collect();
20412
+ init_facts();
20413
+ init_prompt2();
20414
+ init_spawn_claude();
20415
+ init_preflight();
20416
+ EX_OK4 = 0;
20417
+ EX_INFRA4 = 3;
20418
+ EX_USAGE4 = 78;
20419
+ }
20420
+ });
20421
+
18177
20422
  // src/auth/logout.ts
18178
20423
  var logout_exports = {};
18179
20424
  __export(logout_exports, {
@@ -18187,12 +20432,12 @@ async function runLogout(args) {
18187
20432
  let url;
18188
20433
  try {
18189
20434
  url = resolveConfig().supabaseUrl;
18190
- } catch (err) {
18191
- if (err instanceof MissingConfigError) {
18192
- console.error(err.message);
20435
+ } catch (err2) {
20436
+ if (err2 instanceof MissingConfigError) {
20437
+ console.error(err2.message);
18193
20438
  return 1;
18194
20439
  }
18195
- throw err;
20440
+ throw err2;
18196
20441
  }
18197
20442
  const target = args.find((a2) => !a2.startsWith("-"));
18198
20443
  if (!target || args.includes("--all")) {
@@ -18220,10 +20465,22 @@ var init_logout = __esm({
18220
20465
 
18221
20466
  // src/cli.ts
18222
20467
  init_enroll();
18223
- var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["daemon", "analyze", "login", "logout", "keys", "scan", "init"]);
20468
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
20469
+ "daemon",
20470
+ "analyze",
20471
+ "login",
20472
+ "logout",
20473
+ "keys",
20474
+ "scan",
20475
+ "init",
20476
+ "local-watch",
20477
+ "doctor"
20478
+ ]);
20479
+ var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
18224
20480
  function route(argv) {
18225
20481
  if (argv.length === 0) return { name: "help", args: [] };
18226
20482
  const [first, ...rest] = argv;
20483
+ if (first && VERSION_FLAGS.has(first)) return { name: "version", args: [] };
18227
20484
  if (!first || !KNOWN_COMMANDS.has(first)) {
18228
20485
  return { name: "help", args: [] };
18229
20486
  }
@@ -18287,6 +20544,18 @@ async function main(argv) {
18287
20544
  const { dispatchDaemon: dispatchDaemon2 } = await Promise.resolve().then(() => (init_dispatch2(), dispatch_exports2));
18288
20545
  return await dispatchDaemon2(sub);
18289
20546
  }
20547
+ case "version": {
20548
+ const { readDaemonVersion: readDaemonVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
20549
+ const v = await readDaemonVersion2();
20550
+ if (!v) {
20551
+ console.error(
20552
+ "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`."
20553
+ );
20554
+ return 1;
20555
+ }
20556
+ console.log(v);
20557
+ return 0;
20558
+ }
18290
20559
  case "login":
18291
20560
  await runLogin();
18292
20561
  return 0;
@@ -18302,6 +20571,14 @@ async function main(argv) {
18302
20571
  const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
18303
20572
  return await dispatchInit2(r.args);
18304
20573
  }
20574
+ case "local-watch": {
20575
+ const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
20576
+ return await dispatchLocalWatch2(r.args);
20577
+ }
20578
+ case "doctor": {
20579
+ const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
20580
+ return await dispatchDoctor2(r.args);
20581
+ }
18305
20582
  case "logout": {
18306
20583
  const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
18307
20584
  return await runLogout2(r.args);
@@ -18324,7 +20601,12 @@ Other commands:
18324
20601
  disable|install|uninstall|run)
18325
20602
  archme analyze <subcommand> Code analysis (outline|repo|diff|match|prefetch)
18326
20603
  archme scan <repo-path> Single-pass scan into an explicit project/branch
20604
+ archme local-watch [repo-path] Watch a repo without a GitHub App
20605
+ (--list|--stop|--resume)
20606
+ archme doctor Check this machine's setup and explain it
18327
20607
  archme login | logout Authenticate with archme
20608
+ archme keys list|create|revoke Manage MCP API keys
20609
+ archme --version Print the installed version
18328
20610
 
18329
20611
  Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
18330
20612
  'archme scan --help' for subcommand help.`);