@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.
- package/dist/cli.js +213 -83
- 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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
13350
|
-
-
|
|
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
|
|
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,
|
|
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 (
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
19024
|
-
|
|
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/
|
|
22519
|
-
var
|
|
22520
|
-
"../shared/
|
|
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
|
|
22650
|
-
const
|
|
22651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22667
|
-
|
|
22668
|
-
changed
|
|
22776
|
+
absFrom: toAbsolute(conn.from, scopePath),
|
|
22777
|
+
absTo: toAbsolute(conn.to, scopePath)
|
|
22669
22778
|
});
|
|
22670
22779
|
}
|
|
22671
|
-
}
|
|
22672
|
-
|
|
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
|
-
|
|
22682
|
-
|
|
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
|
|
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) => !
|
|
22807
|
+
connections: nav.scope.connections.filter((c3) => !ids.has(c3.id))
|
|
22698
22808
|
});
|
|
22699
22809
|
}
|
|
22700
|
-
|
|
22701
|
-
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
|
|
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:
|
|
22711
|
-
to:
|
|
22712
|
-
id: `${
|
|
22713
|
-
};
|
|
22714
|
-
const existingConns =
|
|
22715
|
-
|
|
22716
|
-
|
|
22717
|
-
|
|
22718
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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",
|