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