recall-os 0.2.0 → 0.3.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 (51) hide show
  1. package/README.md +44 -40
  2. package/dist/cli.js +805 -199
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +805 -199
  5. package/dist/index.js.map +1 -1
  6. package/examples/generated-flutter/AGENTS.md +13 -0
  7. package/examples/generated-flutter/docs/20-security/SECURITY_MODEL.md +25 -4
  8. package/examples/generated-flutter/docs/20-security/THREAT_MODEL.md +35 -3
  9. package/examples/generated-flutter/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  10. package/examples/generated-flutter/docs/ai/RECALL_COMMANDS.md +13 -1
  11. package/examples/generated-generic/AGENTS.md +13 -0
  12. package/examples/generated-generic/docs/20-security/SECURITY_MODEL.md +25 -4
  13. package/examples/generated-generic/docs/20-security/THREAT_MODEL.md +35 -3
  14. package/examples/generated-generic/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  15. package/examples/generated-generic/docs/ai/RECALL_COMMANDS.md +13 -1
  16. package/examples/generated-ios-swift/AGENTS.md +13 -0
  17. package/examples/generated-ios-swift/docs/20-security/SECURITY_MODEL.md +25 -4
  18. package/examples/generated-ios-swift/docs/20-security/THREAT_MODEL.md +35 -3
  19. package/examples/generated-ios-swift/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  20. package/examples/generated-ios-swift/docs/ai/RECALL_COMMANDS.md +13 -1
  21. package/examples/generated-kotlin-android/AGENTS.md +13 -0
  22. package/examples/generated-kotlin-android/docs/20-security/SECURITY_MODEL.md +25 -4
  23. package/examples/generated-kotlin-android/docs/20-security/THREAT_MODEL.md +35 -3
  24. package/examples/generated-kotlin-android/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  25. package/examples/generated-kotlin-android/docs/ai/RECALL_COMMANDS.md +13 -1
  26. package/examples/generated-laravel-api/AGENTS.md +13 -0
  27. package/examples/generated-laravel-api/docs/20-security/SECURITY_MODEL.md +25 -4
  28. package/examples/generated-laravel-api/docs/20-security/THREAT_MODEL.md +35 -3
  29. package/examples/generated-laravel-api/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  30. package/examples/generated-laravel-api/docs/ai/RECALL_COMMANDS.md +13 -1
  31. package/examples/generated-laravel-react/AGENTS.md +13 -0
  32. package/examples/generated-laravel-react/docs/20-security/SECURITY_MODEL.md +25 -4
  33. package/examples/generated-laravel-react/docs/20-security/THREAT_MODEL.md +35 -3
  34. package/examples/generated-laravel-react/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  35. package/examples/generated-laravel-react/docs/ai/RECALL_COMMANDS.md +13 -1
  36. package/examples/generated-laravel-vue/AGENTS.md +13 -0
  37. package/examples/generated-laravel-vue/docs/20-security/SECURITY_MODEL.md +25 -4
  38. package/examples/generated-laravel-vue/docs/20-security/THREAT_MODEL.md +35 -3
  39. package/examples/generated-laravel-vue/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  40. package/examples/generated-laravel-vue/docs/ai/RECALL_COMMANDS.md +13 -1
  41. package/examples/generated-nextjs/AGENTS.md +13 -0
  42. package/examples/generated-nextjs/docs/20-security/SECURITY_MODEL.md +25 -4
  43. package/examples/generated-nextjs/docs/20-security/THREAT_MODEL.md +35 -3
  44. package/examples/generated-nextjs/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  45. package/examples/generated-nextjs/docs/ai/RECALL_COMMANDS.md +13 -1
  46. package/examples/generated-python-fastapi/AGENTS.md +13 -0
  47. package/examples/generated-python-fastapi/docs/20-security/SECURITY_MODEL.md +25 -4
  48. package/examples/generated-python-fastapi/docs/20-security/THREAT_MODEL.md +35 -3
  49. package/examples/generated-python-fastapi/docs/60-engineering/AI_AGENT_RULES.md +9 -0
  50. package/examples/generated-python-fastapi/docs/ai/RECALL_COMMANDS.md +13 -1
  51. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -132,8 +132,8 @@ function parseConfig(value) {
132
132
  if (!result.success) {
133
133
  throw new ConfigValidationError(
134
134
  result.error.issues.map((issue) => {
135
- const path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
136
- return `${path17}${issue.message}`;
135
+ const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
136
+ return `${path19}${issue.message}`;
137
137
  })
138
138
  );
139
139
  }
@@ -798,6 +798,54 @@ function generateAdrFile(options) {
798
798
  }
799
799
  ];
800
800
  }
801
+ var supersedingAdrTemplate = `# {{adrId}}: {{title}}
802
+
803
+ ## Status
804
+
805
+ Accepted
806
+
807
+ ## Supersedes
808
+
809
+ - {{supersedesRef}}
810
+
811
+ ## Context
812
+
813
+ What changed, and why the previous decision no longer holds?
814
+
815
+ ## Decision
816
+
817
+ What is the new decision?
818
+
819
+ ## Alternatives Considered
820
+
821
+ What other options were considered?
822
+
823
+ ## Consequences
824
+
825
+ What improves, what worsens, and what risks remain?
826
+
827
+ ## Related Documents
828
+
829
+ - PRD:
830
+ - Architecture:
831
+ - Security:
832
+ - Feature:
833
+ `;
834
+ function generateSupersedingAdr(options) {
835
+ const slug = slugify(options.title);
836
+ const title = titleizeAdrTitle(options.title);
837
+ const context = createTemplateContext({
838
+ adrId: options.adrId,
839
+ slug,
840
+ title,
841
+ supersedesRef: options.supersedesRef
842
+ });
843
+ return {
844
+ path: path4.posix.join(options.adrDir, `${options.adrId}-${slug}.md`),
845
+ content: renderTemplate(supersedingAdrTemplate, context),
846
+ slug
847
+ };
848
+ }
801
849
  function titleizeAdrTitle(title) {
802
850
  return title.trim().replace(/[-_]+/gu, " ").replace(/\s+/gu, " ").replace(/\b\w/gu, (character) => character.toUpperCase());
803
851
  }
@@ -888,8 +936,148 @@ async function loadRequiredConfig2(rootDir) {
888
936
  }
889
937
  }
890
938
 
891
- // src/core/generator/generate-feature.ts
939
+ // src/commands/adr/supersede.ts
940
+ import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
892
941
  import path5 from "path";
942
+ var AdrSupersedeError = class extends Error {
943
+ code;
944
+ details;
945
+ constructor(code, message, details = []) {
946
+ super(message);
947
+ this.name = "AdrSupersedeError";
948
+ this.code = code;
949
+ this.details = details;
950
+ }
951
+ };
952
+ async function supersedeAdr(options) {
953
+ const oldSlug = createSlug2(options.oldName, "oldName");
954
+ createSlug2(options.newTitle, "newTitle");
955
+ const config = await loadRequiredConfig3(options.rootDir);
956
+ const adrDirAbsolute = resolveSafePath(options.rootDir, config.adrDir).absolutePath;
957
+ const old = await findAcceptedAdr(adrDirAbsolute, oldSlug);
958
+ const next = await getNextAdrNumber(adrDirAbsolute);
959
+ const oldRef = old.fileName.replace(/\.md$/u, "");
960
+ const superseding = generateSupersedingAdr({
961
+ adrDir: config.adrDir,
962
+ adrId: next.id,
963
+ title: options.newTitle,
964
+ supersedesRef: oldRef
965
+ });
966
+ const newRef = `${next.id}-${superseding.slug}`;
967
+ const markedOld = old.content.replace(
968
+ /(##\s+Status\r?\n\r?\n)Accepted[^\n]*/u,
969
+ `$1Accepted \u2014 superseded by ${newRef}`
970
+ );
971
+ const writeNew = await write2(options, superseding.path, superseding.content, options.force);
972
+ const oldRelative = `${config.adrDir}/${old.fileName}`;
973
+ const writeOld = await write2(options, oldRelative, markedOld, true);
974
+ return {
975
+ oldRef,
976
+ oldPath: oldRelative,
977
+ newRef,
978
+ newPath: superseding.path,
979
+ dryRun: options.dryRun ?? false,
980
+ writeResult: {
981
+ created: [...writeNew.created, ...writeOld.created],
982
+ overwritten: [...writeNew.overwritten, ...writeOld.overwritten],
983
+ skipped: [...writeNew.skipped, ...writeOld.skipped],
984
+ dryRun: options.dryRun ?? false
985
+ }
986
+ };
987
+ }
988
+ async function findAcceptedAdr(adrDirAbsolute, slug) {
989
+ const pattern = new RegExp(`^ADR-\\d{4,}-${escapeRegExp2(slug)}\\.md$`, "u");
990
+ let entries;
991
+ try {
992
+ entries = await readdir3(adrDirAbsolute, { withFileTypes: true });
993
+ } catch (error) {
994
+ const nodeError = error;
995
+ if (nodeError.code === "ENOENT") {
996
+ throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`);
997
+ }
998
+ throw error;
999
+ }
1000
+ const match = entries.find((entry) => entry.isFile() && pattern.test(entry.name));
1001
+ if (match === void 0) {
1002
+ throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`, [
1003
+ `Looked for an ADR-####-${slug}.md in the ADR directory.`
1004
+ ]);
1005
+ }
1006
+ const content = await readFile3(path5.join(adrDirAbsolute, match.name), "utf8");
1007
+ if (!/(##\s+Status\r?\n\r?\n)Accepted\b/u.test(content)) {
1008
+ throw new AdrSupersedeError(
1009
+ "NOT_ACCEPTED",
1010
+ `ADR ${match.name} is not Accepted, so there is nothing to supersede.`,
1011
+ ["Only an accepted decision can be superseded. Accept it first with `recall adr accept`."]
1012
+ );
1013
+ }
1014
+ return { fileName: match.name, content };
1015
+ }
1016
+ async function write2(options, relativePath, content, force) {
1017
+ const plan = createWritePlan({
1018
+ rootDir: options.rootDir,
1019
+ files: [{ path: relativePath, content }],
1020
+ force
1021
+ });
1022
+ if (plan.hasErrors) {
1023
+ throw new AdrSupersedeError(
1024
+ "WRITE_PLAN_ERROR",
1025
+ "ADR supersede write plan contains errors.",
1026
+ plan.entries.filter(
1027
+ (entry) => entry.action === "error"
1028
+ ).map((entry) => `${entry.path}: ${entry.reason}`)
1029
+ );
1030
+ }
1031
+ return executeWritePlan(plan, { dryRun: options.dryRun });
1032
+ }
1033
+ function formatAdrSupersedeResult(result) {
1034
+ const lines = [
1035
+ result.dryRun ? "Recall OS ADR supersede dry run complete." : "Recall OS ADR supersede complete.",
1036
+ `Superseded: ${result.oldPath} (now marked superseded by ${result.newRef})`,
1037
+ `New decision: ${result.newPath}`
1038
+ ];
1039
+ appendWriteSummary(lines, { dryRun: result.dryRun, writeResult: result.writeResult });
1040
+ if (!result.dryRun) {
1041
+ appendNextSteps(lines, [
1042
+ `Fill ${result.newPath}: Context (what changed), Decision, Alternatives, Consequences.`,
1043
+ `${result.oldRef} stays in history as superseded; update any memory that still relies on it.`,
1044
+ "Run `recall doctor` \u2014 it flags memory that still references the superseded decision."
1045
+ ]);
1046
+ }
1047
+ return `${lines.join("\n")}
1048
+ `;
1049
+ }
1050
+ function createSlug2(name, field) {
1051
+ const withoutPrefix = name.replace(/^ADR-(?:PROPOSED-|\d{4,}-)/iu, "");
1052
+ try {
1053
+ return slugify(withoutPrefix);
1054
+ } catch (error) {
1055
+ if (error instanceof SlugifyError) {
1056
+ throw new AdrSupersedeError("INVALID_ADR_NAME", `Invalid ${field}: ${error.message}`);
1057
+ }
1058
+ throw error;
1059
+ }
1060
+ }
1061
+ function escapeRegExp2(value) {
1062
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1063
+ }
1064
+ async function loadRequiredConfig3(rootDir) {
1065
+ try {
1066
+ return await loadConfig(rootDir);
1067
+ } catch (error) {
1068
+ if (error instanceof ConfigLoadError || error instanceof ConfigValidationError) {
1069
+ throw new AdrSupersedeError(
1070
+ "CONFIG_REQUIRED",
1071
+ "Recall OS config not found or invalid. Run `recall init` first.",
1072
+ [error.message]
1073
+ );
1074
+ }
1075
+ throw error;
1076
+ }
1077
+ }
1078
+
1079
+ // src/core/generator/generate-feature.ts
1080
+ import path6 from "path";
893
1081
  var featureTemplates = [
894
1082
  {
895
1083
  fileName: "PRD.md",
@@ -1041,7 +1229,7 @@ Pending.
1041
1229
  ];
1042
1230
  function generateFeatureFiles(options) {
1043
1231
  const slug = slugify(options.featureName);
1044
- const featureDir = path5.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
1232
+ const featureDir = path6.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
1045
1233
  const title = titleizeFeatureName(options.featureName);
1046
1234
  const context = createTemplateContext({
1047
1235
  featureId: options.featureId,
@@ -1049,7 +1237,7 @@ function generateFeatureFiles(options) {
1049
1237
  title
1050
1238
  });
1051
1239
  return featureTemplates.map((template) => ({
1052
- path: path5.posix.join(featureDir, template.fileName),
1240
+ path: path6.posix.join(featureDir, template.fileName),
1053
1241
  content: renderTemplate(template.content, context)
1054
1242
  }));
1055
1243
  }
@@ -1058,7 +1246,7 @@ function titleizeFeatureName(featureName) {
1058
1246
  }
1059
1247
 
1060
1248
  // src/core/naming/feature-number.ts
1061
- import { readdir as readdir3 } from "fs/promises";
1249
+ import { readdir as readdir4 } from "fs/promises";
1062
1250
  var FEATURE_FOLDER_PATTERN = /^F-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/u;
1063
1251
  async function getFeatureFolderForSlug(featuresDirAbsolutePath, slug) {
1064
1252
  const existingFolders = await readExistingFeatureFolders(featuresDirAbsolutePath);
@@ -1091,7 +1279,7 @@ function formatFeatureNumber(featureNumber) {
1091
1279
  async function readExistingFeatureFolders(featuresDirAbsolutePath) {
1092
1280
  let entries;
1093
1281
  try {
1094
- entries = await readdir3(featuresDirAbsolutePath, { withFileTypes: true });
1282
+ entries = await readdir4(featuresDirAbsolutePath, { withFileTypes: true });
1095
1283
  } catch (error) {
1096
1284
  const nodeError = error;
1097
1285
  if (nodeError.code === "ENOENT") {
@@ -1130,7 +1318,7 @@ var FeatureCreateError = class extends Error {
1130
1318
  };
1131
1319
  async function createFeature(options) {
1132
1320
  const slug = createFeatureSlug(options.name);
1133
- const config = await loadRequiredConfig3(options.rootDir);
1321
+ const config = await loadRequiredConfig4(options.rootDir);
1134
1322
  const featuresDirPath = resolveSafePath(options.rootDir, config.featuresDir);
1135
1323
  const featureFolder = await getFeatureFolderForSlug(featuresDirPath.absolutePath, slug);
1136
1324
  const files = generateFeatureFiles({
@@ -1189,7 +1377,7 @@ function createFeatureSlug(name) {
1189
1377
  throw error;
1190
1378
  }
1191
1379
  }
1192
- async function loadRequiredConfig3(rootDir) {
1380
+ async function loadRequiredConfig4(rootDir) {
1193
1381
  try {
1194
1382
  return await loadConfig(rootDir);
1195
1383
  } catch (error) {
@@ -1228,113 +1416,33 @@ function createDefaultConfig(overrides = {}) {
1228
1416
  });
1229
1417
  }
1230
1418
 
1231
- // src/core/adopt/generate-adoption.ts
1232
- var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
1233
- function generateAdoptionFiles(options) {
1234
- const files = [
1235
- {
1236
- path: ADOPTION_REPORT_PATH,
1237
- content: renderReport(options.adrDir, options.signals)
1238
- }
1239
- ];
1240
- for (const framework of options.signals.frameworks) {
1241
- files.push({
1242
- path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
1243
- content: renderProposedAdr(framework)
1244
- });
1245
- }
1246
- return files;
1247
- }
1248
- function renderReport(adrDir, signals) {
1249
- return `# Adoption Report
1250
-
1251
- ## Status
1252
-
1253
- Proposed. Everything below is inferred from this repository and requires human review. Nothing here
1254
- is accepted repository memory until you accept it.
1255
-
1256
- ## Detected Signals
1257
-
1258
- - Languages: ${formatList(signals.languages)}
1259
- - Package manager: ${signals.packageManager ?? "none detected"}
1260
- - Frameworks: ${formatList(signals.frameworks)}
1261
- - Tests present: ${formatBool(signals.hasTests)}
1262
- - README present: ${formatBool(signals.hasReadme)}
1263
- - Docs folder present: ${formatBool(signals.hasDocs)}
1264
-
1265
- ## Proposed Decisions
1266
-
1267
- ${renderProposedDecisions(adrDir, signals)}
1268
-
1269
- ## Review Checklist
1270
-
1271
- - [ ] Confirm the detected languages and package manager.
1272
- - [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
1273
- - [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
1274
- - [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
1275
-
1276
- ## Notes
1277
-
1278
- This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
1279
- files. No repository code was executed and no decision was accepted automatically.
1280
- `;
1281
- }
1282
- function renderProposedDecisions(adrDir, signals) {
1283
- if (signals.frameworks.length === 0) {
1284
- return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
1285
- }
1286
- return signals.frameworks.map(
1287
- (framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
1288
- ).join("\n");
1289
- }
1290
- function renderProposedAdr(framework) {
1291
- return `# Proposed ADR: Use ${framework}
1292
-
1293
- ## Status
1294
-
1295
- Proposed
1296
-
1297
- ## Context
1298
-
1299
- \`recall adopt\` detected ${framework} in this repository through read-only inspection.
1300
-
1301
- ## Decision
1302
-
1303
- Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
1304
- and is not accepted until a human reviews and accepts it.
1305
-
1306
- ## Alternatives Considered
1307
-
1308
- - Record a different framework.
1309
- - Leave the decision unrecorded for now.
1310
-
1311
- ## Consequences
1312
-
1313
- - Captures a framework already in use as reviewable repository memory.
1314
- - Requires explicit human acceptance before it becomes repository truth.
1315
-
1316
- ## Related Documents
1317
-
1318
- - \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
1319
- - The adoption report generated alongside this proposal.
1320
- `;
1321
- }
1322
- function frameworkSlug(framework) {
1323
- return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
1324
- }
1325
- function formatList(values) {
1326
- return values.length > 0 ? values.join(", ") : "none detected";
1327
- }
1328
- function formatBool(value) {
1329
- return value ? "yes" : "no";
1330
- }
1331
-
1332
1419
  // src/core/adopt/inspect-repo.ts
1333
1420
  import { existsSync as existsSync3 } from "fs";
1334
- import { readFile as readFile3 } from "fs/promises";
1335
- import path6 from "path";
1421
+ import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
1422
+ import path7 from "path";
1423
+ var FRAMEWORK_SOURCES = {
1424
+ "Next.js": "package.json",
1425
+ React: "package.json",
1426
+ NestJS: "package.json",
1427
+ Express: "package.json",
1428
+ FastAPI: "pyproject.toml / requirements.txt",
1429
+ Flask: "pyproject.toml / requirements.txt",
1430
+ Django: "pyproject.toml / requirements.txt",
1431
+ Gin: "go.mod",
1432
+ Echo: "go.mod",
1433
+ Fiber: "go.mod",
1434
+ Chi: "go.mod",
1435
+ "Spring Boot": "pom.xml / build.gradle",
1436
+ "Actix Web": "Cargo.toml",
1437
+ Axum: "Cargo.toml",
1438
+ Rocket: "Cargo.toml",
1439
+ Laravel: "composer.json",
1440
+ Symfony: "composer.json",
1441
+ "Ruby on Rails": "Gemfile",
1442
+ Flutter: "pubspec.yaml"
1443
+ };
1336
1444
  async function inspectRepo(rootDir) {
1337
- const has = (relativePath) => existsSync3(path6.join(rootDir, relativePath));
1445
+ const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
1338
1446
  const languages = /* @__PURE__ */ new Set();
1339
1447
  const frameworks = /* @__PURE__ */ new Set();
1340
1448
  const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
@@ -1366,6 +1474,22 @@ async function inspectRepo(rootDir) {
1366
1474
  languages.add("Dart");
1367
1475
  frameworks.add("Flutter");
1368
1476
  }
1477
+ if (has("composer.json")) {
1478
+ languages.add("PHP");
1479
+ const composer = (await readText(rootDir, "composer.json")).toLowerCase();
1480
+ if (composer.includes("laravel/framework")) {
1481
+ frameworks.add("Laravel");
1482
+ } else if (composer.includes("symfony/")) {
1483
+ frameworks.add("Symfony");
1484
+ }
1485
+ }
1486
+ if (has("Gemfile")) {
1487
+ languages.add("Ruby");
1488
+ const gemfile = (await readText(rootDir, "Gemfile")).toLowerCase();
1489
+ if (gemfile.includes("rails")) {
1490
+ frameworks.add("Ruby on Rails");
1491
+ }
1492
+ }
1369
1493
  const deps = collectDependencies(pkg);
1370
1494
  if ("next" in deps) {
1371
1495
  frameworks.add("Next.js");
@@ -1411,25 +1535,147 @@ async function inspectRepo(rootDir) {
1411
1535
  frameworks.add("Rocket");
1412
1536
  }
1413
1537
  }
1414
- let packageManager = null;
1415
- if (has("pnpm-lock.yaml")) {
1416
- packageManager = "pnpm";
1417
- } else if (has("yarn.lock")) {
1418
- packageManager = "yarn";
1419
- } else if (has("package-lock.json")) {
1420
- packageManager = "npm";
1421
- }
1538
+ const [packageManager, packageManagerSource] = detectPackageManager(has);
1422
1539
  const scripts = pkg !== null && isRecord(pkg.scripts) ? pkg.scripts : {};
1423
- const hasTests = "test" in scripts || has("test") || has("tests") || has("__tests__") || has("pytest.ini") || python.includes("pytest");
1540
+ const testsEvidence = await detectTestsEvidence(rootDir, has, "test" in scripts, python);
1424
1541
  return {
1425
1542
  languages: [...languages],
1426
1543
  packageManager,
1544
+ packageManagerSource,
1427
1545
  frameworks: [...frameworks],
1428
- hasTests,
1546
+ hasTests: testsEvidence !== null,
1547
+ testsEvidence,
1429
1548
  hasReadme: has("README.md") || has("README"),
1430
1549
  hasDocs: has("docs")
1431
1550
  };
1432
1551
  }
1552
+ function summarizeSignals(signals) {
1553
+ const lines = [];
1554
+ lines.push(`- Languages: ${formatList(signals.languages)}`);
1555
+ lines.push(
1556
+ signals.packageManager === null ? "- Package manager: none detected" : `- Package manager: ${signals.packageManager}${signals.packageManagerSource === null ? "" : ` (from \`${signals.packageManagerSource}\`)`}`
1557
+ );
1558
+ if (signals.frameworks.length === 0) {
1559
+ lines.push("- Frameworks: none detected");
1560
+ } else {
1561
+ const withSource = signals.frameworks.map((framework) => {
1562
+ const source = FRAMEWORK_SOURCES[framework];
1563
+ return source === void 0 ? framework : `${framework} (from \`${source}\`)`;
1564
+ });
1565
+ lines.push(`- Frameworks: ${withSource.join(", ")}`);
1566
+ }
1567
+ lines.push(
1568
+ signals.testsEvidence === null ? "- Tests: none detected \u2014 if tests exist, point Recall at them by correcting this report" : `- Tests: detected via ${signals.testsEvidence}`
1569
+ );
1570
+ lines.push(`- README present: ${signals.hasReadme ? "yes" : "no"}`);
1571
+ lines.push(`- Docs folder present: ${signals.hasDocs ? "yes" : "no"}`);
1572
+ return lines;
1573
+ }
1574
+ function formatList(values) {
1575
+ return values.length === 0 ? "none detected" : values.join(", ");
1576
+ }
1577
+ function detectPackageManager(has) {
1578
+ const candidates = [
1579
+ [has("go.mod"), "Go modules", "go.mod"],
1580
+ [has("Cargo.toml"), "Cargo", "Cargo.toml"],
1581
+ [has("pom.xml"), "Maven", "pom.xml"],
1582
+ [has("build.gradle"), "Gradle", "build.gradle"],
1583
+ [has("build.gradle.kts"), "Gradle", "build.gradle.kts"],
1584
+ [has("composer.json"), "Composer", "composer.json"],
1585
+ [has("Gemfile"), "Bundler", "Gemfile"],
1586
+ [has("Package.swift"), "Swift Package Manager", "Package.swift"],
1587
+ [has("pubspec.yaml"), "pub", "pubspec.yaml"],
1588
+ [has("uv.lock"), "uv", "uv.lock"],
1589
+ [has("poetry.lock"), "Poetry", "poetry.lock"],
1590
+ [has("requirements.txt"), "pip", "requirements.txt"],
1591
+ [has("pyproject.toml"), "pip", "pyproject.toml"],
1592
+ [has("pnpm-lock.yaml"), "pnpm", "pnpm-lock.yaml"],
1593
+ [has("yarn.lock"), "yarn", "yarn.lock"],
1594
+ [has("package-lock.json"), "npm", "package-lock.json"],
1595
+ [has("package.json"), "npm", "package.json"]
1596
+ ];
1597
+ for (const [present, name, source] of candidates) {
1598
+ if (present) {
1599
+ return [name, source];
1600
+ }
1601
+ }
1602
+ return [null, null];
1603
+ }
1604
+ async function detectTestsEvidence(rootDir, has, hasTestScript, pythonText) {
1605
+ if (has("tests")) {
1606
+ return "`tests/` directory";
1607
+ }
1608
+ if (has("test")) {
1609
+ return "`test/` directory";
1610
+ }
1611
+ if (has("__tests__")) {
1612
+ return "`__tests__/` directory";
1613
+ }
1614
+ if (has("pytest.ini") || pythonText.includes("pytest")) {
1615
+ return "pytest configuration";
1616
+ }
1617
+ if (has("phpunit.xml") || has("phpunit.xml.dist")) {
1618
+ return "PHPUnit configuration";
1619
+ }
1620
+ if (hasTestScript) {
1621
+ return '`"test"` script in package.json';
1622
+ }
1623
+ const testFile = await findTestFile(rootDir);
1624
+ if (testFile !== null) {
1625
+ return `\`${testFile}\``;
1626
+ }
1627
+ return null;
1628
+ }
1629
+ var TEST_FILE_PATTERNS = [
1630
+ /_test\.go$/u,
1631
+ /\.(test|spec)\.[cm]?[jt]sx?$/u,
1632
+ /^test_.+\.py$/u,
1633
+ /_test\.py$/u,
1634
+ /.+Tests?\.(java|kt)$/u,
1635
+ /.+Test\.php$/u,
1636
+ /_spec\.rb$/u,
1637
+ /_test\.rb$/u
1638
+ ];
1639
+ var TEST_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
1640
+ "node_modules",
1641
+ "vendor",
1642
+ "dist",
1643
+ "build",
1644
+ "target",
1645
+ "coverage",
1646
+ "Pods",
1647
+ "__pycache__"
1648
+ ]);
1649
+ async function findTestFile(rootDir) {
1650
+ let budget = 4e3;
1651
+ const stack = [rootDir];
1652
+ while (stack.length > 0 && budget > 0) {
1653
+ const dir = stack.pop();
1654
+ if (dir === void 0) {
1655
+ break;
1656
+ }
1657
+ let entries;
1658
+ try {
1659
+ entries = await readdir5(dir, { withFileTypes: true });
1660
+ } catch {
1661
+ continue;
1662
+ }
1663
+ for (const entry of entries) {
1664
+ budget -= 1;
1665
+ if (budget <= 0) {
1666
+ break;
1667
+ }
1668
+ if (entry.isDirectory()) {
1669
+ if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
1670
+ stack.push(path7.join(dir, entry.name));
1671
+ }
1672
+ } else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
1673
+ return path7.relative(rootDir, path7.join(dir, entry.name));
1674
+ }
1675
+ }
1676
+ }
1677
+ return null;
1678
+ }
1433
1679
  function collectDependencies(pkg) {
1434
1680
  if (pkg === null) {
1435
1681
  return {};
@@ -1448,7 +1694,7 @@ async function readJson(rootDir, relativePath) {
1448
1694
  }
1449
1695
  async function readText(rootDir, relativePath) {
1450
1696
  try {
1451
- return await readFile3(path6.join(rootDir, relativePath), "utf8");
1697
+ return await readFile4(path7.join(rootDir, relativePath), "utf8");
1452
1698
  } catch {
1453
1699
  return "";
1454
1700
  }
@@ -1457,6 +1703,100 @@ function isRecord(value) {
1457
1703
  return typeof value === "object" && value !== null && !Array.isArray(value);
1458
1704
  }
1459
1705
 
1706
+ // src/core/adopt/generate-adoption.ts
1707
+ var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
1708
+ function generateAdoptionFiles(options) {
1709
+ const files = [
1710
+ {
1711
+ path: ADOPTION_REPORT_PATH,
1712
+ content: renderReport(options.adrDir, options.signals)
1713
+ }
1714
+ ];
1715
+ for (const framework of options.signals.frameworks) {
1716
+ files.push({
1717
+ path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
1718
+ content: renderProposedAdr(framework)
1719
+ });
1720
+ }
1721
+ return files;
1722
+ }
1723
+ function renderReport(adrDir, signals) {
1724
+ return `# Adoption Report
1725
+
1726
+ ## Status
1727
+
1728
+ Proposed. Everything below is inferred from this repository and requires human review. Nothing here
1729
+ is accepted repository memory until you accept it.
1730
+
1731
+ ## Detected Signals
1732
+
1733
+ Each signal notes the file it was inferred from. If one is wrong, correct the source or edit this
1734
+ report \u2014 nothing here is accepted.
1735
+
1736
+ ${summarizeSignals(signals).join("\n")}
1737
+
1738
+ ## Proposed Decisions
1739
+
1740
+ ${renderProposedDecisions(adrDir, signals)}
1741
+
1742
+ ## Review Checklist
1743
+
1744
+ - [ ] Confirm the detected languages and package manager (and the source each was read from).
1745
+ - [ ] Confirm where tests were detected, or point Recall at the right location if it is wrong.
1746
+ - [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
1747
+ - [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
1748
+ - [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
1749
+
1750
+ ## Notes
1751
+
1752
+ This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
1753
+ files. No repository code was executed and no decision was accepted automatically.
1754
+ `;
1755
+ }
1756
+ function renderProposedDecisions(adrDir, signals) {
1757
+ if (signals.frameworks.length === 0) {
1758
+ return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
1759
+ }
1760
+ return signals.frameworks.map(
1761
+ (framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
1762
+ ).join("\n");
1763
+ }
1764
+ function renderProposedAdr(framework) {
1765
+ return `# Proposed ADR: Use ${framework}
1766
+
1767
+ ## Status
1768
+
1769
+ Proposed
1770
+
1771
+ ## Context
1772
+
1773
+ \`recall adopt\` detected ${framework} in this repository through read-only inspection.
1774
+
1775
+ ## Decision
1776
+
1777
+ Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
1778
+ and is not accepted until a human reviews and accepts it.
1779
+
1780
+ ## Alternatives Considered
1781
+
1782
+ - Record a different framework.
1783
+ - Leave the decision unrecorded for now.
1784
+
1785
+ ## Consequences
1786
+
1787
+ - Captures a framework already in use as reviewable repository memory.
1788
+ - Requires explicit human acceptance before it becomes repository truth.
1789
+
1790
+ ## Related Documents
1791
+
1792
+ - \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
1793
+ - The adoption report generated alongside this proposal.
1794
+ `;
1795
+ }
1796
+ function frameworkSlug(framework) {
1797
+ return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
1798
+ }
1799
+
1460
1800
  // src/commands/adopt.ts
1461
1801
  var AdoptError = class extends Error {
1462
1802
  code;
@@ -1530,8 +1870,8 @@ function formatList2(values) {
1530
1870
 
1531
1871
  // src/core/doctor/checks/code-reference-check.ts
1532
1872
  import { existsSync as existsSync4 } from "fs";
1533
- import { readFile as readFile4, readdir as readdir4 } from "fs/promises";
1534
- import path7 from "path";
1873
+ import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
1874
+ import path8 from "path";
1535
1875
  var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1536
1876
  var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
1537
1877
  var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
@@ -1548,7 +1888,7 @@ async function checkCodeReferences(context) {
1548
1888
  continue;
1549
1889
  }
1550
1890
  for (const doc of FEATURE_DOCS) {
1551
- const relativePath = path7.posix.join(context.config.featuresDir, folder.name, doc);
1891
+ const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
1552
1892
  findings.push(...await checkDoc(context.rootDir, relativePath));
1553
1893
  }
1554
1894
  }
@@ -1558,7 +1898,7 @@ async function checkCodeReferences(context) {
1558
1898
  continue;
1559
1899
  }
1560
1900
  for (const doc of MODULE_DOCS) {
1561
- const relativePath = path7.posix.join(context.config.modulesDir, folder.name, doc);
1901
+ const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
1562
1902
  findings.push(...await checkDoc(context.rootDir, relativePath));
1563
1903
  }
1564
1904
  }
@@ -1577,7 +1917,7 @@ async function checkDoc(rootDir, relativePath) {
1577
1917
  continue;
1578
1918
  }
1579
1919
  seen.add(reference);
1580
- if (!existsSync4(path7.join(rootDir, reference))) {
1920
+ if (!existsSync4(path8.join(rootDir, reference))) {
1581
1921
  findings.push({
1582
1922
  severity: "warning",
1583
1923
  check: "drift-code-reference",
@@ -1590,7 +1930,7 @@ async function checkDoc(rootDir, relativePath) {
1590
1930
  }
1591
1931
  async function readDirIfExists(rootDir, relativePath) {
1592
1932
  try {
1593
- return await readdir4(path7.join(rootDir, relativePath), { withFileTypes: true });
1933
+ return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
1594
1934
  } catch (error) {
1595
1935
  const nodeError = error;
1596
1936
  if (nodeError.code === "ENOENT") {
@@ -1601,7 +1941,7 @@ async function readDirIfExists(rootDir, relativePath) {
1601
1941
  }
1602
1942
  async function readFileIfExists(rootDir, relativePath) {
1603
1943
  try {
1604
- return await readFile4(path7.join(rootDir, relativePath), "utf8");
1944
+ return await readFile5(path8.join(rootDir, relativePath), "utf8");
1605
1945
  } catch (error) {
1606
1946
  const nodeError = error;
1607
1947
  if (nodeError.code === "ENOENT") {
@@ -1612,12 +1952,12 @@ async function readFileIfExists(rootDir, relativePath) {
1612
1952
  }
1613
1953
 
1614
1954
  // src/core/doctor/checks/config-check.ts
1615
- import { readFile as readFile5 } from "fs/promises";
1955
+ import { readFile as readFile6 } from "fs/promises";
1616
1956
  async function checkConfig(rootDir) {
1617
1957
  const configPath = resolveSafePath(rootDir, CONFIG_PATH);
1618
1958
  let rawConfig;
1619
1959
  try {
1620
- rawConfig = await readFile5(configPath.absolutePath, "utf8");
1960
+ rawConfig = await readFile6(configPath.absolutePath, "utf8");
1621
1961
  } catch (error) {
1622
1962
  const nodeError = error;
1623
1963
  if (nodeError.code === "ENOENT") {
@@ -1680,9 +2020,12 @@ async function checkConfig(rootDir) {
1680
2020
  }
1681
2021
 
1682
2022
  // src/core/doctor/checks/content-check.ts
1683
- import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
1684
- import path8 from "path";
2023
+ import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
2024
+ import path9 from "path";
1685
2025
  var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
2026
+ var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
2027
+ var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
2028
+ var THREAT_MODEL_PATH = "docs/20-security/THREAT_MODEL.md";
1686
2029
  async function checkContent(context) {
1687
2030
  if (context.config === void 0) {
1688
2031
  return [];
@@ -1693,7 +2036,7 @@ async function checkContent(context) {
1693
2036
  (entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
1694
2037
  );
1695
2038
  for (const folder of featureFolders) {
1696
- const prdPath = path8.posix.join(context.config.featuresDir, folder.name, "PRD.md");
2039
+ const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
1697
2040
  const prd = await readFileIfExists2(context.rootDir, prdPath);
1698
2041
  if (prd === void 0) {
1699
2042
  continue;
@@ -1717,8 +2060,16 @@ async function checkContent(context) {
1717
2060
  }
1718
2061
  const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
1719
2062
  const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
2063
+ const adrEntries = await readDirIfExists2(context.rootDir, context.config.adrDir);
2064
+ const acceptedAdrs = adrEntries.filter(
2065
+ (entry) => entry.isFile() && acceptedAdrPattern.test(entry.name)
2066
+ );
2067
+ const hasWork = featureFolders.length > 0 || moduleFolders.length > 0 || acceptedAdrs.length > 0;
2068
+ if (hasWork) {
2069
+ findings.push(...await checkSecurityDoc(context.rootDir));
2070
+ }
1720
2071
  for (const folder of moduleFolders) {
1721
- const modulePath = path8.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
2072
+ const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
1722
2073
  const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
1723
2074
  if (moduleDoc === void 0) {
1724
2075
  continue;
@@ -1742,6 +2093,28 @@ async function checkContent(context) {
1742
2093
  }
1743
2094
  return findings;
1744
2095
  }
2096
+ async function checkSecurityDoc(rootDir) {
2097
+ const findings = [];
2098
+ const security = await readFileIfExists2(rootDir, SECURITY_MODEL_PATH);
2099
+ if (security !== void 0 && sectionIsUnfilled(security, "Authentication And Authorization")) {
2100
+ findings.push({
2101
+ severity: "warning",
2102
+ check: "content-security",
2103
+ message: "Security model authentication and authorization section is still an unfilled template.",
2104
+ path: SECURITY_MODEL_PATH
2105
+ });
2106
+ }
2107
+ const threat = await readFileIfExists2(rootDir, THREAT_MODEL_PATH);
2108
+ if (threat !== void 0 && sectionIsUnfilled(threat, "Assets")) {
2109
+ findings.push({
2110
+ severity: "warning",
2111
+ check: "content-threat-model",
2112
+ message: "Threat model assets section is still an unfilled template.",
2113
+ path: THREAT_MODEL_PATH
2114
+ });
2115
+ }
2116
+ return findings;
2117
+ }
1745
2118
  function sectionIsUnfilled(content, heading) {
1746
2119
  const section = getSection(content, heading);
1747
2120
  return section !== void 0 && isUnfilled(section);
@@ -1754,7 +2127,7 @@ function isUnfilled(value) {
1754
2127
  if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
1755
2128
  return true;
1756
2129
  }
1757
- return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns");
2130
+ return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns") || normalized.includes("describe how this repository authenticates") || normalized.includes("describe what this repository must protect");
1758
2131
  }
1759
2132
  function getSection(content, heading) {
1760
2133
  const lines = content.split(/\r?\n/u);
@@ -1774,7 +2147,7 @@ function getSection(content, heading) {
1774
2147
  }
1775
2148
  async function readDirIfExists2(rootDir, relativePath) {
1776
2149
  try {
1777
- return await readdir5(path8.join(rootDir, relativePath), { withFileTypes: true });
2150
+ return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
1778
2151
  } catch (error) {
1779
2152
  const nodeError = error;
1780
2153
  if (nodeError.code === "ENOENT") {
@@ -1785,7 +2158,7 @@ async function readDirIfExists2(rootDir, relativePath) {
1785
2158
  }
1786
2159
  async function readFileIfExists2(rootDir, relativePath) {
1787
2160
  try {
1788
- return await readFile6(path8.join(rootDir, relativePath), "utf8");
2161
+ return await readFile7(path9.join(rootDir, relativePath), "utf8");
1789
2162
  } catch (error) {
1790
2163
  const nodeError = error;
1791
2164
  if (nodeError.code === "ENOENT") {
@@ -1796,8 +2169,8 @@ async function readFileIfExists2(rootDir, relativePath) {
1796
2169
  }
1797
2170
 
1798
2171
  // src/core/doctor/checks/drift-check.ts
1799
- import { readFile as readFile7, readdir as readdir6 } from "fs/promises";
1800
- import path9 from "path";
2172
+ import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
2173
+ import path10 from "path";
1801
2174
  var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
1802
2175
  var adrReferencePattern = /ADR-\d{4,}/giu;
1803
2176
  async function checkDrift(context) {
@@ -1814,12 +2187,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
1814
2187
  const known = /* @__PURE__ */ new Map();
1815
2188
  const files = await readMarkdownFiles(rootDir, adrDir);
1816
2189
  for (const file of files) {
1817
- const match = adrFilePattern.exec(path9.basename(file));
2190
+ const match = adrFilePattern.exec(path10.basename(file));
1818
2191
  if (match === null) {
1819
2192
  continue;
1820
2193
  }
1821
2194
  const id = `ADR-${match[1]}`;
1822
- const content = await readFile7(path9.join(rootDir, file), "utf8");
2195
+ const content = await readFile8(path10.join(rootDir, file), "utf8");
1823
2196
  const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
1824
2197
  const existing = known.get(id);
1825
2198
  if (existing === void 0 || !existing.accepted && accepted) {
@@ -1832,7 +2205,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
1832
2205
  const findings = [];
1833
2206
  const files = await readMarkdownFiles(rootDir, referenceDir);
1834
2207
  for (const file of files) {
1835
- const content = await readFile7(path9.join(rootDir, file), "utf8");
2208
+ const content = await readFile8(path10.join(rootDir, file), "utf8");
1836
2209
  const referenced = /* @__PURE__ */ new Set();
1837
2210
  for (const match of stripCode(content).matchAll(adrReferencePattern)) {
1838
2211
  referenced.add(match[0].toUpperCase());
@@ -1867,7 +2240,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
1867
2240
  const entries = await readDirIfExists3(rootDir, relativeDir);
1868
2241
  const files = [];
1869
2242
  for (const entry of entries) {
1870
- const childRelative = path9.posix.join(relativeDir, entry.name);
2243
+ const childRelative = path10.posix.join(relativeDir, entry.name);
1871
2244
  if (entry.isDirectory()) {
1872
2245
  files.push(...await readMarkdownFiles(rootDir, childRelative));
1873
2246
  continue;
@@ -1900,7 +2273,7 @@ function getSection2(content, heading) {
1900
2273
  }
1901
2274
  async function readDirIfExists3(rootDir, relativePath) {
1902
2275
  try {
1903
- return await readdir6(path9.join(rootDir, relativePath), { withFileTypes: true });
2276
+ return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
1904
2277
  } catch (error) {
1905
2278
  const nodeError = error;
1906
2279
  if (nodeError.code === "ENOENT") {
@@ -1911,8 +2284,8 @@ async function readDirIfExists3(rootDir, relativePath) {
1911
2284
  }
1912
2285
 
1913
2286
  // src/core/doctor/checks/memory-integrity-check.ts
1914
- import { lstat as lstat2, readFile as readFile8, readdir as readdir7 } from "fs/promises";
1915
- import path10 from "path";
2287
+ import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
2288
+ import path11 from "path";
1916
2289
 
1917
2290
  // src/core/adr/adr-sections.ts
1918
2291
  var REQUIRED_ADR_SECTIONS = [
@@ -1976,7 +2349,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
1976
2349
  );
1977
2350
  for (const featureFolder of featureFolders) {
1978
2351
  for (const requiredDoc of requiredFeatureDocs) {
1979
- const filePath = path10.posix.join(featuresDir, featureFolder.name, requiredDoc);
2352
+ const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
1980
2353
  if (!await isFile(rootDir, filePath)) {
1981
2354
  findings.push({
1982
2355
  severity: "error",
@@ -2000,7 +2373,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
2000
2373
  const moduleFolders = entries.filter((entry) => entry.isDirectory());
2001
2374
  for (const moduleFolder of moduleFolders) {
2002
2375
  for (const requiredDoc of requiredModuleDocs) {
2003
- const filePath = path10.posix.join(modulesDir, moduleFolder.name, requiredDoc);
2376
+ const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
2004
2377
  if (!await isFile(rootDir, filePath)) {
2005
2378
  findings.push({
2006
2379
  severity: "error",
@@ -2023,8 +2396,8 @@ async function checkAdrFiles(rootDir, adrDir) {
2023
2396
  const entries = await readDirIfExists4(rootDir, adrDir);
2024
2397
  const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
2025
2398
  for (const adrFile of adrFiles) {
2026
- const filePath = path10.posix.join(adrDir, adrFile.name);
2027
- const content = await readFile8(path10.join(rootDir, filePath), "utf8");
2399
+ const filePath = path11.posix.join(adrDir, adrFile.name);
2400
+ const content = await readFile9(path11.join(rootDir, filePath), "utf8");
2028
2401
  for (const requiredSection of requiredAdrSections) {
2029
2402
  if (!content.includes(requiredSection)) {
2030
2403
  findings.push({
@@ -2045,7 +2418,7 @@ async function checkAdrFiles(rootDir, adrDir) {
2045
2418
  }
2046
2419
  async function readDirIfExists4(rootDir, relativePath) {
2047
2420
  try {
2048
- return await readdir7(path10.join(rootDir, relativePath), { withFileTypes: true });
2421
+ return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
2049
2422
  } catch (error) {
2050
2423
  const nodeError = error;
2051
2424
  if (nodeError.code === "ENOENT") {
@@ -2056,7 +2429,7 @@ async function readDirIfExists4(rootDir, relativePath) {
2056
2429
  }
2057
2430
  async function isFile(rootDir, relativePath) {
2058
2431
  try {
2059
- return (await lstat2(path10.join(rootDir, relativePath))).isFile();
2432
+ return (await lstat2(path11.join(rootDir, relativePath))).isFile();
2060
2433
  } catch (error) {
2061
2434
  const nodeError = error;
2062
2435
  if (nodeError.code === "ENOENT") {
@@ -2068,7 +2441,7 @@ async function isFile(rootDir, relativePath) {
2068
2441
 
2069
2442
  // src/core/doctor/checks/required-files-check.ts
2070
2443
  import { lstat as lstat3 } from "fs/promises";
2071
- import path11 from "path";
2444
+ import path12 from "path";
2072
2445
  var rootFiles = ["AGENTS.md", "CLAUDE.md"];
2073
2446
  var requiredDocs = [
2074
2447
  "00-product/PRD.md",
@@ -2095,12 +2468,12 @@ async function checkRequiredFiles(context) {
2095
2468
  }
2096
2469
  }
2097
2470
  for (const relativeDocPath of requiredDocs) {
2098
- const filePath = path11.posix.join(docsDir, relativeDocPath);
2471
+ const filePath = path12.posix.join(docsDir, relativeDocPath);
2099
2472
  if (!await isFile2(context.rootDir, filePath)) {
2100
2473
  findings.push(missingFile(filePath, "required-docs"));
2101
2474
  }
2102
2475
  }
2103
- const adrIndexPath = path11.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
2476
+ const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
2104
2477
  if (!await isFile2(context.rootDir, adrIndexPath)) {
2105
2478
  findings.push(missingFile(adrIndexPath, "required-docs"));
2106
2479
  }
@@ -2126,7 +2499,7 @@ async function checkRequiredFiles(context) {
2126
2499
  }
2127
2500
  async function isFile2(rootDir, relativePath) {
2128
2501
  try {
2129
- return (await lstat3(path11.join(rootDir, relativePath))).isFile();
2502
+ return (await lstat3(path12.join(rootDir, relativePath))).isFile();
2130
2503
  } catch (error) {
2131
2504
  const nodeError = error;
2132
2505
  if (nodeError.code === "ENOENT") {
@@ -2137,7 +2510,7 @@ async function isFile2(rootDir, relativePath) {
2137
2510
  }
2138
2511
  async function isDirectory(rootDir, relativePath) {
2139
2512
  try {
2140
- return (await lstat3(path11.join(rootDir, relativePath))).isDirectory();
2513
+ return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
2141
2514
  } catch (error) {
2142
2515
  const nodeError = error;
2143
2516
  if (nodeError.code === "ENOENT") {
@@ -2156,8 +2529,8 @@ function missingFile(pathValue, check) {
2156
2529
  }
2157
2530
 
2158
2531
  // src/core/doctor/checks/standards-check.ts
2159
- import { lstat as lstat4, readFile as readFile9, readdir as readdir8 } from "fs/promises";
2160
- import path12 from "path";
2532
+ import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
2533
+ import path13 from "path";
2161
2534
  var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
2162
2535
  var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
2163
2536
  var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
@@ -2177,10 +2550,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
2177
2550
  (entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
2178
2551
  );
2179
2552
  for (const featureFolder of featureFolders) {
2180
- const featureDir = path12.posix.join(featuresDir, featureFolder.name);
2181
- const completionReportPath = path12.posix.join(featureDir, "COMPLETION_REPORT.md");
2182
- const reviewPath = path12.posix.join(featureDir, "REVIEW.md");
2183
- const architectureImpactPath = path12.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
2553
+ const featureDir = path13.posix.join(featuresDir, featureFolder.name);
2554
+ const completionReportPath = path13.posix.join(featureDir, "COMPLETION_REPORT.md");
2555
+ const reviewPath = path13.posix.join(featureDir, "REVIEW.md");
2556
+ const architectureImpactPath = path13.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
2184
2557
  const completionReport = await readFileIfExists3(rootDir, completionReportPath);
2185
2558
  const review = await readFileIfExists3(rootDir, reviewPath);
2186
2559
  const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
@@ -2230,8 +2603,8 @@ async function checkAdrStandards(rootDir, adrDir) {
2230
2603
  const entries = await readDirIfExists5(rootDir, adrDir);
2231
2604
  const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
2232
2605
  for (const adrFile of adrFiles) {
2233
- const adrPath = path12.posix.join(adrDir, adrFile.name);
2234
- const content = await readFile9(path12.join(rootDir, adrPath), "utf8");
2606
+ const adrPath = path13.posix.join(adrDir, adrFile.name);
2607
+ const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
2235
2608
  const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
2236
2609
  if (!hasMeaningfulSection(content, "Consequences")) {
2237
2610
  findings.push({
@@ -2315,7 +2688,7 @@ function isPlaceholder(value) {
2315
2688
  }
2316
2689
  async function readDirIfExists5(rootDir, relativePath) {
2317
2690
  try {
2318
- return await readdir8(path12.join(rootDir, relativePath), { withFileTypes: true });
2691
+ return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
2319
2692
  } catch (error) {
2320
2693
  const nodeError = error;
2321
2694
  if (nodeError.code === "ENOENT") {
@@ -2329,7 +2702,7 @@ async function readFileIfExists3(rootDir, relativePath) {
2329
2702
  if (!await isFile3(rootDir, relativePath)) {
2330
2703
  return void 0;
2331
2704
  }
2332
- return await readFile9(path12.join(rootDir, relativePath), "utf8");
2705
+ return await readFile10(path13.join(rootDir, relativePath), "utf8");
2333
2706
  } catch (error) {
2334
2707
  const nodeError = error;
2335
2708
  if (nodeError.code === "ENOENT") {
@@ -2340,7 +2713,7 @@ async function readFileIfExists3(rootDir, relativePath) {
2340
2713
  }
2341
2714
  async function isFile3(rootDir, relativePath) {
2342
2715
  try {
2343
- return (await lstat4(path12.join(rootDir, relativePath))).isFile();
2716
+ return (await lstat4(path13.join(rootDir, relativePath))).isFile();
2344
2717
  } catch (error) {
2345
2718
  const nodeError = error;
2346
2719
  if (nodeError.code === "ENOENT") {
@@ -2350,6 +2723,110 @@ async function isFile3(rootDir, relativePath) {
2350
2723
  }
2351
2724
  }
2352
2725
 
2726
+ // src/core/doctor/checks/superseded-check.ts
2727
+ import { readFile as readFile11, readdir as readdir11 } from "fs/promises";
2728
+ import path14 from "path";
2729
+ var adrFilePattern4 = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
2730
+ var adrReferencePattern2 = /ADR-\d{4,}/giu;
2731
+ async function checkSuperseded(context) {
2732
+ if (context.config === void 0) {
2733
+ return [];
2734
+ }
2735
+ const supersededIds = await loadSupersededAdrIds(context.rootDir, context.config.adrDir);
2736
+ if (supersededIds.size === 0) {
2737
+ return [];
2738
+ }
2739
+ const findings = [];
2740
+ findings.push(
2741
+ ...await checkReferences2(context.rootDir, context.config.featuresDir, supersededIds)
2742
+ );
2743
+ findings.push(
2744
+ ...await checkReferences2(context.rootDir, context.config.modulesDir, supersededIds)
2745
+ );
2746
+ return findings;
2747
+ }
2748
+ async function loadSupersededAdrIds(rootDir, adrDir) {
2749
+ const superseded = /* @__PURE__ */ new Set();
2750
+ const files = await readMarkdownFiles2(rootDir, adrDir);
2751
+ for (const file of files) {
2752
+ const match = adrFilePattern4.exec(path14.basename(file));
2753
+ if (match === null) {
2754
+ continue;
2755
+ }
2756
+ const content = await readFile11(path14.join(rootDir, file), "utf8");
2757
+ if (statusContains(content, /superseded\s+by/iu)) {
2758
+ superseded.add(`ADR-${match[1]}`.toUpperCase());
2759
+ }
2760
+ }
2761
+ return superseded;
2762
+ }
2763
+ async function checkReferences2(rootDir, referenceDir, supersededIds) {
2764
+ const findings = [];
2765
+ const files = await readMarkdownFiles2(rootDir, referenceDir);
2766
+ for (const file of files) {
2767
+ const content = await readFile11(path14.join(rootDir, file), "utf8");
2768
+ const referenced = /* @__PURE__ */ new Set();
2769
+ for (const match of stripCode2(content).matchAll(adrReferencePattern2)) {
2770
+ referenced.add(match[0].toUpperCase());
2771
+ }
2772
+ for (const id of referenced) {
2773
+ if (supersededIds.has(id)) {
2774
+ findings.push({
2775
+ severity: "warning",
2776
+ check: "superseded-reference",
2777
+ message: `Repository memory references ${id}, which has been superseded \u2014 update it to the current decision.`,
2778
+ path: file
2779
+ });
2780
+ }
2781
+ }
2782
+ }
2783
+ return findings;
2784
+ }
2785
+ function statusContains(content, pattern) {
2786
+ const lines = content.split(/\r?\n/u);
2787
+ const startIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## status");
2788
+ if (startIndex === -1) {
2789
+ return false;
2790
+ }
2791
+ const body = [];
2792
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
2793
+ if (/^##\s+/u.test(lines[index])) {
2794
+ break;
2795
+ }
2796
+ body.push(lines[index]);
2797
+ }
2798
+ return pattern.test(body.join("\n"));
2799
+ }
2800
+ function stripCode2(content) {
2801
+ return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
2802
+ }
2803
+ async function readMarkdownFiles2(rootDir, relativeDir) {
2804
+ const entries = await readDirIfExists6(rootDir, relativeDir);
2805
+ const files = [];
2806
+ for (const entry of entries) {
2807
+ const childRelative = path14.posix.join(relativeDir, entry.name);
2808
+ if (entry.isDirectory()) {
2809
+ files.push(...await readMarkdownFiles2(rootDir, childRelative));
2810
+ continue;
2811
+ }
2812
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
2813
+ files.push(childRelative);
2814
+ }
2815
+ }
2816
+ return files;
2817
+ }
2818
+ async function readDirIfExists6(rootDir, relativePath) {
2819
+ try {
2820
+ return await readdir11(path14.join(rootDir, relativePath), { withFileTypes: true });
2821
+ } catch (error) {
2822
+ const nodeError = error;
2823
+ if (nodeError.code === "ENOENT") {
2824
+ return [];
2825
+ }
2826
+ throw error;
2827
+ }
2828
+ }
2829
+
2353
2830
  // src/core/doctor/doctor-check.ts
2354
2831
  async function runDoctor(rootDir) {
2355
2832
  const findings = [];
@@ -2366,6 +2843,7 @@ async function runDoctor(rootDir) {
2366
2843
  findings.push(...await checkDrift(context));
2367
2844
  findings.push(...await checkContent(context));
2368
2845
  findings.push(...await checkCodeReferences(context));
2846
+ findings.push(...await checkSuperseded(context));
2369
2847
  }
2370
2848
  return createDoctorReport(findings);
2371
2849
  }
@@ -2443,10 +2921,10 @@ function formatDoctorResult(result) {
2443
2921
 
2444
2922
  // src/commands/init.ts
2445
2923
  import { existsSync as existsSync6 } from "fs";
2446
- import path15 from "path";
2924
+ import path17 from "path";
2447
2925
 
2448
2926
  // src/core/generator/generate-init.ts
2449
- import path13 from "path";
2927
+ import path15 from "path";
2450
2928
  var neutralTemplates = [
2451
2929
  {
2452
2930
  path: "AGENTS.md",
@@ -2465,6 +2943,18 @@ Required reading:
2465
2943
  - \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
2466
2944
 
2467
2945
  Repository rules override model preferences. If instructions conflict, stop and report the conflict.
2946
+
2947
+ ## Changing an accepted decision
2948
+
2949
+ Before changing anything an accepted ADR governs (framework, database, auth, API shape, and similar):
2950
+
2951
+ 1. Check \`docs/adrs/\` for an accepted ADR that covers it.
2952
+ 2. If your change contradicts one, stop and confirm with a human first \u2014 do not silently change the
2953
+ code and leave the ADR saying the opposite.
2954
+ 3. Record the change as a new decision with \`recall adr supersede <old> <new-title>\`. That supersedes
2955
+ the old ADR instead of overwriting history, so the reasoning stays auditable.
2956
+
2957
+ Repository memory is only trustworthy if decisions change through this trail, not silently.
2468
2958
  `
2469
2959
  },
2470
2960
  {
@@ -2501,6 +2991,8 @@ Before non-trivial work:
2501
2991
  - Read \`AGENTS.md\` and the docs it routes to.
2502
2992
  - Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
2503
2993
  - If an instruction conflicts with accepted repository memory, stop and report the conflict.
2994
+ - Before changing what an accepted ADR governs, confirm with a human and record it with
2995
+ \`recall adr supersede <old> <new-title>\` \u2014 never silently contradict an accepted decision.
2504
2996
 
2505
2997
  Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
2506
2998
  standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
@@ -2589,26 +3081,78 @@ Default behavior:
2589
3081
  path: "docs/20-security/SECURITY_MODEL.md",
2590
3082
  content: `# Security Model
2591
3083
 
2592
- ## Current Status
3084
+ ## Status
2593
3085
 
2594
- Draft.
3086
+ Draft \u2014 fill the prompted sections below with this repository's real model as it grows. \`recall doctor\`
3087
+ flags these as warnings once the repository has real work (a feature, module, or accepted decision).
2595
3088
 
2596
3089
  ## Baseline Rules
2597
3090
 
2598
- - Do not commit secrets.
2599
- - Do not read or copy \`.env\` files into docs.
3091
+ - Never commit secrets or credentials, and never read or copy \`.env\` files into docs.
3092
+ - Validate and authorize untrusted input at every trust boundary.
2600
3093
  - Do not add network, telemetry, cloud, MCP runtime, or AI API behavior without explicit review.
3094
+
3095
+ ## Authentication And Authorization
3096
+
3097
+ Describe how this repository authenticates users or clients and how it authorizes actions, including
3098
+ where those checks live.
3099
+
3100
+ ## Secrets And Configuration
3101
+
3102
+ Describe where secrets live, how they are injected, and how configuration is kept out of version
3103
+ control.
3104
+
3105
+ ## Sensitive Data
3106
+
3107
+ Describe the sensitive or personal data this repository handles, and how it is protected at rest and
3108
+ in transit.
3109
+
3110
+ ## Dependencies And Supply Chain
3111
+
3112
+ Describe how third-party dependencies are vetted, pinned, and updated.
2601
3113
  `
2602
3114
  },
2603
3115
  {
2604
3116
  path: "docs/20-security/THREAT_MODEL.md",
2605
3117
  content: `# Threat Model
2606
3118
 
2607
- ## Current Status
3119
+ ## Status
2608
3120
 
2609
- Draft.
3121
+ Draft \u2014 replace the prompts below with this repository's real analysis as it grows. \`recall doctor\`
3122
+ flags these as warnings once the repository has real work (a feature, module, or accepted decision).
3123
+
3124
+ ## Assets
2610
3125
 
2611
- Track repository-specific risks here as the project evolves.
3126
+ Describe what this repository must protect: user data, credentials, money, availability, or
3127
+ reputation.
3128
+
3129
+ ## Entry Points
3130
+
3131
+ Describe where untrusted input enters: HTTP endpoints, webhooks, file uploads, queues, CLI input, or
3132
+ third-party callbacks.
3133
+
3134
+ ## Trust Boundaries
3135
+
3136
+ Describe where trust changes: client to server, service to database, your code to third-party APIs.
3137
+
3138
+ ## Threats
3139
+
3140
+ Describe the concrete threats that apply to this repository, by category:
3141
+
3142
+ - Spoofing \u2014 how identities are faked or sessions stolen.
3143
+ - Tampering \u2014 how requests, data, or builds are altered (injection, mass assignment).
3144
+ - Repudiation \u2014 actions that must remain auditable.
3145
+ - Information disclosure \u2014 how sensitive data or secrets could leak.
3146
+ - Denial of service \u2014 how the system can be overwhelmed or abused.
3147
+ - Elevation of privilege \u2014 how a user could gain access they should not have.
3148
+
3149
+ ## Mitigations
3150
+
3151
+ Describe the control in place or planned for each threat above.
3152
+
3153
+ ## Open Risks
3154
+
3155
+ Describe accepted or unresolved risks and who owns them.
2612
3156
  `
2613
3157
  },
2614
3158
  {
@@ -2657,6 +3201,15 @@ Baseline rules:
2657
3201
  AI agents must follow repository memory over model preference.
2658
3202
 
2659
3203
  If a request conflicts with accepted repository memory or engineering standards, stop and report the conflict.
3204
+
3205
+ ## Changing an accepted decision
3206
+
3207
+ When work would change something an accepted ADR governs:
3208
+
3209
+ 1. Find the accepted ADR in \`docs/adrs/\` that covers it.
3210
+ 2. If the change contradicts it, stop and confirm with a human before changing the code.
3211
+ 3. Record the new decision with \`recall adr supersede <old> <new-title>\` so the old ADR is marked
3212
+ superseded and the reasoning is preserved, instead of silently editing or contradicting it.
2660
3213
  `
2661
3214
  },
2662
3215
  {
@@ -2790,6 +3343,17 @@ Options:
2790
3343
  - \`--dry-run\`: show planned writes without writing files.
2791
3344
  - \`--force\`: overwrite existing files explicitly.
2792
3345
 
3346
+ ### \`recall adr supersede <old> <new-title>\`
3347
+
3348
+ Record a changed decision. Marks an accepted ADR as \`Accepted \u2014 superseded by ADR-####\` and creates a
3349
+ new accepted ADR that declares what it supersedes, so the reasoning trail stays auditable instead of
3350
+ being overwritten. Doctor then warns about any memory still referencing the superseded decision.
3351
+
3352
+ Options:
3353
+
3354
+ - \`--dry-run\`: show planned writes without writing files.
3355
+ - \`--force\`: overwrite existing files explicitly.
3356
+
2793
3357
  ### \`recall module create <name>\`
2794
3358
 
2795
3359
  Create module memory docs under the configured modules directory.
@@ -2805,7 +3369,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
2805
3369
  engineering evidence is present, and whether memory references decisions that exist and are accepted.
2806
3370
 
2807
3371
  Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
2808
- is an error, and memory that references a not-yet-accepted ADR is a warning.
3372
+ is an error, memory that references a not-yet-accepted ADR is a warning, and memory that still
3373
+ references a superseded decision is a warning.
2809
3374
 
2810
3375
  Exit codes:
2811
3376
 
@@ -2892,7 +3457,7 @@ jobs:
2892
3457
  }
2893
3458
  ];
2894
3459
  function generateInitFiles(options) {
2895
- const repositoryName = path13.basename(path13.resolve(options.rootDir)) || "repository";
3460
+ const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
2896
3461
  const context = createTemplateContext({ repositoryName });
2897
3462
  const files = neutralTemplates.map((template) => ({
2898
3463
  path: template.path,
@@ -2919,17 +3484,17 @@ function generatePresetFiles(preset) {
2919
3484
 
2920
3485
  // src/core/hooks/detect-gates.ts
2921
3486
  import { existsSync as existsSync5 } from "fs";
2922
- import { readFile as readFile10 } from "fs/promises";
2923
- import path14 from "path";
3487
+ import { readFile as readFile12 } from "fs/promises";
3488
+ import path16 from "path";
2924
3489
  var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
2925
3490
  async function detectPreCommitGates(rootDir) {
2926
- const packageJsonPath = path14.join(rootDir, "package.json");
3491
+ const packageJsonPath = path16.join(rootDir, "package.json");
2927
3492
  if (!existsSync5(packageJsonPath)) {
2928
3493
  return [];
2929
3494
  }
2930
3495
  let scripts;
2931
3496
  try {
2932
- const raw = await readFile10(packageJsonPath, "utf8");
3497
+ const raw = await readFile12(packageJsonPath, "utf8");
2933
3498
  const parsed = JSON.parse(raw);
2934
3499
  scripts = parsed.scripts ?? {};
2935
3500
  } catch {
@@ -2938,16 +3503,16 @@ async function detectPreCommitGates(rootDir) {
2938
3503
  if (typeof scripts !== "object" || scripts === null) {
2939
3504
  return [];
2940
3505
  }
2941
- const packageManager = detectPackageManager(rootDir);
3506
+ const packageManager = detectPackageManager2(rootDir);
2942
3507
  return KNOWN_SCRIPTS.filter((script) => typeof scripts[script] === "string").map(
2943
3508
  (script) => `${packageManager} run ${script}`
2944
3509
  );
2945
3510
  }
2946
- function detectPackageManager(rootDir) {
2947
- if (existsSync5(path14.join(rootDir, "pnpm-lock.yaml"))) {
3511
+ function detectPackageManager2(rootDir) {
3512
+ if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
2948
3513
  return "pnpm";
2949
3514
  }
2950
- if (existsSync5(path14.join(rootDir, "yarn.lock"))) {
3515
+ if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
2951
3516
  return "yarn";
2952
3517
  }
2953
3518
  return "npm";
@@ -4452,8 +5017,8 @@ function parsePreset(value) {
4452
5017
  if (!result.success) {
4453
5018
  throw new PresetValidationError(
4454
5019
  result.error.issues.map((issue) => {
4455
- const path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
4456
- return `${path17}${issue.message}`;
5020
+ const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
5021
+ return `${path19}${issue.message}`;
4457
5022
  })
4458
5023
  );
4459
5024
  }
@@ -5142,7 +5707,7 @@ var InitError = class extends Error {
5142
5707
  }
5143
5708
  };
5144
5709
  async function initProject(options) {
5145
- if (options.force === true && options.reinit !== true && existsSync6(path15.join(options.rootDir, CONFIG_PATH))) {
5710
+ if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
5146
5711
  throw new InitError(
5147
5712
  "EXISTING_INSTALLATION",
5148
5713
  "Refusing to re-initialize an existing Recall OS installation.",
@@ -5154,6 +5719,7 @@ async function initProject(options) {
5154
5719
  );
5155
5720
  }
5156
5721
  const preset = resolvePreset(options.preset);
5722
+ const detected = await inspectRepo(options.rootDir);
5157
5723
  const preCommitGates = await detectPreCommitGates(options.rootDir);
5158
5724
  const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
5159
5725
  const files = createInitWriteFiles(options.rootDir, config, preset);
@@ -5174,7 +5740,8 @@ async function initProject(options) {
5174
5740
  preset: preset?.id ?? null,
5175
5741
  dryRun: options.dryRun ?? false,
5176
5742
  plan,
5177
- writeResult
5743
+ writeResult,
5744
+ detected
5178
5745
  };
5179
5746
  }
5180
5747
  function formatInitResult(result) {
@@ -5191,6 +5758,7 @@ function formatInitResult(result) {
5191
5758
  dryRun: result.dryRun,
5192
5759
  writeResult: result.writeResult
5193
5760
  });
5761
+ appendDetectedStack(lines, result.detected);
5194
5762
  const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
5195
5763
  if (hookWritten) {
5196
5764
  lines.push("");
@@ -5213,6 +5781,21 @@ function formatInitResult(result) {
5213
5781
  return `${lines.join("\n")}
5214
5782
  `;
5215
5783
  }
5784
+ function appendDetectedStack(lines, detected) {
5785
+ const hasSignal = detected.languages.length > 0 || detected.frameworks.length > 0 || detected.packageManager !== null || detected.testsEvidence !== null;
5786
+ if (!hasSignal) {
5787
+ return;
5788
+ }
5789
+ const stack = summarizeSignals(detected).filter(
5790
+ (line) => !line.startsWith("- README") && !line.startsWith("- Docs")
5791
+ );
5792
+ lines.push("");
5793
+ lines.push("Detected in this repository (proposed \u2014 review, nothing was accepted):");
5794
+ lines.push(...stack);
5795
+ lines.push(
5796
+ "If any signal is wrong, correct the source file noted. Run `recall adopt` to record this as proposed memory."
5797
+ );
5798
+ }
5216
5799
  function resolvePreset(presetId) {
5217
5800
  if (presetId === void 0) {
5218
5801
  return null;
@@ -5488,7 +6071,7 @@ async function loadConfigOrDefault2(rootDir) {
5488
6071
  }
5489
6072
 
5490
6073
  // src/core/generator/generate-module.ts
5491
- import path16 from "path";
6074
+ import path18 from "path";
5492
6075
  var moduleTemplates = [
5493
6076
  {
5494
6077
  fileName: "MODULE.md",
@@ -5561,14 +6144,14 @@ Record durable module decisions here.
5561
6144
  ];
5562
6145
  function generateModuleFiles(options) {
5563
6146
  const slug = slugify(options.moduleName);
5564
- const moduleDir = path16.posix.join(options.modulesDir, slug);
6147
+ const moduleDir = path18.posix.join(options.modulesDir, slug);
5565
6148
  const title = titleizeModuleName(options.moduleName);
5566
6149
  const context = createTemplateContext({
5567
6150
  slug,
5568
6151
  title
5569
6152
  });
5570
6153
  return moduleTemplates.map((template) => ({
5571
- path: path16.posix.join(moduleDir, template.fileName),
6154
+ path: path18.posix.join(moduleDir, template.fileName),
5572
6155
  content: renderTemplate(template.content, context)
5573
6156
  }));
5574
6157
  }
@@ -5589,7 +6172,7 @@ var ModuleCreateError = class extends Error {
5589
6172
  };
5590
6173
  async function createModule(options) {
5591
6174
  const slug = createModuleSlug(options.name);
5592
- const config = await loadRequiredConfig4(options.rootDir);
6175
+ const config = await loadRequiredConfig5(options.rootDir);
5593
6176
  const files = generateModuleFiles({
5594
6177
  modulesDir: config.modulesDir,
5595
6178
  moduleName: options.name
@@ -5643,7 +6226,7 @@ function createModuleSlug(name) {
5643
6226
  throw error;
5644
6227
  }
5645
6228
  }
5646
- async function loadRequiredConfig4(rootDir) {
6229
+ async function loadRequiredConfig5(rootDir) {
5647
6230
  try {
5648
6231
  return await loadConfig(rootDir);
5649
6232
  } catch (error) {
@@ -5817,6 +6400,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
5817
6400
  });
5818
6401
  stdout.write(formatAdrAcceptResult(result));
5819
6402
  });
6403
+ adrCommand.command("supersede").description(
6404
+ "Record a changed decision: mark an accepted ADR superseded by a new accepted ADR."
6405
+ ).argument("<old>", "Accepted ADR name or slug being superseded, e.g. database-postgres.").argument("<new-title>", "Title of the new decision that replaces it.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(
6406
+ async (oldName, newTitle, options) => {
6407
+ const result = await supersedeAdr({
6408
+ rootDir: cwd,
6409
+ oldName,
6410
+ newTitle,
6411
+ dryRun: options.dryRun,
6412
+ force: options.force
6413
+ });
6414
+ stdout.write(formatAdrSupersedeResult(result));
6415
+ }
6416
+ );
5820
6417
  const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
5821
6418
  moduleCommand.command("create").description("Create module memory docs.").argument("<name>", "Module name.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(async (name, options) => {
5822
6419
  const result = await createModule({
@@ -5910,6 +6507,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
5910
6507
  `);
5911
6508
  for (const detail of error.details) {
5912
6509
  stderr.write(`- ${detail}
6510
+ `);
6511
+ }
6512
+ return 1;
6513
+ }
6514
+ if (error instanceof AdrSupersedeError) {
6515
+ stderr.write(`${error.message}
6516
+ `);
6517
+ for (const detail of error.details) {
6518
+ stderr.write(`- ${detail}
5913
6519
  `);
5914
6520
  }
5915
6521
  return 1;