@archme/cli 0.11.1 → 0.11.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 +311 -75
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13127,6 +13127,21 @@ Everything a reader sees in a sequence \u2014 arrow labels, Note text, and the c
13127
13127
  }
13128
13128
  });
13129
13129
 
13130
+ // ../shared/waypoints.ts
13131
+ var init_waypoints = __esm({
13132
+ "../shared/waypoints.ts"() {
13133
+ }
13134
+ });
13135
+
13136
+ // ../shared/leafEndpoints.ts
13137
+ var LEAF_ENDPOINT_GUIDANCE;
13138
+ var init_leafEndpoints = __esm({
13139
+ "../shared/leafEndpoints.ts"() {
13140
+ init_waypoints();
13141
+ LEAF_ENDPOINT_GUIDANCE = `- **Arrows connect leaves.** Name the component that does the work, never the parent it sits in. A component with children (other than its input/output interface ports) is a group: an arrow to it lands on the group's box and can't be followed inside, while an arrow to a leaf still draws on the parent's box whenever the parent is collapsed. If a parent's own code acts, model that code as one more child and name that child. An arrow between a component and one of its own children is always this mistake.`;
13142
+ }
13143
+ });
13144
+
13130
13145
  // ../shared/componentOrganization.ts
13131
13146
  var COMPONENT_ORGANIZATION_GUIDANCE;
13132
13147
  var init_componentOrganization = __esm({
@@ -13181,9 +13196,10 @@ ${draftBlock}Sequence diagram format:
13181
13196
  - Use "as" aliases for display names: participant root.api.gateway as API Gateway
13182
13197
  - Supports combined fragments: alt/else, opt, par/and, loop, critical/option, break
13183
13198
  - All participant references MUST be absolute paths prefixed with "root." (e.g., root.backend.api, never backend.api). Scope-relative paths are rejected on write \u2014 they break move_architecture's path-rewriter and the UML grouping logic.
13199
+ ${LEAF_ENDPOINT_GUIDANCE}
13184
13200
  - Example: "sequenceDiagram\\n root.mobile_user->>root.client: opens app\\n root.client->>root.api.post_users: POST /users\\n root.api.post_users->>root.db: Save new user\\n root.db-->>root.api.post_users: user record\\n root.api.post_users-->>root.client: Account created"
13185
- - With branching: "sequenceDiagram\\n root.client->>root.api: POST /upload\\n alt File accepted\\n root.api-->>root.client: Upload saved\\n else Wrong type or too large\\n root.api-->>root.client: Upload rejected\\n end"
13186
- - With parallelism: "sequenceDiagram\\n root.api->>root.db: Record the upload\\n par\\n root.api->>root.storage: Store the file\\n and\\n root.api->>root.search: Make it searchable\\n end"
13201
+ - With branching: "sequenceDiagram\\n root.client->>root.api.upload: POST /upload\\n alt File accepted\\n root.api.upload-->>root.client: Upload saved\\n else Wrong type or too large\\n root.api.upload-->>root.client: Upload rejected\\n end"
13202
+ - With parallelism: "sequenceDiagram\\n root.api.upload->>root.db: Record the upload\\n par\\n root.api.upload->>root.storage: Store the file\\n and\\n root.api.upload->>root.search: Make it searchable\\n end"
13187
13203
 
13188
13204
  ${SEQUENCE_WORDING_GUIDANCE}
13189
13205
 
@@ -13221,6 +13237,7 @@ var init_scanPrompts = __esm({
13221
13237
  init_storyNaming();
13222
13238
  init_storyChapterGuidance();
13223
13239
  init_sequenceWording();
13240
+ init_leafEndpoints();
13224
13241
  init_componentOrganization();
13225
13242
  BUILD_METHODOLOGY = `You are a build sub-agent. You are part of a multi-agent system that builds a comprehensive visual architecture of a codebase \u2014 components, connections, and user stories that trace data flow end-to-end. Your role is to translate research findings into actual architecture: creating components backed by real code references, wiring connections between them, and building story sequence diagrams that trace how data flows through the system.
13226
13243
 
@@ -13229,7 +13246,7 @@ var init_scanPrompts = __esm({
13229
13246
  - Only build what the research findings and code evidence support. Do not invent components, connections, or user stories that aren't backed by real code. If findings are insufficient, flag gaps rather than guessing.
13230
13247
  - Every connection MUST have a non-empty method
13231
13248
  - Keep component hierarchies nested (e.g., backend > api > routes)
13232
- - Use at: "parent" to target nested scopes for connectionUpdates
13249
+ - Name connectionUpdates endpoints with "root."-prefixed paths; each connection is stored where its endpoints meet
13233
13250
  - External services must be created as components with \`role: "external"\` (and \`kind: "human"\` for human actors), then referenced by dot-path in sequences like any other component
13234
13251
  - Small utility modules are implementation details \u2014 omit unless architecturally significant
13235
13252
  - Components must exist before non-draft stories can reference them. Add components first, then wire stories.
@@ -13341,8 +13358,9 @@ Component annotations:
13341
13358
  - Example: { id: "user-org", from: "user", to: "org", method: "belongs_to", reference: true }
13342
13359
 
13343
13360
  Connection format:
13344
- - Use dot-paths to target interface children: "api_server.post_users"
13345
- - ID format: "from-to" (e.g., "client-api_server.post_users")
13361
+ - Name both endpoints with "root."-prefixed dot-paths, as in sequences (or paths relative to the entry's "at"); interface children too: "root.api_server.post_users"
13362
+ - You don't choose the scope: each connection is stored where its endpoints' parents meet, and the result says where and under which id \u2014 use that "at" + id to remove it
13363
+ - id is optional: omitted, it is minted as "<from>-><to>"
13346
13364
  - method describes how components communicate (e.g., REST, gRPC, GraphQL, WebSocket, Kafka, function, event)`;
13347
13365
  FORMAT_REF_DRAFT_STORIES_BLOCK = `Draft stories:
13348
13366
  - Set draft: true for stories where the architecture isn't built yet
@@ -13571,6 +13589,16 @@ var init_validationCodes = __esm({
13571
13589
  // ──────────────────────────────────────────────────────────────────────────
13572
13590
  /** componentUpdates[].remove references an id that doesn't exist at scope. */
13573
13591
  COMPONENT_REMOVE_ID_NOT_FOUND: "component.remove.id-not-found",
13592
+ /**
13593
+ * WARNING (non-blocking): this write removed components, so the connections
13594
+ * naming them (or their descendants), in any scope, were removed with them.
13595
+ */
13596
+ COMPONENT_REMOVE_CONNECTIONS_REMOVED: "component.remove.connections-removed",
13597
+ /**
13598
+ * WARNING (non-blocking): this write removed components that story sequences
13599
+ * still name. The sequences are left as they are for the agent to fix.
13600
+ */
13601
+ COMPONENT_REMOVE_STORY_STILL_REFERENCES: "component.remove.story-still-references",
13574
13602
  // ──────────────────────────────────────────────────────────────────────────
13575
13603
  // connection.add
13576
13604
  // ──────────────────────────────────────────────────────────────────────────
@@ -13584,9 +13612,16 @@ var init_validationCodes = __esm({
13584
13612
  CONNECTION_ADD_MISSING_TO: "connection.add.missing-to",
13585
13613
  /** Missing or empty `method`. */
13586
13614
  CONNECTION_ADD_MISSING_METHOD: "connection.add.missing-method",
13587
- /** Connection emitted at a parent scope outside the current scope. */
13615
+ /**
13616
+ * @deprecated No longer emitted: a connection add is stored where its
13617
+ * endpoints meet (`resolveConnectionUpdates`), whatever scope it names.
13618
+ * Connection emitted at a parent scope outside the current scope.
13619
+ */
13588
13620
  CONNECTION_AT_WRONG_SCOPE_OUTSIDE: "connection.add.wrong-scope-outside",
13589
- /** Connection internal to a child but added at the parent level. */
13621
+ /**
13622
+ * @deprecated No longer emitted, as above.
13623
+ * Connection internal to a child but added at the parent level.
13624
+ */
13590
13625
  CONNECTION_AT_WRONG_SCOPE_INTERNAL: "connection.add.wrong-scope-internal",
13591
13626
  // ──────────────────────────────────────────────────────────────────────────
13592
13627
  // connection.remove
@@ -13606,6 +13641,12 @@ var init_validationCodes = __esm({
13606
13641
  * One code, both sites: to an agent it's the same rule.
13607
13642
  */
13608
13643
  CONNECTION_PAIR_ALREADY_CONNECTED: "connection.add.pair-already-connected",
13644
+ /**
13645
+ * WARNING (non-blocking): an explicit `connectionUpdates.add` was NOT stored —
13646
+ * a finer edge on the same pair (one endpoint a child of this one's) already
13647
+ * says it, and the subsumption prune keeps only the finer edge.
13648
+ */
13649
+ CONNECTION_ADD_SUBSUMED_BY_CHILD: "connection.add.subsumed-by-child",
13609
13650
  // ──────────────────────────────────────────────────────────────────────────
13610
13651
  // story.add — story-add fields
13611
13652
  // ──────────────────────────────────────────────────────────────────────────
@@ -13696,6 +13737,14 @@ var init_validationCodes = __esm({
13696
13737
  PARTICIPANT_NOT_ROOT_PREFIXED: "sequence.participant.not-root-prefixed",
13697
13738
  /** Participant dot-path references a component that doesn't exist. */
13698
13739
  PARTICIPANT_COMPONENT_NOT_FOUND: "sequence.participant.component-not-found",
13740
+ /**
13741
+ * WARNING (non-blocking): an arrow in a sequence the write authored, or a
13742
+ * `connectionUpdates` add, ends on a component with children
13743
+ * (`LEAF_ENDPOINT_GUIDANCE`). The connection lands on the group's box instead
13744
+ * of the part that does the work. One code for both sites, as with
13745
+ * CONNECTION_PAIR_ALREADY_CONNECTED. Silent on a byte-identical re-send.
13746
+ */
13747
+ PARTICIPANT_HAS_CHILDREN: "sequence.participant.has-children",
13699
13748
  // ──────────────────────────────────────────────────────────────────────────
13700
13749
  // sequence.ref — validateSequenceRefs
13701
13750
  // ──────────────────────────────────────────────────────────────────────────
@@ -16540,7 +16589,19 @@ function stripPromptEcho(haystack, prompt) {
16540
16589
  function rateLimitDeferPlanForExit(channels, nowMs = Date.now(), defaultTimeZone = localTimeZone()) {
16541
16590
  const haystack = stripPromptEcho(`${channels.message}
16542
16591
  ${channels.stderr}`, channels.prompt);
16543
- return rateLimitDeferPlan(haystack, nowMs, defaultTimeZone);
16592
+ const plan = rateLimitDeferPlan(haystack, nowMs, defaultTimeZone);
16593
+ if (plan?.parsed) return plan;
16594
+ return claudeErrorDeferPlan(channels.claudeError, nowMs, defaultTimeZone) ?? plan;
16595
+ }
16596
+ function claudeErrorDeferPlan(error, nowMs, defaultTimeZone) {
16597
+ if (!error) return null;
16598
+ const text = error.text.trim();
16599
+ const parsed = rateLimitPauseUntil(text, nowMs, defaultTimeZone);
16600
+ const opensWithLimit = parsed != null && text.startsWith(parsed.info.matched);
16601
+ if (error.apiErrorStatus === 429 || error.apiErrorStatus == null && opensWithLimit) {
16602
+ return parsed ? { parsed: true, pauseUntil: parsed.pauseUntil, info: parsed.info } : { parsed: false, pauseUntil: new Date(nowMs + RATE_LIMIT_TENTATIVE_PAUSE_MS), info: null };
16603
+ }
16604
+ return null;
16544
16605
  }
16545
16606
  var RATE_LIMIT_RE, MONTHS, RATE_LIMIT_TENTATIVE_PAUSE_MS, DATED_RESET_ROLLOVER_GRACE_MS, RATE_LIMIT_PAUSE_PADDING_MS, RATE_LIMIT_SHAPED_RE;
16546
16607
  var init_rateLimit = __esm({
@@ -17230,6 +17291,16 @@ var init_defaultBranch = __esm({
17230
17291
  });
17231
17292
 
17232
17293
  // ../shared/prRefs.ts
17294
+ function parsePrRefs(message) {
17295
+ if (!message) return [];
17296
+ const refs = [...message.matchAll(PR_REF)].map((m) => Number(m[1]));
17297
+ const bare = refs.length ? null : bareMergePrNumber(message);
17298
+ return bare === null ? refs : [bare];
17299
+ }
17300
+ function lastPrRef(message) {
17301
+ const refs = parsePrRefs(message);
17302
+ return refs[refs.length - 1] ?? null;
17303
+ }
17233
17304
  function pushCommitSubject(message) {
17234
17305
  if (message == null) return null;
17235
17306
  const [subject = "", ...body] = message.split("\n");
@@ -17237,10 +17308,15 @@ function pushCommitSubject(message) {
17237
17308
  const title = body.find((line) => line.trim())?.trim();
17238
17309
  return pr && title && !TRAILER_LINE.test(title) ? `${title} (#${pr[1]})` : subject;
17239
17310
  }
17240
- var MERGE_PR_SUBJECT, TRAILER_LINE;
17311
+ function bareMergePrNumber(subject) {
17312
+ const pr = subject ? MERGE_PR_SUBJECT.exec(subject.trim()) : null;
17313
+ return pr ? Number(pr[1]) : null;
17314
+ }
17315
+ var PR_REF, MERGE_PR_SUBJECT, TRAILER_LINE;
17241
17316
  var init_prRefs = __esm({
17242
17317
  "../shared/prRefs.ts"() {
17243
17318
  init_repoContainer();
17319
+ PR_REF = /\(#(\d+)\)/g;
17244
17320
  MERGE_PR_SUBJECT = /^Merge pull request #(\d+) from \S+$/;
17245
17321
  TRAILER_LINE = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+:\s/;
17246
17322
  }
@@ -17615,6 +17691,87 @@ var init_localPrAdapters = __esm({
17615
17691
  }
17616
17692
  });
17617
17693
 
17694
+ // ../shared/pushSubject.ts
17695
+ async function resolvePushSubject(message, push, lookup, timeoutMs = PUSH_SUBJECT_TIMEOUT_MS) {
17696
+ const subject = pushCommitSubject(message);
17697
+ if (!subject) return subject;
17698
+ const bare = bareMergePrNumber(subject);
17699
+ if (bare === null && lastPrRef(subject) !== null) return subject;
17700
+ try {
17701
+ const found = await withTimeout(findPr(subject, bare, push, lookup), timeoutMs);
17702
+ return found ? `${found.title} (#${found.number})` : subject;
17703
+ } catch {
17704
+ return subject;
17705
+ }
17706
+ }
17707
+ async function findPr(subject, bare, push, lookup) {
17708
+ if (bare !== null) {
17709
+ const title = (await lookup.pullTitle(bare))?.trim();
17710
+ return title ? { number: bare, title } : null;
17711
+ }
17712
+ const pr = mergedPrForCommit(await lookup.pullsForCommit(push.sha), push);
17713
+ return pr ? { number: pr.number, title: pr.title?.trim() || subject } : null;
17714
+ }
17715
+ function mergedPrForCommit(prs, push) {
17716
+ const merged = prs.filter((p) => p.mergedAt && (!p.baseRef || p.baseRef === push.branch));
17717
+ const exact = merged.filter((p) => p.mergeCommitSha === push.sha);
17718
+ if (exact.length === 1) return exact[0] ?? null;
17719
+ return merged.length === 1 ? merged[0] ?? null : null;
17720
+ }
17721
+ function withTimeout(work, ms) {
17722
+ let timer;
17723
+ const expired = new Promise((_, reject) => {
17724
+ timer = setTimeout(() => reject(new Error("timed out")), ms);
17725
+ });
17726
+ return Promise.race([work, expired]).finally(() => clearTimeout(timer));
17727
+ }
17728
+ function githubPrLookup(opts) {
17729
+ const doFetch = opts.fetch ?? fetch;
17730
+ let tokens = null;
17731
+ async function get(path7) {
17732
+ tokens ??= opts.tokens();
17733
+ for (const token of await tokens) {
17734
+ const res = await doFetch(`${GITHUB_API}/repos/${opts.repo}${path7}`, {
17735
+ headers: {
17736
+ Accept: "application/vnd.github+json",
17737
+ "X-GitHub-Api-Version": "2022-11-28",
17738
+ "User-Agent": "archme",
17739
+ ...token ? { Authorization: `Bearer ${token}` } : {}
17740
+ }
17741
+ });
17742
+ if (REFUSED.has(res.status)) continue;
17743
+ return res.ok ? await res.json() : null;
17744
+ }
17745
+ return null;
17746
+ }
17747
+ return {
17748
+ async pullTitle(prNumber) {
17749
+ const pr = await get(`/pulls/${prNumber}`);
17750
+ return typeof pr?.title === "string" ? pr.title : null;
17751
+ },
17752
+ async pullsForCommit(sha) {
17753
+ const prs = await get(`/commits/${sha}/pulls`);
17754
+ if (!Array.isArray(prs)) return [];
17755
+ return prs.map((p) => ({
17756
+ number: Number(p.number),
17757
+ title: typeof p.title === "string" ? p.title : null,
17758
+ mergedAt: p.merged_at ?? null,
17759
+ mergeCommitSha: p.merge_commit_sha ?? null,
17760
+ baseRef: p.base?.ref ?? null
17761
+ }));
17762
+ }
17763
+ };
17764
+ }
17765
+ var PUSH_SUBJECT_TIMEOUT_MS, GITHUB_API, REFUSED;
17766
+ var init_pushSubject = __esm({
17767
+ "../shared/pushSubject.ts"() {
17768
+ init_prRefs();
17769
+ PUSH_SUBJECT_TIMEOUT_MS = 3e3;
17770
+ GITHUB_API = "https://api.github.com";
17771
+ REFUSED = /* @__PURE__ */ new Set([401, 403, 404]);
17772
+ }
17773
+ });
17774
+
17618
17775
  // src/daemon/localPoller.ts
17619
17776
  async function pollRepoOnce(entry, deps) {
17620
17777
  const repo = `${entry.repoOwner}/${entry.repoName}`;
@@ -17788,6 +17945,14 @@ function makePollerDeps(client, log, gateTimeoutMs) {
17788
17945
  },
17789
17946
  async enqueue(entry, row, beforeSha, afterSha) {
17790
17947
  const meta = await readCommitMeta(entry.mirrorPath, afterSha);
17948
+ const subject = await resolvePushSubject(
17949
+ meta.message || null,
17950
+ { sha: afterSha, branch: row.repoBranch },
17951
+ githubPrLookup({
17952
+ repo: `${entry.repoOwner}/${entry.repoName}`,
17953
+ tokens: async () => (await resolveGithubTokens()).map((t) => t.token)
17954
+ })
17955
+ );
17791
17956
  const { data, error } = await client.rpc("enqueue_local_archme_job", {
17792
17957
  p_watched_repo_id: entry.watchedRepoId,
17793
17958
  // From the ROW, not the registry: these decide which lane the job lands
@@ -17796,7 +17961,7 @@ function makePollerDeps(client, log, gateTimeoutMs) {
17796
17961
  p_archme_branch: row.archmeBranch,
17797
17962
  p_before_sha: beforeSha,
17798
17963
  p_after_sha: afterSha,
17799
- p_commit_message: meta.message || null,
17964
+ p_commit_message: subject,
17800
17965
  p_head_commit_timestamp: meta.committedAt,
17801
17966
  p_author_name: meta.authorName,
17802
17967
  // `author_login` is a GitHub concept with no git equivalent. The webhook
@@ -17823,6 +17988,7 @@ var init_localPoller = __esm({
17823
17988
  init_branchMarker();
17824
17989
  init_localGit();
17825
17990
  init_localPrAdapters();
17991
+ init_pushSubject();
17826
17992
  DEFAULT_POLL_INTERVAL_MS = 6e4;
17827
17993
  DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
17828
17994
  }
@@ -17840,16 +18006,20 @@ __export(run_exports, {
17840
18006
  buildClaudeExitMessage: () => buildClaudeExitMessage,
17841
18007
  buildDevices: () => buildDevices,
17842
18008
  buildDiffPrompt: () => buildDiffPrompt,
18009
+ claudeErrorDetails: () => claudeErrorDetails,
18010
+ claudeErrorResult: () => claudeErrorResult,
17843
18011
  claudeFailureDetail: () => claudeFailureDetail,
17844
18012
  computeDiff: () => computeDiff,
17845
18013
  forkRepoScope: () => forkRepoScope,
18014
+ isClaudeAuthFailure: () => isClaudeAuthFailure,
17846
18015
  parseClaudeResult: () => parseClaudeResult,
17847
18016
  parseJobTokenResponse: () => parseJobTokenResponse,
17848
18017
  probeMcpAuth: () => probeMcpAuth,
17849
18018
  readBranchMarker: () => readBranchMarker,
17850
18019
  routeShutdownInterrupt: () => routeShutdownInterrupt,
17851
18020
  runDaemon: () => runDaemon,
17852
- teeStream: () => teeStream
18021
+ teeStream: () => teeStream,
18022
+ withFailureDetail: () => withFailureDetail
17853
18023
  });
17854
18024
  import { createClient } from "@supabase/supabase-js";
17855
18025
  import { randomUUID } from "node:crypto";
@@ -17866,12 +18036,29 @@ function attributeKillCause(result, recorded) {
17866
18036
  function buildClaudeExitMessage(exitCode, causeSuffix, stderr) {
17867
18037
  return `claude exited ${exitCode}${causeSuffix}: ${stderr.slice(0, 4e3)}`;
17868
18038
  }
18039
+ function claudeErrorResult(stdout) {
18040
+ return claudeErrorDetails(stdout)?.text || null;
18041
+ }
18042
+ function claudeErrorDetails(stdout) {
18043
+ const parsed = parseClaudeResult(stdout);
18044
+ return parsed.parsed && parsed.isError ? { text: parsed.summary.trim(), apiErrorStatus: parsed.apiErrorStatus } : null;
18045
+ }
17869
18046
  function claudeFailureDetail(stderr, stdout) {
18047
+ const result = claudeErrorResult(stdout);
18048
+ if (result) return result.slice(-FAILURE_DETAIL_MAX);
17870
18049
  if (stderr.trim() || !stdout.trim()) return null;
17871
18050
  const parsed = parseClaudeResult(stdout);
17872
18051
  const text = (parsed.parsed ? parsed.summary : stdout).trim();
17873
18052
  return text ? text.slice(-FAILURE_DETAIL_MAX) : null;
17874
18053
  }
18054
+ function isClaudeAuthFailure(errorResult, stderr) {
18055
+ return /failed to authenticate|oauth session expired|not logged in|please run \/login/i.test(`${errorResult ?? ""}
18056
+ ${stderr}`);
18057
+ }
18058
+ function withFailureDetail(message, detail) {
18059
+ return detail ? `claude said: ${detail}
18060
+ ${message}` : message;
18061
+ }
17875
18062
  async function routeShutdownInterrupt(id, cancelled2, fail, exitCode) {
17876
18063
  if (cancelled2.has(id)) return;
17877
18064
  if (exitCode === 0) {
@@ -18625,10 +18812,11 @@ function parseClaudeResult(stdout) {
18625
18812
  usage: modelUsage ? sumModelUsage(modelUsage) : null,
18626
18813
  costUsd: typeof d.total_cost_usd === "number" ? d.total_cost_usd : null,
18627
18814
  isError: d.is_error === true,
18815
+ apiErrorStatus: typeof d.api_error_status === "number" ? d.api_error_status : null,
18628
18816
  parsed: true
18629
18817
  };
18630
18818
  } catch {
18631
- return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, parsed: false };
18819
+ return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, apiErrorStatus: null, parsed: false };
18632
18820
  }
18633
18821
  }
18634
18822
  async function setupDigestMcp(ctx, job, deviceId) {
@@ -18917,16 +19105,30 @@ async function runJob(ctx, job, claimDeviceId) {
18917
19105
  if (result.exitCode !== 0) {
18918
19106
  const causeSuffix = cause ? ` (${result.signal ?? "exit"}, cause: ${cause})` : "";
18919
19107
  const stderr = result.stderr ? String(result.stderr) : "";
18920
- const detail = claudeFailureDetail(stderr, result.stdout ? String(result.stdout) : "");
19108
+ const stdout = result.stdout ? String(result.stdout) : "";
19109
+ const detail = claudeFailureDetail(stderr, stdout);
18921
19110
  if (detail) log(`claude output: ${detail.replace(/\n/g, " ")}`);
18922
- throw new Error(buildClaudeExitMessage(result.exitCode, causeSuffix, stderr));
19111
+ throw new ClaudeRunError(
19112
+ buildClaudeExitMessage(result.exitCode, causeSuffix, stderr),
19113
+ detail,
19114
+ isClaudeAuthFailure(claudeErrorResult(stdout), stderr),
19115
+ claudeErrorDetails(stdout)
19116
+ );
18923
19117
  }
18924
19118
  const runResult = parseClaudeResult(result.stdout ? String(result.stdout) : "");
18925
19119
  if (!runResult.parsed) {
18926
19120
  log(`warning: claude --output-format json stdout was not a JSON result object; using raw text as summary`);
18927
19121
  }
18928
19122
  if (runResult.isError) {
18929
- throw new Error(`claude reported is_error in its JSON result (no successful completion)`);
19123
+ throw new ClaudeRunError(
19124
+ `claude reported is_error in its JSON result (no successful completion)`,
19125
+ runResult.summary.trim().slice(-FAILURE_DETAIL_MAX) || null,
19126
+ isClaudeAuthFailure(
19127
+ claudeErrorResult(result.stdout ? String(result.stdout) : ""),
19128
+ result.stderr ? String(result.stderr) : ""
19129
+ ),
19130
+ claudeErrorDetails(result.stdout ? String(result.stdout) : "")
19131
+ );
18930
19132
  }
18931
19133
  const summary2 = runResult.summary.trim().slice(0, 4e3);
18932
19134
  if (summary2) log(`claude> ${summary2.slice(0, 500).replace(/\n/g, " ")}`);
@@ -18983,20 +19185,22 @@ async function runJob(ctx, job, claimDeviceId) {
18983
19185
  return;
18984
19186
  }
18985
19187
  const msg = err4 instanceof Error ? err4.message : String(err4);
19188
+ const runErr = err4 instanceof ClaudeRunError ? err4 : null;
18986
19189
  const haystack = stripPromptEcho(`${msg}
18987
19190
  ${logs.stderr}`, sentPrompt);
18988
19191
  const plan = rateLimitDeferPlanForExit({
18989
19192
  message: msg,
18990
19193
  stderr: logs.stderr,
18991
19194
  stdout: logs.stdout,
18992
- prompt: sentPrompt
19195
+ prompt: sentPrompt,
19196
+ claudeError: runErr?.claudeError
18993
19197
  });
18994
19198
  if (plan) {
18995
19199
  const { parsed, pauseUntil, info } = plan;
18996
19200
  if (!parsed) {
18997
19201
  void rpcReportEvent(ctx, job.org_id, "rate_limit_unmatched_exit", {
18998
19202
  job_id: job.id,
18999
- excerpt: haystack.slice(0, 500)
19203
+ excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
19000
19204
  });
19001
19205
  }
19002
19206
  console.log(
@@ -19016,8 +19220,21 @@ ${logs.stderr}`, sentPrompt);
19016
19220
  if (deferred) return;
19017
19221
  console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
19018
19222
  }
19019
- console.log(`[${job.id}] failed: ${msg}`);
19020
- await rpcFail(ctx, job.id, deviceId, msg, metrics, logs);
19223
+ if (!plan && runErr?.authFailure) {
19224
+ const pauseUntil = new Date(Date.now() + AUTH_PAUSE_MS);
19225
+ console.error(
19226
+ `[${job.id}] CLAUDE CODE IS NOT AUTHENTICATED on this host (${runErr.detail ?? "auth failure"}). Run \`claude /login\` from this machine's DESKTOP session, not over SSH: on macOS an SSH login writes ~/.claude/.credentials.json while this daemon reads the keychain. Deferring job + pausing daemon until ${pauseUntil.toISOString()}.`
19227
+ );
19228
+ const detail = { job_id: job.id, claude_said: runErr.detail };
19229
+ await rpcSetDaemonPause(ctx, pauseUntil, "claude_auth_expired", detail);
19230
+ setLocalPause(pauseUntil.getTime());
19231
+ const deferred = await rpcDefer(ctx, job.id, deviceId, "claude_auth_expired", detail);
19232
+ if (deferred) return;
19233
+ console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
19234
+ }
19235
+ const failMessage = withFailureDetail(msg, runErr?.detail ?? null);
19236
+ console.log(`[${job.id}] failed: ${failMessage}`);
19237
+ await rpcFail(ctx, job.id, deviceId, failMessage, metrics, logs);
19021
19238
  } finally {
19022
19239
  clearInterval(heartbeat);
19023
19240
  inFlight.delete(job.id);
@@ -19422,7 +19639,7 @@ ${signal} received, shutting down`);
19422
19639
  process.on("SIGINT", () => shutdown("SIGINT"));
19423
19640
  process.on("SIGTERM", () => shutdown("SIGTERM"));
19424
19641
  }
19425
- var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, 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, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
19642
+ var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, AUTH_PAUSE_MS, ClaudeRunError, 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, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
19426
19643
  var init_run = __esm({
19427
19644
  "src/daemon/run.ts"() {
19428
19645
  "use strict";
@@ -19472,6 +19689,15 @@ var init_run = __esm({
19472
19689
  LOG_STDOUT_CAP_BYTES = 30 * 1024;
19473
19690
  LOG_STDERR_CAP_BYTES = 30 * 1024;
19474
19691
  FAILURE_DETAIL_MAX = 400;
19692
+ AUTH_PAUSE_MS = 15 * 60 * 1e3;
19693
+ ClaudeRunError = class extends Error {
19694
+ constructor(message, detail, authFailure, claudeError) {
19695
+ super(message);
19696
+ this.detail = detail;
19697
+ this.authFailure = authFailure;
19698
+ this.claudeError = claudeError;
19699
+ }
19700
+ };
19475
19701
  PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = CLAUDE_LARGE_DIFF_TIMEOUT_MS + 5 * 60 * 1e3;
19476
19702
  inFlight = /* @__PURE__ */ new Map();
19477
19703
  cancelled = /* @__PURE__ */ new Set();
@@ -22514,9 +22740,9 @@ var init_run_init = __esm({
22514
22740
  }
22515
22741
  });
22516
22742
 
22517
- // ../shared/waypoints.ts
22518
- var init_waypoints = __esm({
22519
- "../shared/waypoints.ts"() {
22743
+ // ../shared/connectionPair.ts
22744
+ var init_connectionPair = __esm({
22745
+ "../shared/connectionPair.ts"() {
22520
22746
  }
22521
22747
  });
22522
22748
 
@@ -22645,80 +22871,87 @@ function toAbsolute(endpoint, scopePath) {
22645
22871
  if (scopePath.length === 0) return "root." + endpoint;
22646
22872
  return "root." + scopePath.join(".") + "." + endpoint;
22647
22873
  }
22648
- function toRelative(absolute, scopePath) {
22649
- const prefix = scopePath.length === 0 ? "root." : "root." + scopePath.join(".") + ".";
22650
- if (absolute.startsWith(prefix)) return absolute.slice(prefix.length);
22651
- return absolute;
22652
- }
22653
- function rewriteAllConnections(arch, rewriter) {
22654
- const entries = [];
22655
- function walkConns(connections, scopePath) {
22874
+ function listAllConnections(arch) {
22875
+ const out = [];
22876
+ const walkConns = (connections, scopePath) => {
22656
22877
  for (const conn of connections) {
22657
- const absFrom = toAbsolute(conn.from, scopePath);
22658
- const absTo = toAbsolute(conn.to, scopePath);
22659
- const newAbsFrom = rewriter(absFrom);
22660
- const newAbsTo = rewriter(absTo);
22661
- const changed = newAbsFrom !== absFrom || newAbsTo !== absTo;
22662
- entries.push({
22878
+ out.push({
22663
22879
  conn,
22664
22880
  scopePath,
22665
- newFrom: newAbsFrom,
22666
- newTo: newAbsTo,
22667
- changed
22881
+ absFrom: toAbsolute(conn.from, scopePath),
22882
+ absTo: toAbsolute(conn.to, scopePath)
22668
22883
  });
22669
22884
  }
22670
- }
22671
- function walkComponents(components, basePath) {
22885
+ };
22886
+ const walkComponents = (components, basePath) => {
22672
22887
  for (const comp of components) {
22673
22888
  const compPath = [...basePath, comp.id];
22674
22889
  if (comp.connections) walkConns(comp.connections, compPath);
22675
22890
  if (comp.components) walkComponents(comp.components, compPath);
22676
22891
  }
22677
- }
22892
+ };
22678
22893
  walkConns(arch.connections, []);
22679
22894
  walkComponents(arch.components, []);
22680
- const changedCount = entries.filter((e) => e.changed).length;
22681
- if (changedCount === 0) return { arch, connectionsUpdated: 0 };
22895
+ return out;
22896
+ }
22897
+ function dropConnections(arch, doomed) {
22898
+ const byScope = /* @__PURE__ */ new Map();
22899
+ for (const { conn, scopePath } of doomed) {
22900
+ const key = scopePath.join(".");
22901
+ const ids = byScope.get(key) ?? /* @__PURE__ */ new Set();
22902
+ ids.add(conn.id);
22903
+ byScope.set(key, ids);
22904
+ }
22682
22905
  let result = arch;
22683
- const removeByScopeKey = /* @__PURE__ */ new Map();
22684
- for (const entry of entries) {
22685
- if (!entry.changed) continue;
22686
- const key = entry.scopePath.join(".");
22687
- if (!removeByScopeKey.has(key)) removeByScopeKey.set(key, /* @__PURE__ */ new Set());
22688
- removeByScopeKey.get(key).add(entry.conn.id);
22689
- }
22690
- for (const [scopeKey, connIds] of removeByScopeKey) {
22906
+ for (const [scopeKey, ids] of byScope) {
22691
22907
  const scopePath = scopeKey === "" ? [] : scopeKey.split(".");
22692
22908
  const nav = navigateToScope(result, scopePath);
22693
22909
  if (!nav) continue;
22694
22910
  result = replaceScopeAtPath(result, scopePath, {
22695
22911
  ...nav.scope,
22696
- connections: nav.scope.connections.filter((c3) => !connIds.has(c3.id))
22912
+ connections: nav.scope.connections.filter((c3) => !ids.has(c3.id))
22697
22913
  });
22698
22914
  }
22699
- for (const entry of entries) {
22700
- if (!entry.changed) continue;
22701
- const fromSegments = entry.newFrom.split(".");
22702
- const toSegments = entry.newTo.split(".");
22703
- const lcaSegments = findLCA(fromSegments, toSegments);
22704
- const lcaScopePath = lcaSegments.length > 1 ? lcaSegments.slice(1) : [];
22705
- const lcaNav = navigateToScope(result, lcaScopePath);
22706
- if (!lcaNav) continue;
22915
+ return result;
22916
+ }
22917
+ function connectionHome(absFrom, absTo) {
22918
+ const a2 = absFrom.split(".").slice(1);
22919
+ const b = absTo.split(".").slice(1);
22920
+ const scopePath = findLCA(a2.slice(0, -1), b.slice(0, -1));
22921
+ return { scopePath, from: a2.slice(scopePath.length).join("."), to: b.slice(scopePath.length).join(".") };
22922
+ }
22923
+ function rewriteAllConnections(arch, rewriter) {
22924
+ const changed = listAllConnections(arch).map((e) => ({ ...e, newFrom: rewriter(e.absFrom), newTo: rewriter(e.absTo) })).filter((e) => e.newFrom !== e.absFrom || e.newTo !== e.absTo);
22925
+ if (changed.length === 0) return { arch, connectionsUpdated: 0, notCarried: [] };
22926
+ let result = dropConnections(arch, changed);
22927
+ const notCarried = [];
22928
+ for (const entry of changed) {
22929
+ const home2 = connectionHome(entry.newFrom, entry.newTo);
22930
+ const nav = navigateToScope(result, home2.scopePath);
22931
+ if (!nav) continue;
22707
22932
  const newConn = {
22708
22933
  ...entry.conn,
22709
- from: toRelative(entry.newFrom, lcaScopePath),
22710
- to: toRelative(entry.newTo, lcaScopePath),
22711
- id: `${toRelative(entry.newFrom, lcaScopePath)}-${toRelative(entry.newTo, lcaScopePath)}`
22712
- };
22713
- const existingConns = lcaNav.scope.connections;
22714
- if (!existingConns.some((c3) => c3.id === newConn.id)) {
22715
- result = replaceScopeAtPath(result, lcaScopePath, {
22716
- ...lcaNav.scope,
22717
- connections: [...existingConns, newConn]
22718
- });
22934
+ from: home2.from,
22935
+ to: home2.to,
22936
+ id: `${home2.from}-${home2.to}`
22937
+ };
22938
+ const existingConns = nav.scope.connections;
22939
+ const taken = existingConns.find((c3) => c3.id === newConn.id);
22940
+ if (taken) {
22941
+ notCarried.push(
22942
+ `connection \`${entry.conn.id}\` (${newConn.from} \u2192 ${newConn.to}) was not carried over: ${scopeLabel(home2.scopePath)} already has \`${taken.id}\` (${taken.from} \u2192 ${taken.to}).`
22943
+ );
22944
+ continue;
22719
22945
  }
22946
+ result = replaceScopeAtPath(result, home2.scopePath, {
22947
+ ...nav.scope,
22948
+ connections: [...existingConns, newConn]
22949
+ });
22720
22950
  }
22721
- return { arch: result, connectionsUpdated: changedCount };
22951
+ return { arch: result, connectionsUpdated: changed.length, notCarried };
22952
+ }
22953
+ function scopeLabel(scopePath) {
22954
+ return scopePath.length === 0 ? "root" : `root.${scopePath.join(".")}`;
22722
22955
  }
22723
22956
  function applyComponentMove(arch, move) {
22724
22957
  const fromPath = move.from === "root" || move.from === "" ? [] : move.from.replace(/^root\./, "").split(".");
@@ -22767,7 +23000,7 @@ function applyComponentMove(arch, move) {
22767
23000
  });
22768
23001
  const rewriter = buildPathRewriter(oldAbsolute, newAbsolute);
22769
23002
  result = rewriteArchPaths(result, rewriter);
22770
- const { arch: afterConns, connectionsUpdated } = rewriteAllConnections(
23003
+ const { arch: afterConns, connectionsUpdated, notCarried } = rewriteAllConnections(
22771
23004
  result,
22772
23005
  rewriter
22773
23006
  );
@@ -22793,13 +23026,16 @@ function applyComponentMove(arch, move) {
22793
23026
  walk(a2.components);
22794
23027
  };
22795
23028
  countAllHops(result);
22796
- const summary2 = `Moved component '${move.id}' from ${move.from} to ${move.to}. As part of this move, ${hopsRewritten} story hop(s) and ${connectionsUpdated} connection(s) were updated to reflect the new path.`;
23029
+ const notCarriedNote = notCarried.length > 0 ? ` Warning: ${notCarried.join(" ")}` : "";
23030
+ const summary2 = `Moved component '${move.id}' from ${move.from} to ${move.to}. As part of this move, ${hopsRewritten} story hop(s) and ${connectionsUpdated} connection(s) were updated to reflect the new path.${notCarriedNote}`;
22797
23031
  return { arch: result, summary: summary2 };
22798
23032
  }
22799
23033
  var init_archUpdate = __esm({
22800
23034
  "../shared/archUpdate.ts"() {
22801
23035
  init_waypoints();
22802
23036
  init_validationCodes();
23037
+ init_connectionPair();
23038
+ init_leafEndpoints();
22803
23039
  }
22804
23040
  });
22805
23041
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
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",