codebyplan 1.13.37 → 1.13.39

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 CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.13.37";
17
+ VERSION = "1.13.39";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -698,7 +698,7 @@ function isRetryable(err) {
698
698
  return false;
699
699
  }
700
700
  function delay(ms) {
701
- return new Promise((resolve11) => setTimeout(resolve11, ms));
701
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
702
702
  }
703
703
  async function request(method, path10, options) {
704
704
  const url = buildUrl(path10, options?.params);
@@ -1056,7 +1056,7 @@ var init_device_flow = __esm({
1056
1056
  this.name = "OAuthInvalidClientError";
1057
1057
  }
1058
1058
  };
1059
- defaultSleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
1059
+ defaultSleep = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
1060
1060
  }
1061
1061
  });
1062
1062
 
@@ -3134,6 +3134,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
3134
3134
  JSON.stringify({}, null, 2) + "\n",
3135
3135
  "utf-8"
3136
3136
  );
3137
+ await writeFile7(
3138
+ join12(codebyplanDir, "architecture.json"),
3139
+ JSON.stringify({ version: 1, modules: [] }, null, 2) + "\n",
3140
+ "utf-8"
3141
+ );
3137
3142
  const statuslinePath = join12(codebyplanDir, "statusline.json");
3138
3143
  let statuslineExists = false;
3139
3144
  try {
@@ -3151,7 +3156,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
3151
3156
  await writeLocalConfig(projectPath, { device_id: deviceId });
3152
3157
  console.log(` Created ${codebyplanDir}/`);
3153
3158
  console.log(
3154
- ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, statusline.json`
3159
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, architecture.json, statusline.json`
3155
3160
  );
3156
3161
  console.log(` device.local.json (gitignored)`);
3157
3162
  const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
@@ -4670,7 +4675,7 @@ function setRetryDelayMs(ms) {
4670
4675
  RETRY_DELAY_MS = ms;
4671
4676
  }
4672
4677
  function sleep(ms) {
4673
- return new Promise((resolve11) => setTimeout(resolve11, ms));
4678
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
4674
4679
  }
4675
4680
  function isTransientMcpError(err) {
4676
4681
  if (!(err instanceof McpError)) return false;
@@ -7142,6 +7147,425 @@ var init_upload_e2e_images = __esm({
7142
7147
  }
7143
7148
  });
7144
7149
 
7150
+ // src/cli/arch-map.ts
7151
+ var arch_map_exports = {};
7152
+ __export(arch_map_exports, {
7153
+ mapFileSlug: () => mapFileSlug,
7154
+ runArchMapCommand: () => runArchMapCommand,
7155
+ updateFrontmatterFields: () => updateFrontmatterFields
7156
+ });
7157
+ import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
7158
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
7159
+ import { join as join23 } from "node:path";
7160
+ import { spawnSync as spawnSync7 } from "node:child_process";
7161
+ function normalizeModulePath(modulePath) {
7162
+ return modulePath.replace(/\/+$/, "");
7163
+ }
7164
+ function repoRootFromCodebyplanDir(codebyplanDir) {
7165
+ return codebyplanDir.endsWith("/.codebyplan") ? codebyplanDir.slice(0, codebyplanDir.lastIndexOf("/")) : codebyplanDir;
7166
+ }
7167
+ function mapFileSlug(modulePath) {
7168
+ const slug = modulePath.replace(/\//g, "-").replace(/^\./, "dot-");
7169
+ return `${slug}.md`;
7170
+ }
7171
+ async function resolveCodebyplanDir(startDir) {
7172
+ try {
7173
+ const found = await findCodebyplanConfig(startDir);
7174
+ if (!found) return null;
7175
+ if (found.path.endsWith("/repo.json")) {
7176
+ return found.path.slice(0, found.path.lastIndexOf("/"));
7177
+ }
7178
+ return found.path.slice(0, found.path.lastIndexOf("/"));
7179
+ } catch {
7180
+ return null;
7181
+ }
7182
+ }
7183
+ async function readArchitectureConfig(codebyplanDir) {
7184
+ const filePath = join23(codebyplanDir, "architecture.json");
7185
+ try {
7186
+ const raw = await readFile17(filePath, "utf-8");
7187
+ const parsed = JSON.parse(raw);
7188
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.modules)) {
7189
+ return { version: 1, modules: [] };
7190
+ }
7191
+ return parsed;
7192
+ } catch {
7193
+ return { version: 1, modules: [] };
7194
+ }
7195
+ }
7196
+ async function writeArchitectureConfig(codebyplanDir, config) {
7197
+ const filePath = join23(codebyplanDir, "architecture.json");
7198
+ await writeFile13(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
7199
+ }
7200
+ function resolveGitSha(modulePath, cwd) {
7201
+ try {
7202
+ const result = spawnSync7(
7203
+ "git",
7204
+ ["log", "--format=%H", "-1", "--", modulePath],
7205
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7206
+ );
7207
+ if (result.status !== 0 || result.error) return null;
7208
+ const sha = (result.stdout ?? "").trim();
7209
+ return sha.length > 0 ? sha : null;
7210
+ } catch {
7211
+ return null;
7212
+ }
7213
+ }
7214
+ async function discoverModulePaths(projectRoot) {
7215
+ const paths = [];
7216
+ const workspacePath = join23(projectRoot, "pnpm-workspace.yaml");
7217
+ let patterns = [];
7218
+ try {
7219
+ const raw = await readFile17(workspacePath, "utf-8");
7220
+ const lines = raw.split("\n");
7221
+ let inPackages = false;
7222
+ for (const line of lines) {
7223
+ const trimmed = line.trim();
7224
+ if (trimmed.split("#")[0].trim() === "packages:") {
7225
+ inPackages = true;
7226
+ continue;
7227
+ }
7228
+ if (inPackages) {
7229
+ if (trimmed.startsWith("-")) {
7230
+ const pat = trimmed.slice(1).trim().replace(/['"]/g, "");
7231
+ patterns.push(pat);
7232
+ } else if (trimmed && !trimmed.startsWith("#")) {
7233
+ break;
7234
+ }
7235
+ }
7236
+ }
7237
+ } catch {
7238
+ patterns = ["apps/*", "packages/*"];
7239
+ }
7240
+ for (const pattern of patterns) {
7241
+ if (pattern.endsWith("/*")) {
7242
+ const dir = pattern.slice(0, -2);
7243
+ const absDir = join23(projectRoot, dir);
7244
+ try {
7245
+ if (existsSync5(absDir)) {
7246
+ const entries = readdirSync2(absDir, { withFileTypes: true });
7247
+ for (const entry of entries) {
7248
+ if (entry.isDirectory()) {
7249
+ paths.push(`${dir}/${entry.name}`);
7250
+ }
7251
+ }
7252
+ }
7253
+ } catch {
7254
+ }
7255
+ } else if (!pattern.includes("*")) {
7256
+ const absPath = join23(projectRoot, pattern);
7257
+ if (existsSync5(absPath)) {
7258
+ paths.push(pattern);
7259
+ }
7260
+ }
7261
+ }
7262
+ const crossCutting = ["supabase", ".github", "scripts"];
7263
+ for (const dir of crossCutting) {
7264
+ const absPath = join23(projectRoot, dir);
7265
+ try {
7266
+ if (existsSync5(absPath)) {
7267
+ paths.push(dir);
7268
+ }
7269
+ } catch {
7270
+ }
7271
+ }
7272
+ return paths;
7273
+ }
7274
+ async function runStatus(projectRoot) {
7275
+ console.log("\n CodeByPlan arch-map status\n");
7276
+ try {
7277
+ const codebyplanDir = await resolveCodebyplanDir(projectRoot);
7278
+ const repoRoot = codebyplanDir ? repoRootFromCodebyplanDir(codebyplanDir) : projectRoot;
7279
+ const config = codebyplanDir ? await readArchitectureConfig(codebyplanDir) : { version: 1, modules: [] };
7280
+ const manifestByPath = /* @__PURE__ */ new Map();
7281
+ for (const entry of config.modules) {
7282
+ manifestByPath.set(normalizeModulePath(entry.path), entry);
7283
+ }
7284
+ let modulePaths = [];
7285
+ try {
7286
+ modulePaths = await discoverModulePaths(repoRoot);
7287
+ } catch {
7288
+ console.warn(" Warning: could not discover modules from workspace");
7289
+ }
7290
+ if (modulePaths.length === 0 && config.modules.length === 0) {
7291
+ console.log(" No modules found.\n");
7292
+ return;
7293
+ }
7294
+ const allPaths = /* @__PURE__ */ new Set([
7295
+ ...modulePaths.map(normalizeModulePath),
7296
+ ...config.modules.map((e) => normalizeModulePath(e.path))
7297
+ ]);
7298
+ console.log(
7299
+ " Module".padEnd(40) + "Map".padEnd(8) + "Stamped".padEnd(12) + "Status"
7300
+ );
7301
+ console.log(" " + "-".repeat(70));
7302
+ for (const modPath of allPaths) {
7303
+ const entry = manifestByPath.get(modPath);
7304
+ const hasMap = !!entry;
7305
+ const stamped = entry?.generated_from_sha != null;
7306
+ let freshness = "-";
7307
+ if (stamped && entry) {
7308
+ const currentSha = resolveGitSha(modPath, repoRoot);
7309
+ if (currentSha == null) {
7310
+ freshness = "unknown";
7311
+ } else if (currentSha === entry.generated_from_sha) {
7312
+ freshness = "fresh";
7313
+ } else {
7314
+ freshness = "stale";
7315
+ }
7316
+ }
7317
+ console.log(
7318
+ " " + modPath.padEnd(38) + (hasMap ? "yes" : "no").padEnd(8) + (stamped ? "yes" : "no").padEnd(12) + freshness
7319
+ );
7320
+ }
7321
+ console.log();
7322
+ } catch (err) {
7323
+ console.warn(
7324
+ ` Warning: arch-map status error: ${err instanceof Error ? err.message : String(err)}`
7325
+ );
7326
+ }
7327
+ }
7328
+ async function runDrift(projectRoot) {
7329
+ console.log("\n CodeByPlan arch-map drift\n");
7330
+ try {
7331
+ const codebyplanDir = await resolveCodebyplanDir(projectRoot);
7332
+ if (!codebyplanDir) {
7333
+ console.log(" No .codebyplan/ directory found \u2014 no manifest to check.");
7334
+ console.log();
7335
+ return;
7336
+ }
7337
+ const repoRoot = repoRootFromCodebyplanDir(codebyplanDir);
7338
+ const config = await readArchitectureConfig(codebyplanDir);
7339
+ const stamped = config.modules.filter((e) => e.generated_from_sha != null);
7340
+ if (stamped.length === 0) {
7341
+ console.log(" No stamped modules found \u2014 run arch-map stamp first.");
7342
+ console.log();
7343
+ return;
7344
+ }
7345
+ let driftCount = 0;
7346
+ for (const entry of stamped) {
7347
+ const currentSha = resolveGitSha(entry.path, repoRoot);
7348
+ if (currentSha == null) {
7349
+ console.log(
7350
+ ` ? ${entry.path} \u2014 could not resolve current SHA (git unavailable or path not tracked)`
7351
+ );
7352
+ continue;
7353
+ }
7354
+ if (currentSha !== entry.generated_from_sha) {
7355
+ console.log(` DRIFTED ${entry.path}`);
7356
+ console.log(` stamped: ${entry.generated_from_sha}`);
7357
+ console.log(` current: ${currentSha}`);
7358
+ driftCount++;
7359
+ }
7360
+ }
7361
+ if (driftCount === 0) {
7362
+ console.log(" All stamped modules are fresh.");
7363
+ } else {
7364
+ console.log(`
7365
+ ${driftCount} module(s) have drifted since last stamp.`);
7366
+ }
7367
+ console.log();
7368
+ } catch (err) {
7369
+ console.warn(
7370
+ ` Warning: arch-map drift error: ${err instanceof Error ? err.message : String(err)}`
7371
+ );
7372
+ }
7373
+ }
7374
+ function updateFrontmatterFields(content, fields) {
7375
+ const lines = content.split("\n");
7376
+ if (lines.length === 0 || lines[0] !== "---") {
7377
+ return null;
7378
+ }
7379
+ let closeIndex = -1;
7380
+ for (let i = 1; i < lines.length; i++) {
7381
+ if (lines[i] === "---") {
7382
+ closeIndex = i;
7383
+ break;
7384
+ }
7385
+ }
7386
+ if (closeIndex === -1) {
7387
+ return null;
7388
+ }
7389
+ const result = lines.map((line, idx) => {
7390
+ if (idx === 0 || idx >= closeIndex) {
7391
+ return line;
7392
+ }
7393
+ if (/^generated_from_sha:\s*/.test(line)) {
7394
+ return `generated_from_sha: ${fields.generated_from_sha}`;
7395
+ }
7396
+ if (/^generated_at:\s*/.test(line)) {
7397
+ return `generated_at: ${fields.generated_at}`;
7398
+ }
7399
+ if (fields.depth !== void 0 && /^depth:\s*/.test(line)) {
7400
+ return `depth: ${fields.depth}`;
7401
+ }
7402
+ return line;
7403
+ });
7404
+ return result.join("\n");
7405
+ }
7406
+ async function runStamp(modulePath, sha, depthArg, projectRoot) {
7407
+ const codebyplanDir = await resolveCodebyplanDir(projectRoot);
7408
+ if (!codebyplanDir) {
7409
+ process.stderr.write(
7410
+ " Error: no .codebyplan/ directory found. Run `codebyplan setup` first.\n"
7411
+ );
7412
+ process.exit(1);
7413
+ }
7414
+ const repoRoot = repoRootFromCodebyplanDir(codebyplanDir);
7415
+ let resolvedSha = sha;
7416
+ if (!resolvedSha) {
7417
+ resolvedSha = resolveGitSha(modulePath, repoRoot);
7418
+ if (!resolvedSha) {
7419
+ process.stderr.write(
7420
+ ` Error: could not resolve SHA for ${modulePath}. Provide --sha <value>.
7421
+ `
7422
+ );
7423
+ process.exit(1);
7424
+ }
7425
+ }
7426
+ const config = await readArchitectureConfig(codebyplanDir);
7427
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7428
+ const mapFile = mapFileSlug(modulePath);
7429
+ const mapFilePath = `.claude/architecture/${mapFile}`;
7430
+ const existingIndex = config.modules.findIndex((e) => e.path === modulePath);
7431
+ if (existingIndex >= 0) {
7432
+ config.modules[existingIndex] = {
7433
+ ...config.modules[existingIndex],
7434
+ map_file: mapFilePath,
7435
+ generated_from_sha: resolvedSha,
7436
+ generated_at: now,
7437
+ ...depthArg !== null ? { depth: depthArg } : {}
7438
+ };
7439
+ } else {
7440
+ config.modules.push({
7441
+ path: modulePath,
7442
+ map_file: mapFilePath,
7443
+ generated_from_sha: resolvedSha,
7444
+ depth: depthArg ?? "skeleton",
7445
+ generated_at: now
7446
+ });
7447
+ }
7448
+ await writeArchitectureConfig(codebyplanDir, config);
7449
+ const stampedEntry = existingIndex >= 0 ? config.modules[existingIndex] : config.modules[config.modules.length - 1];
7450
+ const mapAbsPath = join23(repoRoot, stampedEntry.map_file);
7451
+ let mapContent = null;
7452
+ try {
7453
+ mapContent = await readFile17(mapAbsPath, "utf-8");
7454
+ } catch {
7455
+ console.warn(
7456
+ " Warning: map file " + stampedEntry.map_file + " not found \u2014 stamped manifest only"
7457
+ );
7458
+ }
7459
+ if (mapContent !== null) {
7460
+ const updated = updateFrontmatterFields(mapContent, {
7461
+ generated_from_sha: resolvedSha,
7462
+ generated_at: now,
7463
+ ...depthArg !== null ? { depth: depthArg } : {}
7464
+ });
7465
+ if (updated === null) {
7466
+ console.warn(
7467
+ " Warning: no frontmatter block in " + stampedEntry.map_file + " \u2014 stamped manifest only"
7468
+ );
7469
+ } else if (!updated.includes(`generated_from_sha: ${resolvedSha}`) || !updated.includes(`generated_at: ${now}`)) {
7470
+ console.warn(
7471
+ " Warning: " + stampedEntry.map_file + " frontmatter missing generated_from_sha/generated_at \u2014 not mirrored (manifest stamped)"
7472
+ );
7473
+ } else {
7474
+ try {
7475
+ await writeFile13(mapAbsPath, updated, "utf-8");
7476
+ } catch (writeErr) {
7477
+ console.warn(
7478
+ " Warning: could not write map file " + stampedEntry.map_file + " \u2014 " + (writeErr instanceof Error ? writeErr.message : String(writeErr)) + " \u2014 stamped manifest only"
7479
+ );
7480
+ }
7481
+ }
7482
+ }
7483
+ console.log(` Stamped ${modulePath} @ ${resolvedSha.slice(0, 8)} (${now})`);
7484
+ }
7485
+ async function runArchMapCommand(rest) {
7486
+ const subcommand = rest[0];
7487
+ const projectRoot = process.cwd();
7488
+ if (subcommand === "status") {
7489
+ await runStatus(projectRoot);
7490
+ return;
7491
+ }
7492
+ if (subcommand === "drift") {
7493
+ await runDrift(projectRoot);
7494
+ return;
7495
+ }
7496
+ if (subcommand === "stamp") {
7497
+ const rawModulePath = rest[1];
7498
+ if (!rawModulePath) {
7499
+ process.stderr.write(
7500
+ " Usage: codebyplan arch-map stamp <module> [--sha <value>]\n"
7501
+ );
7502
+ process.exit(1);
7503
+ }
7504
+ if (rawModulePath.startsWith("/")) {
7505
+ process.stderr.write(
7506
+ " Error: module path must be relative to the repo root (e.g. apps/web), not absolute.\n"
7507
+ );
7508
+ process.exit(1);
7509
+ }
7510
+ const modulePath = normalizeModulePath(rawModulePath);
7511
+ if (modulePath.split("/").length > 2) {
7512
+ console.warn(
7513
+ ` Warning: ${modulePath} has more than 2 path segments; expected a top-level module path (e.g. apps/web).`
7514
+ );
7515
+ }
7516
+ let sha = null;
7517
+ const shaIdx = rest.indexOf("--sha");
7518
+ if (shaIdx !== -1) {
7519
+ const shaVal = rest[shaIdx + 1];
7520
+ if (!shaVal || shaVal.startsWith("--")) {
7521
+ process.stderr.write(" Error: --sha requires a value.\n");
7522
+ process.exit(1);
7523
+ }
7524
+ if (!/^[0-9a-f]{40}$/i.test(shaVal)) {
7525
+ process.stderr.write(
7526
+ " Error: --sha value does not look like a git SHA (expected 40 hex chars).\n"
7527
+ );
7528
+ process.exit(1);
7529
+ }
7530
+ sha = shaVal;
7531
+ }
7532
+ let depthArg = null;
7533
+ const depthIdx = rest.indexOf("--depth");
7534
+ if (depthIdx !== -1) {
7535
+ const depthVal = rest[depthIdx + 1];
7536
+ if (!depthVal || depthVal.startsWith("--")) {
7537
+ process.stderr.write(" Error: --depth requires a value.\n");
7538
+ process.exit(1);
7539
+ }
7540
+ if (depthVal !== "skeleton" && depthVal !== "deep") {
7541
+ process.stderr.write(" Error: --depth must be skeleton or deep.\n");
7542
+ process.exit(1);
7543
+ }
7544
+ depthArg = depthVal;
7545
+ }
7546
+ await runStamp(modulePath, sha, depthArg, projectRoot);
7547
+ return;
7548
+ }
7549
+ console.log(`
7550
+ codebyplan arch-map \u2014 Architecture map pipeline
7551
+
7552
+ Note: arch-map commands are intended to run from the repo root.
7553
+
7554
+ Usage:
7555
+ codebyplan arch-map status List all modules and map freshness
7556
+ codebyplan arch-map drift Report modules drifted since last stamp
7557
+ codebyplan arch-map stamp <module> Stamp a module with its current git SHA
7558
+ codebyplan arch-map stamp <module> --sha <sha> Stamp with an explicit SHA
7559
+ codebyplan arch-map stamp <module> --depth <skeleton|deep> Stamp with an explicit depth
7560
+ `);
7561
+ }
7562
+ var init_arch_map = __esm({
7563
+ "src/cli/arch-map.ts"() {
7564
+ "use strict";
7565
+ init_flags();
7566
+ }
7567
+ });
7568
+
7145
7569
  // src/lib/worktree-port-resolver.ts
7146
7570
  async function resolveWorktreePortAllocations(repoId, projectPath) {
7147
7571
  let resolvedWorktreeId;
@@ -7231,19 +7655,19 @@ var init_worktree_port_resolver = __esm({
7231
7655
  });
7232
7656
 
7233
7657
  // src/lib/migrate-local-config.ts
7234
- import { mkdir as mkdir7, readFile as readFile17, unlink as unlink3, writeFile as writeFile13 } from "node:fs/promises";
7235
- import { join as join23 } from "node:path";
7658
+ import { mkdir as mkdir7, readFile as readFile18, unlink as unlink3, writeFile as writeFile14 } from "node:fs/promises";
7659
+ import { join as join24 } from "node:path";
7236
7660
  function legacySharedPath(projectPath) {
7237
- return join23(projectPath, ".codebyplan.json");
7661
+ return join24(projectPath, ".codebyplan.json");
7238
7662
  }
7239
7663
  function legacyLocalPath(projectPath) {
7240
- return join23(projectPath, ".codebyplan.local.json");
7664
+ return join24(projectPath, ".codebyplan.local.json");
7241
7665
  }
7242
7666
  function newDirPath(projectPath) {
7243
- return join23(projectPath, ".codebyplan");
7667
+ return join24(projectPath, ".codebyplan");
7244
7668
  }
7245
7669
  function sentinelPath(projectPath) {
7246
- return join23(projectPath, ".codebyplan", "repo.json");
7670
+ return join24(projectPath, ".codebyplan", "repo.json");
7247
7671
  }
7248
7672
  async function statSafe(p) {
7249
7673
  const { stat: stat2 } = await import("node:fs/promises");
@@ -7282,7 +7706,7 @@ async function runLocalMigration(projectPath) {
7282
7706
  }
7283
7707
  let legacyRaw;
7284
7708
  try {
7285
- legacyRaw = await readFile17(legacySharedPath(projectPath), "utf-8");
7709
+ legacyRaw = await readFile18(legacySharedPath(projectPath), "utf-8");
7286
7710
  } catch {
7287
7711
  return {
7288
7712
  migrated: true,
@@ -7309,7 +7733,7 @@ async function runLocalMigration(projectPath) {
7309
7733
  let deviceId;
7310
7734
  let deviceWrittenByHelper = false;
7311
7735
  try {
7312
- const localRaw = await readFile17(legacyLocalPath(projectPath), "utf-8");
7736
+ const localRaw = await readFile18(legacyLocalPath(projectPath), "utf-8");
7313
7737
  const localParsed = JSON.parse(localRaw);
7314
7738
  if (typeof localParsed.device_id === "string") {
7315
7739
  deviceId = localParsed.device_id;
@@ -7336,8 +7760,8 @@ async function runLocalMigration(projectPath) {
7336
7760
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
7337
7761
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
7338
7762
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
7339
- await writeFile13(
7340
- join23(projectPath, ".codebyplan", "repo.json"),
7763
+ await writeFile14(
7764
+ join24(projectPath, ".codebyplan", "repo.json"),
7341
7765
  JSON.stringify(repoJson, null, 2) + "\n",
7342
7766
  "utf-8"
7343
7767
  );
@@ -7349,8 +7773,8 @@ async function runLocalMigration(projectPath) {
7349
7773
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
7350
7774
  if ("port_allocations" in cfg)
7351
7775
  serverJson.port_allocations = cfg.port_allocations;
7352
- await writeFile13(
7353
- join23(projectPath, ".codebyplan", "server.json"),
7776
+ await writeFile14(
7777
+ join24(projectPath, ".codebyplan", "server.json"),
7354
7778
  JSON.stringify(serverJson, null, 2) + "\n",
7355
7779
  "utf-8"
7356
7780
  );
@@ -7358,44 +7782,44 @@ async function runLocalMigration(projectPath) {
7358
7782
  const gitJson = {};
7359
7783
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
7360
7784
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
7361
- await writeFile13(
7362
- join23(projectPath, ".codebyplan", "git.json"),
7785
+ await writeFile14(
7786
+ join24(projectPath, ".codebyplan", "git.json"),
7363
7787
  JSON.stringify(gitJson, null, 2) + "\n",
7364
7788
  "utf-8"
7365
7789
  );
7366
7790
  filesChanged.push(".codebyplan/git.json");
7367
7791
  const shipmentJson = {};
7368
7792
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
7369
- await writeFile13(
7370
- join23(projectPath, ".codebyplan", "shipment.json"),
7793
+ await writeFile14(
7794
+ join24(projectPath, ".codebyplan", "shipment.json"),
7371
7795
  JSON.stringify(shipmentJson, null, 2) + "\n",
7372
7796
  "utf-8"
7373
7797
  );
7374
7798
  filesChanged.push(".codebyplan/shipment.json");
7375
7799
  const vendorJson = {};
7376
- await writeFile13(
7377
- join23(projectPath, ".codebyplan", "vendor.json"),
7800
+ await writeFile14(
7801
+ join24(projectPath, ".codebyplan", "vendor.json"),
7378
7802
  JSON.stringify(vendorJson, null, 2) + "\n",
7379
7803
  "utf-8"
7380
7804
  );
7381
7805
  filesChanged.push(".codebyplan/vendor.json");
7382
7806
  const e2eJson = {};
7383
- await writeFile13(
7384
- join23(projectPath, ".codebyplan", "e2e.json"),
7807
+ await writeFile14(
7808
+ join24(projectPath, ".codebyplan", "e2e.json"),
7385
7809
  JSON.stringify(e2eJson, null, 2) + "\n",
7386
7810
  "utf-8"
7387
7811
  );
7388
7812
  filesChanged.push(".codebyplan/e2e.json");
7389
7813
  const eslintJson = {};
7390
- await writeFile13(
7391
- join23(projectPath, ".codebyplan", "eslint.json"),
7814
+ await writeFile14(
7815
+ join24(projectPath, ".codebyplan", "eslint.json"),
7392
7816
  JSON.stringify(eslintJson, null, 2) + "\n",
7393
7817
  "utf-8"
7394
7818
  );
7395
7819
  filesChanged.push(".codebyplan/eslint.json");
7396
7820
  if (!deviceWrittenByHelper) {
7397
- await writeFile13(
7398
- join23(projectPath, ".codebyplan", "device.local.json"),
7821
+ await writeFile14(
7822
+ join24(projectPath, ".codebyplan", "device.local.json"),
7399
7823
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
7400
7824
  "utf-8"
7401
7825
  );
@@ -7407,9 +7831,9 @@ async function runLocalMigration(projectPath) {
7407
7831
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
7408
7832
  );
7409
7833
  }
7410
- const gitignorePath = join23(projectPath, ".gitignore");
7834
+ const gitignorePath = join24(projectPath, ".gitignore");
7411
7835
  try {
7412
- const gitignoreContent = await readFile17(gitignorePath, "utf-8");
7836
+ const gitignoreContent = await readFile18(gitignorePath, "utf-8");
7413
7837
  const legacyLine = ".codebyplan.local.json";
7414
7838
  const newLine = ".codebyplan/device.local.json";
7415
7839
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -7428,7 +7852,7 @@ async function runLocalMigration(projectPath) {
7428
7852
  updated = gitignoreContent;
7429
7853
  }
7430
7854
  if (updated !== gitignoreContent) {
7431
- await writeFile13(gitignorePath, updated, "utf-8");
7855
+ await writeFile14(gitignorePath, updated, "utf-8");
7432
7856
  filesChanged.push(".gitignore");
7433
7857
  }
7434
7858
  } catch {
@@ -7469,8 +7893,8 @@ __export(config_exports, {
7469
7893
  readVendorConfig: () => readVendorConfig,
7470
7894
  runConfig: () => runConfig
7471
7895
  });
7472
- import { mkdir as mkdir8, readFile as readFile18, writeFile as writeFile14 } from "node:fs/promises";
7473
- import { join as join24 } from "node:path";
7896
+ import { mkdir as mkdir8, readFile as readFile19, writeFile as writeFile15 } from "node:fs/promises";
7897
+ import { join as join25 } from "node:path";
7474
7898
  async function runConfig() {
7475
7899
  const flags = parseFlags(3);
7476
7900
  const dryRun = hasFlag("dry-run", 3);
@@ -7503,7 +7927,7 @@ async function runConfig() {
7503
7927
  console.log("\n Config complete.\n");
7504
7928
  }
7505
7929
  async function syncConfigToFile(repoId, projectPath, dryRun) {
7506
- const codebyplanDir = join24(projectPath, ".codebyplan");
7930
+ const codebyplanDir = join25(projectPath, ".codebyplan");
7507
7931
  const {
7508
7932
  resolvedWorktreeId,
7509
7933
  portAllocations,
@@ -7571,6 +7995,10 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7571
7995
  const vendorPayload = {};
7572
7996
  const e2ePayload = {};
7573
7997
  const eslintPayload = {};
7998
+ const architecturePayload = {
7999
+ version: 1,
8000
+ modules: []
8001
+ };
7574
8002
  if (dryRun) {
7575
8003
  console.log(" Config would be updated (dry-run).");
7576
8004
  return;
@@ -7583,20 +8011,25 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7583
8011
  { name: "shipment.json", payload: shipmentPayload },
7584
8012
  { name: "vendor.json", payload: vendorPayload },
7585
8013
  { name: "e2e.json", payload: e2ePayload, createOnly: true },
7586
- { name: "eslint.json", payload: eslintPayload, createOnly: true }
8014
+ { name: "eslint.json", payload: eslintPayload, createOnly: true },
8015
+ {
8016
+ name: "architecture.json",
8017
+ payload: architecturePayload,
8018
+ createOnly: true
8019
+ }
7587
8020
  ];
7588
8021
  let anyUpdated = false;
7589
8022
  for (const { name, payload, createOnly } of files) {
7590
- const filePath = join24(codebyplanDir, name);
8023
+ const filePath = join25(codebyplanDir, name);
7591
8024
  const newJson = JSON.stringify(payload, null, 2) + "\n";
7592
8025
  let currentJson = "";
7593
8026
  try {
7594
- currentJson = await readFile18(filePath, "utf-8");
8027
+ currentJson = await readFile19(filePath, "utf-8");
7595
8028
  } catch {
7596
8029
  }
7597
8030
  if (createOnly && currentJson !== "") continue;
7598
8031
  if (currentJson === newJson) continue;
7599
- await writeFile14(filePath, newJson, "utf-8");
8032
+ await writeFile15(filePath, newJson, "utf-8");
7600
8033
  console.log(` Updated .codebyplan/${name}`);
7601
8034
  anyUpdated = true;
7602
8035
  }
@@ -7606,8 +8039,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7606
8039
  }
7607
8040
  async function readRepoConfig(projectPath) {
7608
8041
  try {
7609
- const raw = await readFile18(
7610
- join24(projectPath, ".codebyplan", "repo.json"),
8042
+ const raw = await readFile19(
8043
+ join25(projectPath, ".codebyplan", "repo.json"),
7611
8044
  "utf-8"
7612
8045
  );
7613
8046
  return JSON.parse(raw);
@@ -7617,8 +8050,8 @@ async function readRepoConfig(projectPath) {
7617
8050
  }
7618
8051
  async function readServerConfig(projectPath) {
7619
8052
  try {
7620
- const raw = await readFile18(
7621
- join24(projectPath, ".codebyplan", "server.json"),
8053
+ const raw = await readFile19(
8054
+ join25(projectPath, ".codebyplan", "server.json"),
7622
8055
  "utf-8"
7623
8056
  );
7624
8057
  return JSON.parse(raw);
@@ -7628,8 +8061,8 @@ async function readServerConfig(projectPath) {
7628
8061
  }
7629
8062
  async function readGitConfig(projectPath) {
7630
8063
  try {
7631
- const raw = await readFile18(
7632
- join24(projectPath, ".codebyplan", "git.json"),
8064
+ const raw = await readFile19(
8065
+ join25(projectPath, ".codebyplan", "git.json"),
7633
8066
  "utf-8"
7634
8067
  );
7635
8068
  return JSON.parse(raw);
@@ -7639,8 +8072,8 @@ async function readGitConfig(projectPath) {
7639
8072
  }
7640
8073
  async function readShipmentConfig(projectPath) {
7641
8074
  try {
7642
- const raw = await readFile18(
7643
- join24(projectPath, ".codebyplan", "shipment.json"),
8075
+ const raw = await readFile19(
8076
+ join25(projectPath, ".codebyplan", "shipment.json"),
7644
8077
  "utf-8"
7645
8078
  );
7646
8079
  return JSON.parse(raw);
@@ -7650,8 +8083,8 @@ async function readShipmentConfig(projectPath) {
7650
8083
  }
7651
8084
  async function readVendorConfig(projectPath) {
7652
8085
  try {
7653
- const raw = await readFile18(
7654
- join24(projectPath, ".codebyplan", "vendor.json"),
8086
+ const raw = await readFile19(
8087
+ join25(projectPath, ".codebyplan", "vendor.json"),
7655
8088
  "utf-8"
7656
8089
  );
7657
8090
  return JSON.parse(raw);
@@ -7661,8 +8094,8 @@ async function readVendorConfig(projectPath) {
7661
8094
  }
7662
8095
  async function readE2eConfig2(projectPath) {
7663
8096
  try {
7664
- const raw = await readFile18(
7665
- join24(projectPath, ".codebyplan", "e2e.json"),
8097
+ const raw = await readFile19(
8098
+ join25(projectPath, ".codebyplan", "e2e.json"),
7666
8099
  "utf-8"
7667
8100
  );
7668
8101
  return JSON.parse(raw);
@@ -7672,8 +8105,8 @@ async function readE2eConfig2(projectPath) {
7672
8105
  }
7673
8106
  async function readServerLocalConfig(projectPath) {
7674
8107
  try {
7675
- const raw = await readFile18(
7676
- join24(projectPath, ".codebyplan", "server.local.json"),
8108
+ const raw = await readFile19(
8109
+ join25(projectPath, ".codebyplan", "server.local.json"),
7677
8110
  "utf-8"
7678
8111
  );
7679
8112
  return JSON.parse(raw);
@@ -7728,14 +8161,14 @@ var init_server_detect = __esm({
7728
8161
  });
7729
8162
 
7730
8163
  // src/lib/port-verify.ts
7731
- import { readFile as readFile19 } from "node:fs/promises";
8164
+ import { readFile as readFile20 } from "node:fs/promises";
7732
8165
  async function verifyPorts(projectPath, portAllocations) {
7733
8166
  const mismatches = [];
7734
8167
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
7735
8168
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
7736
8169
  for (const pkgPath of packageJsonPaths) {
7737
8170
  try {
7738
- const raw = await readFile19(pkgPath, "utf-8");
8171
+ const raw = await readFile20(pkgPath, "utf-8");
7739
8172
  const pkg = JSON.parse(raw);
7740
8173
  const scriptPort = detectPortFromScripts(pkg);
7741
8174
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -7798,7 +8231,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
7798
8231
  }
7799
8232
  let pkg;
7800
8233
  try {
7801
- const raw = await readFile19(`${app.absPath}/package.json`, "utf-8");
8234
+ const raw = await readFile20(`${app.absPath}/package.json`, "utf-8");
7802
8235
  pkg = JSON.parse(raw);
7803
8236
  } catch {
7804
8237
  continue;
@@ -7848,8 +8281,8 @@ __export(ports_exports, {
7848
8281
  parseEnvFile: () => parseEnvFile,
7849
8282
  runPorts: () => runPorts
7850
8283
  });
7851
- import { mkdir as mkdir9, readFile as readFile20, writeFile as writeFile15 } from "node:fs/promises";
7852
- import { join as join25 } from "node:path";
8284
+ import { mkdir as mkdir9, readFile as readFile21, writeFile as writeFile16 } from "node:fs/promises";
8285
+ import { join as join26 } from "node:path";
7853
8286
  async function runPorts() {
7854
8287
  const flags = parseFlags(3);
7855
8288
  const dryRun = hasFlag("dry-run", 3);
@@ -7970,12 +8403,12 @@ async function writeServerLocalConfig(repoId, projectPath, dryRun) {
7970
8403
  // and ServerLocalConfig.port_allocations is typed the same — honest end-to-end.
7971
8404
  port_allocations: portAllocations
7972
8405
  };
7973
- const codebyplanDir = join25(projectPath, ".codebyplan");
7974
- const filePath = join25(codebyplanDir, "server.local.json");
8406
+ const codebyplanDir = join26(projectPath, ".codebyplan");
8407
+ const filePath = join26(codebyplanDir, "server.local.json");
7975
8408
  const newJson = JSON.stringify(payload, null, 2) + "\n";
7976
8409
  let currentJson = "";
7977
8410
  try {
7978
- currentJson = await readFile20(filePath, "utf-8");
8411
+ currentJson = await readFile21(filePath, "utf-8");
7979
8412
  } catch {
7980
8413
  }
7981
8414
  if (currentJson === newJson) {
@@ -7987,17 +8420,17 @@ async function writeServerLocalConfig(repoId, projectPath, dryRun) {
7987
8420
  return;
7988
8421
  }
7989
8422
  await mkdir9(codebyplanDir, { recursive: true });
7990
- await writeFile15(filePath, newJson, "utf-8");
8423
+ await writeFile16(filePath, newJson, "utf-8");
7991
8424
  console.log(
7992
8425
  ` Updated .codebyplan/server.local.json (worktree ${resolvedWorktreeId ?? "\u2014"}, ${portAllocations.length} allocation${portAllocations.length === 1 ? "" : "s"}).`
7993
8426
  );
7994
8427
  }
7995
8428
  async function provisionE2eEnv(projectPath, dryRun) {
7996
- const relSource = join25("apps", "web", ".env.local");
7997
- const sourcePath = join25(projectPath, relSource);
8429
+ const relSource = join26("apps", "web", ".env.local");
8430
+ const sourcePath = join26(projectPath, relSource);
7998
8431
  let sourceRaw;
7999
8432
  try {
8000
- sourceRaw = await readFile20(sourcePath, "utf-8");
8433
+ sourceRaw = await readFile21(sourcePath, "utf-8");
8001
8434
  } catch {
8002
8435
  console.warn(
8003
8436
  ` Skipped .codebyplan/e2e.env \u2014 source ${relSource} not found.`
@@ -8026,12 +8459,12 @@ async function provisionE2eEnv(projectPath, dryRun) {
8026
8459
  );
8027
8460
  return;
8028
8461
  }
8029
- const codebyplanDir = join25(projectPath, ".codebyplan");
8030
- const filePath = join25(codebyplanDir, "e2e.env");
8462
+ const codebyplanDir = join26(projectPath, ".codebyplan");
8463
+ const filePath = join26(codebyplanDir, "e2e.env");
8031
8464
  const newContent = lines.join("\n") + "\n";
8032
8465
  let currentContent = "";
8033
8466
  try {
8034
- currentContent = await readFile20(filePath, "utf-8");
8467
+ currentContent = await readFile21(filePath, "utf-8");
8035
8468
  } catch {
8036
8469
  }
8037
8470
  if (currentContent === newContent) {
@@ -8043,7 +8476,7 @@ async function provisionE2eEnv(projectPath, dryRun) {
8043
8476
  return;
8044
8477
  }
8045
8478
  await mkdir9(codebyplanDir, { recursive: true });
8046
- await writeFile15(filePath, newContent, "utf-8");
8479
+ await writeFile16(filePath, newContent, "utf-8");
8047
8480
  console.log(
8048
8481
  ` Provisioned .codebyplan/e2e.env (${lines.length} var${lines.length === 1 ? "" : "s"}).`
8049
8482
  );
@@ -8093,7 +8526,7 @@ __export(tech_stack_exports, {
8093
8526
  runFullTechStack: () => runFullTechStack,
8094
8527
  runTechStack: () => runTechStack
8095
8528
  });
8096
- import { existsSync as existsSync5 } from "node:fs";
8529
+ import { existsSync as existsSync6 } from "node:fs";
8097
8530
  async function runTechStack() {
8098
8531
  const flags = parseFlags(3);
8099
8532
  const dryRun = hasFlag("dry-run", 3);
@@ -8266,7 +8699,7 @@ async function runFullTechStack(dryRun) {
8266
8699
  continue;
8267
8700
  }
8268
8701
  const localWorktrees = worktrees.filter(
8269
- (wt) => wt.path ? existsSync5(wt.path) : false
8702
+ (wt) => wt.path ? existsSync6(wt.path) : false
8270
8703
  );
8271
8704
  if (localWorktrees.length === 0) {
8272
8705
  console.log(` skipping ${repo.name} \u2014 no local worktree on this device`);
@@ -8366,7 +8799,7 @@ var init_claude_plan = __esm({
8366
8799
  // src/cli/claude/status.ts
8367
8800
  var status_exports = {};
8368
8801
  __export(status_exports, {
8369
- runStatus: () => runStatus
8802
+ runStatus: () => runStatus2
8370
8803
  });
8371
8804
  import * as fs6 from "node:fs";
8372
8805
  import * as path7 from "node:path";
@@ -8433,7 +8866,7 @@ function resolveCurrentBranch2() {
8433
8866
  }
8434
8867
  }
8435
8868
  }
8436
- async function runStatus(argv) {
8869
+ async function runStatus2(argv) {
8437
8870
  const checked_at = (/* @__PURE__ */ new Date()).toISOString();
8438
8871
  const writeCache = argv.includes("--write-cache");
8439
8872
  const quiet = argv.includes("--quiet");
@@ -8616,11 +9049,11 @@ async function ask(q, opts) {
8616
9049
  try {
8617
9050
  while (true) {
8618
9051
  const choices = q.choices.map((c) => `[${c.key}] ${c.label}`).join(" ");
8619
- const answer = await new Promise((resolve11) => {
9052
+ const answer = await new Promise((resolve12) => {
8620
9053
  rl.question(`${q.message}
8621
9054
  ${choices}
8622
9055
  > `, (input) => {
8623
- resolve11(input.trim().toLowerCase());
9056
+ resolve12(input.trim().toLowerCase());
8624
9057
  });
8625
9058
  });
8626
9059
  const match = q.choices.find(
@@ -9326,16 +9759,303 @@ var init_structure_generator = __esm({
9326
9759
  }
9327
9760
  });
9328
9761
 
9762
+ // src/lib/readme-generator.ts
9763
+ function managedStartMarker(section) {
9764
+ return `${MANAGED_START_PREFIX}${section}${MANAGED_MARKER_SUFFIX}`;
9765
+ }
9766
+ function managedEndMarker(section) {
9767
+ return `${MANAGED_END_PREFIX}${section}${MANAGED_MARKER_SUFFIX}`;
9768
+ }
9769
+ function hasManagedMarkers(content) {
9770
+ return content.includes(MANAGED_START_PREFIX) || content.includes(MANAGED_END_PREFIX);
9771
+ }
9772
+ function validateManagedMarkers(content) {
9773
+ const errors = [];
9774
+ const startRe = /<!-- codebyplan:managed:start:([a-z0-9][a-z0-9-]*) -->/g;
9775
+ const endRe = /<!-- codebyplan:managed:end:([a-z0-9][a-z0-9-]*) -->/g;
9776
+ const starts = [...content.matchAll(startRe)].map((m) => m[1]);
9777
+ const ends = [...content.matchAll(endRe)].map((m) => m[1]);
9778
+ const seenStarts = /* @__PURE__ */ new Set();
9779
+ for (const s of starts) {
9780
+ if (seenStarts.has(s)) {
9781
+ errors.push(`duplicate managed section '${s}'`);
9782
+ }
9783
+ seenStarts.add(s);
9784
+ if (!ends.includes(s)) {
9785
+ errors.push(`missing end marker for section '${s}'`);
9786
+ }
9787
+ }
9788
+ for (const e of ends) {
9789
+ if (!starts.includes(e)) {
9790
+ errors.push(`end marker without start marker for section '${e}'`);
9791
+ }
9792
+ }
9793
+ return errors;
9794
+ }
9795
+ function renderTechStackSection(techStack) {
9796
+ const entries = Object.entries(techStack).filter(([, value]) => value.trim().length > 0).sort(([a], [b]) => a.localeCompare(b));
9797
+ if (entries.length === 0) return "";
9798
+ const lines = entries.map(([cat, names]) => `- **${cat}**: ${names}`);
9799
+ return `## Tech Stack
9800
+
9801
+ ${lines.join("\n")}
9802
+ `;
9803
+ }
9804
+ function renderStructureSection(packages) {
9805
+ if (packages.length === 0) return "";
9806
+ const sorted = [...packages].sort((a, b) => a.path.localeCompare(b.path));
9807
+ const rows = sorted.map(
9808
+ (p) => `| \`${p.path}\` | \`${p.name}\` | ${p.purpose ?? ""} |`
9809
+ );
9810
+ return `## Structure
9811
+
9812
+ | Location | Package | Purpose |
9813
+ |----------|---------|----------|
9814
+ ` + rows.join("\n") + "\n";
9815
+ }
9816
+ function renderScriptsSection(scripts, installCommand) {
9817
+ const sortedEntries = Object.entries(scripts).filter(([, cmd]) => typeof cmd === "string" && cmd.trim().length > 0).sort(([a], [b]) => a.localeCompare(b));
9818
+ const lines = [];
9819
+ lines.push(`\`\`\`bash`);
9820
+ lines.push(`# Install`);
9821
+ lines.push(installCommand);
9822
+ lines.push(``);
9823
+ if (sortedEntries.length > 0) {
9824
+ lines.push(`# Available scripts`);
9825
+ for (const [name, cmd] of sortedEntries) {
9826
+ lines.push(`# ${name}`);
9827
+ lines.push(cmd);
9828
+ }
9829
+ }
9830
+ lines.push(`\`\`\``);
9831
+ return `## Scripts
9832
+
9833
+ ${lines.join("\n")}
9834
+ `;
9835
+ }
9836
+ function buildManagedSections(config) {
9837
+ const sections = {};
9838
+ if (config.techStack && Object.keys(config.techStack).length > 0) {
9839
+ const rendered = renderTechStackSection(config.techStack);
9840
+ if (rendered) {
9841
+ sections["tech-stack"] = rendered;
9842
+ }
9843
+ }
9844
+ if (config.packages && config.packages.length > 0) {
9845
+ const rendered = renderStructureSection(config.packages);
9846
+ if (rendered) {
9847
+ sections["structure"] = rendered;
9848
+ }
9849
+ }
9850
+ if (config.scripts && Object.keys(config.scripts).length > 0) {
9851
+ const pm = config.packageManager ?? "pnpm";
9852
+ const pmBase = pm.split("@")[0] ?? "pnpm";
9853
+ const installCmd = config.installCommand ?? `${pmBase} install`;
9854
+ const rendered = renderScriptsSection(config.scripts, installCmd);
9855
+ if (rendered) {
9856
+ sections["scripts"] = rendered;
9857
+ }
9858
+ }
9859
+ return sections;
9860
+ }
9861
+ function hashManagedContent(sections) {
9862
+ const sortedKeys = Object.keys(sections).sort();
9863
+ const payload = {};
9864
+ for (const k of sortedKeys) {
9865
+ payload[k] = sections[k];
9866
+ }
9867
+ return sha256(JSON.stringify(payload));
9868
+ }
9869
+ function scaffoldReadme(config) {
9870
+ const title = config.name ?? "Project";
9871
+ const description = config.description ?? "";
9872
+ const sections = buildManagedSections(config);
9873
+ const hash = hashManagedContent(sections);
9874
+ const lines = [];
9875
+ lines.push(`# ${title}`);
9876
+ lines.push(``);
9877
+ if (description) {
9878
+ lines.push(description);
9879
+ lines.push(``);
9880
+ }
9881
+ const sectionKeys = Object.keys(sections).sort();
9882
+ for (const key of sectionKeys) {
9883
+ lines.push(managedStartMarker(key));
9884
+ lines.push(sections[key].trimEnd());
9885
+ lines.push(managedEndMarker(key));
9886
+ lines.push(``);
9887
+ }
9888
+ lines.push(`${HASH_MARKER_PREFIX}${hash}${MANAGED_MARKER_SUFFIX}`);
9889
+ lines.push(``);
9890
+ return lines.join("\n");
9891
+ }
9892
+ function refreshManagedSections(existing, sections) {
9893
+ let content = existing;
9894
+ const startPattern = new RegExp(
9895
+ `${escapeRegex(MANAGED_START_PREFIX)}([a-z0-9][a-z0-9-]*)${escapeRegex(MANAGED_MARKER_SUFFIX)}[\\s\\S]*?${escapeRegex(MANAGED_END_PREFIX)}\\1${escapeRegex(MANAGED_MARKER_SUFFIX)}`,
9896
+ "g"
9897
+ );
9898
+ const seen = /* @__PURE__ */ new Set();
9899
+ content = content.replace(startPattern, (_, sectionName) => {
9900
+ const key = sectionName.trim();
9901
+ seen.add(key);
9902
+ if (Object.prototype.hasOwnProperty.call(sections, key)) {
9903
+ const newBody = sections[key].trimEnd();
9904
+ return managedStartMarker(key) + "\n" + newBody + "\n" + managedEndMarker(key);
9905
+ } else {
9906
+ return "";
9907
+ }
9908
+ });
9909
+ content = content.replace(/\n{3,}/g, "\n\n");
9910
+ const newSectionKeys = Object.keys(sections).sort().filter((k) => !seen.has(k));
9911
+ if (newSectionKeys.length > 0) {
9912
+ const hashCommentPattern = new RegExp(
9913
+ `
9914
+ ${escapeRegex(HASH_MARKER_PREFIX)}[^
9915
+ ]*${escapeRegex(MANAGED_MARKER_SUFFIX)}(
9916
+ |$)`
9917
+ );
9918
+ content = content.replace(hashCommentPattern, "\n");
9919
+ if (!content.endsWith("\n")) content += "\n";
9920
+ for (const key of newSectionKeys) {
9921
+ content += "\n" + managedStartMarker(key) + "\n" + sections[key].trimEnd() + "\n" + managedEndMarker(key) + "\n";
9922
+ }
9923
+ }
9924
+ const newHash = hashManagedContent(sections);
9925
+ const hashLine = `${HASH_MARKER_PREFIX}${newHash}${MANAGED_MARKER_SUFFIX}`;
9926
+ const existingHashPattern = new RegExp(
9927
+ `${escapeRegex(HASH_MARKER_PREFIX)}[^
9928
+ ]*${escapeRegex(MANAGED_MARKER_SUFFIX)}`
9929
+ );
9930
+ if (existingHashPattern.test(content)) {
9931
+ content = content.replace(existingHashPattern, hashLine);
9932
+ } else {
9933
+ if (!content.endsWith("\n")) content += "\n";
9934
+ content += `${hashLine}
9935
+ `;
9936
+ }
9937
+ content = content.trimEnd() + "\n";
9938
+ return content;
9939
+ }
9940
+ function escapeRegex(str) {
9941
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9942
+ }
9943
+ function extractManagedHash(content) {
9944
+ const hashPattern = new RegExp(
9945
+ `${escapeRegex(HASH_MARKER_PREFIX)}(sha256:[0-9a-f]{64})${escapeRegex(MANAGED_MARKER_SUFFIX)}`
9946
+ );
9947
+ const m = hashPattern.exec(content);
9948
+ return m?.[1] ?? null;
9949
+ }
9950
+ var MANAGED_START_PREFIX, MANAGED_END_PREFIX, MANAGED_MARKER_SUFFIX, HASH_MARKER_PREFIX;
9951
+ var init_readme_generator = __esm({
9952
+ "src/lib/readme-generator.ts"() {
9953
+ "use strict";
9954
+ init_hash();
9955
+ MANAGED_START_PREFIX = "<!-- codebyplan:managed:start:";
9956
+ MANAGED_END_PREFIX = "<!-- codebyplan:managed:end:";
9957
+ MANAGED_MARKER_SUFFIX = " -->";
9958
+ HASH_MARKER_PREFIX = "<!-- codebyplan:hash:";
9959
+ }
9960
+ });
9961
+
9962
+ // src/lib/agents-generator.ts
9963
+ function stripStructureSentinels(structureContent) {
9964
+ const lines = structureContent.split("\n");
9965
+ const filtered = lines.filter((line) => {
9966
+ const trimmed = line.trim();
9967
+ return !trimmed.startsWith("<!-- @codebyplan-generated:");
9968
+ });
9969
+ let result = filtered.join("\n");
9970
+ result = result.replace(/^\n+/, "").replace(/\n+$/, "");
9971
+ return result;
9972
+ }
9973
+ function buildCuratedGuidance(cleanStructureContent) {
9974
+ const lines = [];
9975
+ lines.push(`## Project Context`);
9976
+ lines.push(``);
9977
+ lines.push(
9978
+ `This file is auto-generated for cross-tool compatibility. The canonical source of truth for project instructions is \`.claude/CLAUDE.md\`. If your tool supports Claude Code's \`@\` import syntax, read \`.claude/CLAUDE.md\` directly.`
9979
+ );
9980
+ lines.push(``);
9981
+ if (cleanStructureContent.trim().length > 0) {
9982
+ lines.push(cleanStructureContent.trim());
9983
+ } else {
9984
+ lines.push(
9985
+ `_Structure not yet generated. Run \`codebyplan claude generate\` to populate._`
9986
+ );
9987
+ }
9988
+ lines.push(``);
9989
+ lines.push(`## Source of Truth`);
9990
+ lines.push(``);
9991
+ lines.push(
9992
+ `The database is the sole source of truth for all development data. No local markdown files for tracking \u2014 everything is in the DB, accessed via MCP tools or the Web UI.`
9993
+ );
9994
+ lines.push(``);
9995
+ lines.push(`## \`.claude/\` Layout`);
9996
+ lines.push(``);
9997
+ lines.push(`| Directory | Purpose |`);
9998
+ lines.push(`|-----------|---------|`);
9999
+ lines.push(`| \`rules/\` | Auto-loaded behavioral constraints |`);
10000
+ lines.push(`| \`skills/\` | User-invocable \`/cbp-*\` commands |`);
10001
+ lines.push(`| \`agents/\` | Spawned sub-agents |`);
10002
+ lines.push(`| \`context/\` | Agent/skill input data |`);
10003
+ lines.push(
10004
+ `| \`CLAUDE.md\` | Canonical project context (always loaded by Claude Code) |`
10005
+ );
10006
+ lines.push(``);
10007
+ lines.push(`## Port Rule`);
10008
+ lines.push(``);
10009
+ lines.push(
10010
+ `Never hardcode port numbers. Resolve from \`.codebyplan/server.json\` \`port_allocations[]\`. Each entry carries \`port\`, \`label\`, and \`server_type\`.`
10011
+ );
10012
+ lines.push(``);
10013
+ lines.push(`## Git`);
10014
+ lines.push(``);
10015
+ lines.push(
10016
+ `Do not include any mention of AI tools or code-assistant products in git commit messages.`
10017
+ );
10018
+ lines.push(``);
10019
+ return lines.join("\n");
10020
+ }
10021
+ function generateAgentsMd(structureContent) {
10022
+ const cleanStructure = stripStructureSentinels(structureContent);
10023
+ const bodyContent = buildCuratedGuidance(cleanStructure);
10024
+ const managedBody = [
10025
+ managedStartMarker(AGENTS_SECTION_NAME),
10026
+ bodyContent.trimEnd(),
10027
+ managedEndMarker(AGENTS_SECTION_NAME)
10028
+ ].join("\n");
10029
+ const hash = hashManagedContent({ [AGENTS_SECTION_NAME]: bodyContent });
10030
+ const hashLine = `${HASH_MARKER_PREFIX}${hash}${MANAGED_MARKER_SUFFIX}`;
10031
+ const header = [
10032
+ `<!-- GENERATED \u2014 do not hand-edit. Canonical source: .claude/CLAUDE.md -->`,
10033
+ `<!-- Regenerate: codebyplan claude generate -->`
10034
+ ].join("\n");
10035
+ return [header, ``, managedBody, ``, hashLine, ``].join("\n");
10036
+ }
10037
+ function extractAgentsHash(content) {
10038
+ return extractManagedHash(content);
10039
+ }
10040
+ var AGENTS_SECTION_NAME;
10041
+ var init_agents_generator = __esm({
10042
+ "src/lib/agents-generator.ts"() {
10043
+ "use strict";
10044
+ init_readme_generator();
10045
+ AGENTS_SECTION_NAME = "agents-context";
10046
+ }
10047
+ });
10048
+
9329
10049
  // src/cli/claude/generate.ts
9330
10050
  var generate_exports = {};
9331
10051
  __export(generate_exports, {
9332
10052
  runGenerate: () => runGenerate
9333
10053
  });
9334
- import { readFile as readFile21, mkdir as mkdir10, writeFile as writeFile16 } from "node:fs/promises";
9335
- import { join as join30, resolve as resolve8 } from "node:path";
10054
+ import { readFile as readFile22, mkdir as mkdir10, writeFile as writeFile17 } from "node:fs/promises";
10055
+ import { join as join31, resolve as resolve8 } from "node:path";
9336
10056
  async function readJsonFile3(filePath) {
9337
10057
  try {
9338
- const raw = await readFile21(filePath, "utf-8");
10058
+ const raw = await readFile22(filePath, "utf-8");
9339
10059
  return JSON.parse(raw);
9340
10060
  } catch {
9341
10061
  return null;
@@ -9343,7 +10063,7 @@ async function readJsonFile3(filePath) {
9343
10063
  }
9344
10064
  async function readPkgName(absPath) {
9345
10065
  try {
9346
- const raw = await readFile21(join30(absPath, "package.json"), "utf-8");
10066
+ const raw = await readFile22(join31(absPath, "package.json"), "utf-8");
9347
10067
  const pkg = JSON.parse(raw);
9348
10068
  return typeof pkg.name === "string" ? pkg.name : null;
9349
10069
  } catch {
@@ -9353,10 +10073,11 @@ async function readPkgName(absPath) {
9353
10073
  async function runGenerate(opts) {
9354
10074
  const projectDir = opts.projectDir ?? opts["project-dir"] ?? process.cwd();
9355
10075
  const dryRun = opts.dryRun ?? opts["dryRun"] ?? false;
10076
+ const check = opts.check ?? opts["check"] ?? false;
9356
10077
  const rootDir = resolve8(projectDir);
9357
10078
  let packageManager;
9358
10079
  try {
9359
- const raw = await readFile21(join30(rootDir, "package.json"), "utf-8");
10080
+ const raw = await readFile22(join31(rootDir, "package.json"), "utf-8");
9360
10081
  const pkg = JSON.parse(raw);
9361
10082
  if (typeof pkg.packageManager === "string") {
9362
10083
  packageManager = pkg.packageManager;
@@ -9364,7 +10085,7 @@ async function runGenerate(opts) {
9364
10085
  } catch {
9365
10086
  }
9366
10087
  const serverJson = await readJsonFile3(
9367
- join30(rootDir, ".codebyplan", "server.json")
10088
+ join31(rootDir, ".codebyplan", "server.json")
9368
10089
  );
9369
10090
  const ports = [];
9370
10091
  for (const alloc of serverJson?.port_allocations ?? []) {
@@ -9377,7 +10098,7 @@ async function runGenerate(opts) {
9377
10098
  }
9378
10099
  }
9379
10100
  const gitJson = await readJsonFile3(
9380
- join30(rootDir, ".codebyplan", "git.json")
10101
+ join31(rootDir, ".codebyplan", "git.json")
9381
10102
  );
9382
10103
  const branchModel = gitJson?.branch_config?.production ? {
9383
10104
  production: gitJson.branch_config.production,
@@ -9386,7 +10107,7 @@ async function runGenerate(opts) {
9386
10107
  )
9387
10108
  } : void 0;
9388
10109
  const shipmentJson = await readJsonFile3(
9389
- join30(rootDir, ".codebyplan", "shipment.json")
10110
+ join31(rootDir, ".codebyplan", "shipment.json")
9390
10111
  );
9391
10112
  const shipmentSurfaces = [];
9392
10113
  const rawSurfaces = shipmentJson?.shipment?.surfaces ?? shipmentJson?.surfaces ?? {};
@@ -9454,28 +10175,362 @@ async function runGenerate(opts) {
9454
10175
  branchModel,
9455
10176
  shipmentSurfaces: shipmentSurfaces.length > 0 ? shipmentSurfaces : void 0
9456
10177
  };
9457
- const content = generateStructureMd(config);
10178
+ const structureMdContent = generateStructureMd(config);
10179
+ const agentsContent = generateAgentsMd(structureMdContent);
10180
+ if (check) {
10181
+ const agentsMdPath2 = join31(rootDir, "AGENTS.md");
10182
+ let existingAgents = null;
10183
+ try {
10184
+ existingAgents = await readFile22(agentsMdPath2, "utf-8");
10185
+ } catch {
10186
+ existingAgents = null;
10187
+ }
10188
+ if (existingAgents === null) {
10189
+ process.stdout.write(`AGENTS.md missing
10190
+ `);
10191
+ process.exitCode = 1;
10192
+ } else {
10193
+ const existingHash = extractAgentsHash(existingAgents);
10194
+ const freshEmbeddedHash = extractAgentsHash(agentsContent);
10195
+ if (existingHash !== freshEmbeddedHash) {
10196
+ process.stdout.write(`AGENTS.md drifted
10197
+ `);
10198
+ process.exitCode = 1;
10199
+ } else {
10200
+ process.stdout.write(`AGENTS.md up to date
10201
+ `);
10202
+ }
10203
+ }
10204
+ return;
10205
+ }
9458
10206
  if (dryRun) {
9459
10207
  process.stdout.write(
9460
10208
  `[dry-run] Would write: .claude/generated/structure.md
9461
10209
 
9462
10210
  `
9463
10211
  );
9464
- process.stdout.write(content);
10212
+ process.stdout.write(structureMdContent);
10213
+ process.stdout.write(`
10214
+ [dry-run] Would write: AGENTS.md
10215
+
10216
+ `);
10217
+ process.stdout.write(agentsContent);
9465
10218
  return;
9466
10219
  }
9467
- const outputDir = join30(rootDir, ".claude", "generated");
10220
+ const outputDir = join31(rootDir, ".claude", "generated");
9468
10221
  await mkdir10(outputDir, { recursive: true });
9469
- const outputPath = join30(outputDir, "structure.md");
9470
- await writeFile16(outputPath, content, "utf-8");
10222
+ const outputPath = join31(outputDir, "structure.md");
10223
+ await writeFile17(outputPath, structureMdContent, "utf-8");
9471
10224
  process.stdout.write(`Wrote: .claude/generated/structure.md
9472
10225
  `);
10226
+ const agentsMdPath = join31(rootDir, "AGENTS.md");
10227
+ let existingAgentsContent = null;
10228
+ try {
10229
+ existingAgentsContent = await readFile22(agentsMdPath, "utf-8");
10230
+ } catch {
10231
+ existingAgentsContent = null;
10232
+ }
10233
+ if (existingAgentsContent !== null && existingAgentsContent === agentsContent) {
10234
+ process.stdout.write(`Up to date: AGENTS.md
10235
+ `);
10236
+ } else {
10237
+ await writeFile17(agentsMdPath, agentsContent, "utf-8");
10238
+ process.stdout.write(`Wrote: AGENTS.md
10239
+ `);
10240
+ }
9473
10241
  }
9474
10242
  var init_generate = __esm({
9475
10243
  "src/cli/claude/generate.ts"() {
9476
10244
  "use strict";
9477
10245
  init_tech_detect();
9478
10246
  init_structure_generator();
10247
+ init_agents_generator();
10248
+ }
10249
+ });
10250
+
10251
+ // src/cli/claude/readme.ts
10252
+ var readme_exports = {};
10253
+ __export(readme_exports, {
10254
+ runReadme: () => runReadme,
10255
+ runReadmeCommand: () => runReadmeCommand
10256
+ });
10257
+ import { readFile as readFile23, writeFile as writeFile18 } from "node:fs/promises";
10258
+ import { join as join32, resolve as resolve9, relative as relative8 } from "node:path";
10259
+ async function readJsonFile4(filePath) {
10260
+ try {
10261
+ const raw = await readFile23(filePath, "utf-8");
10262
+ return JSON.parse(raw);
10263
+ } catch {
10264
+ return null;
10265
+ }
10266
+ }
10267
+ async function buildConfig(absPath, isRoot, allPackages, rootPkgJson, pkgJson) {
10268
+ let techStack;
10269
+ try {
10270
+ const techResult = await detectTechStack(absPath);
10271
+ const CATEGORY_LABELS = {
10272
+ "component-lib": "Component Library",
10273
+ graphql: "GraphQL",
10274
+ quality: "Code Quality"
10275
+ };
10276
+ const categoryMap = {};
10277
+ for (const entry of techResult.flat) {
10278
+ if (entry.name === SYNTHETIC_CARRIER_NAME) continue;
10279
+ if (!categoryMap[entry.category]) {
10280
+ categoryMap[entry.category] = [];
10281
+ }
10282
+ categoryMap[entry.category].push(entry.name);
10283
+ }
10284
+ if ((categoryMap["mobile"]?.length ?? 0) > 0) {
10285
+ const frameworkEntries = categoryMap["framework"] ?? [];
10286
+ const filtered = frameworkEntries.filter((n) => n !== "React");
10287
+ if (filtered.length > 0) {
10288
+ categoryMap["framework"] = filtered;
10289
+ } else {
10290
+ delete categoryMap["framework"];
10291
+ }
10292
+ }
10293
+ if (Object.keys(categoryMap).length > 0) {
10294
+ techStack = {};
10295
+ for (const [cat, names] of Object.entries(categoryMap)) {
10296
+ const label = CATEGORY_LABELS[cat] ?? cat.charAt(0).toUpperCase() + cat.slice(1);
10297
+ techStack[label] = [...names].sort().join(" + ");
10298
+ }
10299
+ }
10300
+ } catch {
10301
+ }
10302
+ const packages = isRoot ? allPackages : void 0;
10303
+ const packageManager = typeof rootPkgJson?.packageManager === "string" ? rootPkgJson.packageManager : void 0;
10304
+ const pmBase = packageManager?.split("@")[0] ?? "pnpm";
10305
+ const installCommand = `${pmBase} install`;
10306
+ return {
10307
+ name: pkgJson?.name ?? void 0,
10308
+ description: pkgJson?.description ?? void 0,
10309
+ scripts: pkgJson?.scripts && Object.keys(pkgJson.scripts).length > 0 ? pkgJson.scripts : void 0,
10310
+ packageManager,
10311
+ techStack,
10312
+ packages: packages && packages.length > 0 ? packages : void 0,
10313
+ installCommand
10314
+ };
10315
+ }
10316
+ async function discoverUnits(rootDir, rootPkgJson) {
10317
+ const units = [];
10318
+ const allPackages = [];
10319
+ const pkgJsonByPath = /* @__PURE__ */ new Map();
10320
+ units.push({
10321
+ name: "root",
10322
+ path: ".",
10323
+ absPath: rootDir,
10324
+ isRoot: true
10325
+ });
10326
+ pkgJsonByPath.set(rootDir, rootPkgJson);
10327
+ const discovered = await discoverMonorepoApps(rootDir);
10328
+ for (const app of discovered) {
10329
+ const pkgJson = await readJsonFile4(
10330
+ join32(app.absPath, "package.json")
10331
+ );
10332
+ pkgJsonByPath.set(app.absPath, pkgJson);
10333
+ allPackages.push({
10334
+ path: app.path,
10335
+ name: pkgJson?.name ?? app.name,
10336
+ purpose: pkgJson?.description ?? void 0
10337
+ });
10338
+ const isApp = app.path === "apps" || app.path.startsWith("apps/");
10339
+ if (!isApp && pkgJson?.private === true) continue;
10340
+ units.push({
10341
+ name: app.name,
10342
+ path: app.path,
10343
+ absPath: app.absPath,
10344
+ isRoot: false
10345
+ });
10346
+ }
10347
+ return { units, allPackages, pkgJsonByPath };
10348
+ }
10349
+ async function runReadme(opts) {
10350
+ const projectDir = opts.projectDir ?? opts["project-dir"] ?? process.cwd();
10351
+ const dryRun = opts.dryRun ?? opts["dryRun"] ?? false;
10352
+ const check = opts.check ?? opts["check"] ?? false;
10353
+ const init = opts.init ?? opts["init"] ?? false;
10354
+ const rootDir = resolve9(projectDir);
10355
+ const rootPkgJson = await readJsonFile4(
10356
+ join32(rootDir, "package.json")
10357
+ );
10358
+ const { units, allPackages, pkgJsonByPath } = await discoverUnits(
10359
+ rootDir,
10360
+ rootPkgJson
10361
+ );
10362
+ const driftUnits = [];
10363
+ const missingUnits = [];
10364
+ for (const unit of units) {
10365
+ const readmePath = join32(unit.absPath, "README.md");
10366
+ const relPath = unit.isRoot ? "README.md" : join32(relative8(rootDir, unit.absPath), "README.md");
10367
+ let existingContent = null;
10368
+ try {
10369
+ existingContent = await readFile23(readmePath, "utf-8");
10370
+ } catch {
10371
+ existingContent = null;
10372
+ }
10373
+ let config;
10374
+ try {
10375
+ config = await buildConfig(
10376
+ unit.absPath,
10377
+ unit.isRoot,
10378
+ allPackages,
10379
+ rootPkgJson,
10380
+ pkgJsonByPath.get(unit.absPath) ?? null
10381
+ );
10382
+ } catch (err) {
10383
+ process.stderr.write(
10384
+ `readme: error building config for ${relPath}: ${err instanceof Error ? err.message : String(err)}
10385
+ `
10386
+ );
10387
+ continue;
10388
+ }
10389
+ const sections = buildManagedSections(config);
10390
+ if (existingContent === null) {
10391
+ if (check) {
10392
+ missingUnits.push(relPath);
10393
+ continue;
10394
+ }
10395
+ const newContent = scaffoldReadme(config);
10396
+ if (dryRun) {
10397
+ process.stdout.write(
10398
+ `[dry-run] Would write (scaffold): ${relPath}
10399
+
10400
+ ${newContent}
10401
+ `
10402
+ );
10403
+ } else {
10404
+ await writeFile18(readmePath, newContent, "utf-8");
10405
+ process.stdout.write(`Wrote (scaffold): ${relPath}
10406
+ `);
10407
+ }
10408
+ } else if (hasManagedMarkers(existingContent)) {
10409
+ const markerErrors = validateManagedMarkers(existingContent);
10410
+ if (markerErrors.length > 0) {
10411
+ process.stderr.write(
10412
+ `readme: skipping ${relPath} \u2014 malformed managed markers: ${markerErrors.join("; ")}
10413
+ `
10414
+ );
10415
+ if (check) {
10416
+ driftUnits.push(`${relPath} (malformed markers)`);
10417
+ }
10418
+ continue;
10419
+ }
10420
+ const existingHash = extractManagedHash(existingContent);
10421
+ const newHash = hashManagedContent(sections);
10422
+ if (existingHash === newHash) {
10423
+ if (!check) {
10424
+ process.stdout.write(`Up to date: ${relPath}
10425
+ `);
10426
+ }
10427
+ continue;
10428
+ }
10429
+ if (check) {
10430
+ driftUnits.push(relPath);
10431
+ continue;
10432
+ }
10433
+ const newContent = refreshManagedSections(existingContent, sections);
10434
+ if (dryRun) {
10435
+ process.stdout.write(
10436
+ `[dry-run] Would write (refresh): ${relPath}
10437
+
10438
+ ${newContent}
10439
+ `
10440
+ );
10441
+ } else {
10442
+ await writeFile18(readmePath, newContent, "utf-8");
10443
+ process.stdout.write(`Wrote (refresh): ${relPath}
10444
+ `);
10445
+ }
10446
+ } else {
10447
+ if (init) {
10448
+ if (check) {
10449
+ driftUnits.push(`${relPath} (no markers, needs --init)`);
10450
+ continue;
10451
+ }
10452
+ const newContent = appendManagedBlock(existingContent, sections);
10453
+ if (dryRun) {
10454
+ process.stdout.write(
10455
+ `[dry-run] Would write (init): ${relPath}
10456
+
10457
+ ${newContent}
10458
+ `
10459
+ );
10460
+ } else {
10461
+ await writeFile18(readmePath, newContent, "utf-8");
10462
+ process.stdout.write(`Wrote (init): ${relPath}
10463
+ `);
10464
+ }
10465
+ } else {
10466
+ process.stdout.write(`Skipped (no markers, use --init): ${relPath}
10467
+ `);
10468
+ }
10469
+ }
10470
+ }
10471
+ if (check) {
10472
+ if (driftUnits.length > 0 || missingUnits.length > 0) {
10473
+ if (driftUnits.length > 0) {
10474
+ process.stdout.write(
10475
+ `
10476
+ README drift detected:
10477
+ ${driftUnits.map((f) => ` - ${f}`).join("\n")}
10478
+ `
10479
+ );
10480
+ }
10481
+ if (missingUnits.length > 0) {
10482
+ process.stdout.write(
10483
+ `
10484
+ README missing:
10485
+ ${missingUnits.map((f) => ` - ${f}`).join("\n")}
10486
+ `
10487
+ );
10488
+ }
10489
+ process.exitCode = 1;
10490
+ } else {
10491
+ process.stdout.write(
10492
+ `README check passed \u2014 all managed sections current.
10493
+ `
10494
+ );
10495
+ }
10496
+ }
10497
+ }
10498
+ function appendManagedBlock(existing, sections) {
10499
+ let content = existing.trimEnd();
10500
+ const sectionKeys = Object.keys(sections).sort();
10501
+ for (const key of sectionKeys) {
10502
+ content += "\n\n" + managedStartMarker(key) + "\n" + sections[key].trimEnd() + "\n" + managedEndMarker(key);
10503
+ }
10504
+ const newHash = hashManagedContent(sections);
10505
+ content += `
10506
+
10507
+ ${HASH_MARKER_PREFIX}${newHash}${MANAGED_MARKER_SUFFIX}`;
10508
+ content += "\n";
10509
+ return content;
10510
+ }
10511
+ async function runReadmeCommand(args) {
10512
+ const dryRun = args.includes("--dry-run");
10513
+ const check = args.includes("--check");
10514
+ const init = args.includes("--init");
10515
+ let projectDir;
10516
+ const pdIdx = args.indexOf("--project-dir");
10517
+ if (pdIdx !== -1) {
10518
+ const pdVal = args[pdIdx + 1];
10519
+ if (pdVal && !pdVal.startsWith("--")) {
10520
+ projectDir = pdVal;
10521
+ } else {
10522
+ process.stderr.write("error: --project-dir requires a path argument.\n");
10523
+ process.exitCode = 1;
10524
+ return;
10525
+ }
10526
+ }
10527
+ await runReadme({ dryRun, check, init, projectDir });
10528
+ }
10529
+ var init_readme = __esm({
10530
+ "src/cli/claude/readme.ts"() {
10531
+ "use strict";
10532
+ init_tech_detect();
10533
+ init_readme_generator();
9479
10534
  }
9480
10535
  });
9481
10536
 
@@ -9491,18 +10546,18 @@ __export(migrate_memory_exports, {
9491
10546
  runMigrateMemory: () => runMigrateMemory
9492
10547
  });
9493
10548
  import {
9494
- readFile as readFile22,
9495
- writeFile as writeFile17,
10549
+ readFile as readFile24,
10550
+ writeFile as writeFile19,
9496
10551
  mkdir as mkdir11,
9497
10552
  unlink as unlink4,
9498
10553
  rmdir,
9499
10554
  readdir as readdir4
9500
10555
  } from "node:fs/promises";
9501
- import { existsSync as existsSync10 } from "node:fs";
9502
- import { join as join31, resolve as resolve9, dirname as dirname11, sep as sep2 } from "node:path";
10556
+ import { existsSync as existsSync11 } from "node:fs";
10557
+ import { join as join33, resolve as resolve10, dirname as dirname11, sep as sep2 } from "node:path";
9503
10558
  import { homedir as homedir8 } from "node:os";
9504
10559
  function encodeProjectPath(absPath) {
9505
- return resolve9(absPath).replace(/[/\\]/g, "-");
10560
+ return resolve10(absPath).replace(/[/\\]/g, "-");
9506
10561
  }
9507
10562
  function resolveAutoMemoryDir(opts) {
9508
10563
  if (opts.autoMemoryDir) {
@@ -9510,7 +10565,7 @@ function resolveAutoMemoryDir(opts) {
9510
10565
  }
9511
10566
  const projectDir = opts.projectDir ?? process.cwd();
9512
10567
  const encoded = encodeProjectPath(projectDir);
9513
- return join31(homedir8(), ".claude", "projects", encoded, "memory");
10568
+ return join33(homedir8(), ".claude", "projects", encoded, "memory");
9514
10569
  }
9515
10570
  function parseFrontmatter(content) {
9516
10571
  content = content.replace(/\r\n/g, "\n");
@@ -9576,10 +10631,10 @@ async function inventoryFiles(dir) {
9576
10631
  }
9577
10632
  const results = [];
9578
10633
  for (const filename of filenames) {
9579
- const sourcePath = join31(dir, filename);
10634
+ const sourcePath = join33(dir, filename);
9580
10635
  let raw;
9581
10636
  try {
9582
- raw = await readFile22(sourcePath, "utf-8");
10637
+ raw = await readFile24(sourcePath, "utf-8");
9583
10638
  } catch (err) {
9584
10639
  const msg = err instanceof Error ? err.message : String(err);
9585
10640
  results.push({
@@ -9659,15 +10714,15 @@ function buildPlan(entries, opts) {
9659
10714
  return plan;
9660
10715
  }
9661
10716
  async function applyPlan(plan, opts) {
9662
- const projectDir = resolve9(opts.projectDir);
10717
+ const projectDir = resolve10(opts.projectDir);
9663
10718
  const dryRun = opts.dryRun ?? false;
9664
10719
  for (const entry of plan.entries) {
9665
10720
  if (entry.suggested_action !== "keep") continue;
9666
10721
  if (!entry.suggested_target?.startsWith("nested:")) continue;
9667
10722
  const relPath = entry.suggested_target.slice("nested:".length);
9668
- const targetDir = resolve9(join31(projectDir, relPath));
9669
- const targetFile = join31(targetDir, "CLAUDE.md");
9670
- if (!targetDir.startsWith(resolve9(projectDir) + sep2)) {
10723
+ const targetDir = resolve10(join33(projectDir, relPath));
10724
+ const targetFile = join33(targetDir, "CLAUDE.md");
10725
+ if (!targetDir.startsWith(resolve10(projectDir) + sep2)) {
9671
10726
  process.stderr.write(
9672
10727
  `migrate-memory: skipping unsafe suggested_target "${entry.suggested_target}" \u2014 resolves outside projectDir
9673
10728
  `
@@ -9685,8 +10740,8 @@ ${anchor}
9685
10740
  if (dryRun) {
9686
10741
  process.stdout.write(`[dry-run] Would create/append: ${targetFile}
9687
10742
  `);
9688
- if (resolve9(entry.source_path).startsWith(
9689
- resolve9(plan.auto_memory_dir) + sep2
10743
+ if (resolve10(entry.source_path).startsWith(
10744
+ resolve10(plan.auto_memory_dir) + sep2
9690
10745
  )) {
9691
10746
  process.stdout.write(
9692
10747
  `[dry-run] Would delete migrated keep source: ${entry.source_path}
@@ -9698,13 +10753,13 @@ ${anchor}
9698
10753
  await mkdir11(targetDir, { recursive: true });
9699
10754
  let existing = "";
9700
10755
  try {
9701
- existing = await readFile22(targetFile, "utf-8");
10756
+ existing = await readFile24(targetFile, "utf-8");
9702
10757
  } catch {
9703
10758
  }
9704
10759
  if (!existing.includes(anchor)) {
9705
- await writeFile17(targetFile, existing + appendContent, "utf-8");
10760
+ await writeFile19(targetFile, existing + appendContent, "utf-8");
9706
10761
  }
9707
- if (resolve9(entry.source_path).startsWith(resolve9(plan.auto_memory_dir) + sep2)) {
10762
+ if (resolve10(entry.source_path).startsWith(resolve10(plan.auto_memory_dir) + sep2)) {
9708
10763
  try {
9709
10764
  await unlink4(entry.source_path);
9710
10765
  } catch {
@@ -9716,7 +10771,7 @@ ${anchor}
9716
10771
  );
9717
10772
  }
9718
10773
  }
9719
- const rootClaudeMd = join31(projectDir, ".claude", "CLAUDE.md");
10774
+ const rootClaudeMd = join33(projectDir, ".claude", "CLAUDE.md");
9720
10775
  if (dryRun) {
9721
10776
  process.stdout.write(
9722
10777
  `[dry-run] Would ensure ${rootClaudeMd} contains: ${IMPORT_LINE}
@@ -9725,12 +10780,12 @@ ${anchor}
9725
10780
  } else {
9726
10781
  let claudeMdContent = "";
9727
10782
  try {
9728
- claudeMdContent = await readFile22(rootClaudeMd, "utf-8");
10783
+ claudeMdContent = await readFile24(rootClaudeMd, "utf-8");
9729
10784
  } catch {
9730
10785
  await mkdir11(dirname11(rootClaudeMd), { recursive: true });
9731
10786
  }
9732
10787
  if (!claudeMdContent.includes(IMPORT_LINE)) {
9733
- await writeFile17(
10788
+ await writeFile19(
9734
10789
  rootClaudeMd,
9735
10790
  claudeMdContent + `
9736
10791
  ${IMPORT_LINE}
@@ -9741,8 +10796,8 @@ ${IMPORT_LINE}
9741
10796
  }
9742
10797
  for (const entry of plan.entries) {
9743
10798
  if (entry.suggested_action !== "drop") continue;
9744
- if (!resolve9(entry.source_path).startsWith(
9745
- resolve9(plan.auto_memory_dir) + sep2
10799
+ if (!resolve10(entry.source_path).startsWith(
10800
+ resolve10(plan.auto_memory_dir) + sep2
9746
10801
  )) {
9747
10802
  process.stderr.write(
9748
10803
  `migrate-memory: skipping delete of "${entry.source_path}" \u2014 resolves outside auto_memory_dir
@@ -9760,13 +10815,13 @@ ${IMPORT_LINE}
9760
10815
  } catch {
9761
10816
  }
9762
10817
  }
9763
- const memoryMd = join31(plan.auto_memory_dir, "MEMORY.md");
9764
- const safeRmdirBase = join31(homedir8(), ".claude", "projects");
10818
+ const memoryMd = join33(plan.auto_memory_dir, "MEMORY.md");
10819
+ const safeRmdirBase = join33(homedir8(), ".claude", "projects");
9765
10820
  if (dryRun) {
9766
10821
  process.stdout.write(`[dry-run] Would delete MEMORY.md: ${memoryMd}
9767
10822
  `);
9768
10823
  } else {
9769
- if (resolve9(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
10824
+ if (resolve10(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
9770
10825
  try {
9771
10826
  await unlink4(memoryMd);
9772
10827
  } catch {
@@ -9784,7 +10839,7 @@ ${IMPORT_LINE}
9784
10839
  `
9785
10840
  );
9786
10841
  } else {
9787
- if (!resolve9(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
10842
+ if (!resolve10(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
9788
10843
  process.stderr.write(
9789
10844
  `migrate-memory: skipping rmdir of "${plan.auto_memory_dir}" \u2014 not under ~/.claude/projects
9790
10845
  `
@@ -9814,7 +10869,7 @@ async function runMigrateMemory(opts) {
9814
10869
  if (applyFile) {
9815
10870
  let planJson;
9816
10871
  try {
9817
- planJson = await readFile22(resolve9(applyFile), "utf-8");
10872
+ planJson = await readFile24(resolve10(applyFile), "utf-8");
9818
10873
  } catch (err) {
9819
10874
  const msg = err instanceof Error ? err.message : String(err);
9820
10875
  process.stderr.write(
@@ -9842,7 +10897,7 @@ async function runMigrateMemory(opts) {
9842
10897
  );
9843
10898
  return;
9844
10899
  }
9845
- if (!existsSync10(autoMemoryDir)) {
10900
+ if (!existsSync11(autoMemoryDir)) {
9846
10901
  process.stdout.write(
9847
10902
  JSON.stringify(
9848
10903
  {
@@ -9886,11 +10941,11 @@ var init_migrate_memory = __esm({
9886
10941
  // src/index.ts
9887
10942
  init_version();
9888
10943
  import { readFileSync as readFileSync10 } from "node:fs";
9889
- import { resolve as resolve10 } from "node:path";
10944
+ import { resolve as resolve11 } from "node:path";
9890
10945
  void (async () => {
9891
10946
  if (!process.env.CODEBYPLAN_API_KEY) {
9892
10947
  try {
9893
- const envPath = resolve10(process.cwd(), ".env.local");
10948
+ const envPath = resolve11(process.cwd(), ".env.local");
9894
10949
  const content = readFileSync10(envPath, "utf-8");
9895
10950
  for (const line of content.split("\n")) {
9896
10951
  const trimmed = line.trim();
@@ -10029,6 +11084,12 @@ void (async () => {
10029
11084
  await runUploadE2eImagesCommand2(rest);
10030
11085
  process.exit(0);
10031
11086
  }
11087
+ if (arg === "arch-map") {
11088
+ const { runArchMapCommand: runArchMapCommand2 } = await Promise.resolve().then(() => (init_arch_map(), arch_map_exports));
11089
+ const rest = process.argv.slice(3);
11090
+ await runArchMapCommand2(rest);
11091
+ process.exit(0);
11092
+ }
10032
11093
  if (arg === "config") {
10033
11094
  const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
10034
11095
  await runConfig2();
@@ -10054,8 +11115,8 @@ void (async () => {
10054
11115
  const flagArgs = process.argv.slice(3);
10055
11116
  const parsed = parseClaudeFlags(flagArgs);
10056
11117
  if (subcommand === "status") {
10057
- const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
10058
- await runStatus2(process.argv.slice(4));
11118
+ const { runStatus: runStatus3 } = await Promise.resolve().then(() => (init_status(), status_exports));
11119
+ await runStatus3(process.argv.slice(4));
10059
11120
  return;
10060
11121
  }
10061
11122
  if (subcommand === "install") {
@@ -10096,11 +11157,17 @@ void (async () => {
10096
11157
  process.exit(1);
10097
11158
  }
10098
11159
  const { runGenerate: runGenerate2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
11160
+ const check = process.argv.slice(4).includes("--check");
10099
11161
  await runGenerate2(
10100
- parsed.projectDir != null ? { ...parsed.opts, projectDir: parsed.projectDir } : parsed.opts
11162
+ parsed.projectDir != null ? { ...parsed.opts, check, projectDir: parsed.projectDir } : { ...parsed.opts, check }
10101
11163
  );
10102
11164
  process.exit(process.exitCode ?? 0);
10103
11165
  }
11166
+ if (subcommand === "readme") {
11167
+ const { runReadmeCommand: runReadmeCommand2 } = await Promise.resolve().then(() => (init_readme(), readme_exports));
11168
+ await runReadmeCommand2(process.argv.slice(4));
11169
+ process.exit(process.exitCode ?? 0);
11170
+ }
10104
11171
  if (subcommand === "migrate-memory") {
10105
11172
  if (parsed === null) {
10106
11173
  process.exit(1);
@@ -10124,6 +11191,7 @@ void (async () => {
10124
11191
  --write-cache Write result to .codebyplan/claude-status.local.json
10125
11192
  --quiet Suppress stdout output
10126
11193
  codebyplan claude generate [flags] Write .claude/generated/structure.md from local config
11194
+ codebyplan claude readme [flags] Generate / refresh README.md for outward-facing units
10127
11195
  codebyplan claude migrate-memory [flags] Inventory auto-memory files and emit migration plan
10128
11196
 
10129
11197
  Flags (apply to install/update/uninstall):
@@ -10142,6 +11210,13 @@ void (async () => {
10142
11210
  Flags (apply to generate):
10143
11211
  --project-dir <path> Override the project root (default: cwd)
10144
11212
  --dry-run Print what would be written, write nothing
11213
+ --check Exit non-zero when AGENTS.md is missing or drifted
11214
+
11215
+ Flags (apply to readme):
11216
+ --project-dir <path> Override the project root (default: cwd)
11217
+ --dry-run Print what would be written, write nothing
11218
+ --check Exit non-zero on drift or missing READMEs
11219
+ --init Append managed block to marker-less existing READMEs
10145
11220
  `);
10146
11221
  process.exit(0);
10147
11222
  }
@@ -10159,6 +11234,7 @@ void (async () => {
10159
11234
  codebyplan create-project Create a new project within an organization (interactive prompt)
10160
11235
  codebyplan create-repo Create a new repository within a project (interactive prompt)
10161
11236
  codebyplan config Sync repo config from DB to .codebyplan/ files
11237
+ codebyplan arch-map Map architecture modules (status/drift/stamp)
10162
11238
  codebyplan ports Verify port allocations against local package.json scripts
10163
11239
  codebyplan tech-stack Detect and sync tech stack dependencies
10164
11240
  (--full-tech-stack: sync every local worktree on this device)
@@ -10206,6 +11282,7 @@ void (async () => {
10206
11282
  codebyplan claude update [flags] Update installed assets to latest versions
10207
11283
  codebyplan claude uninstall [flags] Remove installed assets from ./.claude/
10208
11284
  codebyplan claude generate [flags] Write .claude/generated/structure.md from local config
11285
+ codebyplan claude readme [flags] Generate / refresh README.md for outward-facing units
10209
11286
  codebyplan claude migrate-memory [flags] Inventory auto-memory files and emit migration plan
10210
11287
 
10211
11288
  Flags (install/update/uninstall):
@@ -10221,6 +11298,13 @@ void (async () => {
10221
11298
  Flags (generate):
10222
11299
  --project-dir <path> Override the project root (default: cwd)
10223
11300
  --dry-run Print what would be written, write nothing
11301
+ --check Exit non-zero when AGENTS.md is missing or drifted
11302
+
11303
+ Flags (readme):
11304
+ --project-dir <path> Override the project root (default: cwd)
11305
+ --dry-run Print what would be written, write nothing
11306
+ --check Exit non-zero on drift or missing READMEs
11307
+ --init Append managed block to marker-less existing READMEs
10224
11308
 
10225
11309
  Flags (migrate-memory):
10226
11310
  --project-dir <path> Override the project root (default: cwd)