@archme/cli 0.16.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);
@@ -13691,6 +13695,89 @@ var init_repoContainer = __esm({
13691
13695
  }
13692
13696
  });
13693
13697
 
13698
+ // ../shared/seedArchitecture.ts
13699
+ function seedArchitectureFromRows(rows) {
13700
+ const components = [];
13701
+ const stories = [];
13702
+ for (const row of rows) {
13703
+ const compPath = componentPathFromFilePath(row.path);
13704
+ if (compPath !== null) {
13705
+ const parsed = tryParse(row.content);
13706
+ components.push({
13707
+ path: compPath,
13708
+ anchors: parsed?.anchors ?? [],
13709
+ unanchored: parsed?.unanchored === true ? true : void 0
13710
+ });
13711
+ continue;
13712
+ }
13713
+ const scope = storyScopeFromFilePath(row.path);
13714
+ if (scope !== null) {
13715
+ const parsed = tryParse(row.content);
13716
+ if (!parsed) continue;
13717
+ collectSeedStories(parsed, scope, row.path, stories);
13718
+ }
13719
+ }
13720
+ return { components, stories };
13721
+ }
13722
+ function tryParse(s) {
13723
+ try {
13724
+ return JSON.parse(s);
13725
+ } catch {
13726
+ return null;
13727
+ }
13728
+ }
13729
+ function componentPathFromFilePath(p) {
13730
+ if (!p.endsWith("/component.json")) return null;
13731
+ const inner = p.slice(0, -"/component.json".length);
13732
+ if (inner.length === 0) return null;
13733
+ const segments = inner.split("/");
13734
+ if (segments.length % 2 !== 0) return null;
13735
+ const ids = [];
13736
+ for (let i2 = 0; i2 < segments.length; i2 += 2) {
13737
+ if (segments[i2] !== "components") return null;
13738
+ const id = segments[i2 + 1];
13739
+ if (!id) return null;
13740
+ ids.push(id);
13741
+ }
13742
+ return `root.${ids.join(".")}`;
13743
+ }
13744
+ function storyScopeFromFilePath(p) {
13745
+ if (!STORY_PATH_RE.test(p)) return null;
13746
+ const prefix = p.slice(0, p.lastIndexOf("stories/"));
13747
+ if (prefix.length === 0) return [];
13748
+ const segments = prefix.split("/").filter(Boolean);
13749
+ const ids = [];
13750
+ for (let i2 = 0; i2 < segments.length; i2 += 2) {
13751
+ if (segments[i2] !== "components") return null;
13752
+ const id = segments[i2 + 1];
13753
+ if (!id) return null;
13754
+ ids.push(id);
13755
+ }
13756
+ return ids;
13757
+ }
13758
+ function collectSeedStories(raw, scope, fileFallbackId, out, parentId) {
13759
+ const id = raw.id ?? fileFallbackId;
13760
+ out.push({
13761
+ id,
13762
+ title: raw.title ?? id,
13763
+ sequence: raw.sequence ?? "",
13764
+ draft: raw.draft === true ? true : void 0,
13765
+ scope,
13766
+ parentId
13767
+ });
13768
+ if (Array.isArray(raw.userStories)) {
13769
+ for (const child of raw.userStories) {
13770
+ collectSeedStories(child, scope, fileFallbackId, out, id);
13771
+ }
13772
+ }
13773
+ }
13774
+ var STORY_PATH_RE;
13775
+ var init_seedArchitecture = __esm({
13776
+ "../shared/seedArchitecture.ts"() {
13777
+ STORY_PATH_RE = /^(?:components\/[^/]+\/)*stories\/[^/]+\.json$/;
13778
+ }
13779
+ });
13780
+
13694
13781
  // src/scan/affected.ts
13695
13782
  function normalizePath(p) {
13696
13783
  return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
@@ -13904,42 +13991,6 @@ function formatAffectedSeed(result, removedSymbols = [], allStories) {
13904
13991
  }
13905
13992
  return lines.join("\n");
13906
13993
  }
13907
- function tryParse(s) {
13908
- try {
13909
- return JSON.parse(s);
13910
- } catch {
13911
- return null;
13912
- }
13913
- }
13914
- function componentPathFromFilePath(p) {
13915
- if (!p.endsWith("/component.json")) return null;
13916
- const inner = p.slice(0, -"/component.json".length);
13917
- if (inner.length === 0) return null;
13918
- const segments = inner.split("/");
13919
- if (segments.length % 2 !== 0) return null;
13920
- const ids = [];
13921
- for (let i2 = 0; i2 < segments.length; i2 += 2) {
13922
- if (segments[i2] !== "components") return null;
13923
- const id = segments[i2 + 1];
13924
- if (!id) return null;
13925
- ids.push(id);
13926
- }
13927
- return `root.${ids.join(".")}`;
13928
- }
13929
- function storyScopeFromFilePath(p) {
13930
- if (!STORY_PATH_RE.test(p)) return null;
13931
- const prefix = p.slice(0, p.lastIndexOf("stories/"));
13932
- if (prefix.length === 0) return [];
13933
- const segments = prefix.split("/").filter(Boolean);
13934
- const ids = [];
13935
- for (let i2 = 0; i2 < segments.length; i2 += 2) {
13936
- if (segments[i2] !== "components") return null;
13937
- const id = segments[i2 + 1];
13938
- if (!id) return null;
13939
- ids.push(id);
13940
- }
13941
- return ids;
13942
- }
13943
13994
  async function loadArchitectureForSeeding(supabase, projectId, branch) {
13944
13995
  const PAGE_SIZE2 = 1e3;
13945
13996
  const rows = [];
@@ -13950,54 +14001,18 @@ async function loadArchitectureForSeeding(supabase, projectId, branch) {
13950
14001
  rows.push(...data);
13951
14002
  if (data.length < PAGE_SIZE2) break;
13952
14003
  }
13953
- const components = [];
13954
- const stories = [];
13955
- for (const row of rows) {
13956
- const compPath = componentPathFromFilePath(row.path);
13957
- if (compPath !== null) {
13958
- const parsed = tryParse(row.content);
13959
- components.push({
13960
- path: compPath,
13961
- anchors: parsed?.anchors ?? [],
13962
- unanchored: parsed?.unanchored === true ? true : void 0
13963
- });
13964
- continue;
13965
- }
13966
- const scope = storyScopeFromFilePath(row.path);
13967
- if (scope !== null) {
13968
- const parsed = tryParse(row.content);
13969
- if (!parsed) continue;
13970
- collectSeedStories(parsed, scope, row.path, stories);
13971
- }
13972
- }
13973
- return { components, stories };
14004
+ return seedArchitectureFromRows(rows);
13974
14005
  }
13975
- function collectSeedStories(raw, scope, fileFallbackId, out, parentId) {
13976
- const id = raw.id ?? fileFallbackId;
13977
- out.push({
13978
- id,
13979
- title: raw.title ?? id,
13980
- sequence: raw.sequence ?? "",
13981
- draft: raw.draft === true ? true : void 0,
13982
- scope,
13983
- parentId
13984
- });
13985
- if (Array.isArray(raw.userStories)) {
13986
- for (const child of raw.userStories) {
13987
- collectSeedStories(child, scope, fileFallbackId, out, id);
13988
- }
13989
- }
13990
- }
13991
- var MESSAGE_RE, PARTICIPANT_RE, DISPLAY_FILE_CAP, STORY_PATH_RE;
14006
+ var MESSAGE_RE, PARTICIPANT_RE, DISPLAY_FILE_CAP;
13992
14007
  var init_affected = __esm({
13993
14008
  "src/scan/affected.ts"() {
13994
14009
  "use strict";
13995
14010
  init_esm3();
13996
14011
  init_repoContainer();
14012
+ init_seedArchitecture();
13997
14013
  MESSAGE_RE = /^\s*(\{[^}]+\}|\S+?)\s*(--?>>|--?>|->|-->|-x|-\))\s*(\{[^}]+\}|\S+?)\s*:\s*(.*)$/;
13998
14014
  PARTICIPANT_RE = /^\s*participant\s+(\S+?)(?:\s+as\s+.+)?$/;
13999
14015
  DISPLAY_FILE_CAP = 200;
14000
- STORY_PATH_RE = /^(?:components\/[^/]+\/)*stories\/[^/]+\.json$/;
14001
14016
  }
14002
14017
  });
14003
14018
 
@@ -16251,10 +16266,11 @@ function buildBugReportGuidance(bugsFolderId) {
16251
16266
 
16252
16267
  ${folderMechanic}`;
16253
16268
  }
16254
- var BUGS_FOLDER_TITLE, BUG_REPORT_CORE;
16269
+ var BUGS_FOLDER_TITLE, BUGS_FOLDER_DESCRIPTION, BUG_REPORT_CORE;
16255
16270
  var init_bugReport = __esm({
16256
16271
  "../shared/bugReport.ts"() {
16257
16272
  BUGS_FOLDER_TITLE = "Bugs found by ArchMe";
16273
+ BUGS_FOLDER_DESCRIPTION = "Bugs noticed by scan/diff agents in the code while updating the architecture. Prose-only proposals; humans triage.";
16258
16274
  BUG_REPORT_CORE = "**Bugs you happen to notice.** Don't hunt for bugs \u2014 stay on the architecture task \u2014 but don't ignore an obvious, real one that surfaces while you read the code (a null-deref, a missing `await`, an unhandled error path, a broken invariant). Have any sub-agents you dispatch report a suspect back to you (a one-line `possible bug: <what> at <file:line>`) rather than filing it themselves. When suspects come back, dispatch ONE bug-validation sub-agent \u2014 it reads code and calls `create_proposal`, exactly like the build sub-agent, and never modifies the user's code \u2014 and have it: verify each suspect against the code, and for every one it confirms as a real defect, FILE it itself as a prose-only proposal (`create_proposal` with `kind: 'proposal'`, `stage: 'backlog'`, an empty graph delta, and a one-paragraph `body`: what is wrong, where by file:line or symbol, and why), `about` the component it lives in (`{ component: '<root.\u2026 path>' }` \u2014 find_components_for_paths maps the file to it), else the user story whose flow it breaks (`{ story: '<id>' }`). It skips anything it can't confirm, and searches the folder by title first to skip anything already logged. Keep the bar high \u2014 obvious, confirmed defects only, a few per run at most, never speculation, style, or a design opinion; record, never fix. Logging a bug is orthogonal to your architecture task and does NOT replace your completion signal \u2014 still call `update_architecture` or `report_no_change` exactly as you otherwise would.";
16259
16275
  }
16260
16276
  });
@@ -16685,7 +16701,11 @@ var init_validationCodes = __esm({
16685
16701
  WALKTHROUGH_COVERAGE_CHAPTER_NOT_COVERED: "walkthrough.coverage.chapter-not-covered",
16686
16702
  /** The delta added, removed or modified a component no step names. */
16687
16703
  WALKTHROUGH_COVERAGE_COMPONENT_NOT_COVERED: "walkthrough.coverage.component-not-covered",
16688
- /** The delta changed a connection no step names, joins, or draws. */
16704
+ /**
16705
+ * The delta changed a connection no step names, joins, or draws.
16706
+ * @deprecated Retired: connections show up through the stories that draw
16707
+ * them, so coverage no longer requires them.
16708
+ */
16689
16709
  WALKTHROUGH_COVERAGE_CONNECTION_NOT_COVERED: "walkthrough.coverage.connection-not-covered",
16690
16710
  // ──────────────────────────────────────────────────────────────────────────
16691
16711
  // mcp — request-level rules applied before any tool handler runs
@@ -16742,7 +16762,7 @@ var init_walkthroughGuidance = __esm({
16742
16762
  init_walkthrough();
16743
16763
  WALKTHROUGH_GUIDANCE = `A walkthrough breaks a change into bite-sized chunks a reviewer can step through. Each step names the elements it is about; the review surface filters the sequence diagram and the component graph down to just those, so the reader sees one chunk at a time instead of a forty-element diff at once.
16744
16764
 
16745
- - **Every step must name what it is about** \u2014 the stories, chapters, components and/or connections it covers. A step with only prose is rejected: there is nothing to filter the diagrams by, and an explanation a reader cannot check against the change is exactly what this view exists to avoid.
16765
+ - **Every step must name what it is about** \u2014 the stories, chapters, components and/or connections it covers. A step with only prose is rejected: there is nothing to filter the diagrams by, and an explanation a reader cannot check against the change is exactly what this view exists to avoid. Name a connection inside a component as \`<component path>::<id>\` (\`daemon::session_auth->key_account\`); a root connection by its plain id.
16746
16766
  - **Steps are in reading order, not build order.** This is not an implementation plan and not a restatement of your sequencing section \u2014 start where a reviewer should start, and put each step where it makes the next one make sense.
16747
16767
  - **Group by idea.** One step is one coherent change, usually 3\u20138 elements. A one-element step is fine and often valuable: an independent fix that would otherwise be lost among the rest. Aim for a handful of steps; the cap is ${MAX_WALKTHROUGH_STEPS}.
16748
16768
  - **Titles are terse** \u2014 a handle, not a summary. Under ${MAX_STEP_TITLE_CHARS} characters. \`Markers move per-repo\`, not \`This step explains how the source marker moves onto a per-repo table\`.
@@ -16754,7 +16774,7 @@ var init_walkthroughGuidance = __esm({
16754
16774
  - Don't use the change's OWN new vocabulary undefined. If the step introduces a term, say what it means in the same breath or use plain words.
16755
16775
  - Bad: \`A correctness item, not a phase-3 nicety: anchors are repo-relative paths, so two repos with the same conventional layout collide silently and seed stories from the wrong one.\`
16756
16776
  - Good: \`With two repos in one project a path like src/index.ts exists in both, so looking up which component owns a changed file can return the wrong repo's. Each repo now records the container its code lives under, and lookups search only that subtree.\`
16757
- - **Cover every change** \u2014 every story, component and connection the change touches \u2014 or \`set_walkthrough\` rejects the walkthrough and lists what is missing. Guided mode walks the steps and nothing after them, so anything no step reaches is a change the reviewer never sees. A connection is covered when a step names it, names both of its components, or names a story page that draws it as an arrow. Name a connection inside a component as \`<component path>::<id>\` (\`daemon::session_auth->key_account\`); a root connection by its plain id.
16777
+ - **Cover every change** \u2014 every story and component the change touches \u2014 or \`set_walkthrough\` rejects the walkthrough and lists what is missing. Guided mode walks the steps and nothing after them, so anything no step reaches is a change the reviewer never sees. Connections need no step of their own: they show up through the stories that draw them.
16758
16778
  - **Skip it for a small change.** A handful of elements needs no walkthrough; omit it rather than writing a step per element.
16759
16779
  - **Prefer CHAPTERS to narrow a diagram.** If a story's sequence carries \`%% chapter: <id> | <Label>\` markers, name the ones your step covers as \`chapters: [{ story, chapter }]\`, using the **id**, never the label \u2014 ids are stable across a rename, labels are not. A chapter is a page break the story's author already wrote, so it is sharper than naming components, where one hub the step mentions decides the whole view.`;
16760
16780
  }
@@ -16865,12 +16885,11 @@ async function ensureBugsFolder(client, projectId) {
16865
16885
  return null;
16866
16886
  }
16867
16887
  }
16868
- var BUGS_FOLDER_DESCRIPTION, cache;
16888
+ var cache;
16869
16889
  var init_bugsFolder = __esm({
16870
16890
  "src/daemon/bugsFolder.ts"() {
16871
16891
  "use strict";
16872
16892
  init_bugReport();
16873
- BUGS_FOLDER_DESCRIPTION = "Bugs noticed by scan/diff agents in the code while updating the architecture. Prose-only proposals; humans triage.";
16874
16893
  cache = /* @__PURE__ */ new Map();
16875
16894
  }
16876
16895
  });
@@ -17406,9 +17425,9 @@ var init_sessionAuth = __esm({
17406
17425
  this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
17407
17426
  this.lastUserInfo = userInfo2;
17408
17427
  }
17409
- let saved = null;
17428
+ let saved2 = null;
17410
17429
  try {
17411
- saved = await saveCredentials({
17430
+ saved2 = await saveCredentials({
17412
17431
  kind: this.kind,
17413
17432
  refresh_token: nextRefreshToken,
17414
17433
  access_token: this.accessToken,
@@ -17423,7 +17442,7 @@ var init_sessionAuth = __esm({
17423
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()}`
17424
17443
  );
17425
17444
  }
17426
- return { userInfo: userInfo2, saved, nextRefreshToken };
17445
+ return { userInfo: userInfo2, saved: saved2, nextRefreshToken };
17427
17446
  }
17428
17447
  // The timer closure reads the LIVE this.refreshToken/deviceId/orgId at fire
17429
17448
  // time (via runScheduledRefresh), never a captured value, so an out-of-band
@@ -17524,13 +17543,13 @@ var init_sessionAuth = __esm({
17524
17543
  }
17525
17544
  throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
17526
17545
  }
17527
- const { saved } = await this.applyRefreshedTokens(refreshed, {
17546
+ const { saved: saved2 } = await this.applyRefreshedTokens(refreshed, {
17528
17547
  prevRefreshToken: refreshToken,
17529
17548
  deviceId,
17530
17549
  orgId,
17531
17550
  fallbackUserId: this.userId ?? void 0
17532
17551
  });
17533
- if (saved) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
17552
+ if (saved2) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved2)}`);
17534
17553
  })();
17535
17554
  this.refreshInFlight = p;
17536
17555
  return p.finally(() => {
@@ -17563,13 +17582,13 @@ var init_sessionAuth = __esm({
17563
17582
  // clearing. Permanent only when there is no saved key or the key is refused;
17564
17583
  // the key ring itself is never modified here.
17565
17584
  async signInWithSavedKey(userId, refreshError) {
17566
- let saved;
17585
+ let saved2;
17567
17586
  try {
17568
- saved = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
17587
+ saved2 = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
17569
17588
  } catch (err4) {
17570
17589
  return { ok: false, permanent: false, error: `${refreshError}; ${err4 instanceof Error ? err4.message : String(err4)}` };
17571
17590
  }
17572
- const parsed = parseAccountKey(saved?.key);
17591
+ const parsed = parseAccountKey(saved2?.key);
17573
17592
  if (!parsed) return { ok: false, permanent: true, error: `${refreshError}; no saved key for this account` };
17574
17593
  console.error(`auth: refresh token rejected (${refreshError}); signing in again with the saved key for ${parsed.handle}`);
17575
17594
  const granted = await passwordGrant(parsed.email, parsed.secret, this.cfg);
@@ -17902,7 +17921,10 @@ function rateLimitPauseUntil(text, nowMs = Date.now(), defaultTimeZone = localTi
17902
17921
  if (!info) return null;
17903
17922
  return {
17904
17923
  info,
17905
- 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))
17906
17928
  };
17907
17929
  }
17908
17930
  function looksLikeRateLimit(text) {
@@ -17943,7 +17965,7 @@ function claudeErrorDeferPlan(error, nowMs, defaultTimeZone) {
17943
17965
  }
17944
17966
  return null;
17945
17967
  }
17946
- 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;
17947
17969
  var init_rateLimit = __esm({
17948
17970
  "src/daemon/rateLimit.ts"() {
17949
17971
  "use strict";
@@ -17965,6 +17987,7 @@ var init_rateLimit = __esm({
17965
17987
  RATE_LIMIT_TENTATIVE_PAUSE_MS = 60 * 60 * 1e3;
17966
17988
  DATED_RESET_ROLLOVER_GRACE_MS = 7 * 24 * 60 * 60 * 1e3;
17967
17989
  RATE_LIMIT_PAUSE_PADDING_MS = 9e4;
17990
+ RATE_LIMIT_MAX_PAUSE_MS = 24 * 60 * 60 * 1e3;
17968
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;
17969
17992
  }
17970
17993
  });
@@ -18298,7 +18321,7 @@ var init_localPrPoll = __esm({
18298
18321
  });
18299
18322
 
18300
18323
  // src/daemon/localRepos.ts
18301
- 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";
18302
18325
  import { homedir as homedir6 } from "node:os";
18303
18326
  import { dirname as dirname12, join as join19 } from "node:path";
18304
18327
  function localReposPath(env = process.env) {
@@ -18401,7 +18424,7 @@ async function saveLocalRepos(repos, env = process.env) {
18401
18424
  const path7 = localReposPath(env);
18402
18425
  await mkdir9(dirname12(path7), { recursive: true, mode: 448 });
18403
18426
  await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
18404
- await chmod3(path7, 384).catch(() => {
18427
+ await chmod4(path7, 384).catch(() => {
18405
18428
  });
18406
18429
  }
18407
18430
  async function registerLocalRepo(entry, env = process.env) {
@@ -18634,6 +18657,21 @@ var init_defaultBranch = __esm({
18634
18657
  }
18635
18658
  });
18636
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
+
18637
18675
  // ../shared/prRefs.ts
18638
18676
  function parsePrRefs(message2) {
18639
18677
  if (!message2) return [];
@@ -18686,7 +18724,8 @@ async function git(dir, args, opts = {}) {
18686
18724
  requireBinary("git");
18687
18725
  const res = await execa("git", args, {
18688
18726
  cwd: dir ?? void 0,
18689
- env: opts.env,
18727
+ // The SSH agent the daemon detached at boot (sshAgent.ts): origin may be ssh.
18728
+ env: { ...localGitEnv(), ...opts.env },
18690
18729
  reject: false,
18691
18730
  timeout: GIT_TIMEOUT_MS,
18692
18731
  // Kill the whole process group: a hung `git fetch` has spawned helper
@@ -18792,6 +18831,7 @@ var init_localGit = __esm({
18792
18831
  init_execa();
18793
18832
  init_preflight();
18794
18833
  init_defaultBranch();
18834
+ init_sshAgent();
18795
18835
  init_prRefs();
18796
18836
  GIT_TIMEOUT_MS = 10 * 60 * 1e3;
18797
18837
  }
@@ -19496,7 +19536,7 @@ var init_setting = __esm({
19496
19536
  }
19497
19537
  });
19498
19538
 
19499
- // src/scan/architectureCounts.ts
19539
+ // ../shared/architectureCounts.ts
19500
19540
  function countFromPaths(paths) {
19501
19541
  let components = 0;
19502
19542
  let stories = 0;
@@ -19510,6 +19550,12 @@ function countFromPaths(paths) {
19510
19550
  }
19511
19551
  return { components, stories, rootStories };
19512
19552
  }
19553
+ var init_architectureCounts = __esm({
19554
+ "../shared/architectureCounts.ts"() {
19555
+ }
19556
+ });
19557
+
19558
+ // src/scan/architectureCounts.ts
19513
19559
  async function countArchitecture(supabase, projectId, branch) {
19514
19560
  const PAGE_SIZE2 = 1e3;
19515
19561
  const paths = [];
@@ -19529,9 +19575,10 @@ function firstScanGate(counts) {
19529
19575
  reason: `first scan incomplete: the branch holds ${counts.components} component(s) and ${counts.rootStories} root story(ies); needs at least one of each`
19530
19576
  };
19531
19577
  }
19532
- var init_architectureCounts = __esm({
19578
+ var init_architectureCounts2 = __esm({
19533
19579
  "src/scan/architectureCounts.ts"() {
19534
19580
  "use strict";
19581
+ init_architectureCounts();
19535
19582
  }
19536
19583
  });
19537
19584
 
@@ -19646,6 +19693,103 @@ var init_platformDevices = __esm({
19646
19693
  }
19647
19694
  });
19648
19695
 
19696
+ // src/daemon/jobReads.ts
19697
+ async function lookupProjectSlug(client, projectId) {
19698
+ const { data, error } = await client.from("projects").select("slug, organizations(slug)").eq("id", projectId).single();
19699
+ if (error || !data) throw new Error(`Project ${projectId} not found: ${error?.message}`);
19700
+ const orgSlug = data.organizations?.slug;
19701
+ if (!orgSlug) throw new Error(`Project ${projectId} has no org slug`);
19702
+ return `${orgSlug}/${data.slug}`;
19703
+ }
19704
+ async function detectWritesByPhase(client, projectId, archmeBranch, jobId) {
19705
+ const { data, error } = await client.from("commits").select("structural_changeset").eq("project_id", projectId).eq("branch", archmeBranch).eq("metadata->source->>job_id", jobId);
19706
+ if (error) {
19707
+ console.error("commits lookup error:", error.message);
19708
+ return { total: 0, diffWrites: 0, structuralWrites: 0 };
19709
+ }
19710
+ let diffWrites = 0;
19711
+ let structuralWrites = 0;
19712
+ for (const row of data ?? []) {
19713
+ if (row.structural_changeset == null) diffWrites++;
19714
+ else structuralWrites++;
19715
+ }
19716
+ return { total: diffWrites + structuralWrites, diffWrites, structuralWrites };
19717
+ }
19718
+ async function detectMcpActivity(client, jobId) {
19719
+ const { data, error } = await client.from("archme_jobs").select("meaningful_tool_calls, no_change_reason").eq("id", jobId).maybeSingle();
19720
+ if (error) {
19721
+ console.error("mcp_activity lookup error:", error.message);
19722
+ return { reads: void 0, noChange: false };
19723
+ }
19724
+ if (!data) return { reads: void 0, noChange: false };
19725
+ const row = data;
19726
+ return { reads: row.meaningful_tool_calls, noChange: row.no_change_reason != null };
19727
+ }
19728
+ function sessionJobReads(client, job) {
19729
+ return {
19730
+ projectSlug: () => lookupProjectSlug(client, job.project_id),
19731
+ marker: (repo) => readBranchMarker(client, job.project_id, job.archme_branch, repo),
19732
+ repoMappings: (log) => loadRepoMappings(client, job.project_id, log),
19733
+ seedArchitecture: () => loadArchitectureForSeeding(client, job.project_id, job.archme_branch),
19734
+ bugsFolderId: () => ensureBugsFolder(client, job.project_id),
19735
+ writesByPhase: () => detectWritesByPhase(client, job.project_id, job.archme_branch, job.id),
19736
+ mcpActivity: () => detectMcpActivity(client, job.id),
19737
+ architectureCounts: () => countArchitecture(client, job.project_id, job.archme_branch)
19738
+ };
19739
+ }
19740
+ function hostedJobReads(post) {
19741
+ let prefetch = null;
19742
+ let outcome = null;
19743
+ const pre = () => prefetch ??= post("prefetch");
19744
+ const out = () => outcome ??= post("outcome");
19745
+ return {
19746
+ projectSlug: async () => {
19747
+ const slug = (await pre()).project_slug;
19748
+ if (typeof slug !== "string") throw new Error("job-context returned no project slug");
19749
+ return slug;
19750
+ },
19751
+ marker: async () => (await pre()).marker,
19752
+ repoMappings: async (log) => {
19753
+ const mappings = (await pre()).repo_mappings;
19754
+ if (mappings === null) log("repo mappings unreadable (proceeding without repo scoping)");
19755
+ return mappings;
19756
+ },
19757
+ seedArchitecture: async () => {
19758
+ const p = await pre();
19759
+ if (!p.seed_architecture) throw new Error(p.seed_error ?? "seed architecture unavailable");
19760
+ return p.seed_architecture;
19761
+ },
19762
+ bugsFolderId: async () => (await pre()).bugs_folder_id,
19763
+ // Fails closed, unlike the session readers: a platform job has no session read to fall back on.
19764
+ writesByPhase: async () => {
19765
+ const { total, diff, structural } = (await out()).writes;
19766
+ return { total, diffWrites: diff, structuralWrites: structural };
19767
+ },
19768
+ mcpActivity: async () => {
19769
+ const { reads, no_change } = (await out()).mcp;
19770
+ return { reads: reads ?? void 0, noChange: no_change };
19771
+ },
19772
+ architectureCounts: async () => {
19773
+ const counts = (await out()).architecture_counts;
19774
+ if (!counts) throw new Error("job-context returned no architecture counts");
19775
+ return counts;
19776
+ }
19777
+ };
19778
+ }
19779
+ function jobReadsFor(deps, job) {
19780
+ return deps.isPlatformDevice ? hostedJobReads(deps.post) : sessionJobReads(deps.client, job);
19781
+ }
19782
+ var init_jobReads = __esm({
19783
+ "src/daemon/jobReads.ts"() {
19784
+ "use strict";
19785
+ init_architectureCounts2();
19786
+ init_affected();
19787
+ init_bugsFolder();
19788
+ init_branchMarker();
19789
+ init_repoMappings();
19790
+ }
19791
+ });
19792
+
19649
19793
  // src/daemon/sandboxJob/result.ts
19650
19794
  import { readFile as readFile13 } from "node:fs/promises";
19651
19795
  function isValidNonce(nonce) {
@@ -19773,7 +19917,7 @@ var init_result2 = __esm({
19773
19917
  });
19774
19918
 
19775
19919
  // src/daemon/sandbox/inputDir.ts
19776
- 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";
19777
19921
  import { tmpdir as tmpdir3 } from "node:os";
19778
19922
  import { join as join22 } from "node:path";
19779
19923
  async function writeInputDir(step, input, base = tmpdir3()) {
@@ -19781,7 +19925,7 @@ async function writeInputDir(step, input, base = tmpdir3()) {
19781
19925
  const cleanup = () => rm7(dir, { recursive: true, force: true }).catch(() => {
19782
19926
  });
19783
19927
  try {
19784
- await chmod4(dir, 448);
19928
+ await chmod5(dir, 448);
19785
19929
  await writeFile10(join22(dir, "input.json"), JSON.stringify(input), { mode: 384, flag: "wx" });
19786
19930
  } catch (e) {
19787
19931
  await cleanup();
@@ -19993,7 +20137,7 @@ var init_confinement = __esm({
19993
20137
 
19994
20138
  // src/daemon/sandbox/claudeToken.ts
19995
20139
  import { constants as fsConstants } from "node:fs";
19996
- 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";
19997
20141
  import { dirname as dirname15 } from "node:path";
19998
20142
  function assertTokenShape(raw, where) {
19999
20143
  const token = raw.trim();
@@ -20066,7 +20210,7 @@ async function storeSandboxClaudeToken(raw, opts) {
20066
20210
  } finally {
20067
20211
  await fh.close();
20068
20212
  }
20069
- await chmod5(opts.path, 384);
20213
+ await chmod6(opts.path, 384);
20070
20214
  return { source: "file", note };
20071
20215
  }
20072
20216
  async function checkClaudeToken(token, fetchFn = fetch) {
@@ -20492,24 +20636,28 @@ var init_runSandboxedJob = __esm({
20492
20636
 
20493
20637
  // src/daemon/sandbox/classifySandboxExit.ts
20494
20638
  function classifySandboxExit(input) {
20495
- const { counters, exitCode, stderr, stepFailure } = input;
20496
- 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) {
20497
20641
  return {
20498
20642
  kind: "terminal",
20499
20643
  reason: SANDBOX_REQUEST_CAP_REASON,
20500
20644
  message: `${SANDBOX_REQUEST_CAP_REASON}: the job exceeded the sandbox model-request cap` + (counters ? ` (${counters.model_requests} requests, ${counters.cap_hits} refused)` : "")
20501
20645
  };
20502
20646
  }
20503
- if (stepFailure == null && exitCode !== null && exitCode !== 0 && counters?.last_upstream_status === 429) {
20504
- 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" };
20505
20651
  }
20506
20652
  return { kind: "normal" };
20507
20653
  }
20508
20654
  function routeSandboxFailure(verdict, planFor, nowMs = Date.now()) {
20509
- if (verdict?.kind === "terminal") return { kind: "terminal", message: verdict.message };
20510
- const plan = planFor();
20511
- if (plan) return { kind: "defer", plan };
20512
- 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") {
20513
20661
  return {
20514
20662
  kind: "defer",
20515
20663
  plan: { parsed: false, pauseUntil: new Date(nowMs + RATE_LIMIT_TENTATIVE_PAUSE_MS), info: null }
@@ -20522,7 +20670,6 @@ var init_classifySandboxExit = __esm({
20522
20670
  "src/daemon/sandbox/classifySandboxExit.ts"() {
20523
20671
  "use strict";
20524
20672
  init_rateLimit();
20525
- init_types();
20526
20673
  SANDBOX_REQUEST_CAP_REASON = "sandbox_request_cap";
20527
20674
  SandboxExitError = class extends Error {
20528
20675
  constructor(message2, verdict) {
@@ -22248,9 +22395,9 @@ var init_run_scan = __esm({
22248
22395
  init_transcript_signals();
22249
22396
  init_report();
22250
22397
  init_github_remote();
22251
- init_architectureCounts();
22398
+ init_architectureCounts2();
22252
22399
  init_claimDefaultProject();
22253
- init_architectureCounts();
22400
+ init_architectureCounts2();
22254
22401
  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.`;
22255
22402
  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.
22256
22403
 
@@ -22376,15 +22523,15 @@ async function runCloneStep(input, deps = {}) {
22376
22523
  }
22377
22524
  }
22378
22525
  function applyEnv(env) {
22379
- const saved = /* @__PURE__ */ new Map();
22526
+ const saved2 = /* @__PURE__ */ new Map();
22380
22527
  for (const [k, v] of Object.entries(env)) {
22381
22528
  if (process.env[k] === v) continue;
22382
- saved.set(k, process.env[k]);
22529
+ saved2.set(k, process.env[k]);
22383
22530
  if (v === void 0) delete process.env[k];
22384
22531
  else process.env[k] = v;
22385
22532
  }
22386
22533
  return () => {
22387
- for (const [k, v] of saved) {
22534
+ for (const [k, v] of saved2) {
22388
22535
  if (v === void 0) delete process.env[k];
22389
22536
  else process.env[k] = v;
22390
22537
  }
@@ -22528,7 +22675,9 @@ async function runAgentStep(input, deps = {}) {
22528
22675
  cwd: repoDir,
22529
22676
  timeoutMs: input.timeoutMs,
22530
22677
  model: input.model,
22531
- ...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"] },
22532
22681
  jsonOutput: true,
22533
22682
  settingSources: "user",
22534
22683
  // A box is Linux: a prompt over 128 KiB cannot be one argv string.
@@ -22678,7 +22827,7 @@ __export(run_exports, {
22678
22827
  });
22679
22828
  import { createClient as createClient2 } from "@supabase/supabase-js";
22680
22829
  import { randomUUID as randomUUID3 } from "node:crypto";
22681
- 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";
22682
22831
  import { tmpdir as tmpdir4 } from "node:os";
22683
22832
  import { join as join25 } from "node:path";
22684
22833
  import { homedir as homedir8, networkInterfaces as networkInterfaces2 } from "node:os";
@@ -22739,13 +22888,6 @@ async function routeShutdownInterrupt(id, cancelled2, fail2, exitCode) {
22739
22888
  console.error(` shutdown retry-route failed for ${id}: ${e instanceof Error ? e.message : e}`);
22740
22889
  }
22741
22890
  }
22742
- async function lookupProjectSlug(client, projectId) {
22743
- const { data, error } = await client.from("projects").select("slug, organizations(slug)").eq("id", projectId).single();
22744
- if (error || !data) throw new Error(`Project ${projectId} not found: ${error?.message}`);
22745
- const orgSlug = data.organizations?.slug;
22746
- if (!orgSlug) throw new Error(`Project ${projectId} has no org slug`);
22747
- return `${orgSlug}/${data.slug}`;
22748
- }
22749
22891
  function cloneDirFor(baseDir, owner, name) {
22750
22892
  return join25(baseDir, owner, name);
22751
22893
  }
@@ -22857,7 +22999,7 @@ async function buildGitEnv(credentials) {
22857
22999
  ].join("\n"),
22858
23000
  { mode: 448 }
22859
23001
  );
22860
- await chmod6(askpassPath, 448).catch(() => {
23002
+ await chmod7(askpassPath, 448).catch(() => {
22861
23003
  });
22862
23004
  const gitConfigEntries = [
22863
23005
  ["credential.helper", ""],
@@ -22891,24 +23033,24 @@ async function ensureLocalCloneAndFetch(job, entry) {
22891
23033
  `local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
22892
23034
  );
22893
23035
  }
22894
- 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 });
22895
23037
  if (job.source_mode === "pr_update") {
22896
23038
  await verifyLocalPrJob(job, async (args) => {
22897
- const r = await execa("git", args, { cwd: dir, reject: false });
23039
+ const r = await execa("git", args, { cwd: dir, env: localGitEnv(), reject: false });
22898
23040
  return { exitCode: r.exitCode ?? 1, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") };
22899
23041
  });
22900
23042
  return;
22901
23043
  }
22902
- 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 });
22903
23045
  if (afterCheck.exitCode !== 0) {
22904
- await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
23046
+ await execa("git", ["fetch", "origin"], { cwd: dir, env: localGitEnv(), reject: false });
22905
23047
  }
22906
23048
  }).catch((err4) => {
22907
23049
  if (err4 instanceof LocalPrJobRejectedError) throw err4;
22908
23050
  const msg = err4 instanceof Error ? err4.message : String(err4);
22909
23051
  throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
22910
23052
  });
22911
- return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
23053
+ return { dir, gitEnv: localGitEnv(), cleanupGitEnv: async () => {
22912
23054
  } };
22913
23055
  }
22914
23056
  async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
@@ -23221,30 +23363,6 @@ async function rpcComplete(ctx, jobId, deviceId, summary2, kind, changed, metric
23221
23363
  function affectedFieldForComplete(sourceMode, affected) {
23222
23364
  return sourceMode === "pr_update" && affected ? affected : void 0;
23223
23365
  }
23224
- async function detectWritesByPhase(client, projectId, archmeBranch, jobId) {
23225
- const { data, error } = await client.from("commits").select("structural_changeset").eq("project_id", projectId).eq("branch", archmeBranch).eq("metadata->source->>job_id", jobId);
23226
- if (error) {
23227
- console.error("commits lookup error:", error.message);
23228
- return { total: 0, diffWrites: 0, structuralWrites: 0 };
23229
- }
23230
- let diffWrites = 0;
23231
- let structuralWrites = 0;
23232
- for (const row of data ?? []) {
23233
- if (row.structural_changeset == null) diffWrites++;
23234
- else structuralWrites++;
23235
- }
23236
- return { total: diffWrites + structuralWrites, diffWrites, structuralWrites };
23237
- }
23238
- async function detectMcpActivity(client, jobId) {
23239
- const { data, error } = await client.from("archme_jobs").select("meaningful_tool_calls, no_change_reason").eq("id", jobId).maybeSingle();
23240
- if (error) {
23241
- console.error("mcp_activity lookup error:", error.message);
23242
- return { reads: void 0, noChange: false };
23243
- }
23244
- if (!data) return { reads: void 0, noChange: false };
23245
- const row = data;
23246
- return { reads: row.meaningful_tool_calls, noChange: row.no_change_reason != null };
23247
- }
23248
23366
  function setLocalPause(untilMs) {
23249
23367
  if (!Number.isFinite(untilMs) || untilMs <= Date.now()) return;
23250
23368
  if (pauseUntilMs != null && pauseUntilMs >= untilMs) return;
@@ -23518,11 +23636,36 @@ async function setupDigestMcp(ctx, job, deviceId) {
23518
23636
  return {};
23519
23637
  }
23520
23638
  }
23639
+ function readsFor(ctx, job, deviceId) {
23640
+ return jobReadsFor(
23641
+ { client: ctx.supabase, post: (part) => postJobContext(ctx, job.id, deviceId, part), isPlatformDevice: platformDevices.has(deviceId) },
23642
+ job
23643
+ );
23644
+ }
23645
+ async function postJobContext(ctx, jobId, deviceId, part) {
23646
+ const response = await fetchWithAuthRetry(
23647
+ "job-context",
23648
+ () => {
23649
+ const accessToken = ctx.auth.getAccessToken();
23650
+ if (!accessToken) throw new Error("daemon is not signed in with a registered device");
23651
+ return fetch(`${ctx.supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/job-context`, {
23652
+ method: "POST",
23653
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
23654
+ body: JSON.stringify({ device_id: deviceId, job_id: jobId, part })
23655
+ });
23656
+ },
23657
+ ctx.reLogin
23658
+ );
23659
+ const body = await parseJsonResponse(response);
23660
+ if (!response.ok) throw new Error(`job-context ${part} failed: ${responseError(body, `status ${response.status}`)}`);
23661
+ return body;
23662
+ }
23521
23663
  async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoCredentials) {
23522
23664
  const log = (msg) => console.log(`[${job.id}] ${msg}`);
23523
23665
  const firstScan = job.job_kind === "first_scan";
23524
23666
  assertLocalJobShape(firstScan ? { ...job, after_sha: job.scan_ref ?? "" } : job);
23525
- const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
23667
+ const reads = readsFor(ctx, job, deviceId);
23668
+ const projectSlug = await reads.projectSlug();
23526
23669
  const jobToken = await mintJobToken(ctx, job.id, deviceId);
23527
23670
  const probe = await probeMcpAuth(
23528
23671
  `${ctx.mcpBaseUrl}?project=${encodeURIComponent(projectSlug)}&branch=${encodeURIComponent(job.archme_branch)}`,
@@ -23533,15 +23676,15 @@ async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoC
23533
23676
  `MCP auth preflight failed after ${probe.attempts} attempt(s) (last status ${probe.lastStatus ?? "n/a"}): ${probe.detail}. Not starting the sandbox \u2014 failing the job so it retries with a freshly minted token.`
23534
23677
  );
23535
23678
  }
23536
- const marker = job.source_mode === "pr_update" || firstScan ? { sha: null, repo: null } : await readBranchMarker(ctx.supabase, job.project_id, job.archme_branch, `${job.repo_owner}/${job.repo_name}`);
23679
+ const marker = job.source_mode === "pr_update" || firstScan ? { sha: null, repo: null } : await reads.marker(`${job.repo_owner}/${job.repo_name}`);
23537
23680
  const jobRepo = `${job.repo_owner}/${job.repo_name}`;
23538
- const mappings = await loadRepoMappings(ctx.supabase, job.project_id, log);
23681
+ const mappings = await reads.repoMappings(log);
23539
23682
  let jobScope = jobRepoScope(mappings, jobRepo);
23540
23683
  if (!jobScope) log("repo mappings unreadable: proceeding unseeded");
23541
23684
  let seedArch = null;
23542
23685
  if (mappings && jobScope && !firstScan) {
23543
23686
  try {
23544
- seedArch = await loadArchitectureForSeeding(ctx.supabase, job.project_id, job.archme_branch);
23687
+ seedArch = await reads.seedArchitecture();
23545
23688
  if (job.source_mode === "pr_update") {
23546
23689
  jobScope = forkRepoScope(mappings, jobRepo, new Set(seedArch.components.map((c3) => c3.path)));
23547
23690
  }
@@ -23549,7 +23692,7 @@ async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoC
23549
23692
  log(`seed architecture unavailable (the agent runs unseeded): ${e.message}`);
23550
23693
  }
23551
23694
  }
23552
- const bugsFolderId = await ensureBugsFolder(ctx.supabase, job.project_id);
23695
+ const bugsFolderId = await reads.bugsFolderId();
23553
23696
  const model = await resolveRunnableModel(ctx.supabase, { warn: log });
23554
23697
  const credentials = await repoCredentials(ctx, job.id, deviceId);
23555
23698
  return {
@@ -23594,8 +23737,9 @@ async function completeClaudeRun(ctx, job, deviceId, run) {
23594
23737
  throw new ClaudeRunError(
23595
23738
  `claude reported is_error in its JSON result (no successful completion)`,
23596
23739
  runResult.summary.trim().slice(-FAILURE_DETAIL_MAX) || null,
23597
- isClaudeAuthFailure(claudeErrorResult(run.stdout), run.stderr),
23598
- 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
23599
23743
  );
23600
23744
  }
23601
23745
  const summary2 = runResult.summary.trim().slice(0, 4e3);
@@ -23603,8 +23747,9 @@ async function completeClaudeRun(ctx, job, deviceId, run) {
23603
23747
  metrics.model_served = runResult.modelServed;
23604
23748
  metrics.usage = runResult.usage;
23605
23749
  metrics.cost_usd = runResult.costUsd;
23606
- const { total: writes, diffWrites, structuralWrites } = await detectWritesByPhase(ctx.supabase, job.project_id, job.archme_branch, job.id);
23607
- const { reads, noChange } = await detectMcpActivity(ctx.supabase, job.id);
23750
+ const jobReads = run.reads ?? readsFor(ctx, job, deviceId);
23751
+ const { total: writes, diffWrites, structuralWrites } = await jobReads.writesByPhase();
23752
+ const { reads, noChange } = await jobReads.mcpActivity();
23608
23753
  markPhase("write_detect", {
23609
23754
  commits: writes,
23610
23755
  diff_writes: diffWrites,
@@ -23860,7 +24005,12 @@ async function runJob(ctx, job, claimDeviceId) {
23860
24005
  await rpcRollback(ctx, job.id, deviceId);
23861
24006
  return;
23862
24007
  }
23863
- 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
+ });
23864
24014
  if (verdict.kind === "terminal") throw new SandboxExitError(verdict.message, verdict);
23865
24015
  if (outcome.exitCode !== 0) {
23866
24016
  const detail = claudeFailureDetail(outcome.stderr, outcome.claudeStdout);
@@ -23868,13 +24018,24 @@ async function runJob(ctx, job, claimDeviceId) {
23868
24018
  throw new ClaudeRunError(
23869
24019
  buildClaudeExitMessage(outcome.exitCode, outcome.signal ? ` (${outcome.signal})` : "", outcome.stderr),
23870
24020
  detail,
23871
- 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",
23872
24023
  claudeErrorDetails(outcome.claudeStdout),
23873
24024
  verdict
23874
24025
  );
23875
24026
  }
24027
+ const jobReads = readsFor(ctx, job, deviceId);
23876
24028
  if (firstScan) {
23877
- const gate = firstScanGate(await countArchitecture(ctx.supabase, job.project_id, job.archme_branch));
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
+ }
24038
+ const gate = firstScanGate(await jobReads.architectureCounts());
23878
24039
  if (!gate.ok) throw new Error(gate.reason);
23879
24040
  }
23880
24041
  await completeClaudeRun(ctx, job, deviceId, {
@@ -23885,7 +24046,9 @@ async function runJob(ctx, job, claimDeviceId) {
23885
24046
  affected: outcome.affected,
23886
24047
  startedAt,
23887
24048
  markPhase,
23888
- log
24049
+ log,
24050
+ reads: jobReads,
24051
+ sandboxVerdict: verdict
23889
24052
  });
23890
24053
  return;
23891
24054
  }
@@ -24155,8 +24318,9 @@ async function runJob(ctx, job, claimDeviceId) {
24155
24318
  const runErr = err4 instanceof ClaudeRunError ? err4 : null;
24156
24319
  const haystack = stripPromptEcho(`${msg}
24157
24320
  ${logs.stderr}`, sentPrompt);
24321
+ const boxed = boxedJobs.has(job.id);
24158
24322
  const route2 = routeSandboxFailure(
24159
- err4 instanceof SandboxExitError ? err4.verdict : runErr?.sandboxVerdict ?? null,
24323
+ err4 instanceof SandboxExitError ? err4.verdict : runErr?.sandboxVerdict ?? (boxed ? { kind: "normal" } : null),
24160
24324
  () => rateLimitDeferPlanForExit({
24161
24325
  message: msg,
24162
24326
  stderr: logs.stderr,
@@ -24173,7 +24337,7 @@ ${logs.stderr}`, sentPrompt);
24173
24337
  const plan = route2.kind === "defer" ? route2.plan : null;
24174
24338
  if (plan) {
24175
24339
  const { parsed, pauseUntil, info } = plan;
24176
- if (!parsed) {
24340
+ if (!parsed && !boxed) {
24177
24341
  await rpcReportEvent(ctx, job, deviceId, "rate_limit_unmatched_exit", {
24178
24342
  job_id: job.id,
24179
24343
  excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
@@ -24205,8 +24369,12 @@ ${logs.stderr}`, sentPrompt);
24205
24369
  const notice = authFailureNotice(boxedJobs.has(job.id), runErr.detail);
24206
24370
  console.error(`[${job.id}] ${notice.message} Deferring job + pausing daemon until ${pauseUntil.toISOString()}.`);
24207
24371
  const detail = { job_id: job.id, claude_said: runErr.detail };
24208
- await rpcSetDaemonPause(ctx, pauseUntil, notice.reason, detail);
24209
- 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
+ }
24210
24378
  const deferred = await rpcDefer(ctx, job.id, deviceId, notice.reason, detail);
24211
24379
  if (deferred) return;
24212
24380
  console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
@@ -24401,6 +24569,7 @@ function exitDaemon(code) {
24401
24569
  process.exit(resolveExitCode(code, process.env));
24402
24570
  }
24403
24571
  async function runDaemon() {
24572
+ detachSshAgent();
24404
24573
  let lockHandle = null;
24405
24574
  try {
24406
24575
  lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
@@ -24766,14 +24935,16 @@ var init_run = __esm({
24766
24935
  init_localPrJob();
24767
24936
  init_localPoller();
24768
24937
  init_setting();
24769
- init_architectureCounts();
24938
+ init_architectureCounts2();
24770
24939
  init_imageManifest();
24771
24940
  init_claimStance();
24772
24941
  init_platformDevices();
24942
+ init_jobReads();
24773
24943
  init_claudeVersion();
24774
24944
  init_host();
24775
24945
  init_runSandboxedJob();
24776
24946
  init_inputDir();
24947
+ init_sshAgent();
24777
24948
  init_classifySandboxExit();
24778
24949
  HEARTBEAT_INTERVAL_MS = 6e4;
24779
24950
  CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -30597,7 +30768,7 @@ async function runConciergeToken(opts, deps) {
30597
30768
  async function reserveStdout(fn) {
30598
30769
  const stdout = process.stdout;
30599
30770
  const realWrite = stdout.write;
30600
- const saved = { log: console.log, info: console.info, debug: console.debug };
30771
+ const saved2 = { log: console.log, info: console.info, debug: console.debug };
30601
30772
  const toStderr = (...args) => console.error(...args);
30602
30773
  console.log = toStderr;
30603
30774
  console.info = toStderr;
@@ -30609,9 +30780,9 @@ async function reserveStdout(fn) {
30609
30780
  });
30610
30781
  } finally {
30611
30782
  stdout.write = realWrite;
30612
- console.log = saved.log;
30613
- console.info = saved.info;
30614
- console.debug = saved.debug;
30783
+ console.log = saved2.log;
30784
+ console.info = saved2.info;
30785
+ console.debug = saved2.debug;
30615
30786
  }
30616
30787
  }
30617
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.16.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:3f8b289540503cc18a2794fa9de858e75a4ceb2d3cf709d814cf816569c36a96"
3
+ "digest": "sha256:bedd452cb1fb7f0474d06296b8033b426c4a808e762ee61c9043b19d723fb43b"
4
4
  }