@archme/cli 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +241 -164
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9838,6 +9838,7 @@ var install_exports = {};
|
|
|
9838
9838
|
__export(install_exports, {
|
|
9839
9839
|
NotSignedInError: () => NotSignedInError,
|
|
9840
9840
|
PUBLISHED_BIN_RELATIVE: () => PUBLISHED_BIN_RELATIVE,
|
|
9841
|
+
archmeSelfCommand: () => archmeSelfCommand,
|
|
9841
9842
|
ensureStartPrerequisites: () => ensureStartPrerequisites,
|
|
9842
9843
|
findPackageRoot: () => findPackageRoot,
|
|
9843
9844
|
pickCliEntry: () => pickCliEntry,
|
|
@@ -9881,6 +9882,9 @@ function resolveCliEntry() {
|
|
|
9881
9882
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
9882
9883
|
return pickCliEntry(findPackageRoot(here, existsSync2), existsSync2);
|
|
9883
9884
|
}
|
|
9885
|
+
function archmeSelfCommand() {
|
|
9886
|
+
return [process.execPath, resolveCliEntry()];
|
|
9887
|
+
}
|
|
9884
9888
|
async function ensureStartPrerequisites(opts = {}) {
|
|
9885
9889
|
const out = opts.out ?? process.stdout;
|
|
9886
9890
|
const platform3 = opts.platform ?? process.platform;
|
|
@@ -10399,6 +10403,11 @@ function buildClaudeArgs(opts) {
|
|
|
10399
10403
|
"--allowedTools",
|
|
10400
10404
|
tools
|
|
10401
10405
|
];
|
|
10406
|
+
if (opts.resumeSessionId) {
|
|
10407
|
+
args.push("--resume", opts.resumeSessionId);
|
|
10408
|
+
} else if (opts.sessionId) {
|
|
10409
|
+
args.push("--session-id", opts.sessionId);
|
|
10410
|
+
}
|
|
10402
10411
|
if (opts.builtinTools) {
|
|
10403
10412
|
args.push("--tools", opts.builtinTools.join(","));
|
|
10404
10413
|
}
|
|
@@ -10410,6 +10419,9 @@ function buildClaudeArgs(opts) {
|
|
|
10410
10419
|
}
|
|
10411
10420
|
return args.map(stripNulBytes);
|
|
10412
10421
|
}
|
|
10422
|
+
function claudeTimeoutMs(timeoutMs) {
|
|
10423
|
+
return timeoutMs === null ? void 0 : timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
|
|
10424
|
+
}
|
|
10413
10425
|
function spawnClaude(opts) {
|
|
10414
10426
|
requireBinary("claude");
|
|
10415
10427
|
const env = scrubPrivilegedEnv(process.env);
|
|
@@ -10433,7 +10445,7 @@ function spawnClaude(opts) {
|
|
|
10433
10445
|
args,
|
|
10434
10446
|
{
|
|
10435
10447
|
reject: false,
|
|
10436
|
-
timeout: opts.timeoutMs
|
|
10448
|
+
timeout: claudeTimeoutMs(opts.timeoutMs),
|
|
10437
10449
|
cwd: opts.cwd,
|
|
10438
10450
|
env,
|
|
10439
10451
|
// Claude reads stdin opportunistically and prints a warning after 3s
|
|
@@ -10530,7 +10542,12 @@ var init_spawn_claude = __esm({
|
|
|
10530
10542
|
// "Agent" is inherited from DEFAULT_ALLOWED_TOOLS (sub-agent dispatch).
|
|
10531
10543
|
"Write",
|
|
10532
10544
|
"Edit",
|
|
10533
|
-
"Bash"
|
|
10545
|
+
"Bash",
|
|
10546
|
+
// The scan prompts research the product's public site and docs. A scan could
|
|
10547
|
+
// already fetch pages through Bash (curl), so this adds search, not reach.
|
|
10548
|
+
// Interactive sessions still ask the person before every fetch.
|
|
10549
|
+
"WebSearch",
|
|
10550
|
+
"WebFetch"
|
|
10534
10551
|
];
|
|
10535
10552
|
DEFAULT_CLAUDE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
10536
10553
|
}
|
|
@@ -13140,7 +13157,7 @@ function buildStoryCoverageGate(opts) {
|
|
|
13140
13157
|
function buildFormatRefStories(opts) {
|
|
13141
13158
|
const draftBlock = opts.persistentDrafts ? FORMAT_REF_DRAFT_STORIES_BLOCK : "";
|
|
13142
13159
|
const uncoveredSubFlowRule = opts.persistentDrafts ? CROSS_REF_UNCOVERED_DRAFT_STUB : CROSS_REF_UNCOVERED_REAL_STORY;
|
|
13143
|
-
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
|
|
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}]}]}]` : `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", userStories: [{id: "login", title: "log in with email", sequence: "sequenceDiagram\\n ..."}]}]}]`;
|
|
13144
13161
|
return `Recursive story hierarchy:
|
|
13145
13162
|
- Stories can contain child stories via userStories: [...] \u2014 parent stories act as feature groups
|
|
13146
13163
|
- Use top-level stories with description, links, and nested children to organize by feature area
|
|
@@ -13157,11 +13174,6 @@ Story description format (markdown, 1-4 sentences \u2014 render target is a narr
|
|
|
13157
13174
|
- 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."
|
|
13158
13175
|
- 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..."
|
|
13159
13176
|
|
|
13160
|
-
Acceptance criteria format:
|
|
13161
|
-
- Each criterion should be an observable guarantee or behavior someone could verify without reading the code.
|
|
13162
|
-
- Bad: "RLS policy checks auth.uid() against org_members", "Uses FOR UPDATE SKIP LOCKED in claim query"
|
|
13163
|
-
- Good: "Only org members can view jobs", "Concurrent workers never claim the same job"
|
|
13164
|
-
|
|
13165
13177
|
${draftBlock}Sequence diagram format:
|
|
13166
13178
|
- Each story has a "sequence" field containing a Mermaid sequence diagram string
|
|
13167
13179
|
- Use architecture dot-paths as participant IDs (e.g., root.api.gateway)
|
|
@@ -13335,14 +13347,13 @@ Connection format:
|
|
|
13335
13347
|
FORMAT_REF_DRAFT_STORIES_BLOCK = `Draft stories:
|
|
13336
13348
|
- Set draft: true for stories where the architecture isn't built yet
|
|
13337
13349
|
- Draft inheritance: if a parent has draft: true, all descendants are implicitly draft (skip validation)
|
|
13338
|
-
- Draft stories can omit sequence entirely \u2014
|
|
13350
|
+
- Draft stories can omit sequence entirely \u2014 a title and description are enough
|
|
13339
13351
|
- Draft stories skip all validation (waypoints, connectivity)
|
|
13340
|
-
- Use acceptanceCriteria: string[] for PRD-style requirements on draft stories
|
|
13341
13352
|
- When architecture supports the story, add a sequence and set draft: false
|
|
13342
|
-
- Example draft: {id: "send_push", title: "Send push notification", draft: true,
|
|
13353
|
+
- 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."}
|
|
13343
13354
|
|
|
13344
13355
|
`;
|
|
13345
|
-
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 (
|
|
13356
|
+
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.`;
|
|
13346
13357
|
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.`;
|
|
13347
13358
|
FORMAT_REF_STORIES = buildFormatRefStories({ persistentDrafts: true });
|
|
13348
13359
|
}
|
|
@@ -13608,6 +13619,8 @@ var init_validationCodes = __esm({
|
|
|
13608
13619
|
STORY_MISSING_SEQUENCE_FOR_LEAF: "story.add.missing-sequence-for-leaf",
|
|
13609
13620
|
/** Parent story id referenced by `under` was not found. */
|
|
13610
13621
|
STORY_UNDER_PARENT_NOT_FOUND: "story.add.under-parent-not-found",
|
|
13622
|
+
/** An MCP write set `acceptanceCriteria`, which only people author. */
|
|
13623
|
+
STORY_ACCEPTANCE_CRITERIA_AGENT_WRITE: "story.add.acceptance-criteria-agent-write",
|
|
13611
13624
|
// ──────────────────────────────────────────────────────────────────────────
|
|
13612
13625
|
// story.remove
|
|
13613
13626
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -13904,7 +13917,7 @@ function buildRepoContainerDiffGuidance(containerPath) {
|
|
|
13904
13917
|
return `## Multi-repo project
|
|
13905
13918
|
This project holds several repos, each a root-level \`kind: "repo"\` container; this repo's code lives under \`${containerPath}\`. Before modelling anything that crosses repos, read the project's modelling conventions with ${READ_CONVENTIONS}. Never create a root-level external for a system that has a repo container \u2014 connect to the components inside that container instead.`;
|
|
13906
13919
|
}
|
|
13907
|
-
var READ_CONVENTIONS, PLAN_CHANGES_ID, STITCH_PLAN_GUIDANCE;
|
|
13920
|
+
var READ_CONVENTIONS, PLAN_CHANGES_ID, STORY_PLAN_SHAPE, STORY_PLAN_GUIDANCE, PLAN_CHANGES_EXPLANATION, STITCH_PLAN_GUIDANCE, READ_FIRST;
|
|
13908
13921
|
var init_multiRepoScan = __esm({
|
|
13909
13922
|
"../shared/multiRepoScan.ts"() {
|
|
13910
13923
|
init_scanPrompts();
|
|
@@ -13913,7 +13926,11 @@ var init_multiRepoScan = __esm({
|
|
|
13913
13926
|
init_repoContainer();
|
|
13914
13927
|
READ_CONVENTIONS = `read_context({ path: "root", type: "decision" })`;
|
|
13915
13928
|
PLAN_CHANGES_ID = "multi-repo-plan-changes";
|
|
13916
|
-
|
|
13929
|
+
STORY_PLAN_SHAPE = `Write it in one \`update_architecture\` call at root: feature groups holding leaf stories, every leaf \`draft: true\` with a \`title\` and a \`description\` and no \`sequence\`. Each leaf's description names the repos it touches, ending \`Repos: owner/a, owner/b\`. A flow that crosses repos is ONE story naming all of them, never one per repo. If the board already has stories, they are the plan's starting point: keep, edit or remove them rather than starting over.`;
|
|
13930
|
+
STORY_PLAN_GUIDANCE = `**Draft the story plan** every repo's scan will build against. Read each clone only as far as it takes to say what the system does for the people and systems that use it (READMEs, entry points, API and resource definitions) \u2014 don't trace flows. ${STORY_PLAN_SHAPE}`;
|
|
13931
|
+
PLAN_CHANGES_EXPLANATION = `write ONE root-context decision \u2014 \`update_node_context({ path: "root", add: [{ id: "${PLAN_CHANGES_ID}", type: "decision", title: "What changed from the approved plan, and why", content: "\u2026" }] })\` \u2014 listing every story added, dropped, renamed or re-scoped against the plan, the repos' departures and your own, each with its reason in a plain sentence ("No changes from the approved plan." if there were none). Present it to the user and answer their questions about it before you finish.`;
|
|
13932
|
+
STITCH_PLAN_GUIDANCE = `**Finish the approved story plan, then explain what changed from it.** The user approved a draft story plan before the repos were scanned. Each repo's scan left the stories that cross repos to you, and recorded its own departures from the plan in root context (\`${PLAN_CHANGES_ID}-<container>\` decisions). Write each remaining draft story's sequence against the code on every side it touches, and set \`draft: false\`. Then ${PLAN_CHANGES_EXPLANATION}`;
|
|
13933
|
+
READ_FIRST = `**Read first.** \`read_architecture\` at root and ${READ_CONVENTIONS}. A rerun reuses what is already there under the ids below and rewrites it in place.`;
|
|
13917
13934
|
}
|
|
13918
13935
|
});
|
|
13919
13936
|
|
|
@@ -19528,6 +19545,9 @@ Rules:
|
|
|
19528
19545
|
## Data Shape (schemas)
|
|
19529
19546
|
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.
|
|
19530
19547
|
|
|
19548
|
+
## Behaviour at a step
|
|
19549
|
+
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.
|
|
19550
|
+
|
|
19531
19551
|
## Draft Story Guidance
|
|
19532
19552
|
\`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.
|
|
19533
19553
|
|
|
@@ -19886,6 +19906,8 @@ var init_identity = __esm({
|
|
|
19886
19906
|
// src/scan/authedClient.ts
|
|
19887
19907
|
var authedClient_exports = {};
|
|
19888
19908
|
__export(authedClient_exports, {
|
|
19909
|
+
ORCHESTRATED_SCAN_TIMEOUT_MS: () => ORCHESTRATED_SCAN_TIMEOUT_MS,
|
|
19910
|
+
ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS: () => ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS,
|
|
19889
19911
|
SCAN_CLAUDE_TIMEOUT_MS: () => SCAN_CLAUDE_TIMEOUT_MS,
|
|
19890
19912
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: () => SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS,
|
|
19891
19913
|
createAuthedClient: () => createAuthedClient
|
|
@@ -19924,7 +19946,7 @@ async function createAuthedClient(opts) {
|
|
|
19924
19946
|
accountKind: loaded.kind ?? "oauth"
|
|
19925
19947
|
};
|
|
19926
19948
|
}
|
|
19927
|
-
var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
|
|
19949
|
+
var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, ORCHESTRATED_SCAN_TIMEOUT_MS, ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS;
|
|
19928
19950
|
var init_authedClient = __esm({
|
|
19929
19951
|
"src/scan/authedClient.ts"() {
|
|
19930
19952
|
"use strict";
|
|
@@ -19932,6 +19954,8 @@ var init_authedClient = __esm({
|
|
|
19932
19954
|
init_identity();
|
|
19933
19955
|
SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
19934
19956
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
19957
|
+
ORCHESTRATED_SCAN_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
19958
|
+
ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS = ORCHESTRATED_SCAN_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
19935
19959
|
}
|
|
19936
19960
|
});
|
|
19937
19961
|
|
|
@@ -20507,24 +20531,32 @@ var init_claimDefaultProject = __esm({
|
|
|
20507
20531
|
});
|
|
20508
20532
|
|
|
20509
20533
|
// src/scan/run-scan.ts
|
|
20534
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20510
20535
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
20511
|
-
import { mkdir as mkdir13, readFile as readFile14 } from "node:fs/promises";
|
|
20512
|
-
import { basename as basename2, join as join21 } from "node:path";
|
|
20536
|
+
import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
|
|
20537
|
+
import { basename as basename2, dirname as dirname15, join as join21 } from "node:path";
|
|
20513
20538
|
function modeInstruction(opts) {
|
|
20514
20539
|
const line = modeLine(opts);
|
|
20540
|
+
if (opts.mode === "orchestrator" && opts.plan) {
|
|
20541
|
+
throw new Error("the orchestrator always plans; start_scan refuses `plan` with it");
|
|
20542
|
+
}
|
|
20515
20543
|
return line && opts.plan ? `${line} Also pass \`plan: true\`: this bootstrap builds against a draft story plan.` : line;
|
|
20516
20544
|
}
|
|
20545
|
+
function reposArg(opts, mode) {
|
|
20546
|
+
const repos = opts.repos ?? [];
|
|
20547
|
+
if (repos.length === 0 || !repos.every((r) => REPO_SLUG_RE.test(r))) {
|
|
20548
|
+
throw new Error(`${mode} mode needs at least one repo, each \`owner/name\``);
|
|
20549
|
+
}
|
|
20550
|
+
return repos.map((r) => `"${r}"`).join(", ");
|
|
20551
|
+
}
|
|
20517
20552
|
function modeLine(opts) {
|
|
20518
20553
|
switch (opts.mode) {
|
|
20519
20554
|
case void 0:
|
|
20520
20555
|
return null;
|
|
20521
|
-
case "roster":
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
}
|
|
20526
|
-
return `This is the multi-repo roster session \u2014 pass \`mode: "roster"\` and \`repos: [${repos.map((r) => `"${r}"`).join(", ")}]\` to \`start_scan\`.`;
|
|
20527
|
-
}
|
|
20556
|
+
case "roster":
|
|
20557
|
+
return `This is the multi-repo roster session \u2014 pass \`mode: "roster"\` and \`repos: [${reposArg(opts, "roster")}]\` to \`start_scan\`.`;
|
|
20558
|
+
case "orchestrator":
|
|
20559
|
+
return `This is the multi-repo orchestrator session \u2014 pass \`mode: "orchestrator"\` and \`repos: [${reposArg(opts, "orchestrator")}]\` to \`start_scan\`.`;
|
|
20528
20560
|
case "repo":
|
|
20529
20561
|
if (!opts.container || !CONTAINER_ID_RE.test(opts.container)) {
|
|
20530
20562
|
throw new Error(`invalid container id \`${opts.container ?? ""}\``);
|
|
@@ -20543,6 +20575,9 @@ ${briefing}` : BOOTSTRAP_INTERACTIVE_NO_BRIEFING : BOOTSTRAP_HEADLESS;
|
|
|
20543
20575
|
if (modeLine2) base += `
|
|
20544
20576
|
|
|
20545
20577
|
${modeLine2}`;
|
|
20578
|
+
if (opts.runSheet) base += `
|
|
20579
|
+
|
|
20580
|
+
${opts.runSheet}`;
|
|
20546
20581
|
if (!promptVariant) return base;
|
|
20547
20582
|
if (!PROMPT_VARIANT_RE.test(promptVariant)) {
|
|
20548
20583
|
throw new Error(
|
|
@@ -20554,6 +20589,26 @@ ${modeLine2}`;
|
|
|
20554
20589
|
Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVariant}"\` to \`start_scan\`.`;
|
|
20555
20590
|
return base;
|
|
20556
20591
|
}
|
|
20592
|
+
async function writeScanSession(repoPath, session) {
|
|
20593
|
+
const path7 = join21(repoPath, SCAN_SESSION_FILE);
|
|
20594
|
+
await mkdir13(dirname15(path7), { recursive: true });
|
|
20595
|
+
await writeFile12(path7, JSON.stringify(session, null, 2) + "\n");
|
|
20596
|
+
}
|
|
20597
|
+
async function readScanSession(repoPath) {
|
|
20598
|
+
const path7 = join21(repoPath, SCAN_SESSION_FILE);
|
|
20599
|
+
const raw = await readFile14(path7, "utf8").catch(() => null);
|
|
20600
|
+
if (raw === null) throw new Error(`no scan session to resume at ${path7} \u2014 launch it instead`);
|
|
20601
|
+
let parsed;
|
|
20602
|
+
try {
|
|
20603
|
+
parsed = JSON.parse(raw);
|
|
20604
|
+
} catch {
|
|
20605
|
+
throw new Error(`${path7} is not valid JSON; its session id cannot be read`);
|
|
20606
|
+
}
|
|
20607
|
+
if (typeof parsed.sessionId !== "string" || !SESSION_ID_RE.test(parsed.sessionId)) {
|
|
20608
|
+
throw new Error(`${path7} holds no valid session id`);
|
|
20609
|
+
}
|
|
20610
|
+
return { sessionId: parsed.sessionId, pid: typeof parsed.pid === "number" ? parsed.pid : 0 };
|
|
20611
|
+
}
|
|
20557
20612
|
function todayDate() {
|
|
20558
20613
|
const d = /* @__PURE__ */ new Date();
|
|
20559
20614
|
const yyyy = d.getUTCFullYear();
|
|
@@ -20837,7 +20892,7 @@ async function runScan(opts, deps = {}) {
|
|
|
20837
20892
|
});
|
|
20838
20893
|
log(` signed in as ${authed2.identity}`);
|
|
20839
20894
|
const { supabase, userId } = authed2;
|
|
20840
|
-
const workspace = opts.mode === "roster" || opts.mode === "stitch";
|
|
20895
|
+
const workspace = opts.mode === "roster" || opts.mode === "stitch" || opts.mode === "orchestrator";
|
|
20841
20896
|
let headSha = null;
|
|
20842
20897
|
if (!workspace) {
|
|
20843
20898
|
try {
|
|
@@ -20898,25 +20953,31 @@ async function runScan(opts, deps = {}) {
|
|
|
20898
20953
|
try {
|
|
20899
20954
|
const model = await resolveRunnableModel(supabase, { warn: log });
|
|
20900
20955
|
const interactive = opts.interactive !== false;
|
|
20956
|
+
if (interactive && opts.resume) throw new Error("only a headless scan can be resumed");
|
|
20957
|
+
const session = interactive ? null : { sessionId: opts.resume ? (await readScanSession(opts.repoPath)).sessionId : randomUUID2(), pid: process.pid };
|
|
20958
|
+
if (session) await writeScanSession(opts.repoPath, session);
|
|
20959
|
+
const timeoutMs = opts.timeoutMs === void 0 ? SCAN_CLAUDE_TIMEOUT_MS : opts.timeoutMs;
|
|
20901
20960
|
log(
|
|
20902
|
-
interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` :
|
|
20961
|
+
interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` : `${opts.resume ? "resuming" : "spawning"} claude (session ${session.sessionId}, mcp config at ${configPath}, model ${model}, ${timeoutMs === null ? "no timeout" : `timeout ${Math.round(timeoutMs / 6e4)} min`})`
|
|
20903
20962
|
);
|
|
20904
20963
|
const spawn2 = deps.spawn ?? spawnClaude;
|
|
20905
20964
|
const proc = spawn2({
|
|
20906
|
-
prompt: buildBootstrapPrompt({
|
|
20965
|
+
prompt: opts.resume ? opts.resume.message : buildBootstrapPrompt({
|
|
20907
20966
|
promptVariant: opts.promptVariant,
|
|
20908
20967
|
interactive,
|
|
20909
20968
|
briefing: opts.briefing,
|
|
20910
20969
|
mode: opts.mode,
|
|
20911
20970
|
container: opts.container,
|
|
20912
20971
|
repos: opts.repos,
|
|
20913
|
-
plan: opts.plan
|
|
20972
|
+
plan: opts.plan,
|
|
20973
|
+
runSheet: opts.runSheet
|
|
20914
20974
|
}),
|
|
20915
20975
|
mcpConfigPath: configPath,
|
|
20916
20976
|
cwd: opts.repoPath,
|
|
20917
20977
|
// No timeout interactively — a human is present and can Ctrl-C. Killing a
|
|
20918
20978
|
// session mid-answer would be worse than any runaway.
|
|
20919
|
-
timeoutMs: interactive ? void 0 :
|
|
20979
|
+
timeoutMs: interactive ? void 0 : timeoutMs,
|
|
20980
|
+
...session ? opts.resume ? { resumeSessionId: session.sessionId } : { sessionId: session.sessionId } : {},
|
|
20920
20981
|
allowedTools: SCAN_ALLOWED_TOOLS,
|
|
20921
20982
|
model,
|
|
20922
20983
|
interactive,
|
|
@@ -20929,7 +20990,7 @@ async function runScan(opts, deps = {}) {
|
|
|
20929
20990
|
const transcriptDir = join21(opts.repoPath, ".archme", "scan");
|
|
20930
20991
|
const transcriptPath = join21(transcriptDir, "transcript.log");
|
|
20931
20992
|
await mkdir13(transcriptDir, { recursive: true });
|
|
20932
|
-
const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
|
|
20993
|
+
const transcriptStream = createWriteStream2(transcriptPath, { flags: opts.resume ? "a" : "w" });
|
|
20933
20994
|
let finalResultText = "";
|
|
20934
20995
|
const echo = opts.echo ?? ((line) => process.stdout.write(line + "\n"));
|
|
20935
20996
|
if (proc.stdout) {
|
|
@@ -21037,7 +21098,7 @@ async function runScan(opts, deps = {}) {
|
|
|
21037
21098
|
});
|
|
21038
21099
|
}
|
|
21039
21100
|
}
|
|
21040
|
-
var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE, CONTAINER_ID_RE, REPO_SLUG_RE;
|
|
21101
|
+
var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE, CONTAINER_ID_RE, REPO_SLUG_RE, SCAN_SESSION_FILE, SESSION_ID_RE;
|
|
21041
21102
|
var init_run_scan = __esm({
|
|
21042
21103
|
"src/scan/run-scan.ts"() {
|
|
21043
21104
|
"use strict";
|
|
@@ -21064,6 +21125,8 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
|
|
|
21064
21125
|
PROMPT_VARIANT_RE = /^\d{2}-\d{2}-\d{2}-\d{4}(-[A-Za-z0-9_-]+)?$/;
|
|
21065
21126
|
CONTAINER_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
21066
21127
|
REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
21128
|
+
SCAN_SESSION_FILE = ".archme/scan/session.json";
|
|
21129
|
+
SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21067
21130
|
}
|
|
21068
21131
|
});
|
|
21069
21132
|
|
|
@@ -22125,7 +22188,7 @@ var init_plan = __esm({
|
|
|
22125
22188
|
});
|
|
22126
22189
|
|
|
22127
22190
|
// src/init/run-init.ts
|
|
22128
|
-
import { mkdtemp as mkdtemp4, rm as rm9, writeFile as
|
|
22191
|
+
import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
|
|
22129
22192
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
22130
22193
|
import { join as join24 } from "node:path";
|
|
22131
22194
|
async function defaultStartDaemon() {
|
|
@@ -22175,7 +22238,7 @@ async function defaultRunInstallScript(script, cwd) {
|
|
|
22175
22238
|
const dir = await mkdtemp4(join24(tmpdir4(), "archme-init-"));
|
|
22176
22239
|
const scriptPath = join24(dir, "install-agent.sh");
|
|
22177
22240
|
try {
|
|
22178
|
-
await
|
|
22241
|
+
await writeFile13(scriptPath, script, { mode: 448 });
|
|
22179
22242
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
22180
22243
|
} finally {
|
|
22181
22244
|
await rm9(dir, { recursive: true, force: true });
|
|
@@ -23405,7 +23468,8 @@ async function addRepoToBoard(board, add, deps = {}) {
|
|
|
23405
23468
|
mode: "repo",
|
|
23406
23469
|
container: containerId,
|
|
23407
23470
|
interactive: add.interactive,
|
|
23408
|
-
...add.plan ? { plan: true } : {},
|
|
23471
|
+
...add.plan ? { plan: true, timeoutMs: ORCHESTRATED_SCAN_TIMEOUT_MS } : {},
|
|
23472
|
+
...add.resume ? { resume: add.resume } : {},
|
|
23409
23473
|
...add.echo ? { echo: add.echo } : {},
|
|
23410
23474
|
log
|
|
23411
23475
|
},
|
|
@@ -23458,7 +23522,7 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23458
23522
|
supabaseUrl: cfg.supabaseUrl,
|
|
23459
23523
|
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
23460
23524
|
clientId: cfg.archmeDaemonClientId,
|
|
23461
|
-
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
23525
|
+
preSpawnMinLifetimeMs: opts.plan ? ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS : SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
23462
23526
|
});
|
|
23463
23527
|
if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
|
|
23464
23528
|
const project = await (deps.resolveProject ?? resolveOrCreateProject)(authed2.supabase, authed2.userId, opts.project);
|
|
@@ -23471,7 +23535,9 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23471
23535
|
target: { ...early.target, archmeBranch: scanBranch },
|
|
23472
23536
|
container: flagContainer,
|
|
23473
23537
|
local: opts.local,
|
|
23474
|
-
interactive: !opts.yes
|
|
23538
|
+
interactive: !opts.yes,
|
|
23539
|
+
...opts.plan ? { plan: true } : {},
|
|
23540
|
+
...opts.resume !== void 0 ? { resume: { message: opts.resume } } : {}
|
|
23475
23541
|
},
|
|
23476
23542
|
deps
|
|
23477
23543
|
);
|
|
@@ -23479,6 +23545,7 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23479
23545
|
const containerPath = added.containerPath;
|
|
23480
23546
|
log(`
|
|
23481
23547
|
\u2713 ${repo} is in ${containerPath}.`);
|
|
23548
|
+
if (opts.plan) return 0;
|
|
23482
23549
|
log(" Next: run `archme add-repo` inside each remaining repo's clone; once all are in,");
|
|
23483
23550
|
log(` run \`archme scan --stitch --project ${opts.project}\` from the directory holding the clones.`);
|
|
23484
23551
|
log(" Watching queues work; the daemon runs it: archme daemon start");
|
|
@@ -24634,6 +24701,9 @@ function parseAddRepoArgs(argv) {
|
|
|
24634
24701
|
let container;
|
|
24635
24702
|
let local = false;
|
|
24636
24703
|
let yes = false;
|
|
24704
|
+
let plan = false;
|
|
24705
|
+
let resume = false;
|
|
24706
|
+
let message;
|
|
24637
24707
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
24638
24708
|
const arg = argv[i2];
|
|
24639
24709
|
const next = argv[i2 + 1];
|
|
@@ -24649,6 +24719,14 @@ function parseAddRepoArgs(argv) {
|
|
|
24649
24719
|
local = true;
|
|
24650
24720
|
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
24651
24721
|
yes = true;
|
|
24722
|
+
} else if (arg === "--plan") {
|
|
24723
|
+
plan = true;
|
|
24724
|
+
} else if (arg === "--resume") {
|
|
24725
|
+
resume = true;
|
|
24726
|
+
} else if (arg === "--message") {
|
|
24727
|
+
if (!next) return err2("--message needs the text to resume with");
|
|
24728
|
+
message = next;
|
|
24729
|
+
i2++;
|
|
24652
24730
|
} else if (arg.startsWith("-")) {
|
|
24653
24731
|
return err2(`unknown flag '${arg}'`);
|
|
24654
24732
|
} else if (repoPath === void 0) {
|
|
@@ -24658,7 +24736,11 @@ function parseAddRepoArgs(argv) {
|
|
|
24658
24736
|
}
|
|
24659
24737
|
}
|
|
24660
24738
|
if (!project) return err2("--project <org/slug> is required \u2014 the multi-repo project to add this repo to");
|
|
24661
|
-
|
|
24739
|
+
if (resume !== (message !== void 0)) return err2("--resume and --message go together: the message is what the scan resumes with");
|
|
24740
|
+
if (resume) {
|
|
24741
|
+
return { kind: "run", opts: { repoPath, project, container, local, yes: true, plan, resume: message } };
|
|
24742
|
+
}
|
|
24743
|
+
return { kind: "run", opts: { repoPath, project, container, local, yes, plan } };
|
|
24662
24744
|
}
|
|
24663
24745
|
function printAddRepoHelp() {
|
|
24664
24746
|
console.log(`archme add-repo \u2014 add one repo to a multi-repo project
|
|
@@ -24678,7 +24760,15 @@ Options:
|
|
|
24678
24760
|
--local Poll from this machine instead of the GitHub App
|
|
24679
24761
|
--yes Run the scan headless (no questions)
|
|
24680
24762
|
|
|
24681
|
-
|
|
24763
|
+
Used by the \`archme add-repos\` orchestrator:
|
|
24764
|
+
--plan Build against the approved story plan, with a
|
|
24765
|
+
${ORCHESTRATED_SCAN_HOURS}-hour timeout
|
|
24766
|
+
--resume --message <text>
|
|
24767
|
+
Continue this repo's last headless scan session,
|
|
24768
|
+
with its full history, from this message
|
|
24769
|
+
|
|
24770
|
+
Several repos at once (one agent plans with you, scans each, stitches):
|
|
24771
|
+
\`archme add-repos\`.`);
|
|
24682
24772
|
}
|
|
24683
24773
|
async function dispatchAddRepo(argv) {
|
|
24684
24774
|
const parsed = parseAddRepoArgs(argv);
|
|
@@ -24699,10 +24789,12 @@ async function dispatchAddRepo(argv) {
|
|
|
24699
24789
|
return 3;
|
|
24700
24790
|
}
|
|
24701
24791
|
}
|
|
24702
|
-
var PROJECT_SLUG_RE4;
|
|
24792
|
+
var ORCHESTRATED_SCAN_HOURS, PROJECT_SLUG_RE4;
|
|
24703
24793
|
var init_dispatch6 = __esm({
|
|
24704
24794
|
"src/addRepo/dispatch.ts"() {
|
|
24705
24795
|
"use strict";
|
|
24796
|
+
init_authedClient();
|
|
24797
|
+
ORCHESTRATED_SCAN_HOURS = ORCHESTRATED_SCAN_TIMEOUT_MS / 36e5;
|
|
24706
24798
|
PROJECT_SLUG_RE4 = /^[^/\s]+\/[^/\s]+$/;
|
|
24707
24799
|
}
|
|
24708
24800
|
});
|
|
@@ -24710,6 +24802,7 @@ var init_dispatch6 = __esm({
|
|
|
24710
24802
|
// src/addRepos/run.ts
|
|
24711
24803
|
var run_exports3 = {};
|
|
24712
24804
|
__export(run_exports3, {
|
|
24805
|
+
buildRunSheet: () => buildRunSheet,
|
|
24713
24806
|
cloneFromGitHub: () => cloneFromGitHub,
|
|
24714
24807
|
listClones: () => listClones,
|
|
24715
24808
|
runAddRepos: () => runAddRepos,
|
|
@@ -24717,7 +24810,7 @@ __export(run_exports3, {
|
|
|
24717
24810
|
watchedElsewhere: () => watchedElsewhere
|
|
24718
24811
|
});
|
|
24719
24812
|
import { readdir as readdir2, stat as stat11 } from "node:fs/promises";
|
|
24720
|
-
import { dirname as
|
|
24813
|
+
import { dirname as dirname16, join as join26, resolve as resolve6, sep as sep2 } from "node:path";
|
|
24721
24814
|
function isThisRunsWrite(metadata) {
|
|
24722
24815
|
return metadata?.kind === "scan" || !!metadata?.repo_fold;
|
|
24723
24816
|
}
|
|
@@ -24752,7 +24845,7 @@ async function listClones(dir) {
|
|
|
24752
24845
|
return out.sort();
|
|
24753
24846
|
}
|
|
24754
24847
|
function commonDir(paths) {
|
|
24755
|
-
const split = paths.map((p) =>
|
|
24848
|
+
const split = paths.map((p) => dirname16(p).split(sep2));
|
|
24756
24849
|
const common2 = [];
|
|
24757
24850
|
for (let i2 = 0; split.every((s) => s[i2] !== void 0 && s[i2] === split[0][i2]); i2++) common2.push(split[0][i2]);
|
|
24758
24851
|
return common2.join(sep2) || sep2;
|
|
@@ -24764,9 +24857,6 @@ function duration(ms) {
|
|
|
24764
24857
|
const s = Math.round(ms / 1e3);
|
|
24765
24858
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
24766
24859
|
}
|
|
24767
|
-
function plural(n2, word) {
|
|
24768
|
-
return `${n2} ${word}${n2 === 1 ? "" : "s"}`;
|
|
24769
|
-
}
|
|
24770
24860
|
function serialized(fn) {
|
|
24771
24861
|
let tail = Promise.resolve();
|
|
24772
24862
|
return (...args) => {
|
|
@@ -24776,18 +24866,28 @@ function serialized(fn) {
|
|
|
24776
24866
|
return run;
|
|
24777
24867
|
};
|
|
24778
24868
|
}
|
|
24779
|
-
|
|
24780
|
-
const
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24869
|
+
function buildRunSheet(opts) {
|
|
24870
|
+
const launch = (r) => `${opts.archme} add-repo ${shellQuote(r.root)} --project ${shellQuote(opts.project)} --container ${r.containerId} --plan --yes${opts.local ? " --local" : ""}`;
|
|
24871
|
+
const lines = [
|
|
24872
|
+
"## Session brief",
|
|
24873
|
+
"",
|
|
24874
|
+
`Project \`${opts.project}\`, branch \`${opts.scanBranch}\`; the clones are under \`${opts.workspace}\`.`,
|
|
24875
|
+
"",
|
|
24876
|
+
"Scan these repos. Launch each once; resume a scan rather than relaunch it.",
|
|
24877
|
+
...opts.scan.flatMap((r) => [
|
|
24878
|
+
"",
|
|
24879
|
+
`### ${r.repo} \u2192 \`root.${r.containerId}\``,
|
|
24880
|
+
`- Clone: \`${r.root}\``,
|
|
24881
|
+
`- Launch: \`${launch(r)}\``,
|
|
24882
|
+
`- Resume: \`${launch(r)} --resume --message "<answer or correction>"\``,
|
|
24883
|
+
`- Session file: \`${join26(r.root, SCAN_SESSION_FILE)}\``
|
|
24884
|
+
])
|
|
24885
|
+
];
|
|
24886
|
+
if (opts.context.length > 0) {
|
|
24887
|
+
lines.push("", "Already on the board \u2014 context only, never launch a scan for these:");
|
|
24888
|
+
for (const r of opts.context) lines.push(`- ${r.repo} (\`root.${r.containerId}\`): ${r.why}`);
|
|
24889
|
+
}
|
|
24890
|
+
return lines.join("\n");
|
|
24791
24891
|
}
|
|
24792
24892
|
async function runAddRepos(opts, deps = {}) {
|
|
24793
24893
|
const log = deps.log ?? ((m) => console.log(m));
|
|
@@ -24802,7 +24902,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24802
24902
|
const session = deps.session ?? runScan;
|
|
24803
24903
|
const confirm = deps.confirm ?? ((q) => promptYesNo(q, { defaultYes: false }));
|
|
24804
24904
|
if (!opts.yes && !(deps.isInteractive ?? stdinIsInteractive)()) {
|
|
24805
|
-
return fail(78, "the
|
|
24905
|
+
return fail(78, "the orchestrator session is interactive and needs a terminal \u2014 pass --yes to run everything headless");
|
|
24806
24906
|
}
|
|
24807
24907
|
const explicit = opts.targets.length > 0;
|
|
24808
24908
|
const toClone = (inspected) => {
|
|
@@ -24855,7 +24955,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24855
24955
|
supabaseUrl: cfg.supabaseUrl,
|
|
24856
24956
|
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
24857
24957
|
clientId: cfg.archmeDaemonClientId,
|
|
24858
|
-
preSpawnMinLifetimeMs:
|
|
24958
|
+
preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
|
|
24859
24959
|
});
|
|
24860
24960
|
if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
|
|
24861
24961
|
const { supabase } = authed2;
|
|
@@ -24885,7 +24985,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24885
24985
|
const toWatch = done.filter((c3) => !live(c3.target.repo));
|
|
24886
24986
|
const skipped = done.filter((c3) => live(c3.target.repo));
|
|
24887
24987
|
const noClone = roster.added.filter((r) => !clones.some((c3) => sameRepo(c3.target.repo, r)));
|
|
24888
|
-
log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}),
|
|
24988
|
+
log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}), the orchestrator runs in ${workspace}`);
|
|
24889
24989
|
for (const c3 of todo) log(` add: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)}`);
|
|
24890
24990
|
for (const c3 of toWatch) log(` watch: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)} (already scanned, so not rescanned)`);
|
|
24891
24991
|
if (skipped.length) {
|
|
@@ -24921,24 +25021,24 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24921
25021
|
Nothing was changed. Unwatch it there first, or add it to that project instead.`);
|
|
24922
25022
|
}
|
|
24923
25023
|
if (!opts.local) {
|
|
24924
|
-
const
|
|
25024
|
+
const missing2 = [];
|
|
24925
25025
|
for (const c3 of watching) {
|
|
24926
25026
|
const found = await (deps.findInstallation ?? findInstallation)(supabase, authed2.orgId, c3.target.repoOwner);
|
|
24927
|
-
if (!found.ok)
|
|
25027
|
+
if (!found.ok) missing2.push(`${c3.target.repo}: ${found.reason}`);
|
|
24928
25028
|
}
|
|
24929
|
-
if (
|
|
24930
|
-
return fail(3, `${
|
|
25029
|
+
if (missing2.length) {
|
|
25030
|
+
return fail(3, `${missing2.join("\n ")}
|
|
24931
25031
|
Nothing was changed. Install the GitHub App there, or re-run with --local to poll from this machine.`);
|
|
24932
25032
|
}
|
|
24933
25033
|
}
|
|
24934
25034
|
const project = existing ?? await (deps.resolveProject ?? resolveOrCreateProject)(supabase, authed2.userId, opts.project);
|
|
24935
25035
|
const board = { authed: authed2, orgId: authed2.orgId, projectId: project.projectId, project: opts.project, scanBranch };
|
|
24936
25036
|
if (roster.fold) {
|
|
24937
|
-
const
|
|
25037
|
+
const failed = await foldForRepos({ supabase, projectId: project.projectId, scanBranch }, roster.fold, "multi-repo add-repos", {
|
|
24938
25038
|
foldBoard: deps.foldBoard,
|
|
24939
25039
|
log
|
|
24940
25040
|
});
|
|
24941
|
-
if (
|
|
25041
|
+
if (failed) return fail(3, `${failed}
|
|
24942
25042
|
Nothing was scanned.`);
|
|
24943
25043
|
}
|
|
24944
25044
|
const register = serialized(registerLocalWatch);
|
|
@@ -24967,23 +25067,23 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24967
25067
|
${MULTI_REPO_DAEMON_NOTE}`);
|
|
24968
25068
|
return 0;
|
|
24969
25069
|
}
|
|
24970
|
-
const offerUndo = async () => {
|
|
25070
|
+
const offerUndo = async (client) => {
|
|
24971
25071
|
if (!await confirm("Remove what this run wrote to the board? It goes back to how it was before this run, as one new commit.")) {
|
|
24972
25072
|
log(" Left the board as it is.");
|
|
24973
25073
|
return;
|
|
24974
25074
|
}
|
|
24975
25075
|
try {
|
|
24976
|
-
const architecture = before.headCommitId ? before.architecture : (await load2(
|
|
24977
|
-
const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(
|
|
25076
|
+
const architecture = before.headCommitId ? before.architecture : (await load2(client, project.projectId, project.defaultBranch)).architecture;
|
|
25077
|
+
const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(client, {
|
|
24978
25078
|
projectId: project.projectId,
|
|
24979
25079
|
branch: scanBranch,
|
|
24980
25080
|
snapshot: { architecture, headCommitId: before.headCommitId },
|
|
24981
|
-
message: "Undo archme add-repos (
|
|
25081
|
+
message: "Undo archme add-repos (no repo was scanned)",
|
|
24982
25082
|
isOwn: isThisRunsWrite
|
|
24983
25083
|
});
|
|
24984
25084
|
if (roster.fold && !hasRepoContainers(before.architecture)) {
|
|
24985
25085
|
await (deps.setRepoContainer ?? ((s, p, r, c3) => setRepoContainer(s, p, r, c3)))(
|
|
24986
|
-
|
|
25086
|
+
client,
|
|
24987
25087
|
project.projectId,
|
|
24988
25088
|
roster.fold.repo,
|
|
24989
25089
|
null
|
|
@@ -24995,105 +25095,82 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24995
25095
|
}
|
|
24996
25096
|
};
|
|
24997
25097
|
const interactive = !opts.yes;
|
|
25098
|
+
const [nodeBin, cliEntry] = deps.archmeCommand === void 0 ? archmeSelfCommand() : [null, null];
|
|
25099
|
+
const runSheet = buildRunSheet({
|
|
25100
|
+
project: opts.project,
|
|
25101
|
+
scanBranch,
|
|
25102
|
+
workspace,
|
|
25103
|
+
archme: deps.archmeCommand ?? `${shellQuote(nodeBin)} ${shellQuote(cliEntry)}`,
|
|
25104
|
+
local: opts.local,
|
|
25105
|
+
scan: todo.map((c3) => ({ repo: c3.target.repo, root: c3.root, containerId: containerFor(c3.target.repo) })),
|
|
25106
|
+
context: [
|
|
25107
|
+
...done.map((c3) => ({ repo: c3.target.repo, containerId: containerFor(c3.target.repo), why: "already scanned" })),
|
|
25108
|
+
...noClone.map((repo) => ({ repo, containerId: containerFor(repo), why: "no clone here; read it through the graph" }))
|
|
25109
|
+
]
|
|
25110
|
+
});
|
|
24998
25111
|
const t0 = Date.now();
|
|
24999
25112
|
log(`
|
|
25000
|
-
\u2500\u2500
|
|
25001
|
-
const
|
|
25002
|
-
{
|
|
25113
|
+
\u2500\u2500 Orchestrator session (${interactive ? "interactive \u2014 keep it open until it finishes; closing it stops the scans it runs" : "headless"}) \u2500\u2500`);
|
|
25114
|
+
const orchestrated = await session(
|
|
25115
|
+
{
|
|
25116
|
+
repoPath: workspace,
|
|
25117
|
+
project: opts.project,
|
|
25118
|
+
branch: scanBranch,
|
|
25119
|
+
mode: "orchestrator",
|
|
25120
|
+
repos: roster.repos,
|
|
25121
|
+
runSheet,
|
|
25122
|
+
interactive,
|
|
25123
|
+
timeoutMs: null,
|
|
25124
|
+
log
|
|
25125
|
+
},
|
|
25003
25126
|
{ authed: authed2 }
|
|
25004
25127
|
);
|
|
25005
|
-
const
|
|
25006
|
-
log(`
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
if (interactive) {
|
|
25013
|
-
if (!await confirm(`Scan ${plural(todo.length, "repo")} against this plan?`)) {
|
|
25014
|
-
await offerUndo();
|
|
25015
|
-
return 1;
|
|
25016
|
-
}
|
|
25017
|
-
} else {
|
|
25018
|
-
log(" plan approved (--yes)");
|
|
25019
|
-
}
|
|
25020
|
-
log(`
|
|
25021
|
-
\u2500\u2500 Scanning ${plural(todo.length, "repo")}, ${Math.min(opts.parallel, todo.length)} at a time \u2500\u2500`);
|
|
25022
|
-
const results = await mapLimited(todo, opts.parallel, async (clone) => {
|
|
25023
|
-
const tag = `[${clone.target.repoName}]`;
|
|
25024
|
-
const started = Date.now();
|
|
25025
|
-
const containerPath = `root.${containerFor(clone.target.repo)}`;
|
|
25026
|
-
try {
|
|
25027
|
-
const out = await addRepo(
|
|
25028
|
-
board,
|
|
25029
|
-
{
|
|
25030
|
-
root: clone.root,
|
|
25031
|
-
originUrl: clone.originUrl,
|
|
25032
|
-
target: { ...clone.target, archmeBranch: scanBranch },
|
|
25033
|
-
container: containerFor(clone.target.repo),
|
|
25034
|
-
local: opts.local,
|
|
25035
|
-
interactive: false,
|
|
25036
|
-
plan: true,
|
|
25037
|
-
echo: (line) => log(`${tag} ${line}`)
|
|
25038
|
-
},
|
|
25039
|
-
{ log: tagged(clone), registerLocalWatch: register }
|
|
25040
|
-
);
|
|
25041
|
-
const ms = Date.now() - started;
|
|
25042
|
-
if (!out.ok) return { clone, containerPath, marker: null, ms, error: { exitCode: out.exitCode, reason: out.reason ?? "the scan failed" } };
|
|
25043
|
-
const marker = await readMarker(supabase, project.projectId, scanBranch, clone.target.repo);
|
|
25044
|
-
const error = marker ? null : { exitCode: 1, reason: "the scan ended without stamping its resume marker (complete_scan)" };
|
|
25045
|
-
return { clone, containerPath: out.containerPath, marker, ms, error };
|
|
25046
|
-
} catch (e) {
|
|
25047
|
-
return { clone, containerPath, marker: null, ms: Date.now() - started, error: { exitCode: 3, reason: e instanceof Error ? e.message : String(e) } };
|
|
25048
|
-
}
|
|
25128
|
+
const sessionExit = dispatchExitCode(orchestrated);
|
|
25129
|
+
log(sessionExit === 0 ? ` orchestrator session: ${duration(Date.now() - t0)}` : ` orchestrator session exited ${sessionExit} after ${duration(Date.now() - t0)}`);
|
|
25130
|
+
const after = await (deps.authenticate ?? createAuthedClient)({
|
|
25131
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
25132
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
25133
|
+
clientId: cfg.archmeDaemonClientId,
|
|
25134
|
+
preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
|
|
25049
25135
|
});
|
|
25050
|
-
const
|
|
25051
|
-
const
|
|
25052
|
-
|
|
25053
|
-
if (added.length > 0) {
|
|
25054
|
-
const t1 = Date.now();
|
|
25055
|
-
log(`
|
|
25056
|
-
\u2500\u2500 Stitch session (${interactive ? "interactive" : "headless"}) \u2500\u2500`);
|
|
25057
|
-
stitchExit = dispatchExitCode(await session(
|
|
25058
|
-
{ repoPath: workspace, project: opts.project, branch: scanBranch, mode: "stitch", plan: true, interactive, log },
|
|
25059
|
-
{ authed: authed2 }
|
|
25060
|
-
));
|
|
25061
|
-
log(` stitch session: ${duration(Date.now() - t1)}`);
|
|
25136
|
+
const results = [];
|
|
25137
|
+
for (const c3 of todo) {
|
|
25138
|
+
results.push({ clone: c3, marker: await readMarker(after.supabase, project.projectId, scanBranch, c3.target.repo) });
|
|
25062
25139
|
}
|
|
25140
|
+
const added = results.filter((r) => r.marker);
|
|
25141
|
+
const missing = results.filter((r) => !r.marker);
|
|
25142
|
+
if (added.length === 0 && interactive) await offerUndo(after.supabase);
|
|
25063
25143
|
const rerun = [
|
|
25064
25144
|
explicit ? "" : `cd ${shellQuote(workspace)} && `,
|
|
25065
25145
|
"archme add-repos",
|
|
25066
25146
|
...explicit ? clones.map((c3) => ` ${shellQuote(c3.root)}`) : [],
|
|
25067
25147
|
` --project ${opts.project}`,
|
|
25068
25148
|
opts.local ? " --local" : "",
|
|
25069
|
-
opts.parallel !== 2 ? ` --parallel ${opts.parallel}` : "",
|
|
25070
25149
|
opts.yes ? " --yes" : ""
|
|
25071
25150
|
].join("");
|
|
25072
25151
|
const width = Math.max(...results.map((r) => r.clone.target.repo.length));
|
|
25073
25152
|
log("\n\u2500\u2500 Summary \u2500\u2500");
|
|
25074
25153
|
for (const r of added) {
|
|
25075
|
-
log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192
|
|
25154
|
+
log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192 root.${containerFor(r.clone.target.repo)} marker ${r.marker.slice(0, 7)}`);
|
|
25076
25155
|
}
|
|
25077
|
-
for (const r of
|
|
25078
|
-
log(` \u2717 ${r.clone.target.repo.padEnd(width)}
|
|
25156
|
+
for (const r of missing) {
|
|
25157
|
+
log(` \u2717 ${r.clone.target.repo.padEnd(width)} has no resume marker: its scan did not finish`);
|
|
25079
25158
|
}
|
|
25080
25159
|
if (toWatch.length) log(` watched, already scanned: ${toWatch.map((c3) => c3.target.repo).join(", ")}`);
|
|
25081
25160
|
if (skipped.length) log(` skipped, already on the board: ${skipped.map((c3) => c3.target.repo).join(", ")}`);
|
|
25082
|
-
if (
|
|
25161
|
+
if (missing.length) log(` Re-run (skips what is already on the board):
|
|
25083
25162
|
${rerun}`);
|
|
25084
|
-
if (stitchExit === null) log(" Stitch: not run \u2014 no repo was added.");
|
|
25085
|
-
else if (stitchExit !== 0) log(` Stitch: exited ${stitchExit}. Re-run it alone: cd ${shellQuote(workspace)} && archme scan --stitch --project ${opts.project}`);
|
|
25086
25163
|
log(` Board: ${deriveWebOrigin(cfg.archmeMcpBaseUrl, "fallback")}/a/${encodeURIComponent(project.orgSlug)}/${encodeURIComponent(project.projectSlug)}/${encodeURIComponent(scanBranch)}`);
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25164
|
+
const end = await load2(after.supabase, project.projectId, scanBranch).catch(() => null);
|
|
25165
|
+
const changes = end?.architecture.context?.entries.find((e) => e.id === PLAN_CHANGES_ID);
|
|
25166
|
+
if (changes) {
|
|
25090
25167
|
log("\nWhat changed from the approved plan, and why:");
|
|
25091
|
-
log(changes
|
|
25168
|
+
log(changes.content.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
25092
25169
|
}
|
|
25093
25170
|
log(`
|
|
25094
25171
|
Watching queues work; the daemon runs it: archme daemon start
|
|
25095
25172
|
${MULTI_REPO_DAEMON_NOTE}`);
|
|
25096
|
-
return
|
|
25173
|
+
return sessionExit !== 0 ? sessionExit : missing.length > 0 ? 1 : 0;
|
|
25097
25174
|
}
|
|
25098
25175
|
var EMPTY, REPO_NAME_RE;
|
|
25099
25176
|
var init_run3 = __esm({
|
|
@@ -25103,6 +25180,7 @@ var init_run3 = __esm({
|
|
|
25103
25180
|
init_defaults();
|
|
25104
25181
|
init_authedClient();
|
|
25105
25182
|
init_run_scan();
|
|
25183
|
+
init_install();
|
|
25106
25184
|
init_dispatch3();
|
|
25107
25185
|
init_web_url();
|
|
25108
25186
|
init_run_init();
|
|
@@ -25129,7 +25207,6 @@ var init_run3 = __esm({
|
|
|
25129
25207
|
// src/addRepos/dispatch.ts
|
|
25130
25208
|
var dispatch_exports7 = {};
|
|
25131
25209
|
__export(dispatch_exports7, {
|
|
25132
|
-
MAX_PARALLEL: () => MAX_PARALLEL,
|
|
25133
25210
|
dispatchAddRepos: () => dispatchAddRepos,
|
|
25134
25211
|
parseAddReposArgs: () => parseAddReposArgs,
|
|
25135
25212
|
printAddReposHelp: () => printAddReposHelp
|
|
@@ -25143,7 +25220,6 @@ function parseAddReposArgs(argv) {
|
|
|
25143
25220
|
let project;
|
|
25144
25221
|
let local = false;
|
|
25145
25222
|
let yes = false;
|
|
25146
|
-
let parallel = 2;
|
|
25147
25223
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
25148
25224
|
const arg = argv[i2];
|
|
25149
25225
|
const next = argv[i2 + 1];
|
|
@@ -25151,11 +25227,6 @@ function parseAddReposArgs(argv) {
|
|
|
25151
25227
|
if (!next || !PROJECT_SLUG_RE5.test(next)) return err3("--project needs `org/slug`");
|
|
25152
25228
|
project = next;
|
|
25153
25229
|
i2++;
|
|
25154
|
-
} else if (arg === "--parallel") {
|
|
25155
|
-
const n2 = Number(next);
|
|
25156
|
-
if (!next || !Number.isInteger(n2) || n2 < 1 || n2 > MAX_PARALLEL) return err3(`--parallel needs a number from 1 to ${MAX_PARALLEL}`);
|
|
25157
|
-
parallel = n2;
|
|
25158
|
-
i2++;
|
|
25159
25230
|
} else if (arg === "--local") {
|
|
25160
25231
|
local = true;
|
|
25161
25232
|
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
@@ -25167,33 +25238,39 @@ function parseAddReposArgs(argv) {
|
|
|
25167
25238
|
}
|
|
25168
25239
|
}
|
|
25169
25240
|
if (!project) return err3("--project <org/slug> is required \u2014 the multi-repo project to add the repos to");
|
|
25170
|
-
return { kind: "run", opts: { targets, project, local,
|
|
25241
|
+
return { kind: "run", opts: { targets, project, local, yes } };
|
|
25171
25242
|
}
|
|
25172
25243
|
function printAddReposHelp() {
|
|
25173
25244
|
console.log(`archme add-repos \u2014 add several repos to a multi-repo project in one run
|
|
25174
25245
|
|
|
25175
25246
|
Usage:
|
|
25176
|
-
archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--
|
|
25247
|
+
archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--yes]
|
|
25177
25248
|
|
|
25178
25249
|
Each repo is a clone path, or owner/name: a clone of it already in the current
|
|
25179
25250
|
folder is used, otherwise it is cloned there with your own git access. With
|
|
25180
25251
|
none given, every git clone directly under the current folder is added.
|
|
25181
25252
|
Clones of repos the board already holds are read as context, not re-added.
|
|
25182
25253
|
|
|
25183
|
-
|
|
25184
|
-
|
|
25185
|
-
|
|
25186
|
-
|
|
25187
|
-
|
|
25188
|
-
|
|
25189
|
-
|
|
25190
|
-
|
|
25254
|
+
Checks everything first (project, GitHub App, containers) and writes nothing
|
|
25255
|
+
until all pass; a single-repo board is folded into its repo's container. Then
|
|
25256
|
+
ONE orchestrator agent runs the rest, in an interactive session:
|
|
25257
|
+
|
|
25258
|
+
1. The gist: names every repo's container and the shared externals, studies
|
|
25259
|
+
each repo and the product (web included), and drafts the story plan with
|
|
25260
|
+
you until you approve it.
|
|
25261
|
+
2. Depth: studies each repo's code and git history, revises the plan, and
|
|
25262
|
+
shows you what changed until you approve it.
|
|
25263
|
+
3. Build: launches one scan per repo (\`archme add-repo --plan\`, hours each),
|
|
25264
|
+
supervises them \u2014 answering their questions, pausing and resuming any that
|
|
25265
|
+
go off course \u2014 and stitches what crosses repos as each side lands, then
|
|
25266
|
+
explains what changed from the plan.
|
|
25267
|
+
|
|
25268
|
+
Keep the session open until it finishes: closing it stops the scans it runs.
|
|
25191
25269
|
Re-running skips repos already on the board, so a failed run resumes.
|
|
25192
25270
|
|
|
25193
25271
|
Options:
|
|
25194
25272
|
--project <org/slug> The multi-repo project (required)
|
|
25195
25273
|
--local Poll from this machine instead of the GitHub App
|
|
25196
|
-
--parallel <n> Repos scanned at once, 1-${MAX_PARALLEL} (default 2)
|
|
25197
25274
|
--yes Everything headless, no questions; the plan is approved`);
|
|
25198
25275
|
}
|
|
25199
25276
|
async function dispatchAddRepos(argv) {
|
|
@@ -25215,12 +25292,11 @@ async function dispatchAddRepos(argv) {
|
|
|
25215
25292
|
return 3;
|
|
25216
25293
|
}
|
|
25217
25294
|
}
|
|
25218
|
-
var PROJECT_SLUG_RE5
|
|
25295
|
+
var PROJECT_SLUG_RE5;
|
|
25219
25296
|
var init_dispatch7 = __esm({
|
|
25220
25297
|
"src/addRepos/dispatch.ts"() {
|
|
25221
25298
|
"use strict";
|
|
25222
25299
|
PROJECT_SLUG_RE5 = /^[^/\s]+\/[^/\s]+$/;
|
|
25223
|
-
MAX_PARALLEL = 3;
|
|
25224
25300
|
}
|
|
25225
25301
|
});
|
|
25226
25302
|
|
|
@@ -25616,7 +25692,7 @@ __export(dispatch_exports8, {
|
|
|
25616
25692
|
parseDoctorArgs: () => parseDoctorArgs,
|
|
25617
25693
|
printDoctorHelp: () => printDoctorHelp
|
|
25618
25694
|
});
|
|
25619
|
-
import { mkdtemp as mkdtemp5, rm as rm10, writeFile as
|
|
25695
|
+
import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile14 } from "node:fs/promises";
|
|
25620
25696
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
25621
25697
|
import { join as join28 } from "node:path";
|
|
25622
25698
|
function parseDoctorArgs(argv) {
|
|
@@ -25681,7 +25757,7 @@ async function dispatchDoctor(argv) {
|
|
|
25681
25757
|
}
|
|
25682
25758
|
const dir = await mkdtemp5(join28(tmpdir5(), "archme-doctor-"));
|
|
25683
25759
|
const mcpConfigPath = join28(dir, "mcp.json");
|
|
25684
|
-
await
|
|
25760
|
+
await writeFile14(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
25685
25761
|
try {
|
|
25686
25762
|
const result = await spawnClaude({
|
|
25687
25763
|
prompt: buildDoctorPrompt(facts, issues),
|
|
@@ -26090,7 +26166,8 @@ Other commands:
|
|
|
26090
26166
|
(--list|--stop|--resume)
|
|
26091
26167
|
archme add-repos [clone-path | owner/name \u2026] --project <org/slug>
|
|
26092
26168
|
Add several repos to a multi-repo project:
|
|
26093
|
-
|
|
26169
|
+
one agent plans with you, scans each,
|
|
26170
|
+
stitches as they land (resumable)
|
|
26094
26171
|
archme add-repo [repo-path] --project <org/slug>
|
|
26095
26172
|
Add one repo to a multi-repo project
|
|
26096
26173
|
archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archme/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|