@archme/cli 0.11.0 → 0.11.2

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 +213 -83
  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({
@@ -13157,7 +13172,7 @@ function buildStoryCoverageGate(opts) {
13157
13172
  function buildFormatRefStories(opts) {
13158
13173
  const draftBlock = opts.persistentDrafts ? FORMAT_REF_DRAFT_STORIES_BLOCK : "";
13159
13174
  const uncoveredSubFlowRule = opts.persistentDrafts ? CROSS_REF_UNCOVERED_DRAFT_STUB : CROSS_REF_UNCOVERED_REAL_STORY;
13160
- const hierarchyExample = opts.persistentDrafts ? `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", draft: true, userStories: [{id: "login", title: "log in with email", draft: true, acceptanceCriteria: ["< 1s response"]}]}]}]` : `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", userStories: [{id: "login", title: "log in with email", acceptanceCriteria: ["< 1s response"], sequence: "sequenceDiagram\\n ..."}]}]}]`;
13175
+ const hierarchyExample = opts.persistentDrafts ? `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", draft: true, userStories: [{id: "login", title: "log in with email", draft: true}]}]}]` : `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", userStories: [{id: "login", title: "log in with email", sequence: "sequenceDiagram\\n ..."}]}]}]`;
13161
13176
  return `Recursive story hierarchy:
13162
13177
  - Stories can contain child stories via userStories: [...] \u2014 parent stories act as feature groups
13163
13178
  - Use top-level stories with description, links, and nested children to organize by feature area
@@ -13174,11 +13189,6 @@ Story description format (markdown, 1-4 sentences \u2014 render target is a narr
13174
13189
  - Good: "A signed-out user requests a reset link by email and sets a new password from that link. Links expire after an hour and work once."
13175
13190
  - Bad (3x too long, implementation-level, changelog-style): "The forgot-password handler generates a token via crypto.randomUUID(), inserts a row into password_reset_tokens with expires_at = now() + 1 hour, emails it through the SendGrid client, and the reset endpoint validates the token with FOR UPDATE SKIP LOCKED before calling auth.admin.updateUserById..."
13176
13191
 
13177
- Acceptance criteria format:
13178
- - Each criterion should be an observable guarantee or behavior someone could verify without reading the code.
13179
- - Bad: "RLS policy checks auth.uid() against org_members", "Uses FOR UPDATE SKIP LOCKED in claim query"
13180
- - Good: "Only org members can view jobs", "Concurrent workers never claim the same job"
13181
-
13182
13192
  ${draftBlock}Sequence diagram format:
13183
13193
  - Each story has a "sequence" field containing a Mermaid sequence diagram string
13184
13194
  - Use architecture dot-paths as participant IDs (e.g., root.api.gateway)
@@ -13186,9 +13196,10 @@ ${draftBlock}Sequence diagram format:
13186
13196
  - Use "as" aliases for display names: participant root.api.gateway as API Gateway
13187
13197
  - Supports combined fragments: alt/else, opt, par/and, loop, critical/option, break
13188
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}
13189
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"
13190
- - 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"
13191
- - 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"
13192
13203
 
13193
13204
  ${SEQUENCE_WORDING_GUIDANCE}
13194
13205
 
@@ -13226,6 +13237,7 @@ var init_scanPrompts = __esm({
13226
13237
  init_storyNaming();
13227
13238
  init_storyChapterGuidance();
13228
13239
  init_sequenceWording();
13240
+ init_leafEndpoints();
13229
13241
  init_componentOrganization();
13230
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.
13231
13243
 
@@ -13234,7 +13246,7 @@ var init_scanPrompts = __esm({
13234
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.
13235
13247
  - Every connection MUST have a non-empty method
13236
13248
  - Keep component hierarchies nested (e.g., backend > api > routes)
13237
- - 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
13238
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
13239
13251
  - Small utility modules are implementation details \u2014 omit unless architecturally significant
13240
13252
  - Components must exist before non-draft stories can reference them. Add components first, then wire stories.
@@ -13346,20 +13358,20 @@ Component annotations:
13346
13358
  - Example: { id: "user-org", from: "user", to: "org", method: "belongs_to", reference: true }
13347
13359
 
13348
13360
  Connection format:
13349
- - Use dot-paths to target interface children: "api_server.post_users"
13350
- - 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>"
13351
13364
  - method describes how components communicate (e.g., REST, gRPC, GraphQL, WebSocket, Kafka, function, event)`;
13352
13365
  FORMAT_REF_DRAFT_STORIES_BLOCK = `Draft stories:
13353
13366
  - Set draft: true for stories where the architecture isn't built yet
13354
13367
  - Draft inheritance: if a parent has draft: true, all descendants are implicitly draft (skip validation)
13355
- - Draft stories can omit sequence entirely \u2014 they're pure requirements
13368
+ - Draft stories can omit sequence entirely \u2014 a title and description are enough
13356
13369
  - Draft stories skip all validation (waypoints, connectivity)
13357
- - Use acceptanceCriteria: string[] for PRD-style requirements on draft stories
13358
13370
  - When architecture supports the story, add a sequence and set draft: false
13359
- - Example draft: {id: "send_push", title: "Send push notification", draft: true, acceptanceCriteria: ["Delivered within 5s", "Supports iOS and Android"]}
13371
+ - Example draft: {id: "send_push", title: "Send push notification", draft: true, description: "A user gets a push notification on iOS or Android within seconds of the event."}
13360
13372
 
13361
13373
  `;
13362
- CROSS_REF_UNCOVERED_DRAFT_STUB = `- If no existing story covers the sub-flow but it is reusable across multiple parent stories, create a draft story for it (acceptanceCriteria only, no sequence) and reference that draft. The draft can be wired up later.`;
13374
+ CROSS_REF_UNCOVERED_DRAFT_STUB = `- If no existing story covers the sub-flow but it is reusable across multiple parent stories, create a draft story for it (title and description only, no sequence) and reference that draft. The draft can be wired up later.`;
13363
13375
  CROSS_REF_UNCOVERED_REAL_STORY = `- If no existing story covers the sub-flow but it is reusable across multiple parent stories, create a real story for it \u2014 with its own sequence traced from the code \u2014 and reference that. Do not leave a sequence-less draft stub.`;
13364
13376
  FORMAT_REF_STORIES = buildFormatRefStories({ persistentDrafts: true });
13365
13377
  }
@@ -13577,6 +13589,16 @@ var init_validationCodes = __esm({
13577
13589
  // ──────────────────────────────────────────────────────────────────────────
13578
13590
  /** componentUpdates[].remove references an id that doesn't exist at scope. */
13579
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",
13580
13602
  // ──────────────────────────────────────────────────────────────────────────
13581
13603
  // connection.add
13582
13604
  // ──────────────────────────────────────────────────────────────────────────
@@ -13590,9 +13612,16 @@ var init_validationCodes = __esm({
13590
13612
  CONNECTION_ADD_MISSING_TO: "connection.add.missing-to",
13591
13613
  /** Missing or empty `method`. */
13592
13614
  CONNECTION_ADD_MISSING_METHOD: "connection.add.missing-method",
13593
- /** 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
+ */
13594
13620
  CONNECTION_AT_WRONG_SCOPE_OUTSIDE: "connection.add.wrong-scope-outside",
13595
- /** 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
+ */
13596
13625
  CONNECTION_AT_WRONG_SCOPE_INTERNAL: "connection.add.wrong-scope-internal",
13597
13626
  // ──────────────────────────────────────────────────────────────────────────
13598
13627
  // connection.remove
@@ -13612,6 +13641,12 @@ var init_validationCodes = __esm({
13612
13641
  * One code, both sites: to an agent it's the same rule.
13613
13642
  */
13614
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",
13615
13650
  // ──────────────────────────────────────────────────────────────────────────
13616
13651
  // story.add — story-add fields
13617
13652
  // ──────────────────────────────────────────────────────────────────────────
@@ -13625,6 +13660,8 @@ var init_validationCodes = __esm({
13625
13660
  STORY_MISSING_SEQUENCE_FOR_LEAF: "story.add.missing-sequence-for-leaf",
13626
13661
  /** Parent story id referenced by `under` was not found. */
13627
13662
  STORY_UNDER_PARENT_NOT_FOUND: "story.add.under-parent-not-found",
13663
+ /** An MCP write set `acceptanceCriteria`, which only people author. */
13664
+ STORY_ACCEPTANCE_CRITERIA_AGENT_WRITE: "story.add.acceptance-criteria-agent-write",
13628
13665
  // ──────────────────────────────────────────────────────────────────────────
13629
13666
  // story.remove
13630
13667
  // ──────────────────────────────────────────────────────────────────────────
@@ -13700,6 +13737,14 @@ var init_validationCodes = __esm({
13700
13737
  PARTICIPANT_NOT_ROOT_PREFIXED: "sequence.participant.not-root-prefixed",
13701
13738
  /** Participant dot-path references a component that doesn't exist. */
13702
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",
13703
13748
  // ──────────────────────────────────────────────────────────────────────────
13704
13749
  // sequence.ref — validateSequenceRefs
13705
13750
  // ──────────────────────────────────────────────────────────────────────────
@@ -16544,7 +16589,19 @@ function stripPromptEcho(haystack, prompt) {
16544
16589
  function rateLimitDeferPlanForExit(channels, nowMs = Date.now(), defaultTimeZone = localTimeZone()) {
16545
16590
  const haystack = stripPromptEcho(`${channels.message}
16546
16591
  ${channels.stderr}`, channels.prompt);
16547
- 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;
16548
16605
  }
16549
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;
16550
16607
  var init_rateLimit = __esm({
@@ -17844,16 +17901,20 @@ __export(run_exports, {
17844
17901
  buildClaudeExitMessage: () => buildClaudeExitMessage,
17845
17902
  buildDevices: () => buildDevices,
17846
17903
  buildDiffPrompt: () => buildDiffPrompt,
17904
+ claudeErrorDetails: () => claudeErrorDetails,
17905
+ claudeErrorResult: () => claudeErrorResult,
17847
17906
  claudeFailureDetail: () => claudeFailureDetail,
17848
17907
  computeDiff: () => computeDiff,
17849
17908
  forkRepoScope: () => forkRepoScope,
17909
+ isClaudeAuthFailure: () => isClaudeAuthFailure,
17850
17910
  parseClaudeResult: () => parseClaudeResult,
17851
17911
  parseJobTokenResponse: () => parseJobTokenResponse,
17852
17912
  probeMcpAuth: () => probeMcpAuth,
17853
17913
  readBranchMarker: () => readBranchMarker,
17854
17914
  routeShutdownInterrupt: () => routeShutdownInterrupt,
17855
17915
  runDaemon: () => runDaemon,
17856
- teeStream: () => teeStream
17916
+ teeStream: () => teeStream,
17917
+ withFailureDetail: () => withFailureDetail
17857
17918
  });
17858
17919
  import { createClient } from "@supabase/supabase-js";
17859
17920
  import { randomUUID } from "node:crypto";
@@ -17870,12 +17931,29 @@ function attributeKillCause(result, recorded) {
17870
17931
  function buildClaudeExitMessage(exitCode, causeSuffix, stderr) {
17871
17932
  return `claude exited ${exitCode}${causeSuffix}: ${stderr.slice(0, 4e3)}`;
17872
17933
  }
17934
+ function claudeErrorResult(stdout) {
17935
+ return claudeErrorDetails(stdout)?.text || null;
17936
+ }
17937
+ function claudeErrorDetails(stdout) {
17938
+ const parsed = parseClaudeResult(stdout);
17939
+ return parsed.parsed && parsed.isError ? { text: parsed.summary.trim(), apiErrorStatus: parsed.apiErrorStatus } : null;
17940
+ }
17873
17941
  function claudeFailureDetail(stderr, stdout) {
17942
+ const result = claudeErrorResult(stdout);
17943
+ if (result) return result.slice(-FAILURE_DETAIL_MAX);
17874
17944
  if (stderr.trim() || !stdout.trim()) return null;
17875
17945
  const parsed = parseClaudeResult(stdout);
17876
17946
  const text = (parsed.parsed ? parsed.summary : stdout).trim();
17877
17947
  return text ? text.slice(-FAILURE_DETAIL_MAX) : null;
17878
17948
  }
17949
+ function isClaudeAuthFailure(errorResult, stderr) {
17950
+ return /failed to authenticate|oauth session expired|not logged in|please run \/login/i.test(`${errorResult ?? ""}
17951
+ ${stderr}`);
17952
+ }
17953
+ function withFailureDetail(message, detail) {
17954
+ return detail ? `claude said: ${detail}
17955
+ ${message}` : message;
17956
+ }
17879
17957
  async function routeShutdownInterrupt(id, cancelled2, fail, exitCode) {
17880
17958
  if (cancelled2.has(id)) return;
17881
17959
  if (exitCode === 0) {
@@ -18629,10 +18707,11 @@ function parseClaudeResult(stdout) {
18629
18707
  usage: modelUsage ? sumModelUsage(modelUsage) : null,
18630
18708
  costUsd: typeof d.total_cost_usd === "number" ? d.total_cost_usd : null,
18631
18709
  isError: d.is_error === true,
18710
+ apiErrorStatus: typeof d.api_error_status === "number" ? d.api_error_status : null,
18632
18711
  parsed: true
18633
18712
  };
18634
18713
  } catch {
18635
- return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, parsed: false };
18714
+ return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, apiErrorStatus: null, parsed: false };
18636
18715
  }
18637
18716
  }
18638
18717
  async function setupDigestMcp(ctx, job, deviceId) {
@@ -18921,16 +19000,30 @@ async function runJob(ctx, job, claimDeviceId) {
18921
19000
  if (result.exitCode !== 0) {
18922
19001
  const causeSuffix = cause ? ` (${result.signal ?? "exit"}, cause: ${cause})` : "";
18923
19002
  const stderr = result.stderr ? String(result.stderr) : "";
18924
- const detail = claudeFailureDetail(stderr, result.stdout ? String(result.stdout) : "");
19003
+ const stdout = result.stdout ? String(result.stdout) : "";
19004
+ const detail = claudeFailureDetail(stderr, stdout);
18925
19005
  if (detail) log(`claude output: ${detail.replace(/\n/g, " ")}`);
18926
- throw new Error(buildClaudeExitMessage(result.exitCode, causeSuffix, stderr));
19006
+ throw new ClaudeRunError(
19007
+ buildClaudeExitMessage(result.exitCode, causeSuffix, stderr),
19008
+ detail,
19009
+ isClaudeAuthFailure(claudeErrorResult(stdout), stderr),
19010
+ claudeErrorDetails(stdout)
19011
+ );
18927
19012
  }
18928
19013
  const runResult = parseClaudeResult(result.stdout ? String(result.stdout) : "");
18929
19014
  if (!runResult.parsed) {
18930
19015
  log(`warning: claude --output-format json stdout was not a JSON result object; using raw text as summary`);
18931
19016
  }
18932
19017
  if (runResult.isError) {
18933
- throw new Error(`claude reported is_error in its JSON result (no successful completion)`);
19018
+ throw new ClaudeRunError(
19019
+ `claude reported is_error in its JSON result (no successful completion)`,
19020
+ runResult.summary.trim().slice(-FAILURE_DETAIL_MAX) || null,
19021
+ isClaudeAuthFailure(
19022
+ claudeErrorResult(result.stdout ? String(result.stdout) : ""),
19023
+ result.stderr ? String(result.stderr) : ""
19024
+ ),
19025
+ claudeErrorDetails(result.stdout ? String(result.stdout) : "")
19026
+ );
18934
19027
  }
18935
19028
  const summary2 = runResult.summary.trim().slice(0, 4e3);
18936
19029
  if (summary2) log(`claude> ${summary2.slice(0, 500).replace(/\n/g, " ")}`);
@@ -18987,20 +19080,22 @@ async function runJob(ctx, job, claimDeviceId) {
18987
19080
  return;
18988
19081
  }
18989
19082
  const msg = err4 instanceof Error ? err4.message : String(err4);
19083
+ const runErr = err4 instanceof ClaudeRunError ? err4 : null;
18990
19084
  const haystack = stripPromptEcho(`${msg}
18991
19085
  ${logs.stderr}`, sentPrompt);
18992
19086
  const plan = rateLimitDeferPlanForExit({
18993
19087
  message: msg,
18994
19088
  stderr: logs.stderr,
18995
19089
  stdout: logs.stdout,
18996
- prompt: sentPrompt
19090
+ prompt: sentPrompt,
19091
+ claudeError: runErr?.claudeError
18997
19092
  });
18998
19093
  if (plan) {
18999
19094
  const { parsed, pauseUntil, info } = plan;
19000
19095
  if (!parsed) {
19001
19096
  void rpcReportEvent(ctx, job.org_id, "rate_limit_unmatched_exit", {
19002
19097
  job_id: job.id,
19003
- excerpt: haystack.slice(0, 500)
19098
+ excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
19004
19099
  });
19005
19100
  }
19006
19101
  console.log(
@@ -19020,8 +19115,21 @@ ${logs.stderr}`, sentPrompt);
19020
19115
  if (deferred) return;
19021
19116
  console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
19022
19117
  }
19023
- console.log(`[${job.id}] failed: ${msg}`);
19024
- await rpcFail(ctx, job.id, deviceId, msg, metrics, logs);
19118
+ if (!plan && runErr?.authFailure) {
19119
+ const pauseUntil = new Date(Date.now() + AUTH_PAUSE_MS);
19120
+ console.error(
19121
+ `[${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()}.`
19122
+ );
19123
+ const detail = { job_id: job.id, claude_said: runErr.detail };
19124
+ await rpcSetDaemonPause(ctx, pauseUntil, "claude_auth_expired", detail);
19125
+ setLocalPause(pauseUntil.getTime());
19126
+ const deferred = await rpcDefer(ctx, job.id, deviceId, "claude_auth_expired", detail);
19127
+ if (deferred) return;
19128
+ console.warn(`[${job.id}] defer no-op \u2014 falling through to fail`);
19129
+ }
19130
+ const failMessage = withFailureDetail(msg, runErr?.detail ?? null);
19131
+ console.log(`[${job.id}] failed: ${failMessage}`);
19132
+ await rpcFail(ctx, job.id, deviceId, failMessage, metrics, logs);
19025
19133
  } finally {
19026
19134
  clearInterval(heartbeat);
19027
19135
  inFlight.delete(job.id);
@@ -19426,7 +19534,7 @@ ${signal} received, shutting down`);
19426
19534
  process.on("SIGINT", () => shutdown("SIGINT"));
19427
19535
  process.on("SIGTERM", () => shutdown("SIGTERM"));
19428
19536
  }
19429
- 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;
19537
+ 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;
19430
19538
  var init_run = __esm({
19431
19539
  "src/daemon/run.ts"() {
19432
19540
  "use strict";
@@ -19476,6 +19584,15 @@ var init_run = __esm({
19476
19584
  LOG_STDOUT_CAP_BYTES = 30 * 1024;
19477
19585
  LOG_STDERR_CAP_BYTES = 30 * 1024;
19478
19586
  FAILURE_DETAIL_MAX = 400;
19587
+ AUTH_PAUSE_MS = 15 * 60 * 1e3;
19588
+ ClaudeRunError = class extends Error {
19589
+ constructor(message, detail, authFailure, claudeError) {
19590
+ super(message);
19591
+ this.detail = detail;
19592
+ this.authFailure = authFailure;
19593
+ this.claudeError = claudeError;
19594
+ }
19595
+ };
19479
19596
  PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = CLAUDE_LARGE_DIFF_TIMEOUT_MS + 5 * 60 * 1e3;
19480
19597
  inFlight = /* @__PURE__ */ new Map();
19481
19598
  cancelled = /* @__PURE__ */ new Set();
@@ -19549,6 +19666,9 @@ Rules:
19549
19666
  ## Data Shape (schemas)
19550
19667
  A data-shape change with unchanged topology is still an architectural change \u2014 the same components can talk in the same order while the shape of the data on the wire changes. If this change adds, removes, or retypes a **DB column**, a **function/RPC parameter**, or a **response/return field**, update the \`schema\` of the affected storage component (table, RPC, queue, any persistence layer) and the **input/output interface children** the data flows through. Compact format: \`{ field: type, field?: type, field!: generated, arr: type[], ... }\`. For a **persisted table**, set \`schemaKind: "table"\` + \`schemaName\` on the component and tag key columns inline (\`id: uuid @pk\`, \`user_id?: uuid @fk(auth.users.id)\`); keep DDL, RLS policies, and indexes OUT of \`schema\` \u2014 those are code (the anchored migration). Match the code exactly \u2014 do NOT invent fields the code doesn't have; a \`schema\` more complete than the code can prove is over-claiming.
19551
19668
 
19669
+ ## Behaviour at a step
19670
+ A behaviour change with unchanged participants is still an architectural change: the same components talk in the same order, but what a step does or guarantees has changed (the caller's real IP now reaches the backend, a failed call is now retried, a limit is now enforced). Show it in the \`sequence\` of the story that draws that step \u2014 reword that arrow, or add a short Note at that step when no arrow can carry it (see Sequence wording). A component \`description\` edit on its own barely shows in review, so it does not stand in for the sequence edit.
19671
+
19552
19672
  ## Draft Story Guidance
19553
19673
  \`draft: true\` is a transient build mechanic, not a deliverable: it skips sequence validation while a story is being assembled, and a parent's draft flag propagates to all descendants (suppressing \`autoCreateConnectionsFromStories\`). Feature-group parent stories that have real, sequenced children are NOT drafts. You may set it mid-run to defer validation while assembling a story, but finish the run with zero drafts you created or touched: give each a real sequence and set \`draft: false\`, or remove it. Pre-existing drafts you didn't touch are not yours to sweep.
19554
19674
 
@@ -22515,9 +22635,9 @@ var init_run_init = __esm({
22515
22635
  }
22516
22636
  });
22517
22637
 
22518
- // ../shared/waypoints.ts
22519
- var init_waypoints = __esm({
22520
- "../shared/waypoints.ts"() {
22638
+ // ../shared/connectionPair.ts
22639
+ var init_connectionPair = __esm({
22640
+ "../shared/connectionPair.ts"() {
22521
22641
  }
22522
22642
  });
22523
22643
 
@@ -22646,80 +22766,87 @@ function toAbsolute(endpoint, scopePath) {
22646
22766
  if (scopePath.length === 0) return "root." + endpoint;
22647
22767
  return "root." + scopePath.join(".") + "." + endpoint;
22648
22768
  }
22649
- function toRelative(absolute, scopePath) {
22650
- const prefix = scopePath.length === 0 ? "root." : "root." + scopePath.join(".") + ".";
22651
- if (absolute.startsWith(prefix)) return absolute.slice(prefix.length);
22652
- return absolute;
22653
- }
22654
- function rewriteAllConnections(arch, rewriter) {
22655
- const entries = [];
22656
- function walkConns(connections, scopePath) {
22769
+ function listAllConnections(arch) {
22770
+ const out = [];
22771
+ const walkConns = (connections, scopePath) => {
22657
22772
  for (const conn of connections) {
22658
- const absFrom = toAbsolute(conn.from, scopePath);
22659
- const absTo = toAbsolute(conn.to, scopePath);
22660
- const newAbsFrom = rewriter(absFrom);
22661
- const newAbsTo = rewriter(absTo);
22662
- const changed = newAbsFrom !== absFrom || newAbsTo !== absTo;
22663
- entries.push({
22773
+ out.push({
22664
22774
  conn,
22665
22775
  scopePath,
22666
- newFrom: newAbsFrom,
22667
- newTo: newAbsTo,
22668
- changed
22776
+ absFrom: toAbsolute(conn.from, scopePath),
22777
+ absTo: toAbsolute(conn.to, scopePath)
22669
22778
  });
22670
22779
  }
22671
- }
22672
- function walkComponents(components, basePath) {
22780
+ };
22781
+ const walkComponents = (components, basePath) => {
22673
22782
  for (const comp of components) {
22674
22783
  const compPath = [...basePath, comp.id];
22675
22784
  if (comp.connections) walkConns(comp.connections, compPath);
22676
22785
  if (comp.components) walkComponents(comp.components, compPath);
22677
22786
  }
22678
- }
22787
+ };
22679
22788
  walkConns(arch.connections, []);
22680
22789
  walkComponents(arch.components, []);
22681
- const changedCount = entries.filter((e) => e.changed).length;
22682
- if (changedCount === 0) return { arch, connectionsUpdated: 0 };
22790
+ return out;
22791
+ }
22792
+ function dropConnections(arch, doomed) {
22793
+ const byScope = /* @__PURE__ */ new Map();
22794
+ for (const { conn, scopePath } of doomed) {
22795
+ const key = scopePath.join(".");
22796
+ const ids = byScope.get(key) ?? /* @__PURE__ */ new Set();
22797
+ ids.add(conn.id);
22798
+ byScope.set(key, ids);
22799
+ }
22683
22800
  let result = arch;
22684
- const removeByScopeKey = /* @__PURE__ */ new Map();
22685
- for (const entry of entries) {
22686
- if (!entry.changed) continue;
22687
- const key = entry.scopePath.join(".");
22688
- if (!removeByScopeKey.has(key)) removeByScopeKey.set(key, /* @__PURE__ */ new Set());
22689
- removeByScopeKey.get(key).add(entry.conn.id);
22690
- }
22691
- for (const [scopeKey, connIds] of removeByScopeKey) {
22801
+ for (const [scopeKey, ids] of byScope) {
22692
22802
  const scopePath = scopeKey === "" ? [] : scopeKey.split(".");
22693
22803
  const nav = navigateToScope(result, scopePath);
22694
22804
  if (!nav) continue;
22695
22805
  result = replaceScopeAtPath(result, scopePath, {
22696
22806
  ...nav.scope,
22697
- connections: nav.scope.connections.filter((c3) => !connIds.has(c3.id))
22807
+ connections: nav.scope.connections.filter((c3) => !ids.has(c3.id))
22698
22808
  });
22699
22809
  }
22700
- for (const entry of entries) {
22701
- if (!entry.changed) continue;
22702
- const fromSegments = entry.newFrom.split(".");
22703
- const toSegments = entry.newTo.split(".");
22704
- const lcaSegments = findLCA(fromSegments, toSegments);
22705
- const lcaScopePath = lcaSegments.length > 1 ? lcaSegments.slice(1) : [];
22706
- const lcaNav = navigateToScope(result, lcaScopePath);
22707
- if (!lcaNav) continue;
22810
+ return result;
22811
+ }
22812
+ function connectionHome(absFrom, absTo) {
22813
+ const a2 = absFrom.split(".").slice(1);
22814
+ const b = absTo.split(".").slice(1);
22815
+ const scopePath = findLCA(a2.slice(0, -1), b.slice(0, -1));
22816
+ return { scopePath, from: a2.slice(scopePath.length).join("."), to: b.slice(scopePath.length).join(".") };
22817
+ }
22818
+ function rewriteAllConnections(arch, rewriter) {
22819
+ 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);
22820
+ if (changed.length === 0) return { arch, connectionsUpdated: 0, notCarried: [] };
22821
+ let result = dropConnections(arch, changed);
22822
+ const notCarried = [];
22823
+ for (const entry of changed) {
22824
+ const home2 = connectionHome(entry.newFrom, entry.newTo);
22825
+ const nav = navigateToScope(result, home2.scopePath);
22826
+ if (!nav) continue;
22708
22827
  const newConn = {
22709
22828
  ...entry.conn,
22710
- from: toRelative(entry.newFrom, lcaScopePath),
22711
- to: toRelative(entry.newTo, lcaScopePath),
22712
- id: `${toRelative(entry.newFrom, lcaScopePath)}-${toRelative(entry.newTo, lcaScopePath)}`
22713
- };
22714
- const existingConns = lcaNav.scope.connections;
22715
- if (!existingConns.some((c3) => c3.id === newConn.id)) {
22716
- result = replaceScopeAtPath(result, lcaScopePath, {
22717
- ...lcaNav.scope,
22718
- connections: [...existingConns, newConn]
22719
- });
22829
+ from: home2.from,
22830
+ to: home2.to,
22831
+ id: `${home2.from}-${home2.to}`
22832
+ };
22833
+ const existingConns = nav.scope.connections;
22834
+ const taken = existingConns.find((c3) => c3.id === newConn.id);
22835
+ if (taken) {
22836
+ notCarried.push(
22837
+ `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}).`
22838
+ );
22839
+ continue;
22720
22840
  }
22841
+ result = replaceScopeAtPath(result, home2.scopePath, {
22842
+ ...nav.scope,
22843
+ connections: [...existingConns, newConn]
22844
+ });
22721
22845
  }
22722
- return { arch: result, connectionsUpdated: changedCount };
22846
+ return { arch: result, connectionsUpdated: changed.length, notCarried };
22847
+ }
22848
+ function scopeLabel(scopePath) {
22849
+ return scopePath.length === 0 ? "root" : `root.${scopePath.join(".")}`;
22723
22850
  }
22724
22851
  function applyComponentMove(arch, move) {
22725
22852
  const fromPath = move.from === "root" || move.from === "" ? [] : move.from.replace(/^root\./, "").split(".");
@@ -22768,7 +22895,7 @@ function applyComponentMove(arch, move) {
22768
22895
  });
22769
22896
  const rewriter = buildPathRewriter(oldAbsolute, newAbsolute);
22770
22897
  result = rewriteArchPaths(result, rewriter);
22771
- const { arch: afterConns, connectionsUpdated } = rewriteAllConnections(
22898
+ const { arch: afterConns, connectionsUpdated, notCarried } = rewriteAllConnections(
22772
22899
  result,
22773
22900
  rewriter
22774
22901
  );
@@ -22794,13 +22921,16 @@ function applyComponentMove(arch, move) {
22794
22921
  walk(a2.components);
22795
22922
  };
22796
22923
  countAllHops(result);
22797
- 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.`;
22924
+ const notCarriedNote = notCarried.length > 0 ? ` Warning: ${notCarried.join(" ")}` : "";
22925
+ 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}`;
22798
22926
  return { arch: result, summary: summary2 };
22799
22927
  }
22800
22928
  var init_archUpdate = __esm({
22801
22929
  "../shared/archUpdate.ts"() {
22802
22930
  init_waypoints();
22803
22931
  init_validationCodes();
22932
+ init_connectionPair();
22933
+ init_leafEndpoints();
22804
22934
  }
22805
22935
  });
22806
22936
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",