@archme/cli 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8649,11 +8649,11 @@ async function findKey(supabaseUrl, userId) {
8649
8649
  return (await listKeys(supabaseUrl)).find((k) => k.user_id === userId) ?? null;
8650
8650
  }
8651
8651
  async function saveKey(supabaseUrl, toSave) {
8652
- const saved = { ...toSave, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
8652
+ const saved2 = { ...toSave, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
8653
8653
  return withRingLock(async () => {
8654
- const others = (await readEntries()).filter((k) => !isFor(k, supabaseUrl, saved.user_id));
8655
- await writeEntries([...others, { ...saved, supabase_url: supabaseUrl }]);
8656
- return saved;
8654
+ const others = (await readEntries()).filter((k) => !isFor(k, supabaseUrl, saved2.user_id));
8655
+ await writeEntries([...others, { ...saved2, supabase_url: supabaseUrl }]);
8656
+ return saved2;
8657
8657
  });
8658
8658
  }
8659
8659
  async function forgetKey(supabaseUrl, userId) {
@@ -9316,10 +9316,10 @@ async function mintKeyedAccount(deps) {
9316
9316
  if (res.status !== 200 || !parsed || !data.user_id) {
9317
9317
  throw new LoginError(`Could not create an account (${res.status}): ${redactKey(res.data)}`);
9318
9318
  }
9319
- let saved = false;
9319
+ let saved2 = false;
9320
9320
  try {
9321
9321
  await deps.saveKey(deps.supabaseUrl, { user_id: data.user_id, key: data.key, handle: parsed.handle });
9322
- saved = true;
9322
+ saved2 = true;
9323
9323
  } catch (err4) {
9324
9324
  deps.log(`! Created ${parsed.handle}, but its key could not be written to ${deps.keyRingPath}: ${err4 instanceof Error ? err4.message : String(err4)}`);
9325
9325
  deps.log(" It is the only way back into this account. Save this now (a password manager is a good place):");
@@ -9331,7 +9331,7 @@ async function mintKeyedAccount(deps) {
9331
9331
  const grant = await deps.passwordGrant(parsed.email, parsed.secret);
9332
9332
  if (!grant.ok) {
9333
9333
  throw new LoginError(
9334
- `Created ${parsed.handle}${saved ? " and saved its key" : ""}, but signing in failed: ${grant.error}. ` + (saved ? "Run `archme login --key` to try again." : `Try again with the key printed above: ${KEY_VIA_ENV}.`)
9334
+ `Created ${parsed.handle}${saved2 ? " and saved its key" : ""}, but signing in failed: ${grant.error}. ` + (saved2 ? "Run `archme login --key` to try again." : `Try again with the key printed above: ${KEY_VIA_ENV}.`)
9335
9335
  );
9336
9336
  }
9337
9337
  try {
@@ -9339,11 +9339,11 @@ async function mintKeyedAccount(deps) {
9339
9339
  } catch (err4) {
9340
9340
  if (!(err4 instanceof CredentialStoreError)) throw err4;
9341
9341
  throw new LoginError(
9342
- `${err4.message} Your account ${parsed.handle} was created${saved ? " and its key saved" : ""}; once that's fixed, sign in with it: ${saved ? "`archme login --key`" : KEY_VIA_ENV}.`
9342
+ `${err4.message} Your account ${parsed.handle} was created${saved2 ? " and its key saved" : ""}; once that's fixed, sign in with it: ${saved2 ? "`archme login --key`" : KEY_VIA_ENV}.`
9343
9343
  );
9344
9344
  }
9345
9345
  deps.log(`\u2713 Created your ArchMe account ${parsed.handle} \u2014 no email or password, just a key.`);
9346
- if (saved) {
9346
+ if (saved2) {
9347
9347
  deps.log(` The key is saved in ${deps.keyRingPath}. Save it in your password manager too: it is the`);
9348
9348
  deps.log(" only way back in, including from another computer. `archme key show` prints it.");
9349
9349
  }
@@ -10692,7 +10692,7 @@ __export(install_exports, {
10692
10692
  import { isatty } from "node:tty";
10693
10693
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10694
10694
  import { dirname as dirname6, join as join5 } from "node:path";
10695
- import { mkdir as mkdir5 } from "node:fs/promises";
10695
+ import { chmod as chmod3, mkdir as mkdir5 } from "node:fs/promises";
10696
10696
  import { existsSync as existsSync2 } from "node:fs";
10697
10697
  function bakedSystemdEnv(unit) {
10698
10698
  const out = {};
@@ -10771,12 +10771,16 @@ Starting the installed service anyway; it reads its own sign-in when it boots.`)
10771
10771
  }
10772
10772
  }
10773
10773
  }
10774
+ async function ensurePrivateDir(dir) {
10775
+ await mkdir5(dir, { recursive: true, mode: 448 });
10776
+ await chmod3(dir, 448);
10777
+ }
10774
10778
  async function runStartFlow(opts = {}) {
10775
10779
  const platform3 = opts.platform ?? process.platform;
10776
10780
  const env = opts.env ?? process.env;
10777
10781
  await ensureStartPrerequisites(opts);
10778
10782
  const logPaths = resolveLogPath(platform3, env);
10779
- await mkdir5(resolveLogDir(platform3, env), { recursive: true });
10783
+ await ensurePrivateDir(resolveLogDir(platform3, env));
10780
10784
  const controller = opts.controller ?? pickController(platform3, env);
10781
10785
  const installed = await controller.readUnitContent();
10782
10786
  const unitEnv2 = installed === null ? {} : platform3 === "darwin" ? bakedUnitEnv(installed) : bakedSystemdEnv(installed);
@@ -17421,9 +17425,9 @@ var init_sessionAuth = __esm({
17421
17425
  this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
17422
17426
  this.lastUserInfo = userInfo2;
17423
17427
  }
17424
- let saved = null;
17428
+ let saved2 = null;
17425
17429
  try {
17426
- saved = await saveCredentials({
17430
+ saved2 = await saveCredentials({
17427
17431
  kind: this.kind,
17428
17432
  refresh_token: nextRefreshToken,
17429
17433
  access_token: this.accessToken,
@@ -17438,7 +17442,7 @@ var init_sessionAuth = __esm({
17438
17442
  `auth: ${err4.message} The refreshed sign-in was NOT saved: this process keeps working, but the saved one is now out of date. If this process ends before a later refresh saves it, sign in again then. ${this.reloginHint()}`
17439
17443
  );
17440
17444
  }
17441
- return { userInfo: userInfo2, saved, nextRefreshToken };
17445
+ return { userInfo: userInfo2, saved: saved2, nextRefreshToken };
17442
17446
  }
17443
17447
  // The timer closure reads the LIVE this.refreshToken/deviceId/orgId at fire
17444
17448
  // time (via runScheduledRefresh), never a captured value, so an out-of-band
@@ -17539,13 +17543,13 @@ var init_sessionAuth = __esm({
17539
17543
  }
17540
17544
  throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
17541
17545
  }
17542
- const { saved } = await this.applyRefreshedTokens(refreshed, {
17546
+ const { saved: saved2 } = await this.applyRefreshedTokens(refreshed, {
17543
17547
  prevRefreshToken: refreshToken,
17544
17548
  deviceId,
17545
17549
  orgId,
17546
17550
  fallbackUserId: this.userId ?? void 0
17547
17551
  });
17548
- if (saved) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
17552
+ if (saved2) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved2)}`);
17549
17553
  })();
17550
17554
  this.refreshInFlight = p;
17551
17555
  return p.finally(() => {
@@ -17578,13 +17582,13 @@ var init_sessionAuth = __esm({
17578
17582
  // clearing. Permanent only when there is no saved key or the key is refused;
17579
17583
  // the key ring itself is never modified here.
17580
17584
  async signInWithSavedKey(userId, refreshError) {
17581
- let saved;
17585
+ let saved2;
17582
17586
  try {
17583
- saved = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
17587
+ saved2 = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
17584
17588
  } catch (err4) {
17585
17589
  return { ok: false, permanent: false, error: `${refreshError}; ${err4 instanceof Error ? err4.message : String(err4)}` };
17586
17590
  }
17587
- const parsed = parseAccountKey(saved?.key);
17591
+ const parsed = parseAccountKey(saved2?.key);
17588
17592
  if (!parsed) return { ok: false, permanent: true, error: `${refreshError}; no saved key for this account` };
17589
17593
  console.error(`auth: refresh token rejected (${refreshError}); signing in again with the saved key for ${parsed.handle}`);
17590
17594
  const granted = await passwordGrant(parsed.email, parsed.secret, this.cfg);
@@ -17917,7 +17921,10 @@ function rateLimitPauseUntil(text, nowMs = Date.now(), defaultTimeZone = localTi
17917
17921
  if (!info) return null;
17918
17922
  return {
17919
17923
  info,
17920
- pauseUntil: new Date(info.resetAt.getTime() + RATE_LIMIT_PAUSE_PADDING_MS)
17924
+ // The reset is text the run reported (a box can forge it, and a dated one a
17925
+ // week past rolls a year forward), and the pause stalls the whole account:
17926
+ // never longer than a day. A job still limited then just defers again.
17927
+ pauseUntil: new Date(Math.min(info.resetAt.getTime() + RATE_LIMIT_PAUSE_PADDING_MS, nowMs + RATE_LIMIT_MAX_PAUSE_MS))
17921
17928
  };
17922
17929
  }
17923
17930
  function looksLikeRateLimit(text) {
@@ -17958,7 +17965,7 @@ function claudeErrorDeferPlan(error, nowMs, defaultTimeZone) {
17958
17965
  }
17959
17966
  return null;
17960
17967
  }
17961
- var RATE_LIMIT_RE, MONTHS, RATE_LIMIT_TENTATIVE_PAUSE_MS, DATED_RESET_ROLLOVER_GRACE_MS, RATE_LIMIT_PAUSE_PADDING_MS, RATE_LIMIT_SHAPED_RE;
17968
+ var RATE_LIMIT_RE, MONTHS, RATE_LIMIT_TENTATIVE_PAUSE_MS, DATED_RESET_ROLLOVER_GRACE_MS, RATE_LIMIT_PAUSE_PADDING_MS, RATE_LIMIT_MAX_PAUSE_MS, RATE_LIMIT_SHAPED_RE;
17962
17969
  var init_rateLimit = __esm({
17963
17970
  "src/daemon/rateLimit.ts"() {
17964
17971
  "use strict";
@@ -17980,6 +17987,7 @@ var init_rateLimit = __esm({
17980
17987
  RATE_LIMIT_TENTATIVE_PAUSE_MS = 60 * 60 * 1e3;
17981
17988
  DATED_RESET_ROLLOVER_GRACE_MS = 7 * 24 * 60 * 60 * 1e3;
17982
17989
  RATE_LIMIT_PAUSE_PADDING_MS = 9e4;
17990
+ RATE_LIMIT_MAX_PAUSE_MS = 24 * 60 * 60 * 1e3;
17983
17991
  RATE_LIMIT_SHAPED_RE = /(\brate[ -]?limit\b|\blimit\b.{0,40}\bresets?\b|\bresets?\b.{0,40}\blimit\b|\b429\b|\bquota\b|\busage[ -]?limit\b)/i;
17984
17992
  }
17985
17993
  });
@@ -18313,7 +18321,7 @@ var init_localPrPoll = __esm({
18313
18321
  });
18314
18322
 
18315
18323
  // src/daemon/localRepos.ts
18316
- import { chmod as chmod3, mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
18324
+ import { chmod as chmod4, mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
18317
18325
  import { homedir as homedir6 } from "node:os";
18318
18326
  import { dirname as dirname12, join as join19 } from "node:path";
18319
18327
  function localReposPath(env = process.env) {
@@ -18416,7 +18424,7 @@ async function saveLocalRepos(repos, env = process.env) {
18416
18424
  const path7 = localReposPath(env);
18417
18425
  await mkdir9(dirname12(path7), { recursive: true, mode: 448 });
18418
18426
  await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
18419
- await chmod3(path7, 384).catch(() => {
18427
+ await chmod4(path7, 384).catch(() => {
18420
18428
  });
18421
18429
  }
18422
18430
  async function registerLocalRepo(entry, env = process.env) {
@@ -18649,6 +18657,21 @@ var init_defaultBranch = __esm({
18649
18657
  }
18650
18658
  });
18651
18659
 
18660
+ // src/daemon/sshAgent.ts
18661
+ function detachSshAgent(env = process.env) {
18662
+ if (env.SSH_AUTH_SOCK !== void 0) saved = env.SSH_AUTH_SOCK;
18663
+ delete env.SSH_AUTH_SOCK;
18664
+ }
18665
+ function localGitEnv() {
18666
+ return saved === void 0 ? process.env : { ...process.env, SSH_AUTH_SOCK: saved };
18667
+ }
18668
+ var saved;
18669
+ var init_sshAgent = __esm({
18670
+ "src/daemon/sshAgent.ts"() {
18671
+ "use strict";
18672
+ }
18673
+ });
18674
+
18652
18675
  // ../shared/prRefs.ts
18653
18676
  function parsePrRefs(message2) {
18654
18677
  if (!message2) return [];
@@ -18701,7 +18724,8 @@ async function git(dir, args, opts = {}) {
18701
18724
  requireBinary("git");
18702
18725
  const res = await execa("git", args, {
18703
18726
  cwd: dir ?? void 0,
18704
- env: opts.env,
18727
+ // The SSH agent the daemon detached at boot (sshAgent.ts): origin may be ssh.
18728
+ env: { ...localGitEnv(), ...opts.env },
18705
18729
  reject: false,
18706
18730
  timeout: GIT_TIMEOUT_MS,
18707
18731
  // Kill the whole process group: a hung `git fetch` has spawned helper
@@ -18807,6 +18831,7 @@ var init_localGit = __esm({
18807
18831
  init_execa();
18808
18832
  init_preflight();
18809
18833
  init_defaultBranch();
18834
+ init_sshAgent();
18810
18835
  init_prRefs();
18811
18836
  GIT_TIMEOUT_MS = 10 * 60 * 1e3;
18812
18837
  }
@@ -19892,7 +19917,7 @@ var init_result2 = __esm({
19892
19917
  });
19893
19918
 
19894
19919
  // src/daemon/sandbox/inputDir.ts
19895
- import { mkdtemp as mkdtemp3, readdir as readdir2, rm as rm7, writeFile as writeFile10, chmod as chmod4 } from "node:fs/promises";
19920
+ import { mkdtemp as mkdtemp3, readdir as readdir2, rm as rm7, writeFile as writeFile10, chmod as chmod5 } from "node:fs/promises";
19896
19921
  import { tmpdir as tmpdir3 } from "node:os";
19897
19922
  import { join as join22 } from "node:path";
19898
19923
  async function writeInputDir(step, input, base = tmpdir3()) {
@@ -19900,7 +19925,7 @@ async function writeInputDir(step, input, base = tmpdir3()) {
19900
19925
  const cleanup = () => rm7(dir, { recursive: true, force: true }).catch(() => {
19901
19926
  });
19902
19927
  try {
19903
- await chmod4(dir, 448);
19928
+ await chmod5(dir, 448);
19904
19929
  await writeFile10(join22(dir, "input.json"), JSON.stringify(input), { mode: 384, flag: "wx" });
19905
19930
  } catch (e) {
19906
19931
  await cleanup();
@@ -20112,7 +20137,7 @@ var init_confinement = __esm({
20112
20137
 
20113
20138
  // src/daemon/sandbox/claudeToken.ts
20114
20139
  import { constants as fsConstants } from "node:fs";
20115
- import { chmod as chmod5, mkdir as mkdir11, open as open3 } from "node:fs/promises";
20140
+ import { chmod as chmod6, mkdir as mkdir11, open as open3 } from "node:fs/promises";
20116
20141
  import { dirname as dirname15 } from "node:path";
20117
20142
  function assertTokenShape(raw, where) {
20118
20143
  const token = raw.trim();
@@ -20185,7 +20210,7 @@ async function storeSandboxClaudeToken(raw, opts) {
20185
20210
  } finally {
20186
20211
  await fh.close();
20187
20212
  }
20188
- await chmod5(opts.path, 384);
20213
+ await chmod6(opts.path, 384);
20189
20214
  return { source: "file", note };
20190
20215
  }
20191
20216
  async function checkClaudeToken(token, fetchFn = fetch) {
@@ -20611,24 +20636,28 @@ var init_runSandboxedJob = __esm({
20611
20636
 
20612
20637
  // src/daemon/sandbox/classifySandboxExit.ts
20613
20638
  function classifySandboxExit(input) {
20614
- const { counters, exitCode, stderr, stepFailure } = input;
20615
- if ((counters?.cap_hits ?? 0) > 0 || stderr.includes(SANDBOX_CAP_BODY)) {
20639
+ const { counters, exitCode, stepFailure, claudeIsError } = input;
20640
+ if ((counters?.cap_hits ?? 0) > 0) {
20616
20641
  return {
20617
20642
  kind: "terminal",
20618
20643
  reason: SANDBOX_REQUEST_CAP_REASON,
20619
20644
  message: `${SANDBOX_REQUEST_CAP_REASON}: the job exceeded the sandbox model-request cap` + (counters ? ` (${counters.model_requests} requests, ${counters.cap_hits} refused)` : "")
20620
20645
  };
20621
20646
  }
20622
- if (stepFailure == null && exitCode !== null && exitCode !== 0 && counters?.last_upstream_status === 429) {
20623
- return { kind: "upstream_rate_limit" };
20647
+ if (stepFailure == null && exitCode !== null && (exitCode !== 0 || claudeIsError === true)) {
20648
+ const status = counters?.last_upstream_status;
20649
+ if (status === 429) return { kind: "upstream_rate_limit" };
20650
+ if (status === 401 || status === 403) return { kind: "upstream_auth_failure" };
20624
20651
  }
20625
20652
  return { kind: "normal" };
20626
20653
  }
20627
20654
  function routeSandboxFailure(verdict, planFor, nowMs = Date.now()) {
20628
- if (verdict?.kind === "terminal") return { kind: "terminal", message: verdict.message };
20629
- const plan = planFor();
20630
- if (plan) return { kind: "defer", plan };
20631
- if (verdict?.kind === "upstream_rate_limit") {
20655
+ if (verdict === null) {
20656
+ const plan = planFor();
20657
+ return plan ? { kind: "defer", plan } : { kind: "fail" };
20658
+ }
20659
+ if (verdict.kind === "terminal") return { kind: "terminal", message: verdict.message };
20660
+ if (verdict.kind === "upstream_rate_limit") {
20632
20661
  return {
20633
20662
  kind: "defer",
20634
20663
  plan: { parsed: false, pauseUntil: new Date(nowMs + RATE_LIMIT_TENTATIVE_PAUSE_MS), info: null }
@@ -20641,7 +20670,6 @@ var init_classifySandboxExit = __esm({
20641
20670
  "src/daemon/sandbox/classifySandboxExit.ts"() {
20642
20671
  "use strict";
20643
20672
  init_rateLimit();
20644
- init_types();
20645
20673
  SANDBOX_REQUEST_CAP_REASON = "sandbox_request_cap";
20646
20674
  SandboxExitError = class extends Error {
20647
20675
  constructor(message2, verdict) {
@@ -22495,15 +22523,15 @@ async function runCloneStep(input, deps = {}) {
22495
22523
  }
22496
22524
  }
22497
22525
  function applyEnv(env) {
22498
- const saved = /* @__PURE__ */ new Map();
22526
+ const saved2 = /* @__PURE__ */ new Map();
22499
22527
  for (const [k, v] of Object.entries(env)) {
22500
22528
  if (process.env[k] === v) continue;
22501
- saved.set(k, process.env[k]);
22529
+ saved2.set(k, process.env[k]);
22502
22530
  if (v === void 0) delete process.env[k];
22503
22531
  else process.env[k] = v;
22504
22532
  }
22505
22533
  return () => {
22506
- for (const [k, v] of saved) {
22534
+ for (const [k, v] of saved2) {
22507
22535
  if (v === void 0) delete process.env[k];
22508
22536
  else process.env[k] = v;
22509
22537
  }
@@ -22647,7 +22675,9 @@ async function runAgentStep(input, deps = {}) {
22647
22675
  cwd: repoDir,
22648
22676
  timeoutMs: input.timeoutMs,
22649
22677
  model: input.model,
22650
- ...firstScan ? { allowedTools: FIRST_SCAN_ALLOWED_TOOLS } : {},
22678
+ // Diff/PR: no Bash. `-p` mode does not bind built-ins to --allowedTools,
22679
+ // and a build step run through Bash can forge claude's stderr (BUG-143).
22680
+ ...firstScan ? { allowedTools: FIRST_SCAN_ALLOWED_TOOLS } : { builtinTools: ["Read", "Glob", "Grep", "Agent"] },
22651
22681
  jsonOutput: true,
22652
22682
  settingSources: "user",
22653
22683
  // A box is Linux: a prompt over 128 KiB cannot be one argv string.
@@ -22797,7 +22827,7 @@ __export(run_exports, {
22797
22827
  });
22798
22828
  import { createClient as createClient2 } from "@supabase/supabase-js";
22799
22829
  import { randomUUID as randomUUID3 } from "node:crypto";
22800
- import { mkdtemp as mkdtemp4, mkdir as mkdir16, writeFile as writeFile15, rm as rm8, stat as stat8, chmod as chmod6 } from "node:fs/promises";
22830
+ import { mkdtemp as mkdtemp4, mkdir as mkdir16, writeFile as writeFile15, rm as rm8, stat as stat8, chmod as chmod7 } from "node:fs/promises";
22801
22831
  import { tmpdir as tmpdir4 } from "node:os";
22802
22832
  import { join as join25 } from "node:path";
22803
22833
  import { homedir as homedir8, networkInterfaces as networkInterfaces2 } from "node:os";
@@ -22969,7 +22999,7 @@ async function buildGitEnv(credentials) {
22969
22999
  ].join("\n"),
22970
23000
  { mode: 448 }
22971
23001
  );
22972
- await chmod6(askpassPath, 448).catch(() => {
23002
+ await chmod7(askpassPath, 448).catch(() => {
22973
23003
  });
22974
23004
  const gitConfigEntries = [
22975
23005
  ["credential.helper", ""],
@@ -23003,24 +23033,24 @@ async function ensureLocalCloneAndFetch(job, entry) {
23003
23033
  `local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
23004
23034
  );
23005
23035
  }
23006
- await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
23036
+ await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, env: localGitEnv(), reject: false });
23007
23037
  if (job.source_mode === "pr_update") {
23008
23038
  await verifyLocalPrJob(job, async (args) => {
23009
- const r = await execa("git", args, { cwd: dir, reject: false });
23039
+ const r = await execa("git", args, { cwd: dir, env: localGitEnv(), reject: false });
23010
23040
  return { exitCode: r.exitCode ?? 1, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") };
23011
23041
  });
23012
23042
  return;
23013
23043
  }
23014
- const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
23044
+ const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, env: localGitEnv(), reject: false });
23015
23045
  if (afterCheck.exitCode !== 0) {
23016
- await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
23046
+ await execa("git", ["fetch", "origin"], { cwd: dir, env: localGitEnv(), reject: false });
23017
23047
  }
23018
23048
  }).catch((err4) => {
23019
23049
  if (err4 instanceof LocalPrJobRejectedError) throw err4;
23020
23050
  const msg = err4 instanceof Error ? err4.message : String(err4);
23021
23051
  throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
23022
23052
  });
23023
- return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
23053
+ return { dir, gitEnv: localGitEnv(), cleanupGitEnv: async () => {
23024
23054
  } };
23025
23055
  }
23026
23056
  async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
@@ -23707,8 +23737,9 @@ async function completeClaudeRun(ctx, job, deviceId, run) {
23707
23737
  throw new ClaudeRunError(
23708
23738
  `claude reported is_error in its JSON result (no successful completion)`,
23709
23739
  runResult.summary.trim().slice(-FAILURE_DETAIL_MAX) || null,
23710
- isClaudeAuthFailure(claudeErrorResult(run.stdout), run.stderr),
23711
- claudeErrorDetails(run.stdout)
23740
+ run.sandboxVerdict ? run.sandboxVerdict.kind === "upstream_auth_failure" : isClaudeAuthFailure(claudeErrorResult(run.stdout), run.stderr),
23741
+ claudeErrorDetails(run.stdout),
23742
+ run.sandboxVerdict ?? null
23712
23743
  );
23713
23744
  }
23714
23745
  const summary2 = runResult.summary.trim().slice(0, 4e3);
@@ -23974,7 +24005,12 @@ async function runJob(ctx, job, claimDeviceId) {
23974
24005
  await rpcRollback(ctx, job.id, deviceId);
23975
24006
  return;
23976
24007
  }
23977
- const verdict = classifySandboxExit({ counters: sandboxMetrics, exitCode: outcome.exitCode, stderr: outcome.stderr });
24008
+ const verdict = classifySandboxExit({
24009
+ counters: sandboxMetrics,
24010
+ exitCode: outcome.exitCode,
24011
+ stderr: outcome.stderr,
24012
+ claudeIsError: parseClaudeResult(outcome.claudeStdout).isError
24013
+ });
23978
24014
  if (verdict.kind === "terminal") throw new SandboxExitError(verdict.message, verdict);
23979
24015
  if (outcome.exitCode !== 0) {
23980
24016
  const detail = claudeFailureDetail(outcome.stderr, outcome.claudeStdout);
@@ -23982,13 +24018,23 @@ async function runJob(ctx, job, claimDeviceId) {
23982
24018
  throw new ClaudeRunError(
23983
24019
  buildClaudeExitMessage(outcome.exitCode, outcome.signal ? ` (${outcome.signal})` : "", outcome.stderr),
23984
24020
  detail,
23985
- isClaudeAuthFailure(claudeErrorResult(outcome.claudeStdout), outcome.stderr),
24021
+ // The gate's own 401/403, never the box's text (BUG-143).
24022
+ verdict.kind === "upstream_auth_failure",
23986
24023
  claudeErrorDetails(outcome.claudeStdout),
23987
24024
  verdict
23988
24025
  );
23989
24026
  }
23990
24027
  const jobReads = readsFor(ctx, job, deviceId);
23991
24028
  if (firstScan) {
24029
+ if (verdict.kind === "upstream_rate_limit" || verdict.kind === "upstream_auth_failure") {
24030
+ throw new ClaudeRunError(
24031
+ `claude reported is_error in its JSON result (no successful completion)`,
24032
+ null,
24033
+ verdict.kind === "upstream_auth_failure",
24034
+ claudeErrorDetails(outcome.claudeStdout),
24035
+ verdict
24036
+ );
24037
+ }
23992
24038
  const gate = firstScanGate(await jobReads.architectureCounts());
23993
24039
  if (!gate.ok) throw new Error(gate.reason);
23994
24040
  }
@@ -24001,7 +24047,8 @@ async function runJob(ctx, job, claimDeviceId) {
24001
24047
  startedAt,
24002
24048
  markPhase,
24003
24049
  log,
24004
- reads: jobReads
24050
+ reads: jobReads,
24051
+ sandboxVerdict: verdict
24005
24052
  });
24006
24053
  return;
24007
24054
  }
@@ -24271,8 +24318,9 @@ async function runJob(ctx, job, claimDeviceId) {
24271
24318
  const runErr = err4 instanceof ClaudeRunError ? err4 : null;
24272
24319
  const haystack = stripPromptEcho(`${msg}
24273
24320
  ${logs.stderr}`, sentPrompt);
24321
+ const boxed = boxedJobs.has(job.id);
24274
24322
  const route2 = routeSandboxFailure(
24275
- err4 instanceof SandboxExitError ? err4.verdict : runErr?.sandboxVerdict ?? null,
24323
+ err4 instanceof SandboxExitError ? err4.verdict : runErr?.sandboxVerdict ?? (boxed ? { kind: "normal" } : null),
24276
24324
  () => rateLimitDeferPlanForExit({
24277
24325
  message: msg,
24278
24326
  stderr: logs.stderr,
@@ -24289,7 +24337,7 @@ ${logs.stderr}`, sentPrompt);
24289
24337
  const plan = route2.kind === "defer" ? route2.plan : null;
24290
24338
  if (plan) {
24291
24339
  const { parsed, pauseUntil, info } = plan;
24292
- if (!parsed) {
24340
+ if (!parsed && !boxed) {
24293
24341
  await rpcReportEvent(ctx, job, deviceId, "rate_limit_unmatched_exit", {
24294
24342
  job_id: job.id,
24295
24343
  excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
@@ -24321,8 +24369,12 @@ ${logs.stderr}`, sentPrompt);
24321
24369
  const notice = authFailureNotice(boxedJobs.has(job.id), runErr.detail);
24322
24370
  console.error(`[${job.id}] ${notice.message} Deferring job + pausing daemon until ${pauseUntil.toISOString()}.`);
24323
24371
  const detail = { job_id: job.id, claude_said: runErr.detail };
24324
- await rpcSetDaemonPause(ctx, pauseUntil, notice.reason, detail);
24325
- setLocalPause(pauseUntil.getTime());
24372
+ if (job.job_kind !== "first_scan") {
24373
+ await rpcSetDaemonPause(ctx, pauseUntil, notice.reason, detail);
24374
+ setLocalPause(pauseUntil.getTime());
24375
+ } else {
24376
+ firstScanPauseUntilMs = Math.max(firstScanPauseUntilMs, pauseUntil.getTime());
24377
+ }
24326
24378
  const deferred = await rpcDefer(ctx, job.id, deviceId, notice.reason, detail);
24327
24379
  if (deferred) return;
24328
24380
  console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
@@ -24517,6 +24569,7 @@ function exitDaemon(code) {
24517
24569
  process.exit(resolveExitCode(code, process.env));
24518
24570
  }
24519
24571
  async function runDaemon() {
24572
+ detachSshAgent();
24520
24573
  let lockHandle = null;
24521
24574
  try {
24522
24575
  lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
@@ -24891,6 +24944,7 @@ var init_run = __esm({
24891
24944
  init_host();
24892
24945
  init_runSandboxedJob();
24893
24946
  init_inputDir();
24947
+ init_sshAgent();
24894
24948
  init_classifySandboxExit();
24895
24949
  HEARTBEAT_INTERVAL_MS = 6e4;
24896
24950
  CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -30714,7 +30768,7 @@ async function runConciergeToken(opts, deps) {
30714
30768
  async function reserveStdout(fn) {
30715
30769
  const stdout = process.stdout;
30716
30770
  const realWrite = stdout.write;
30717
- const saved = { log: console.log, info: console.info, debug: console.debug };
30771
+ const saved2 = { log: console.log, info: console.info, debug: console.debug };
30718
30772
  const toStderr = (...args) => console.error(...args);
30719
30773
  console.log = toStderr;
30720
30774
  console.info = toStderr;
@@ -30726,9 +30780,9 @@ async function reserveStdout(fn) {
30726
30780
  });
30727
30781
  } finally {
30728
30782
  stdout.write = realWrite;
30729
- console.log = saved.log;
30730
- console.info = saved.info;
30731
- console.debug = saved.debug;
30783
+ console.log = saved2.log;
30784
+ console.info = saved2.info;
30785
+ console.debug = saved2.debug;
30732
30786
  }
30733
30787
  }
30734
30788
  async function postConciergeTokenWith(supabaseUrl, accessToken, body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "image": "ghcr.io/architectme/archme-job",
3
- "digest": "sha256:e82027063082a90ad4e6bf0a551da676bc57d6aad7778c737652acc95eae3999"
3
+ "digest": "sha256:bedd452cb1fb7f0474d06296b8033b426c4a808e762ee61c9043b19d723fb43b"
4
4
  }