@archme/cli 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +232 -154
  2. 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 ?? DEFAULT_CLAUDE_TIMEOUT_MS,
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
  }
@@ -13904,7 +13921,7 @@ function buildRepoContainerDiffGuidance(containerPath) {
13904
13921
  return `## Multi-repo project
13905
13922
  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
13923
  }
13907
- var READ_CONVENTIONS, PLAN_CHANGES_ID, STITCH_PLAN_GUIDANCE;
13924
+ var READ_CONVENTIONS, PLAN_CHANGES_ID, STORY_PLAN_SHAPE, STORY_PLAN_GUIDANCE, PLAN_CHANGES_EXPLANATION, STITCH_PLAN_GUIDANCE, READ_FIRST;
13908
13925
  var init_multiRepoScan = __esm({
13909
13926
  "../shared/multiRepoScan.ts"() {
13910
13927
  init_scanPrompts();
@@ -13913,7 +13930,11 @@ var init_multiRepoScan = __esm({
13913
13930
  init_repoContainer();
13914
13931
  READ_CONVENTIONS = `read_context({ path: "root", type: "decision" })`;
13915
13932
  PLAN_CHANGES_ID = "multi-repo-plan-changes";
13916
- 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 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.`;
13933
+ 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.`;
13934
+ 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}`;
13935
+ 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.`;
13936
+ 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}`;
13937
+ 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
13938
  }
13918
13939
  });
13919
13940
 
@@ -19886,6 +19907,8 @@ var init_identity = __esm({
19886
19907
  // src/scan/authedClient.ts
19887
19908
  var authedClient_exports = {};
19888
19909
  __export(authedClient_exports, {
19910
+ ORCHESTRATED_SCAN_TIMEOUT_MS: () => ORCHESTRATED_SCAN_TIMEOUT_MS,
19911
+ ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS: () => ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS,
19889
19912
  SCAN_CLAUDE_TIMEOUT_MS: () => SCAN_CLAUDE_TIMEOUT_MS,
19890
19913
  SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: () => SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS,
19891
19914
  createAuthedClient: () => createAuthedClient
@@ -19924,7 +19947,7 @@ async function createAuthedClient(opts) {
19924
19947
  accountKind: loaded.kind ?? "oauth"
19925
19948
  };
19926
19949
  }
19927
- var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
19950
+ var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, ORCHESTRATED_SCAN_TIMEOUT_MS, ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS;
19928
19951
  var init_authedClient = __esm({
19929
19952
  "src/scan/authedClient.ts"() {
19930
19953
  "use strict";
@@ -19932,6 +19955,8 @@ var init_authedClient = __esm({
19932
19955
  init_identity();
19933
19956
  SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
19934
19957
  SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
19958
+ ORCHESTRATED_SCAN_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
19959
+ ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS = ORCHESTRATED_SCAN_TIMEOUT_MS + 5 * 60 * 1e3;
19935
19960
  }
19936
19961
  });
19937
19962
 
@@ -20507,24 +20532,32 @@ var init_claimDefaultProject = __esm({
20507
20532
  });
20508
20533
 
20509
20534
  // src/scan/run-scan.ts
20535
+ import { randomUUID as randomUUID2 } from "node:crypto";
20510
20536
  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";
20537
+ import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
20538
+ import { basename as basename2, dirname as dirname15, join as join21 } from "node:path";
20513
20539
  function modeInstruction(opts) {
20514
20540
  const line = modeLine(opts);
20541
+ if (opts.mode === "orchestrator" && opts.plan) {
20542
+ throw new Error("the orchestrator always plans; start_scan refuses `plan` with it");
20543
+ }
20515
20544
  return line && opts.plan ? `${line} Also pass \`plan: true\`: this bootstrap builds against a draft story plan.` : line;
20516
20545
  }
20546
+ function reposArg(opts, mode) {
20547
+ const repos = opts.repos ?? [];
20548
+ if (repos.length === 0 || !repos.every((r) => REPO_SLUG_RE.test(r))) {
20549
+ throw new Error(`${mode} mode needs at least one repo, each \`owner/name\``);
20550
+ }
20551
+ return repos.map((r) => `"${r}"`).join(", ");
20552
+ }
20517
20553
  function modeLine(opts) {
20518
20554
  switch (opts.mode) {
20519
20555
  case void 0:
20520
20556
  return null;
20521
- case "roster": {
20522
- const repos = opts.repos ?? [];
20523
- if (repos.length === 0 || !repos.every((r) => REPO_SLUG_RE.test(r))) {
20524
- throw new Error("roster mode needs at least one repo, each `owner/name`");
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
- }
20557
+ case "roster":
20558
+ return `This is the multi-repo roster session \u2014 pass \`mode: "roster"\` and \`repos: [${reposArg(opts, "roster")}]\` to \`start_scan\`.`;
20559
+ case "orchestrator":
20560
+ return `This is the multi-repo orchestrator session \u2014 pass \`mode: "orchestrator"\` and \`repos: [${reposArg(opts, "orchestrator")}]\` to \`start_scan\`.`;
20528
20561
  case "repo":
20529
20562
  if (!opts.container || !CONTAINER_ID_RE.test(opts.container)) {
20530
20563
  throw new Error(`invalid container id \`${opts.container ?? ""}\``);
@@ -20543,6 +20576,9 @@ ${briefing}` : BOOTSTRAP_INTERACTIVE_NO_BRIEFING : BOOTSTRAP_HEADLESS;
20543
20576
  if (modeLine2) base += `
20544
20577
 
20545
20578
  ${modeLine2}`;
20579
+ if (opts.runSheet) base += `
20580
+
20581
+ ${opts.runSheet}`;
20546
20582
  if (!promptVariant) return base;
20547
20583
  if (!PROMPT_VARIANT_RE.test(promptVariant)) {
20548
20584
  throw new Error(
@@ -20554,6 +20590,26 @@ ${modeLine2}`;
20554
20590
  Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVariant}"\` to \`start_scan\`.`;
20555
20591
  return base;
20556
20592
  }
20593
+ async function writeScanSession(repoPath, session) {
20594
+ const path7 = join21(repoPath, SCAN_SESSION_FILE);
20595
+ await mkdir13(dirname15(path7), { recursive: true });
20596
+ await writeFile12(path7, JSON.stringify(session, null, 2) + "\n");
20597
+ }
20598
+ async function readScanSession(repoPath) {
20599
+ const path7 = join21(repoPath, SCAN_SESSION_FILE);
20600
+ const raw = await readFile14(path7, "utf8").catch(() => null);
20601
+ if (raw === null) throw new Error(`no scan session to resume at ${path7} \u2014 launch it instead`);
20602
+ let parsed;
20603
+ try {
20604
+ parsed = JSON.parse(raw);
20605
+ } catch {
20606
+ throw new Error(`${path7} is not valid JSON; its session id cannot be read`);
20607
+ }
20608
+ if (typeof parsed.sessionId !== "string" || !SESSION_ID_RE.test(parsed.sessionId)) {
20609
+ throw new Error(`${path7} holds no valid session id`);
20610
+ }
20611
+ return { sessionId: parsed.sessionId, pid: typeof parsed.pid === "number" ? parsed.pid : 0 };
20612
+ }
20557
20613
  function todayDate() {
20558
20614
  const d = /* @__PURE__ */ new Date();
20559
20615
  const yyyy = d.getUTCFullYear();
@@ -20837,7 +20893,7 @@ async function runScan(opts, deps = {}) {
20837
20893
  });
20838
20894
  log(` signed in as ${authed2.identity}`);
20839
20895
  const { supabase, userId } = authed2;
20840
- const workspace = opts.mode === "roster" || opts.mode === "stitch";
20896
+ const workspace = opts.mode === "roster" || opts.mode === "stitch" || opts.mode === "orchestrator";
20841
20897
  let headSha = null;
20842
20898
  if (!workspace) {
20843
20899
  try {
@@ -20898,25 +20954,31 @@ async function runScan(opts, deps = {}) {
20898
20954
  try {
20899
20955
  const model = await resolveRunnableModel(supabase, { warn: log });
20900
20956
  const interactive = opts.interactive !== false;
20957
+ if (interactive && opts.resume) throw new Error("only a headless scan can be resumed");
20958
+ const session = interactive ? null : { sessionId: opts.resume ? (await readScanSession(opts.repoPath)).sessionId : randomUUID2(), pid: process.pid };
20959
+ if (session) await writeScanSession(opts.repoPath, session);
20960
+ const timeoutMs = opts.timeoutMs === void 0 ? SCAN_CLAUDE_TIMEOUT_MS : opts.timeoutMs;
20901
20961
  log(
20902
- interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` : `spawning claude (mcp config at ${configPath}, model ${model}, timeout ${Math.round(SCAN_CLAUDE_TIMEOUT_MS / 6e4)} min)`
20962
+ 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
20963
  );
20904
20964
  const spawn2 = deps.spawn ?? spawnClaude;
20905
20965
  const proc = spawn2({
20906
- prompt: buildBootstrapPrompt({
20966
+ prompt: opts.resume ? opts.resume.message : buildBootstrapPrompt({
20907
20967
  promptVariant: opts.promptVariant,
20908
20968
  interactive,
20909
20969
  briefing: opts.briefing,
20910
20970
  mode: opts.mode,
20911
20971
  container: opts.container,
20912
20972
  repos: opts.repos,
20913
- plan: opts.plan
20973
+ plan: opts.plan,
20974
+ runSheet: opts.runSheet
20914
20975
  }),
20915
20976
  mcpConfigPath: configPath,
20916
20977
  cwd: opts.repoPath,
20917
20978
  // No timeout interactively — a human is present and can Ctrl-C. Killing a
20918
20979
  // session mid-answer would be worse than any runaway.
20919
- timeoutMs: interactive ? void 0 : SCAN_CLAUDE_TIMEOUT_MS,
20980
+ timeoutMs: interactive ? void 0 : timeoutMs,
20981
+ ...session ? opts.resume ? { resumeSessionId: session.sessionId } : { sessionId: session.sessionId } : {},
20920
20982
  allowedTools: SCAN_ALLOWED_TOOLS,
20921
20983
  model,
20922
20984
  interactive,
@@ -20929,7 +20991,7 @@ async function runScan(opts, deps = {}) {
20929
20991
  const transcriptDir = join21(opts.repoPath, ".archme", "scan");
20930
20992
  const transcriptPath = join21(transcriptDir, "transcript.log");
20931
20993
  await mkdir13(transcriptDir, { recursive: true });
20932
- const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
20994
+ const transcriptStream = createWriteStream2(transcriptPath, { flags: opts.resume ? "a" : "w" });
20933
20995
  let finalResultText = "";
20934
20996
  const echo = opts.echo ?? ((line) => process.stdout.write(line + "\n"));
20935
20997
  if (proc.stdout) {
@@ -21037,7 +21099,7 @@ async function runScan(opts, deps = {}) {
21037
21099
  });
21038
21100
  }
21039
21101
  }
21040
- var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE, CONTAINER_ID_RE, REPO_SLUG_RE;
21102
+ 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
21103
  var init_run_scan = __esm({
21042
21104
  "src/scan/run-scan.ts"() {
21043
21105
  "use strict";
@@ -21064,6 +21126,8 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
21064
21126
  PROMPT_VARIANT_RE = /^\d{2}-\d{2}-\d{2}-\d{4}(-[A-Za-z0-9_-]+)?$/;
21065
21127
  CONTAINER_ID_RE = /^[A-Za-z0-9_]+$/;
21066
21128
  REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
21129
+ SCAN_SESSION_FILE = ".archme/scan/session.json";
21130
+ 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
21131
  }
21068
21132
  });
21069
21133
 
@@ -22125,7 +22189,7 @@ var init_plan = __esm({
22125
22189
  });
22126
22190
 
22127
22191
  // src/init/run-init.ts
22128
- import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile12 } from "node:fs/promises";
22192
+ import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
22129
22193
  import { tmpdir as tmpdir4 } from "node:os";
22130
22194
  import { join as join24 } from "node:path";
22131
22195
  async function defaultStartDaemon() {
@@ -22175,7 +22239,7 @@ async function defaultRunInstallScript(script, cwd) {
22175
22239
  const dir = await mkdtemp4(join24(tmpdir4(), "archme-init-"));
22176
22240
  const scriptPath = join24(dir, "install-agent.sh");
22177
22241
  try {
22178
- await writeFile12(scriptPath, script, { mode: 448 });
22242
+ await writeFile13(scriptPath, script, { mode: 448 });
22179
22243
  await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
22180
22244
  } finally {
22181
22245
  await rm9(dir, { recursive: true, force: true });
@@ -23405,7 +23469,8 @@ async function addRepoToBoard(board, add, deps = {}) {
23405
23469
  mode: "repo",
23406
23470
  container: containerId,
23407
23471
  interactive: add.interactive,
23408
- ...add.plan ? { plan: true } : {},
23472
+ ...add.plan ? { plan: true, timeoutMs: ORCHESTRATED_SCAN_TIMEOUT_MS } : {},
23473
+ ...add.resume ? { resume: add.resume } : {},
23409
23474
  ...add.echo ? { echo: add.echo } : {},
23410
23475
  log
23411
23476
  },
@@ -23458,7 +23523,7 @@ async function runAddRepo(opts, deps = {}) {
23458
23523
  supabaseUrl: cfg.supabaseUrl,
23459
23524
  supabaseAnonKey: cfg.supabaseAnonKey,
23460
23525
  clientId: cfg.archmeDaemonClientId,
23461
- preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
23526
+ preSpawnMinLifetimeMs: opts.plan ? ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS : SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
23462
23527
  });
23463
23528
  if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
23464
23529
  const project = await (deps.resolveProject ?? resolveOrCreateProject)(authed2.supabase, authed2.userId, opts.project);
@@ -23471,7 +23536,9 @@ async function runAddRepo(opts, deps = {}) {
23471
23536
  target: { ...early.target, archmeBranch: scanBranch },
23472
23537
  container: flagContainer,
23473
23538
  local: opts.local,
23474
- interactive: !opts.yes
23539
+ interactive: !opts.yes,
23540
+ ...opts.plan ? { plan: true } : {},
23541
+ ...opts.resume !== void 0 ? { resume: { message: opts.resume } } : {}
23475
23542
  },
23476
23543
  deps
23477
23544
  );
@@ -23479,6 +23546,7 @@ async function runAddRepo(opts, deps = {}) {
23479
23546
  const containerPath = added.containerPath;
23480
23547
  log(`
23481
23548
  \u2713 ${repo} is in ${containerPath}.`);
23549
+ if (opts.plan) return 0;
23482
23550
  log(" Next: run `archme add-repo` inside each remaining repo's clone; once all are in,");
23483
23551
  log(` run \`archme scan --stitch --project ${opts.project}\` from the directory holding the clones.`);
23484
23552
  log(" Watching queues work; the daemon runs it: archme daemon start");
@@ -24634,6 +24702,9 @@ function parseAddRepoArgs(argv) {
24634
24702
  let container;
24635
24703
  let local = false;
24636
24704
  let yes = false;
24705
+ let plan = false;
24706
+ let resume = false;
24707
+ let message;
24637
24708
  for (let i2 = 0; i2 < argv.length; i2++) {
24638
24709
  const arg = argv[i2];
24639
24710
  const next = argv[i2 + 1];
@@ -24649,6 +24720,14 @@ function parseAddRepoArgs(argv) {
24649
24720
  local = true;
24650
24721
  } else if (arg === "--yes" || arg === "--no-interactive") {
24651
24722
  yes = true;
24723
+ } else if (arg === "--plan") {
24724
+ plan = true;
24725
+ } else if (arg === "--resume") {
24726
+ resume = true;
24727
+ } else if (arg === "--message") {
24728
+ if (!next) return err2("--message needs the text to resume with");
24729
+ message = next;
24730
+ i2++;
24652
24731
  } else if (arg.startsWith("-")) {
24653
24732
  return err2(`unknown flag '${arg}'`);
24654
24733
  } else if (repoPath === void 0) {
@@ -24658,7 +24737,11 @@ function parseAddRepoArgs(argv) {
24658
24737
  }
24659
24738
  }
24660
24739
  if (!project) return err2("--project <org/slug> is required \u2014 the multi-repo project to add this repo to");
24661
- return { kind: "run", opts: { repoPath, project, container, local, yes } };
24740
+ if (resume !== (message !== void 0)) return err2("--resume and --message go together: the message is what the scan resumes with");
24741
+ if (resume) {
24742
+ return { kind: "run", opts: { repoPath, project, container, local, yes: true, plan, resume: message } };
24743
+ }
24744
+ return { kind: "run", opts: { repoPath, project, container, local, yes, plan } };
24662
24745
  }
24663
24746
  function printAddRepoHelp() {
24664
24747
  console.log(`archme add-repo \u2014 add one repo to a multi-repo project
@@ -24678,7 +24761,15 @@ Options:
24678
24761
  --local Poll from this machine instead of the GitHub App
24679
24762
  --yes Run the scan headless (no questions)
24680
24763
 
24681
- Several repos at once (plan, scan each, stitch): \`archme add-repos\`.`);
24764
+ Used by the \`archme add-repos\` orchestrator:
24765
+ --plan Build against the approved story plan, with a
24766
+ ${ORCHESTRATED_SCAN_HOURS}-hour timeout
24767
+ --resume --message <text>
24768
+ Continue this repo's last headless scan session,
24769
+ with its full history, from this message
24770
+
24771
+ Several repos at once (one agent plans with you, scans each, stitches):
24772
+ \`archme add-repos\`.`);
24682
24773
  }
24683
24774
  async function dispatchAddRepo(argv) {
24684
24775
  const parsed = parseAddRepoArgs(argv);
@@ -24699,10 +24790,12 @@ async function dispatchAddRepo(argv) {
24699
24790
  return 3;
24700
24791
  }
24701
24792
  }
24702
- var PROJECT_SLUG_RE4;
24793
+ var ORCHESTRATED_SCAN_HOURS, PROJECT_SLUG_RE4;
24703
24794
  var init_dispatch6 = __esm({
24704
24795
  "src/addRepo/dispatch.ts"() {
24705
24796
  "use strict";
24797
+ init_authedClient();
24798
+ ORCHESTRATED_SCAN_HOURS = ORCHESTRATED_SCAN_TIMEOUT_MS / 36e5;
24706
24799
  PROJECT_SLUG_RE4 = /^[^/\s]+\/[^/\s]+$/;
24707
24800
  }
24708
24801
  });
@@ -24710,6 +24803,7 @@ var init_dispatch6 = __esm({
24710
24803
  // src/addRepos/run.ts
24711
24804
  var run_exports3 = {};
24712
24805
  __export(run_exports3, {
24806
+ buildRunSheet: () => buildRunSheet,
24713
24807
  cloneFromGitHub: () => cloneFromGitHub,
24714
24808
  listClones: () => listClones,
24715
24809
  runAddRepos: () => runAddRepos,
@@ -24717,7 +24811,7 @@ __export(run_exports3, {
24717
24811
  watchedElsewhere: () => watchedElsewhere
24718
24812
  });
24719
24813
  import { readdir as readdir2, stat as stat11 } from "node:fs/promises";
24720
- import { dirname as dirname15, join as join26, resolve as resolve6, sep as sep2 } from "node:path";
24814
+ import { dirname as dirname16, join as join26, resolve as resolve6, sep as sep2 } from "node:path";
24721
24815
  function isThisRunsWrite(metadata) {
24722
24816
  return metadata?.kind === "scan" || !!metadata?.repo_fold;
24723
24817
  }
@@ -24752,7 +24846,7 @@ async function listClones(dir) {
24752
24846
  return out.sort();
24753
24847
  }
24754
24848
  function commonDir(paths) {
24755
- const split = paths.map((p) => dirname15(p).split(sep2));
24849
+ const split = paths.map((p) => dirname16(p).split(sep2));
24756
24850
  const common2 = [];
24757
24851
  for (let i2 = 0; split.every((s) => s[i2] !== void 0 && s[i2] === split[0][i2]); i2++) common2.push(split[0][i2]);
24758
24852
  return common2.join(sep2) || sep2;
@@ -24764,9 +24858,6 @@ function duration(ms) {
24764
24858
  const s = Math.round(ms / 1e3);
24765
24859
  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
24766
24860
  }
24767
- function plural(n2, word) {
24768
- return `${n2} ${word}${n2 === 1 ? "" : "s"}`;
24769
- }
24770
24861
  function serialized(fn) {
24771
24862
  let tail = Promise.resolve();
24772
24863
  return (...args) => {
@@ -24776,18 +24867,28 @@ function serialized(fn) {
24776
24867
  return run;
24777
24868
  };
24778
24869
  }
24779
- async function mapLimited(items, limit, fn) {
24780
- const out = new Array(items.length);
24781
- let next = 0;
24782
- await Promise.all(
24783
- Array.from({ length: Math.min(limit, items.length) }, async () => {
24784
- while (next < items.length) {
24785
- const i2 = next++;
24786
- out[i2] = await fn(items[i2]);
24787
- }
24788
- })
24789
- );
24790
- return out;
24870
+ function buildRunSheet(opts) {
24871
+ const launch = (r) => `${opts.archme} add-repo ${shellQuote(r.root)} --project ${shellQuote(opts.project)} --container ${r.containerId} --plan --yes${opts.local ? " --local" : ""}`;
24872
+ const lines = [
24873
+ "## Session brief",
24874
+ "",
24875
+ `Project \`${opts.project}\`, branch \`${opts.scanBranch}\`; the clones are under \`${opts.workspace}\`.`,
24876
+ "",
24877
+ "Scan these repos. Launch each once; resume a scan rather than relaunch it.",
24878
+ ...opts.scan.flatMap((r) => [
24879
+ "",
24880
+ `### ${r.repo} \u2192 \`root.${r.containerId}\``,
24881
+ `- Clone: \`${r.root}\``,
24882
+ `- Launch: \`${launch(r)}\``,
24883
+ `- Resume: \`${launch(r)} --resume --message "<answer or correction>"\``,
24884
+ `- Session file: \`${join26(r.root, SCAN_SESSION_FILE)}\``
24885
+ ])
24886
+ ];
24887
+ if (opts.context.length > 0) {
24888
+ lines.push("", "Already on the board \u2014 context only, never launch a scan for these:");
24889
+ for (const r of opts.context) lines.push(`- ${r.repo} (\`root.${r.containerId}\`): ${r.why}`);
24890
+ }
24891
+ return lines.join("\n");
24791
24892
  }
24792
24893
  async function runAddRepos(opts, deps = {}) {
24793
24894
  const log = deps.log ?? ((m) => console.log(m));
@@ -24802,7 +24903,7 @@ async function runAddRepos(opts, deps = {}) {
24802
24903
  const session = deps.session ?? runScan;
24803
24904
  const confirm = deps.confirm ?? ((q) => promptYesNo(q, { defaultYes: false }));
24804
24905
  if (!opts.yes && !(deps.isInteractive ?? stdinIsInteractive)()) {
24805
- return fail(78, "the plan and stitch sessions are interactive and need a terminal \u2014 pass --yes to run everything headless");
24906
+ return fail(78, "the orchestrator session is interactive and needs a terminal \u2014 pass --yes to run everything headless");
24806
24907
  }
24807
24908
  const explicit = opts.targets.length > 0;
24808
24909
  const toClone = (inspected) => {
@@ -24855,7 +24956,7 @@ async function runAddRepos(opts, deps = {}) {
24855
24956
  supabaseUrl: cfg.supabaseUrl,
24856
24957
  supabaseAnonKey: cfg.supabaseAnonKey,
24857
24958
  clientId: cfg.archmeDaemonClientId,
24858
- preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
24959
+ preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
24859
24960
  });
24860
24961
  if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
24861
24962
  const { supabase } = authed2;
@@ -24885,7 +24986,7 @@ async function runAddRepos(opts, deps = {}) {
24885
24986
  const toWatch = done.filter((c3) => !live(c3.target.repo));
24886
24987
  const skipped = done.filter((c3) => live(c3.target.repo));
24887
24988
  const noClone = roster.added.filter((r) => !clones.some((c3) => sameRepo(c3.target.repo, r)));
24888
- log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}), sessions run in ${workspace}`);
24989
+ log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}), the orchestrator runs in ${workspace}`);
24889
24990
  for (const c3 of todo) log(` add: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)}`);
24890
24991
  for (const c3 of toWatch) log(` watch: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)} (already scanned, so not rescanned)`);
24891
24992
  if (skipped.length) {
@@ -24921,24 +25022,24 @@ async function runAddRepos(opts, deps = {}) {
24921
25022
  Nothing was changed. Unwatch it there first, or add it to that project instead.`);
24922
25023
  }
24923
25024
  if (!opts.local) {
24924
- const missing = [];
25025
+ const missing2 = [];
24925
25026
  for (const c3 of watching) {
24926
25027
  const found = await (deps.findInstallation ?? findInstallation)(supabase, authed2.orgId, c3.target.repoOwner);
24927
- if (!found.ok) missing.push(`${c3.target.repo}: ${found.reason}`);
25028
+ if (!found.ok) missing2.push(`${c3.target.repo}: ${found.reason}`);
24928
25029
  }
24929
- if (missing.length) {
24930
- return fail(3, `${missing.join("\n ")}
25030
+ if (missing2.length) {
25031
+ return fail(3, `${missing2.join("\n ")}
24931
25032
  Nothing was changed. Install the GitHub App there, or re-run with --local to poll from this machine.`);
24932
25033
  }
24933
25034
  }
24934
25035
  const project = existing ?? await (deps.resolveProject ?? resolveOrCreateProject)(supabase, authed2.userId, opts.project);
24935
25036
  const board = { authed: authed2, orgId: authed2.orgId, projectId: project.projectId, project: opts.project, scanBranch };
24936
25037
  if (roster.fold) {
24937
- const failed2 = await foldForRepos({ supabase, projectId: project.projectId, scanBranch }, roster.fold, "multi-repo add-repos", {
25038
+ const failed = await foldForRepos({ supabase, projectId: project.projectId, scanBranch }, roster.fold, "multi-repo add-repos", {
24938
25039
  foldBoard: deps.foldBoard,
24939
25040
  log
24940
25041
  });
24941
- if (failed2) return fail(3, `${failed2}
25042
+ if (failed) return fail(3, `${failed}
24942
25043
  Nothing was scanned.`);
24943
25044
  }
24944
25045
  const register = serialized(registerLocalWatch);
@@ -24967,23 +25068,23 @@ async function runAddRepos(opts, deps = {}) {
24967
25068
  ${MULTI_REPO_DAEMON_NOTE}`);
24968
25069
  return 0;
24969
25070
  }
24970
- const offerUndo = async () => {
25071
+ const offerUndo = async (client) => {
24971
25072
  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
25073
  log(" Left the board as it is.");
24973
25074
  return;
24974
25075
  }
24975
25076
  try {
24976
- const architecture = before.headCommitId ? before.architecture : (await load2(supabase, project.projectId, project.defaultBranch)).architecture;
24977
- const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(supabase, {
25077
+ const architecture = before.headCommitId ? before.architecture : (await load2(client, project.projectId, project.defaultBranch)).architecture;
25078
+ const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(client, {
24978
25079
  projectId: project.projectId,
24979
25080
  branch: scanBranch,
24980
25081
  snapshot: { architecture, headCommitId: before.headCommitId },
24981
- message: "Undo archme add-repos (plan not approved)",
25082
+ message: "Undo archme add-repos (no repo was scanned)",
24982
25083
  isOwn: isThisRunsWrite
24983
25084
  });
24984
25085
  if (roster.fold && !hasRepoContainers(before.architecture)) {
24985
25086
  await (deps.setRepoContainer ?? ((s, p, r, c3) => setRepoContainer(s, p, r, c3)))(
24986
- supabase,
25087
+ client,
24987
25088
  project.projectId,
24988
25089
  roster.fold.repo,
24989
25090
  null
@@ -24995,105 +25096,82 @@ async function runAddRepos(opts, deps = {}) {
24995
25096
  }
24996
25097
  };
24997
25098
  const interactive = !opts.yes;
25099
+ const [nodeBin, cliEntry] = deps.archmeCommand === void 0 ? archmeSelfCommand() : [null, null];
25100
+ const runSheet = buildRunSheet({
25101
+ project: opts.project,
25102
+ scanBranch,
25103
+ workspace,
25104
+ archme: deps.archmeCommand ?? `${shellQuote(nodeBin)} ${shellQuote(cliEntry)}`,
25105
+ local: opts.local,
25106
+ scan: todo.map((c3) => ({ repo: c3.target.repo, root: c3.root, containerId: containerFor(c3.target.repo) })),
25107
+ context: [
25108
+ ...done.map((c3) => ({ repo: c3.target.repo, containerId: containerFor(c3.target.repo), why: "already scanned" })),
25109
+ ...noClone.map((repo) => ({ repo, containerId: containerFor(repo), why: "no clone here; read it through the graph" }))
25110
+ ]
25111
+ });
24998
25112
  const t0 = Date.now();
24999
25113
  log(`
25000
- \u2500\u2500 Plan session (${interactive ? "interactive \u2014 exit it when the plan looks right" : "headless"}) \u2500\u2500`);
25001
- const plan = await session(
25002
- { repoPath: workspace, project: opts.project, branch: scanBranch, mode: "roster", repos: roster.repos, plan: true, interactive, log },
25114
+ \u2500\u2500 Orchestrator session (${interactive ? "interactive \u2014 keep it open until it finishes; closing it stops the scans it runs" : "headless"}) \u2500\u2500`);
25115
+ const orchestrated = await session(
25116
+ {
25117
+ repoPath: workspace,
25118
+ project: opts.project,
25119
+ branch: scanBranch,
25120
+ mode: "orchestrator",
25121
+ repos: roster.repos,
25122
+ runSheet,
25123
+ interactive,
25124
+ timeoutMs: null,
25125
+ log
25126
+ },
25003
25127
  { authed: authed2 }
25004
25128
  );
25005
- const planExit = dispatchExitCode(plan);
25006
- log(` plan session: ${duration(Date.now() - t0)}`);
25007
- if (planExit !== 0) {
25008
- log(`archme add-repos: the plan session exited ${planExit}; nothing was scanned.`);
25009
- if (interactive) await offerUndo();
25010
- return planExit;
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
- }
25129
+ const sessionExit = dispatchExitCode(orchestrated);
25130
+ log(sessionExit === 0 ? ` orchestrator session: ${duration(Date.now() - t0)}` : ` orchestrator session exited ${sessionExit} after ${duration(Date.now() - t0)}`);
25131
+ const after = await (deps.authenticate ?? createAuthedClient)({
25132
+ supabaseUrl: cfg.supabaseUrl,
25133
+ supabaseAnonKey: cfg.supabaseAnonKey,
25134
+ clientId: cfg.archmeDaemonClientId,
25135
+ preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
25049
25136
  });
25050
- const added = results.filter((r) => !r.error);
25051
- const failed = results.filter((r) => r.error);
25052
- let stitchExit = null;
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)}`);
25137
+ const results = [];
25138
+ for (const c3 of todo) {
25139
+ results.push({ clone: c3, marker: await readMarker(after.supabase, project.projectId, scanBranch, c3.target.repo) });
25062
25140
  }
25141
+ const added = results.filter((r) => r.marker);
25142
+ const missing = results.filter((r) => !r.marker);
25143
+ if (added.length === 0 && interactive) await offerUndo(after.supabase);
25063
25144
  const rerun = [
25064
25145
  explicit ? "" : `cd ${shellQuote(workspace)} && `,
25065
25146
  "archme add-repos",
25066
25147
  ...explicit ? clones.map((c3) => ` ${shellQuote(c3.root)}`) : [],
25067
25148
  ` --project ${opts.project}`,
25068
25149
  opts.local ? " --local" : "",
25069
- opts.parallel !== 2 ? ` --parallel ${opts.parallel}` : "",
25070
25150
  opts.yes ? " --yes" : ""
25071
25151
  ].join("");
25072
25152
  const width = Math.max(...results.map((r) => r.clone.target.repo.length));
25073
25153
  log("\n\u2500\u2500 Summary \u2500\u2500");
25074
25154
  for (const r of added) {
25075
- log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192 ${r.containerPath} marker ${r.marker.slice(0, 7)} (${duration(r.ms)})`);
25155
+ log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192 root.${containerFor(r.clone.target.repo)} marker ${r.marker.slice(0, 7)}`);
25076
25156
  }
25077
- for (const r of failed) {
25078
- log(` \u2717 ${r.clone.target.repo.padEnd(width)} failed (exit ${r.error.exitCode}): ${r.error.reason}`);
25157
+ for (const r of missing) {
25158
+ log(` \u2717 ${r.clone.target.repo.padEnd(width)} has no resume marker: its scan did not finish`);
25079
25159
  }
25080
25160
  if (toWatch.length) log(` watched, already scanned: ${toWatch.map((c3) => c3.target.repo).join(", ")}`);
25081
25161
  if (skipped.length) log(` skipped, already on the board: ${skipped.map((c3) => c3.target.repo).join(", ")}`);
25082
- if (failed.length) log(` Re-run (skips what is already on the board):
25162
+ if (missing.length) log(` Re-run (skips what is already on the board):
25083
25163
  ${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
25164
  log(` Board: ${deriveWebOrigin(cfg.archmeMcpBaseUrl, "fallback")}/a/${encodeURIComponent(project.orgSlug)}/${encodeURIComponent(project.projectSlug)}/${encodeURIComponent(scanBranch)}`);
25087
- if (stitchExit !== null) {
25088
- const after = await load2(supabase, project.projectId, scanBranch).catch(() => null);
25089
- const changes = after?.architecture.context?.entries.find((e) => e.id === PLAN_CHANGES_ID);
25165
+ const end = await load2(after.supabase, project.projectId, scanBranch).catch(() => null);
25166
+ const changes = end?.architecture.context?.entries.find((e) => e.id === PLAN_CHANGES_ID);
25167
+ if (changes) {
25090
25168
  log("\nWhat changed from the approved plan, and why:");
25091
- log(changes ? changes.content.split("\n").map((l) => ` ${l}`).join("\n") : " (the stitch session recorded no explanation)");
25169
+ log(changes.content.split("\n").map((l) => ` ${l}`).join("\n"));
25092
25170
  }
25093
25171
  log(`
25094
25172
  Watching queues work; the daemon runs it: archme daemon start
25095
25173
  ${MULTI_REPO_DAEMON_NOTE}`);
25096
- return failed[0]?.error.exitCode ?? (stitchExit && stitchExit !== 0 ? stitchExit : 0);
25174
+ return sessionExit !== 0 ? sessionExit : missing.length > 0 ? 1 : 0;
25097
25175
  }
25098
25176
  var EMPTY, REPO_NAME_RE;
25099
25177
  var init_run3 = __esm({
@@ -25103,6 +25181,7 @@ var init_run3 = __esm({
25103
25181
  init_defaults();
25104
25182
  init_authedClient();
25105
25183
  init_run_scan();
25184
+ init_install();
25106
25185
  init_dispatch3();
25107
25186
  init_web_url();
25108
25187
  init_run_init();
@@ -25129,7 +25208,6 @@ var init_run3 = __esm({
25129
25208
  // src/addRepos/dispatch.ts
25130
25209
  var dispatch_exports7 = {};
25131
25210
  __export(dispatch_exports7, {
25132
- MAX_PARALLEL: () => MAX_PARALLEL,
25133
25211
  dispatchAddRepos: () => dispatchAddRepos,
25134
25212
  parseAddReposArgs: () => parseAddReposArgs,
25135
25213
  printAddReposHelp: () => printAddReposHelp
@@ -25143,7 +25221,6 @@ function parseAddReposArgs(argv) {
25143
25221
  let project;
25144
25222
  let local = false;
25145
25223
  let yes = false;
25146
- let parallel = 2;
25147
25224
  for (let i2 = 0; i2 < argv.length; i2++) {
25148
25225
  const arg = argv[i2];
25149
25226
  const next = argv[i2 + 1];
@@ -25151,11 +25228,6 @@ function parseAddReposArgs(argv) {
25151
25228
  if (!next || !PROJECT_SLUG_RE5.test(next)) return err3("--project needs `org/slug`");
25152
25229
  project = next;
25153
25230
  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
25231
  } else if (arg === "--local") {
25160
25232
  local = true;
25161
25233
  } else if (arg === "--yes" || arg === "--no-interactive") {
@@ -25167,33 +25239,39 @@ function parseAddReposArgs(argv) {
25167
25239
  }
25168
25240
  }
25169
25241
  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, parallel, yes } };
25242
+ return { kind: "run", opts: { targets, project, local, yes } };
25171
25243
  }
25172
25244
  function printAddReposHelp() {
25173
25245
  console.log(`archme add-repos \u2014 add several repos to a multi-repo project in one run
25174
25246
 
25175
25247
  Usage:
25176
- archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--parallel <n>] [--yes]
25248
+ archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--yes]
25177
25249
 
25178
25250
  Each repo is a clone path, or owner/name: a clone of it already in the current
25179
25251
  folder is used, otherwise it is cloned there with your own git access. With
25180
25252
  none given, every git clone directly under the current folder is added.
25181
25253
  Clones of repos the board already holds are read as context, not re-added.
25182
25254
 
25183
- 1. Checks everything first (project, GitHub App, containers); writes nothing
25184
- until all pass. A single-repo board is folded into its repo's container.
25185
- 2. Plan session, interactive: names every repo's container and the shared
25186
- externals, and drafts the story plan with you. Exit it, then confirm.
25187
- 3. One headless scan per repo, inside its own clone, against the plan.
25188
- 4. Stitch session, interactive: wires what crosses repos and explains what
25189
- changed from the plan.
25190
-
25255
+ Checks everything first (project, GitHub App, containers) and writes nothing
25256
+ until all pass; a single-repo board is folded into its repo's container. Then
25257
+ ONE orchestrator agent runs the rest, in an interactive session:
25258
+
25259
+ 1. The gist: names every repo's container and the shared externals, studies
25260
+ each repo and the product (web included), and drafts the story plan with
25261
+ you until you approve it.
25262
+ 2. Depth: studies each repo's code and git history, revises the plan, and
25263
+ shows you what changed until you approve it.
25264
+ 3. Build: launches one scan per repo (\`archme add-repo --plan\`, hours each),
25265
+ supervises them \u2014 answering their questions, pausing and resuming any that
25266
+ go off course \u2014 and stitches what crosses repos as each side lands, then
25267
+ explains what changed from the plan.
25268
+
25269
+ Keep the session open until it finishes: closing it stops the scans it runs.
25191
25270
  Re-running skips repos already on the board, so a failed run resumes.
25192
25271
 
25193
25272
  Options:
25194
25273
  --project <org/slug> The multi-repo project (required)
25195
25274
  --local Poll from this machine instead of the GitHub App
25196
- --parallel <n> Repos scanned at once, 1-${MAX_PARALLEL} (default 2)
25197
25275
  --yes Everything headless, no questions; the plan is approved`);
25198
25276
  }
25199
25277
  async function dispatchAddRepos(argv) {
@@ -25215,12 +25293,11 @@ async function dispatchAddRepos(argv) {
25215
25293
  return 3;
25216
25294
  }
25217
25295
  }
25218
- var PROJECT_SLUG_RE5, MAX_PARALLEL;
25296
+ var PROJECT_SLUG_RE5;
25219
25297
  var init_dispatch7 = __esm({
25220
25298
  "src/addRepos/dispatch.ts"() {
25221
25299
  "use strict";
25222
25300
  PROJECT_SLUG_RE5 = /^[^/\s]+\/[^/\s]+$/;
25223
- MAX_PARALLEL = 3;
25224
25301
  }
25225
25302
  });
25226
25303
 
@@ -25616,7 +25693,7 @@ __export(dispatch_exports8, {
25616
25693
  parseDoctorArgs: () => parseDoctorArgs,
25617
25694
  printDoctorHelp: () => printDoctorHelp
25618
25695
  });
25619
- import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile13 } from "node:fs/promises";
25696
+ import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile14 } from "node:fs/promises";
25620
25697
  import { tmpdir as tmpdir5 } from "node:os";
25621
25698
  import { join as join28 } from "node:path";
25622
25699
  function parseDoctorArgs(argv) {
@@ -25681,7 +25758,7 @@ async function dispatchDoctor(argv) {
25681
25758
  }
25682
25759
  const dir = await mkdtemp5(join28(tmpdir5(), "archme-doctor-"));
25683
25760
  const mcpConfigPath = join28(dir, "mcp.json");
25684
- await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
25761
+ await writeFile14(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
25685
25762
  try {
25686
25763
  const result = await spawnClaude({
25687
25764
  prompt: buildDoctorPrompt(facts, issues),
@@ -26090,7 +26167,8 @@ Other commands:
26090
26167
  (--list|--stop|--resume)
26091
26168
  archme add-repos [clone-path | owner/name \u2026] --project <org/slug>
26092
26169
  Add several repos to a multi-repo project:
26093
- plan, scan each, stitch (resumable)
26170
+ one agent plans with you, scans each,
26171
+ stitches as they land (resumable)
26094
26172
  archme add-repo [repo-path] --project <org/slug>
26095
26173
  Add one repo to a multi-repo project
26096
26174
  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.10.0",
3
+ "version": "0.11.0",
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",