recall-os 0.1.0 → 0.2.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 (99) hide show
  1. package/README.md +52 -20
  2. package/dist/cli.js +1291 -547
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +1291 -547
  5. package/dist/index.js.map +1 -1
  6. package/examples/generated-laravel-api/.recall/config.json +17 -0
  7. package/examples/generated-laravel-api/.recall/hooks/pre-commit +9 -0
  8. package/examples/generated-laravel-api/AGENTS.md +15 -0
  9. package/examples/generated-laravel-api/CLAUDE.md +9 -0
  10. package/examples/generated-laravel-api/README.md +11 -0
  11. package/examples/generated-laravel-api/docs/00-product/BRD.md +9 -0
  12. package/examples/generated-laravel-api/docs/00-product/PRD.md +13 -0
  13. package/examples/generated-laravel-api/docs/10-architecture/ARCHITECTURE.md +11 -0
  14. package/examples/generated-laravel-api/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
  15. package/examples/generated-laravel-api/docs/10-architecture/MEMORY_ENGINE.md +16 -0
  16. package/examples/generated-laravel-api/docs/20-security/SECURITY_MODEL.md +11 -0
  17. package/examples/generated-laravel-api/docs/20-security/THREAT_MODEL.md +7 -0
  18. package/examples/generated-laravel-api/docs/30-modules/README.md +17 -0
  19. package/examples/generated-laravel-api/docs/40-features/README.md +22 -0
  20. package/examples/generated-laravel-api/docs/50-quality/QUALITY_GATES.md +11 -0
  21. package/examples/generated-laravel-api/docs/50-quality/TESTING_STRATEGY.md +5 -0
  22. package/examples/generated-laravel-api/docs/60-engineering/AI_AGENT_RULES.md +6 -0
  23. package/examples/generated-laravel-api/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
  24. package/examples/generated-laravel-api/docs/adrs/README.md +9 -0
  25. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-api-design-rest.md +31 -0
  26. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-application-structure.md +30 -0
  27. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-auth-sanctum.md +30 -0
  28. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-database-eloquent.md +31 -0
  29. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-framework.md +29 -0
  30. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-queues-horizon.md +29 -0
  31. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-testing-pest.md +30 -0
  32. package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-validation-authorization.md +30 -0
  33. package/examples/generated-laravel-api/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
  34. package/examples/generated-laravel-api/docs/ai/MCP_STRATEGY.md +6 -0
  35. package/examples/generated-laravel-api/docs/ai/RECALL_COMMANDS.md +133 -0
  36. package/examples/generated-laravel-api/docs/ai/presets/laravel-api-guidance.md +62 -0
  37. package/examples/generated-laravel-react/.recall/config.json +17 -0
  38. package/examples/generated-laravel-react/.recall/hooks/pre-commit +9 -0
  39. package/examples/generated-laravel-react/AGENTS.md +15 -0
  40. package/examples/generated-laravel-react/CLAUDE.md +9 -0
  41. package/examples/generated-laravel-react/README.md +11 -0
  42. package/examples/generated-laravel-react/docs/00-product/BRD.md +9 -0
  43. package/examples/generated-laravel-react/docs/00-product/PRD.md +13 -0
  44. package/examples/generated-laravel-react/docs/10-architecture/ARCHITECTURE.md +11 -0
  45. package/examples/generated-laravel-react/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
  46. package/examples/generated-laravel-react/docs/10-architecture/MEMORY_ENGINE.md +16 -0
  47. package/examples/generated-laravel-react/docs/20-security/SECURITY_MODEL.md +11 -0
  48. package/examples/generated-laravel-react/docs/20-security/THREAT_MODEL.md +7 -0
  49. package/examples/generated-laravel-react/docs/30-modules/README.md +17 -0
  50. package/examples/generated-laravel-react/docs/40-features/README.md +22 -0
  51. package/examples/generated-laravel-react/docs/50-quality/QUALITY_GATES.md +11 -0
  52. package/examples/generated-laravel-react/docs/50-quality/TESTING_STRATEGY.md +5 -0
  53. package/examples/generated-laravel-react/docs/60-engineering/AI_AGENT_RULES.md +6 -0
  54. package/examples/generated-laravel-react/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
  55. package/examples/generated-laravel-react/docs/adrs/README.md +9 -0
  56. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-application-structure.md +30 -0
  57. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-auth-sanctum.md +31 -0
  58. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-database-eloquent.md +31 -0
  59. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-framework.md +29 -0
  60. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-frontend-inertia-react.md +31 -0
  61. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-queues-horizon.md +29 -0
  62. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-testing-pest.md +30 -0
  63. package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-validation-authorization.md +30 -0
  64. package/examples/generated-laravel-react/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
  65. package/examples/generated-laravel-react/docs/ai/MCP_STRATEGY.md +6 -0
  66. package/examples/generated-laravel-react/docs/ai/RECALL_COMMANDS.md +133 -0
  67. package/examples/generated-laravel-react/docs/ai/presets/laravel-react-guidance.md +64 -0
  68. package/examples/generated-laravel-vue/.recall/config.json +17 -0
  69. package/examples/generated-laravel-vue/.recall/hooks/pre-commit +9 -0
  70. package/examples/generated-laravel-vue/AGENTS.md +15 -0
  71. package/examples/generated-laravel-vue/CLAUDE.md +9 -0
  72. package/examples/generated-laravel-vue/README.md +11 -0
  73. package/examples/generated-laravel-vue/docs/00-product/BRD.md +9 -0
  74. package/examples/generated-laravel-vue/docs/00-product/PRD.md +13 -0
  75. package/examples/generated-laravel-vue/docs/10-architecture/ARCHITECTURE.md +11 -0
  76. package/examples/generated-laravel-vue/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
  77. package/examples/generated-laravel-vue/docs/10-architecture/MEMORY_ENGINE.md +16 -0
  78. package/examples/generated-laravel-vue/docs/20-security/SECURITY_MODEL.md +11 -0
  79. package/examples/generated-laravel-vue/docs/20-security/THREAT_MODEL.md +7 -0
  80. package/examples/generated-laravel-vue/docs/30-modules/README.md +17 -0
  81. package/examples/generated-laravel-vue/docs/40-features/README.md +22 -0
  82. package/examples/generated-laravel-vue/docs/50-quality/QUALITY_GATES.md +11 -0
  83. package/examples/generated-laravel-vue/docs/50-quality/TESTING_STRATEGY.md +5 -0
  84. package/examples/generated-laravel-vue/docs/60-engineering/AI_AGENT_RULES.md +6 -0
  85. package/examples/generated-laravel-vue/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
  86. package/examples/generated-laravel-vue/docs/adrs/README.md +9 -0
  87. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-application-structure.md +30 -0
  88. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-auth-sanctum.md +31 -0
  89. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-database-eloquent.md +31 -0
  90. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-framework.md +29 -0
  91. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-frontend-inertia-vue.md +31 -0
  92. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-queues-horizon.md +29 -0
  93. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-testing-pest.md +30 -0
  94. package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-validation-authorization.md +30 -0
  95. package/examples/generated-laravel-vue/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
  96. package/examples/generated-laravel-vue/docs/ai/MCP_STRATEGY.md +6 -0
  97. package/examples/generated-laravel-vue/docs/ai/RECALL_COMMANDS.md +133 -0
  98. package/examples/generated-laravel-vue/docs/ai/presets/laravel-vue-guidance.md +64 -0
  99. package/package.json +1 -1
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 path16 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
138
- return `${path16}${issue.message}`;
137
+ const path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
138
+ return `${path17}${issue.message}`;
139
139
  })
140
140
  );
141
141
  }
@@ -1314,6 +1314,11 @@ and is not accepted until a human reviews and accepts it.
1314
1314
 
1315
1315
  - Captures a framework already in use as reviewable repository memory.
1316
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.
1317
1322
  `;
1318
1323
  }
1319
1324
  function frameworkSlug(framework) {
@@ -1382,6 +1387,32 @@ async function inspectRepo(rootDir) {
1382
1387
  } else if (python.includes("django")) {
1383
1388
  frameworks.add("Django");
1384
1389
  }
1390
+ if (has("go.mod")) {
1391
+ const goModules = `${await readText(rootDir, "go.mod")}${await readText(rootDir, "go.sum")}`;
1392
+ if (goModules.includes("gin-gonic/gin")) {
1393
+ frameworks.add("Gin");
1394
+ } else if (goModules.includes("labstack/echo")) {
1395
+ frameworks.add("Echo");
1396
+ } else if (goModules.includes("gofiber/fiber")) {
1397
+ frameworks.add("Fiber");
1398
+ } else if (goModules.includes("go-chi/chi")) {
1399
+ frameworks.add("Chi");
1400
+ }
1401
+ }
1402
+ const jvmManifest = `${await readText(rootDir, "pom.xml")}${await readText(rootDir, "build.gradle")}${await readText(rootDir, "build.gradle.kts")}`.toLowerCase();
1403
+ if (jvmManifest.includes("spring-boot") || jvmManifest.includes("springframework")) {
1404
+ frameworks.add("Spring Boot");
1405
+ }
1406
+ if (has("Cargo.toml")) {
1407
+ const cargo = (await readText(rootDir, "Cargo.toml")).toLowerCase();
1408
+ if (cargo.includes("actix-web")) {
1409
+ frameworks.add("Actix Web");
1410
+ } else if (cargo.includes("axum")) {
1411
+ frameworks.add("Axum");
1412
+ } else if (cargo.includes("rocket")) {
1413
+ frameworks.add("Rocket");
1414
+ }
1415
+ }
1385
1416
  let packageManager = null;
1386
1417
  if (has("pnpm-lock.yaml")) {
1387
1418
  packageManager = "pnpm";
@@ -1499,13 +1530,96 @@ function formatList2(values) {
1499
1530
  return values.length > 0 ? values.join(", ") : "none detected";
1500
1531
  }
1501
1532
 
1533
+ // src/core/doctor/checks/code-reference-check.ts
1534
+ import { existsSync as existsSync4 } from "fs";
1535
+ import { readFile as readFile4, readdir as readdir4 } from "fs/promises";
1536
+ import path7 from "path";
1537
+ var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1538
+ var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
1539
+ var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
1540
+ var codePathPattern = /`((?:src|tests)\/[A-Za-z0-9._/-]+\.[A-Za-z0-9]+)`/gu;
1541
+ var placeholderMarkers = /[<>*]|\.\.\./u;
1542
+ async function checkCodeReferences(context) {
1543
+ if (context.config === void 0) {
1544
+ return [];
1545
+ }
1546
+ const findings = [];
1547
+ const featureEntries = await readDirIfExists(context.rootDir, context.config.featuresDir);
1548
+ for (const folder of featureEntries) {
1549
+ if (!folder.isDirectory() || !featureFolderPattern.test(folder.name)) {
1550
+ continue;
1551
+ }
1552
+ for (const doc of FEATURE_DOCS) {
1553
+ const relativePath = path7.posix.join(context.config.featuresDir, folder.name, doc);
1554
+ findings.push(...await checkDoc(context.rootDir, relativePath));
1555
+ }
1556
+ }
1557
+ const moduleEntries = await readDirIfExists(context.rootDir, context.config.modulesDir);
1558
+ for (const folder of moduleEntries) {
1559
+ if (!folder.isDirectory()) {
1560
+ continue;
1561
+ }
1562
+ for (const doc of MODULE_DOCS) {
1563
+ const relativePath = path7.posix.join(context.config.modulesDir, folder.name, doc);
1564
+ findings.push(...await checkDoc(context.rootDir, relativePath));
1565
+ }
1566
+ }
1567
+ return findings;
1568
+ }
1569
+ async function checkDoc(rootDir, relativePath) {
1570
+ const content = await readFileIfExists(rootDir, relativePath);
1571
+ if (content === void 0) {
1572
+ return [];
1573
+ }
1574
+ const findings = [];
1575
+ const seen = /* @__PURE__ */ new Set();
1576
+ for (const match of content.matchAll(codePathPattern)) {
1577
+ const reference = match[1];
1578
+ if (placeholderMarkers.test(reference) || seen.has(reference)) {
1579
+ continue;
1580
+ }
1581
+ seen.add(reference);
1582
+ if (!existsSync4(path7.join(rootDir, reference))) {
1583
+ findings.push({
1584
+ severity: "warning",
1585
+ check: "drift-code-reference",
1586
+ message: `Repository memory references ${reference}, which does not exist.`,
1587
+ path: relativePath
1588
+ });
1589
+ }
1590
+ }
1591
+ return findings;
1592
+ }
1593
+ async function readDirIfExists(rootDir, relativePath) {
1594
+ try {
1595
+ return await readdir4(path7.join(rootDir, relativePath), { withFileTypes: true });
1596
+ } catch (error) {
1597
+ const nodeError = error;
1598
+ if (nodeError.code === "ENOENT") {
1599
+ return [];
1600
+ }
1601
+ throw error;
1602
+ }
1603
+ }
1604
+ async function readFileIfExists(rootDir, relativePath) {
1605
+ try {
1606
+ return await readFile4(path7.join(rootDir, relativePath), "utf8");
1607
+ } catch (error) {
1608
+ const nodeError = error;
1609
+ if (nodeError.code === "ENOENT") {
1610
+ return void 0;
1611
+ }
1612
+ throw error;
1613
+ }
1614
+ }
1615
+
1502
1616
  // src/core/doctor/checks/config-check.ts
1503
- import { readFile as readFile4 } from "fs/promises";
1617
+ import { readFile as readFile5 } from "fs/promises";
1504
1618
  async function checkConfig(rootDir) {
1505
1619
  const configPath = resolveSafePath(rootDir, CONFIG_PATH);
1506
1620
  let rawConfig;
1507
1621
  try {
1508
- rawConfig = await readFile4(configPath.absolutePath, "utf8");
1622
+ rawConfig = await readFile5(configPath.absolutePath, "utf8");
1509
1623
  } catch (error) {
1510
1624
  const nodeError = error;
1511
1625
  if (nodeError.code === "ENOENT") {
@@ -1568,21 +1682,21 @@ async function checkConfig(rootDir) {
1568
1682
  }
1569
1683
 
1570
1684
  // src/core/doctor/checks/content-check.ts
1571
- import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
1572
- import path7 from "path";
1573
- var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1685
+ import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
1686
+ import path8 from "path";
1687
+ var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1574
1688
  async function checkContent(context) {
1575
1689
  if (context.config === void 0) {
1576
1690
  return [];
1577
1691
  }
1578
1692
  const findings = [];
1579
- const entries = await readDirIfExists(context.rootDir, context.config.featuresDir);
1693
+ const entries = await readDirIfExists2(context.rootDir, context.config.featuresDir);
1580
1694
  const featureFolders = entries.filter(
1581
- (entry) => entry.isDirectory() && featureFolderPattern.test(entry.name)
1695
+ (entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
1582
1696
  );
1583
1697
  for (const folder of featureFolders) {
1584
- const prdPath = path7.posix.join(context.config.featuresDir, folder.name, "PRD.md");
1585
- const prd = await readFileIfExists(context.rootDir, prdPath);
1698
+ const prdPath = path8.posix.join(context.config.featuresDir, folder.name, "PRD.md");
1699
+ const prd = await readFileIfExists2(context.rootDir, prdPath);
1586
1700
  if (prd === void 0) {
1587
1701
  continue;
1588
1702
  }
@@ -1603,6 +1717,31 @@ async function checkContent(context) {
1603
1717
  });
1604
1718
  }
1605
1719
  }
1720
+ const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
1721
+ const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
1722
+ for (const folder of moduleFolders) {
1723
+ const modulePath = path8.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
1724
+ const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
1725
+ if (moduleDoc === void 0) {
1726
+ continue;
1727
+ }
1728
+ if (sectionIsUnfilled(moduleDoc, "Purpose")) {
1729
+ findings.push({
1730
+ severity: "warning",
1731
+ check: "content-module",
1732
+ message: "Module memory purpose is still an unfilled template.",
1733
+ path: modulePath
1734
+ });
1735
+ }
1736
+ if (sectionIsUnfilled(moduleDoc, "Owns")) {
1737
+ findings.push({
1738
+ severity: "warning",
1739
+ check: "content-module",
1740
+ message: "Module memory owns section is still an unfilled template.",
1741
+ path: modulePath
1742
+ });
1743
+ }
1744
+ }
1606
1745
  return findings;
1607
1746
  }
1608
1747
  function sectionIsUnfilled(content, heading) {
@@ -1617,7 +1756,7 @@ function isUnfilled(value) {
1617
1756
  if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
1618
1757
  return true;
1619
1758
  }
1620
- return normalized.includes("describe why this feature exists");
1759
+ return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns");
1621
1760
  }
1622
1761
  function getSection(content, heading) {
1623
1762
  const lines = content.split(/\r?\n/u);
@@ -1635,9 +1774,9 @@ function getSection(content, heading) {
1635
1774
  }
1636
1775
  return body.join("\n").trim();
1637
1776
  }
1638
- async function readDirIfExists(rootDir, relativePath) {
1777
+ async function readDirIfExists2(rootDir, relativePath) {
1639
1778
  try {
1640
- return await readdir4(path7.join(rootDir, relativePath), { withFileTypes: true });
1779
+ return await readdir5(path8.join(rootDir, relativePath), { withFileTypes: true });
1641
1780
  } catch (error) {
1642
1781
  const nodeError = error;
1643
1782
  if (nodeError.code === "ENOENT") {
@@ -1646,9 +1785,9 @@ async function readDirIfExists(rootDir, relativePath) {
1646
1785
  throw error;
1647
1786
  }
1648
1787
  }
1649
- async function readFileIfExists(rootDir, relativePath) {
1788
+ async function readFileIfExists2(rootDir, relativePath) {
1650
1789
  try {
1651
- return await readFile5(path7.join(rootDir, relativePath), "utf8");
1790
+ return await readFile6(path8.join(rootDir, relativePath), "utf8");
1652
1791
  } catch (error) {
1653
1792
  const nodeError = error;
1654
1793
  if (nodeError.code === "ENOENT") {
@@ -1659,8 +1798,8 @@ async function readFileIfExists(rootDir, relativePath) {
1659
1798
  }
1660
1799
 
1661
1800
  // src/core/doctor/checks/drift-check.ts
1662
- import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
1663
- import path8 from "path";
1801
+ import { readFile as readFile7, readdir as readdir6 } from "fs/promises";
1802
+ import path9 from "path";
1664
1803
  var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
1665
1804
  var adrReferencePattern = /ADR-\d{4,}/giu;
1666
1805
  async function checkDrift(context) {
@@ -1677,12 +1816,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
1677
1816
  const known = /* @__PURE__ */ new Map();
1678
1817
  const files = await readMarkdownFiles(rootDir, adrDir);
1679
1818
  for (const file of files) {
1680
- const match = adrFilePattern.exec(path8.basename(file));
1819
+ const match = adrFilePattern.exec(path9.basename(file));
1681
1820
  if (match === null) {
1682
1821
  continue;
1683
1822
  }
1684
1823
  const id = `ADR-${match[1]}`;
1685
- const content = await readFile6(path8.join(rootDir, file), "utf8");
1824
+ const content = await readFile7(path9.join(rootDir, file), "utf8");
1686
1825
  const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
1687
1826
  const existing = known.get(id);
1688
1827
  if (existing === void 0 || !existing.accepted && accepted) {
@@ -1695,7 +1834,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
1695
1834
  const findings = [];
1696
1835
  const files = await readMarkdownFiles(rootDir, referenceDir);
1697
1836
  for (const file of files) {
1698
- const content = await readFile6(path8.join(rootDir, file), "utf8");
1837
+ const content = await readFile7(path9.join(rootDir, file), "utf8");
1699
1838
  const referenced = /* @__PURE__ */ new Set();
1700
1839
  for (const match of stripCode(content).matchAll(adrReferencePattern)) {
1701
1840
  referenced.add(match[0].toUpperCase());
@@ -1727,10 +1866,10 @@ function stripCode(content) {
1727
1866
  return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
1728
1867
  }
1729
1868
  async function readMarkdownFiles(rootDir, relativeDir) {
1730
- const entries = await readDirIfExists2(rootDir, relativeDir);
1869
+ const entries = await readDirIfExists3(rootDir, relativeDir);
1731
1870
  const files = [];
1732
1871
  for (const entry of entries) {
1733
- const childRelative = path8.posix.join(relativeDir, entry.name);
1872
+ const childRelative = path9.posix.join(relativeDir, entry.name);
1734
1873
  if (entry.isDirectory()) {
1735
1874
  files.push(...await readMarkdownFiles(rootDir, childRelative));
1736
1875
  continue;
@@ -1761,9 +1900,9 @@ function getSection2(content, heading) {
1761
1900
  }
1762
1901
  return body.join("\n").trim();
1763
1902
  }
1764
- async function readDirIfExists2(rootDir, relativePath) {
1903
+ async function readDirIfExists3(rootDir, relativePath) {
1765
1904
  try {
1766
- return await readdir5(path8.join(rootDir, relativePath), { withFileTypes: true });
1905
+ return await readdir6(path9.join(rootDir, relativePath), { withFileTypes: true });
1767
1906
  } catch (error) {
1768
1907
  const nodeError = error;
1769
1908
  if (nodeError.code === "ENOENT") {
@@ -1774,9 +1913,39 @@ async function readDirIfExists2(rootDir, relativePath) {
1774
1913
  }
1775
1914
 
1776
1915
  // src/core/doctor/checks/memory-integrity-check.ts
1777
- import { lstat as lstat2, readFile as readFile7, readdir as readdir6 } from "fs/promises";
1778
- import path9 from "path";
1779
- var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1916
+ import { lstat as lstat2, readFile as readFile8, readdir as readdir7 } from "fs/promises";
1917
+ import path10 from "path";
1918
+
1919
+ // src/core/adr/adr-sections.ts
1920
+ var REQUIRED_ADR_SECTIONS = [
1921
+ "## Status",
1922
+ "## Context",
1923
+ "## Decision",
1924
+ "## Alternatives Considered",
1925
+ "## Consequences",
1926
+ "## Related Documents"
1927
+ ];
1928
+ var SECTION_PLACEHOLDERS = {
1929
+ "## Related Documents": "- None yet. Link related ADRs, features, or modules as they are accepted."
1930
+ };
1931
+ function ensureRequiredAdrSections(body) {
1932
+ let result = body.replace(/\s+$/u, "");
1933
+ for (const section of REQUIRED_ADR_SECTIONS) {
1934
+ if (!result.includes(section)) {
1935
+ const placeholder = SECTION_PLACEHOLDERS[section] ?? "To be documented.";
1936
+ result += `
1937
+
1938
+ ${section}
1939
+
1940
+ ${placeholder}`;
1941
+ }
1942
+ }
1943
+ return `${result}
1944
+ `;
1945
+ }
1946
+
1947
+ // src/core/doctor/checks/memory-integrity-check.ts
1948
+ var featureFolderPattern3 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1780
1949
  var adrFilePattern2 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
1781
1950
  var requiredFeatureDocs = [
1782
1951
  "PRD.md",
@@ -1790,14 +1959,7 @@ var requiredFeatureDocs = [
1790
1959
  "COMPLETION_REPORT.md"
1791
1960
  ];
1792
1961
  var requiredModuleDocs = ["MODULE.md", "TASKS.md", "TEST_PLAN.md", "DECISIONS.md"];
1793
- var requiredAdrSections = [
1794
- "## Status",
1795
- "## Context",
1796
- "## Decision",
1797
- "## Alternatives Considered",
1798
- "## Consequences",
1799
- "## Related Documents"
1800
- ];
1962
+ var requiredAdrSections = REQUIRED_ADR_SECTIONS;
1801
1963
  async function checkMemoryIntegrity(context) {
1802
1964
  if (context.config === void 0) {
1803
1965
  return [];
@@ -1810,13 +1972,13 @@ async function checkMemoryIntegrity(context) {
1810
1972
  }
1811
1973
  async function checkFeatureFolders(rootDir, featuresDir) {
1812
1974
  const findings = [];
1813
- const entries = await readDirIfExists3(rootDir, featuresDir);
1975
+ const entries = await readDirIfExists4(rootDir, featuresDir);
1814
1976
  const featureFolders = entries.filter(
1815
- (entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
1977
+ (entry) => entry.isDirectory() && featureFolderPattern3.test(entry.name)
1816
1978
  );
1817
1979
  for (const featureFolder of featureFolders) {
1818
1980
  for (const requiredDoc of requiredFeatureDocs) {
1819
- const filePath = path9.posix.join(featuresDir, featureFolder.name, requiredDoc);
1981
+ const filePath = path10.posix.join(featuresDir, featureFolder.name, requiredDoc);
1820
1982
  if (!await isFile(rootDir, filePath)) {
1821
1983
  findings.push({
1822
1984
  severity: "error",
@@ -1836,11 +1998,11 @@ async function checkFeatureFolders(rootDir, featuresDir) {
1836
1998
  }
1837
1999
  async function checkModuleFolders(rootDir, modulesDir) {
1838
2000
  const findings = [];
1839
- const entries = await readDirIfExists3(rootDir, modulesDir);
2001
+ const entries = await readDirIfExists4(rootDir, modulesDir);
1840
2002
  const moduleFolders = entries.filter((entry) => entry.isDirectory());
1841
2003
  for (const moduleFolder of moduleFolders) {
1842
2004
  for (const requiredDoc of requiredModuleDocs) {
1843
- const filePath = path9.posix.join(modulesDir, moduleFolder.name, requiredDoc);
2005
+ const filePath = path10.posix.join(modulesDir, moduleFolder.name, requiredDoc);
1844
2006
  if (!await isFile(rootDir, filePath)) {
1845
2007
  findings.push({
1846
2008
  severity: "error",
@@ -1860,11 +2022,11 @@ async function checkModuleFolders(rootDir, modulesDir) {
1860
2022
  }
1861
2023
  async function checkAdrFiles(rootDir, adrDir) {
1862
2024
  const findings = [];
1863
- const entries = await readDirIfExists3(rootDir, adrDir);
2025
+ const entries = await readDirIfExists4(rootDir, adrDir);
1864
2026
  const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
1865
2027
  for (const adrFile of adrFiles) {
1866
- const filePath = path9.posix.join(adrDir, adrFile.name);
1867
- const content = await readFile7(path9.join(rootDir, filePath), "utf8");
2028
+ const filePath = path10.posix.join(adrDir, adrFile.name);
2029
+ const content = await readFile8(path10.join(rootDir, filePath), "utf8");
1868
2030
  for (const requiredSection of requiredAdrSections) {
1869
2031
  if (!content.includes(requiredSection)) {
1870
2032
  findings.push({
@@ -1883,9 +2045,9 @@ async function checkAdrFiles(rootDir, adrDir) {
1883
2045
  });
1884
2046
  return findings;
1885
2047
  }
1886
- async function readDirIfExists3(rootDir, relativePath) {
2048
+ async function readDirIfExists4(rootDir, relativePath) {
1887
2049
  try {
1888
- return await readdir6(path9.join(rootDir, relativePath), { withFileTypes: true });
2050
+ return await readdir7(path10.join(rootDir, relativePath), { withFileTypes: true });
1889
2051
  } catch (error) {
1890
2052
  const nodeError = error;
1891
2053
  if (nodeError.code === "ENOENT") {
@@ -1896,7 +2058,7 @@ async function readDirIfExists3(rootDir, relativePath) {
1896
2058
  }
1897
2059
  async function isFile(rootDir, relativePath) {
1898
2060
  try {
1899
- return (await lstat2(path9.join(rootDir, relativePath))).isFile();
2061
+ return (await lstat2(path10.join(rootDir, relativePath))).isFile();
1900
2062
  } catch (error) {
1901
2063
  const nodeError = error;
1902
2064
  if (nodeError.code === "ENOENT") {
@@ -1908,7 +2070,7 @@ async function isFile(rootDir, relativePath) {
1908
2070
 
1909
2071
  // src/core/doctor/checks/required-files-check.ts
1910
2072
  import { lstat as lstat3 } from "fs/promises";
1911
- import path10 from "path";
2073
+ import path11 from "path";
1912
2074
  var rootFiles = ["AGENTS.md", "CLAUDE.md"];
1913
2075
  var requiredDocs = [
1914
2076
  "00-product/PRD.md",
@@ -1935,12 +2097,12 @@ async function checkRequiredFiles(context) {
1935
2097
  }
1936
2098
  }
1937
2099
  for (const relativeDocPath of requiredDocs) {
1938
- const filePath = path10.posix.join(docsDir, relativeDocPath);
2100
+ const filePath = path11.posix.join(docsDir, relativeDocPath);
1939
2101
  if (!await isFile2(context.rootDir, filePath)) {
1940
2102
  findings.push(missingFile(filePath, "required-docs"));
1941
2103
  }
1942
2104
  }
1943
- const adrIndexPath = path10.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
2105
+ const adrIndexPath = path11.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
1944
2106
  if (!await isFile2(context.rootDir, adrIndexPath)) {
1945
2107
  findings.push(missingFile(adrIndexPath, "required-docs"));
1946
2108
  }
@@ -1966,7 +2128,7 @@ async function checkRequiredFiles(context) {
1966
2128
  }
1967
2129
  async function isFile2(rootDir, relativePath) {
1968
2130
  try {
1969
- return (await lstat3(path10.join(rootDir, relativePath))).isFile();
2131
+ return (await lstat3(path11.join(rootDir, relativePath))).isFile();
1970
2132
  } catch (error) {
1971
2133
  const nodeError = error;
1972
2134
  if (nodeError.code === "ENOENT") {
@@ -1977,7 +2139,7 @@ async function isFile2(rootDir, relativePath) {
1977
2139
  }
1978
2140
  async function isDirectory(rootDir, relativePath) {
1979
2141
  try {
1980
- return (await lstat3(path10.join(rootDir, relativePath))).isDirectory();
2142
+ return (await lstat3(path11.join(rootDir, relativePath))).isDirectory();
1981
2143
  } catch (error) {
1982
2144
  const nodeError = error;
1983
2145
  if (nodeError.code === "ENOENT") {
@@ -1996,9 +2158,9 @@ function missingFile(pathValue, check) {
1996
2158
  }
1997
2159
 
1998
2160
  // src/core/doctor/checks/standards-check.ts
1999
- import { lstat as lstat4, readFile as readFile8, readdir as readdir7 } from "fs/promises";
2000
- import path11 from "path";
2001
- var featureFolderPattern3 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
2161
+ import { lstat as lstat4, readFile as readFile9, readdir as readdir8 } from "fs/promises";
2162
+ import path12 from "path";
2163
+ var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
2002
2164
  var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
2003
2165
  var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
2004
2166
  async function checkStandards(context) {
@@ -2012,18 +2174,18 @@ async function checkStandards(context) {
2012
2174
  }
2013
2175
  async function checkFeatureStandards(rootDir, featuresDir) {
2014
2176
  const findings = [];
2015
- const entries = await readDirIfExists4(rootDir, featuresDir);
2177
+ const entries = await readDirIfExists5(rootDir, featuresDir);
2016
2178
  const featureFolders = entries.filter(
2017
- (entry) => entry.isDirectory() && featureFolderPattern3.test(entry.name)
2179
+ (entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
2018
2180
  );
2019
2181
  for (const featureFolder of featureFolders) {
2020
- const featureDir = path11.posix.join(featuresDir, featureFolder.name);
2021
- const completionReportPath = path11.posix.join(featureDir, "COMPLETION_REPORT.md");
2022
- const reviewPath = path11.posix.join(featureDir, "REVIEW.md");
2023
- const architectureImpactPath = path11.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
2024
- const completionReport = await readFileIfExists2(rootDir, completionReportPath);
2025
- const review = await readFileIfExists2(rootDir, reviewPath);
2026
- const architectureImpact = await readFileIfExists2(rootDir, architectureImpactPath);
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");
2186
+ const completionReport = await readFileIfExists3(rootDir, completionReportPath);
2187
+ const review = await readFileIfExists3(rootDir, reviewPath);
2188
+ const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
2027
2189
  if (completionReport !== void 0) {
2028
2190
  const featureIsComplete = sectionContains2(completionReport, "Status", /\bcomplete\b/iu);
2029
2191
  if (featureIsComplete) {
@@ -2067,11 +2229,11 @@ async function checkFeatureStandards(rootDir, featuresDir) {
2067
2229
  }
2068
2230
  async function checkAdrStandards(rootDir, adrDir) {
2069
2231
  const findings = [];
2070
- const entries = await readDirIfExists4(rootDir, adrDir);
2232
+ const entries = await readDirIfExists5(rootDir, adrDir);
2071
2233
  const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
2072
2234
  for (const adrFile of adrFiles) {
2073
- const adrPath = path11.posix.join(adrDir, adrFile.name);
2074
- const content = await readFile8(path11.join(rootDir, adrPath), "utf8");
2235
+ const adrPath = path12.posix.join(adrDir, adrFile.name);
2236
+ const content = await readFile9(path12.join(rootDir, adrPath), "utf8");
2075
2237
  const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
2076
2238
  if (!hasMeaningfulSection(content, "Consequences")) {
2077
2239
  findings.push({
@@ -2153,9 +2315,9 @@ function isPlaceholder(value) {
2153
2315
  }
2154
2316
  return normalized.includes("implementation is in progress") || normalized.includes("will be completed after implementation");
2155
2317
  }
2156
- async function readDirIfExists4(rootDir, relativePath) {
2318
+ async function readDirIfExists5(rootDir, relativePath) {
2157
2319
  try {
2158
- return await readdir7(path11.join(rootDir, relativePath), { withFileTypes: true });
2320
+ return await readdir8(path12.join(rootDir, relativePath), { withFileTypes: true });
2159
2321
  } catch (error) {
2160
2322
  const nodeError = error;
2161
2323
  if (nodeError.code === "ENOENT") {
@@ -2164,12 +2326,12 @@ async function readDirIfExists4(rootDir, relativePath) {
2164
2326
  throw error;
2165
2327
  }
2166
2328
  }
2167
- async function readFileIfExists2(rootDir, relativePath) {
2329
+ async function readFileIfExists3(rootDir, relativePath) {
2168
2330
  try {
2169
2331
  if (!await isFile3(rootDir, relativePath)) {
2170
2332
  return void 0;
2171
2333
  }
2172
- return await readFile8(path11.join(rootDir, relativePath), "utf8");
2334
+ return await readFile9(path12.join(rootDir, relativePath), "utf8");
2173
2335
  } catch (error) {
2174
2336
  const nodeError = error;
2175
2337
  if (nodeError.code === "ENOENT") {
@@ -2180,7 +2342,7 @@ async function readFileIfExists2(rootDir, relativePath) {
2180
2342
  }
2181
2343
  async function isFile3(rootDir, relativePath) {
2182
2344
  try {
2183
- return (await lstat4(path11.join(rootDir, relativePath))).isFile();
2345
+ return (await lstat4(path12.join(rootDir, relativePath))).isFile();
2184
2346
  } catch (error) {
2185
2347
  const nodeError = error;
2186
2348
  if (nodeError.code === "ENOENT") {
@@ -2205,6 +2367,7 @@ async function runDoctor(rootDir) {
2205
2367
  findings.push(...await checkStandards(context));
2206
2368
  findings.push(...await checkDrift(context));
2207
2369
  findings.push(...await checkContent(context));
2370
+ findings.push(...await checkCodeReferences(context));
2208
2371
  }
2209
2372
  return createDoctorReport(findings);
2210
2373
  }
@@ -2281,11 +2444,11 @@ function formatDoctorResult(result) {
2281
2444
  }
2282
2445
 
2283
2446
  // src/commands/init.ts
2284
- import { existsSync as existsSync5 } from "fs";
2285
- import path14 from "path";
2447
+ import { existsSync as existsSync6 } from "fs";
2448
+ import path15 from "path";
2286
2449
 
2287
2450
  // src/core/generator/generate-init.ts
2288
- import path12 from "path";
2451
+ import path13 from "path";
2289
2452
  var neutralTemplates = [
2290
2453
  {
2291
2454
  path: "AGENTS.md",
@@ -2310,11 +2473,41 @@ Repository rules override model preferences. If instructions conflict, stop and
2310
2473
  path: "CLAUDE.md",
2311
2474
  content: `# {{repositoryName}} Claude Instructions
2312
2475
 
2313
- Use this file as a short routing guide.
2476
+ This file is loaded automatically every Claude session. The durable project memory lives in \`docs/\`;
2477
+ do not rely on chat history as source of truth, and repository rules override model preference.
2478
+
2479
+ @AGENTS.md
2480
+
2481
+ Read the docs that \`AGENTS.md\` routes to before changing code or repository memory. A SessionStart
2482
+ hook (\`.claude/hooks/session-start.sh\`) also injects a memory map at the start of each session.
2483
+ `
2484
+ },
2485
+ {
2486
+ // Cursor auto-applies rules under .cursor/rules. alwaysApply makes this the portable equivalent
2487
+ // of the Claude Code SessionStart hook: Cursor injects it into every request so the agent loads
2488
+ // repository memory even though it cannot run the Claude-specific hook.
2489
+ path: ".cursor/rules/recall-memory.mdc",
2490
+ content: `---
2491
+ description: {{repositoryName}} repository memory and rules (Recall OS). Read before non-trivial work.
2492
+ globs:
2493
+ alwaysApply: true
2494
+ ---
2495
+
2496
+ # {{repositoryName}} repository memory
2497
+
2498
+ This repository uses Recall OS. Durable memory lives in \`docs/\` and is the source of truth over chat
2499
+ history. Do not treat chat history as truth, and repository rules override model preference.
2314
2500
 
2315
- The durable project memory lives in \`docs/\`. Do not rely on chat history as source of truth.
2501
+ Before non-trivial work:
2316
2502
 
2317
- Read \`AGENTS.md\` and the relevant docs before changing code or repository memory.
2503
+ - Read \`AGENTS.md\` and the docs it routes to.
2504
+ - Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
2505
+ - If an instruction conflicts with accepted repository memory, stop and report the conflict.
2506
+
2507
+ Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
2508
+ standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
2509
+
2510
+ Before claiming work is complete, run \`recall doctor\` and fix reported errors.
2318
2511
  `
2319
2512
  },
2320
2513
  {
@@ -2670,14 +2863,38 @@ Agents should not implement meaningful feature work without a feature plan or cl
2670
2863
  path: "docs/adrs/README.md",
2671
2864
  content: `# Architecture Decision Records
2672
2865
 
2673
- Accepted architecture choices belong here.
2866
+ Accepted ADRs live in this directory as \`ADR-####-<slug>.md\` with \`## Status\` set to \`Accepted\`.
2867
+ Proposed ADRs live under \`docs/adrs/proposed/\`.
2674
2868
 
2675
- Presets and AI agents may propose decisions, but humans accept them.
2869
+ There is no \`accepted/\` subdirectory: accepted ADRs sit at the top level of \`docs/adrs/\`.
2870
+
2871
+ Presets and AI agents may propose decisions; humans accept them with \`recall adr accept <name>\`,
2872
+ which promotes a proposal into an accepted ADR here.
2873
+ `
2874
+ },
2875
+ {
2876
+ path: ".github/workflows/recall.yml",
2877
+ content: `name: Recall OS
2878
+
2879
+ on:
2880
+ push:
2881
+ pull_request:
2882
+
2883
+ jobs:
2884
+ doctor:
2885
+ runs-on: ubuntu-latest
2886
+ steps:
2887
+ - uses: actions/checkout@v4
2888
+ - uses: actions/setup-node@v4
2889
+ with:
2890
+ node-version: 20
2891
+ - name: Validate repository memory
2892
+ run: npx --yes recall-os@latest doctor
2676
2893
  `
2677
2894
  }
2678
2895
  ];
2679
2896
  function generateInitFiles(options) {
2680
- const repositoryName = path12.basename(path12.resolve(options.rootDir)) || "repository";
2897
+ const repositoryName = path13.basename(path13.resolve(options.rootDir)) || "repository";
2681
2898
  const context = createTemplateContext({ repositoryName });
2682
2899
  const files = neutralTemplates.map((template) => ({
2683
2900
  path: template.path,
@@ -2696,24 +2913,25 @@ function generatePresetFiles(preset) {
2696
2913
  })),
2697
2914
  ...preset.proposedDecisions.map((decision) => ({
2698
2915
  path: decision.destination,
2699
- content: decision.body
2916
+ // Normalize every preset's proposed ADR so it stays Doctor-healthy once accepted.
2917
+ content: ensureRequiredAdrSections(decision.body)
2700
2918
  }))
2701
2919
  ];
2702
2920
  }
2703
2921
 
2704
2922
  // src/core/hooks/detect-gates.ts
2705
- import { existsSync as existsSync4 } from "fs";
2706
- import { readFile as readFile9 } from "fs/promises";
2707
- import path13 from "path";
2923
+ import { existsSync as existsSync5 } from "fs";
2924
+ import { readFile as readFile10 } from "fs/promises";
2925
+ import path14 from "path";
2708
2926
  var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
2709
2927
  async function detectPreCommitGates(rootDir) {
2710
- const packageJsonPath = path13.join(rootDir, "package.json");
2711
- if (!existsSync4(packageJsonPath)) {
2928
+ const packageJsonPath = path14.join(rootDir, "package.json");
2929
+ if (!existsSync5(packageJsonPath)) {
2712
2930
  return [];
2713
2931
  }
2714
2932
  let scripts;
2715
2933
  try {
2716
- const raw = await readFile9(packageJsonPath, "utf8");
2934
+ const raw = await readFile10(packageJsonPath, "utf8");
2717
2935
  const parsed = JSON.parse(raw);
2718
2936
  scripts = parsed.scripts ?? {};
2719
2937
  } catch {
@@ -2728,10 +2946,10 @@ async function detectPreCommitGates(rootDir) {
2728
2946
  );
2729
2947
  }
2730
2948
  function detectPackageManager(rootDir) {
2731
- if (existsSync4(path13.join(rootDir, "pnpm-lock.yaml"))) {
2949
+ if (existsSync5(path14.join(rootDir, "pnpm-lock.yaml"))) {
2732
2950
  return "pnpm";
2733
2951
  }
2734
- if (existsSync4(path13.join(rootDir, "yarn.lock"))) {
2952
+ if (existsSync5(path14.join(rootDir, "yarn.lock"))) {
2735
2953
  return "yarn";
2736
2954
  }
2737
2955
  return "npm";
@@ -2740,6 +2958,39 @@ function detectPackageManager(rootDir) {
2740
2958
  // src/core/hooks/generate-hook.ts
2741
2959
  var PRE_COMMIT_HOOK_PATH = ".recall/hooks/pre-commit";
2742
2960
  var HOOKS_PATH_ACTIVATION_COMMAND = "git config core.hooksPath .recall/hooks";
2961
+ var SESSION_START_HOOK_PATH = ".claude/hooks/session-start.sh";
2962
+ var CLAUDE_SETTINGS_PATH = ".claude/settings.json";
2963
+ function renderSessionStartHook() {
2964
+ return `#!/bin/sh
2965
+ # Recall OS Claude Code SessionStart hook.
2966
+ # Generated by \`recall init\`. Injects a repository-memory map into every Claude Code session so a
2967
+ # fresh agent reliably loads durable memory. Wired in .claude/settings.json. Read-only.
2968
+
2969
+ adrs=$(ls docs/adrs/ADR-*.md 2>/dev/null | sed 's|.*/||;s|\\.md$||' | tr '\\n' ' ')
2970
+ modules=$(ls -d docs/30-modules/*/ 2>/dev/null | sed 's|docs/30-modules/||;s|/$||' | tr '\\n' ' ')
2971
+
2972
+ context="Recall OS repository memory is the source of truth over chat history. Before non-trivial work, read AGENTS.md and the docs it routes to; repository rules override model preference. Accepted ADRs (docs/adrs/): \${adrs:-none yet}. Modules (docs/30-modules/): \${modules:-none yet}. Run 'recall doctor' before claiming work complete."
2973
+
2974
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$context"
2975
+ `;
2976
+ }
2977
+ function renderClaudeSettings() {
2978
+ return `${JSON.stringify(
2979
+ {
2980
+ hooks: {
2981
+ SessionStart: [
2982
+ {
2983
+ matcher: "startup",
2984
+ hooks: [{ type: "command", command: `./${SESSION_START_HOOK_PATH}` }]
2985
+ }
2986
+ ]
2987
+ }
2988
+ },
2989
+ null,
2990
+ 2
2991
+ )}
2992
+ `;
2993
+ }
2743
2994
  function renderPreCommitHook(gates) {
2744
2995
  const lines = [
2745
2996
  "#!/bin/sh",
@@ -3251,73 +3502,129 @@ Consider MVVM with unidirectional state from ViewModels, awaiting human acceptan
3251
3502
  ]
3252
3503
  };
3253
3504
 
3254
- // src/presets/nextjs/preset.ts
3255
- var guidance3 = `# Next.js Preset Guidance
3505
+ // src/presets/laravel/shared.ts
3506
+ var VARIANTS = {
3507
+ react: {
3508
+ id: "laravel-react",
3509
+ label: "React via Inertia",
3510
+ description: "Opinionated Laravel + Inertia + React opinion pack (proposed decisions only). Matches the official Laravel React starter kit.",
3511
+ frontendLine: "Frontend: Inertia 2 + React 19 + TypeScript + Tailwind, built with Vite (the official React starter kit, with shadcn/ui for components).",
3512
+ deliveryLine: "The app is a server-driven SPA: Laravel controllers return Inertia responses with typed props; there is no separate REST client for first-party screens."
3513
+ },
3514
+ vue: {
3515
+ id: "laravel-vue",
3516
+ label: "Vue via Inertia",
3517
+ description: "Opinionated Laravel + Inertia + Vue opinion pack (proposed decisions only). Matches the official Laravel Vue starter kit.",
3518
+ frontendLine: "Frontend: Inertia 2 + Vue 3 (script setup) + TypeScript + Tailwind, built with Vite (the official Vue starter kit).",
3519
+ deliveryLine: "The app is a server-driven SPA: Laravel controllers return Inertia responses with typed props; there is no separate REST client for first-party screens."
3520
+ },
3521
+ api: {
3522
+ id: "laravel-api",
3523
+ label: "API / SPA backend",
3524
+ description: "Opinionated Laravel API-only opinion pack (proposed decisions only) for a decoupled SPA or mobile client.",
3525
+ frontendLine: "No server-rendered frontend: Laravel is an HTTP JSON API consumed by a separate SPA or mobile app.",
3526
+ deliveryLine: "Controllers return JSON via API Resources; first-party SPAs authenticate with Sanctum cookies, mobile clients with Sanctum tokens."
3527
+ }
3528
+ };
3529
+ function adr(presetId, topic, title, body) {
3530
+ return {
3531
+ id: `${presetId}-${topic}`,
3532
+ title,
3533
+ status: "proposed",
3534
+ destination: `docs/adrs/proposed/ADR-PROPOSED-${presetId}-${topic}.md`,
3535
+ body
3536
+ };
3537
+ }
3538
+ function related(presetId, ...extra) {
3539
+ return [
3540
+ `## Related Documents`,
3541
+ ``,
3542
+ `- \`docs/ai/presets/${presetId}-guidance.md\` \u2014 the proposed Laravel stack guidance.`,
3543
+ `- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.`,
3544
+ ...extra.map((line) => `- ${line}`),
3545
+ ``
3546
+ ].join("\n");
3547
+ }
3548
+ function laravelGuidance(variant) {
3549
+ const profile = VARIANTS[variant];
3550
+ return `# Laravel Preset Guidance (${profile.label})
3256
3551
 
3257
3552
  This is proposed guidance, not accepted. Convert any architecture choice into a proposed ADR, then an
3258
- accepted ADR, before treating it as repository truth.
3553
+ accepted ADR, before treating it as repository truth. Repository rules override model preference.
3554
+
3555
+ ## The stack (proposed)
3556
+
3557
+ - Laravel 12 on PHP 8.3+, using the official conventions and directory layout.
3558
+ - ${profile.frontendLine}
3559
+ - Database: PostgreSQL (MySQL is the alternative) through Eloquent and migrations.
3560
+ - Auth: Laravel Sanctum for first-party SPA and mobile clients (Passport only if you need third-party OAuth2).
3561
+ - Background work: queues, with Redis and Laravel Horizon when throughput grows.
3562
+ - Tests: Pest, with database factories and feature tests over real routes.
3563
+
3564
+ ${profile.deliveryLine}
3259
3565
 
3260
3566
  ## Decision forks this stack forces
3261
3567
 
3262
- - Routing: App Router vs Pages Router.
3263
- - Rendering: Server Components and server actions vs client-heavy rendering.
3264
- - Data layer: Drizzle or Prisma with PostgreSQL vs a hosted backend.
3265
- - Styling: Tailwind CSS vs CSS Modules vs a component library.
3266
- - Testing: Vitest with Testing Library and Playwright vs other runners.
3568
+ - Frontend delivery: Inertia (server-driven SPA) vs a decoupled API + separate SPA vs Blade + Livewire.
3569
+ - Auth: Sanctum (first-party SPA and mobile) vs Passport (third-party OAuth2) vs a managed identity provider.
3570
+ - Database: PostgreSQL vs MySQL, and where read scaling and queues live (Redis vs database driver).
3571
+ - Authorization: Policies and Gates vs ad-hoc checks; validation via Form Requests vs inline.
3572
+ - Business logic: thin controllers with Action/Service classes vs fat controllers and models.
3573
+ - Testing: Pest vs PHPUnit.
3267
3574
 
3268
3575
  ## Recommended structure (proposed)
3269
3576
 
3270
- - The App Router with route groups and colocated server components.
3271
- - A typed data layer isolated from UI components.
3272
- - Shared UI primitives and a consistent styling system.
3577
+ - Keep controllers thin: they validate, authorize, delegate, and return a response \u2014 nothing more.
3578
+ - Put request validation **and** authorization in Form Requests (\`authorize()\` + \`rules()\`).
3579
+ - Put per-model and per-action permission logic in Policies and Gates, not in controllers.
3580
+ - Put business logic in single-purpose Action or Service classes, not in controllers or models.
3581
+ - Shape every outbound payload with API Resources (or typed Inertia props), never raw models.
3582
+ - Declare \`$fillable\` (or \`$guarded\`) explicitly on every Eloquent model to stop mass assignment.
3583
+
3584
+ ## Data and performance (proposed)
3585
+
3586
+ - Eager-load relationships (\`with(...)\`) to avoid N+1 queries; enable \`Model::preventLazyLoading()\` in local and CI.
3587
+ - Wrap multi-write operations in database transactions.
3588
+ - Paginate list endpoints; never return unbounded collections.
3589
+ - Move email, exports, third-party calls, and other slow work into queued jobs.
3590
+ - Cache expensive reads deliberately, with explicit invalidation.
3273
3591
 
3274
3592
  ## Testing (proposed)
3275
3593
 
3276
- - Unit and component tests with Vitest and Testing Library.
3277
- - End-to-end tests with Playwright for critical flows.
3278
- - Type-safe data access tested against a disposable database.
3594
+ - Write Pest feature tests that exercise real routes end to end, using \`RefreshDatabase\`.
3595
+ - Build state with model factories, not hand-rolled fixtures.
3596
+ - Test authorization explicitly: a forbidden action must assert a 403, not just a happy path.
3597
+ - Cover validation failures, not only the success case.
3279
3598
 
3280
3599
  ## Security considerations (proposed)
3281
3600
 
3282
- - Keep secrets in server-only environment variables, never in client bundles.
3283
- - Validate input on the server, including server actions and route handlers.
3284
- - Scope authentication and authorization on the server, not the client.
3285
- `;
3286
- function proposedAdr3(topic, title, body) {
3287
- return {
3288
- id: `nextjs-${topic}`,
3289
- title,
3290
- status: "proposed",
3291
- destination: `docs/adrs/proposed/ADR-PROPOSED-nextjs-${topic}.md`,
3292
- body
3293
- };
3601
+ - Validate every inbound request through Form Requests; persist only validated data.
3602
+ - Authorize every state-changing action through a Policy or Gate.
3603
+ - Keep secrets in \`.env\`; never commit \`.env\` or hardcode credentials.
3604
+ - Apply rate limiting to auth and write endpoints.
3605
+ - Keep mass assignment locked down and never trust client-supplied IDs without an ownership check.
3606
+ ${variant === "api" ? "- Scope Sanctum tokens to least privilege; SPA clients use the cookie guard with CSRF protection.\n" : "- Inertia uses Laravel's session and CSRF protection; keep auth and authorization on the server, never the client.\n"}`;
3294
3607
  }
3295
- var nextjsPreset = {
3296
- id: "nextjs",
3297
- name: "Next.js",
3298
- description: "Opinionated Next.js opinion pack with proposed decisions only.",
3299
- templates: [
3300
- {
3301
- destination: "docs/ai/presets/nextjs-guidance.md",
3302
- description: "Next.js guidance that remains proposed until accepted.",
3303
- content: guidance3
3304
- }
3305
- ],
3306
- guidance: [
3608
+ function laravelGuidanceItems() {
3609
+ return [
3307
3610
  {
3308
- title: "Keep framework choices proposed",
3309
- body: "Routing, rendering, data layer, styling, and testing choices must remain proposed until accepted in repository memory."
3611
+ title: "Keep stack choices proposed",
3612
+ body: "Framework, frontend delivery, database, auth, and testing choices stay proposed until accepted in repository memory."
3310
3613
  },
3311
3614
  {
3312
- title: "Keep secrets server-side",
3313
- body: "Never expose secrets to the client bundle, and record the data and auth approach as proposed decisions before acceptance."
3615
+ title: "Thin controllers, explicit authorization",
3616
+ body: "Validate and authorize in Form Requests and Policies, keep business logic in Action or Service classes, and shape output with Resources \u2014 propose these as decisions before treating them as truth."
3314
3617
  }
3315
- ],
3316
- proposedDecisions: [
3317
- proposedAdr3(
3618
+ ];
3619
+ }
3620
+ function laravelProposedDecisions(variant) {
3621
+ const presetId = VARIANTS[variant].id;
3622
+ const base = [
3623
+ adr(
3624
+ presetId,
3318
3625
  "framework",
3319
- "Use Next.js",
3320
- `# Proposed ADR: Use Next.js
3626
+ "Use Laravel",
3627
+ `# Proposed ADR: Use Laravel
3321
3628
 
3322
3629
  ## Status
3323
3630
 
@@ -3325,28 +3632,30 @@ Proposed
3325
3632
 
3326
3633
  ## Context
3327
3634
 
3328
- The team needs a React framework for a production web application.
3635
+ The team needs a productive, batteries-included PHP framework for a production web application.
3329
3636
 
3330
3637
  ## Decision
3331
3638
 
3332
- Consider Next.js as the application framework. This is not accepted until a human reviews and accepts
3333
- it.
3639
+ Consider Laravel 12 on PHP 8.3+ as the application framework, following its standard conventions and
3640
+ directory structure. This is not accepted until a human reviews and accepts it.
3334
3641
 
3335
3642
  ## Alternatives Considered
3336
3643
 
3337
- - A Vite single-page app with a separate API.
3338
- - Remix or another framework.
3644
+ - Symfony for a more component-assembled approach.
3645
+ - A different language or framework entirely.
3339
3646
 
3340
3647
  ## Consequences
3341
3648
 
3342
- - Server rendering, routing, and a large ecosystem.
3343
- - Couples the app to Next.js conventions.
3344
- `
3649
+ - A mature ecosystem (Eloquent, queues, Sanctum, Horizon) and strong conventions.
3650
+ - Couples the application to Laravel's conventions and release cadence.
3651
+
3652
+ ${related(presetId)}`
3345
3653
  ),
3346
- proposedAdr3(
3347
- "routing-app-router",
3348
- "Use the App Router",
3349
- `# Proposed ADR: Use the App Router
3654
+ adr(
3655
+ presetId,
3656
+ "database-eloquent",
3657
+ "Use Eloquent and migrations on PostgreSQL",
3658
+ `# Proposed ADR: Use Eloquent and migrations on PostgreSQL
3350
3659
 
3351
3660
  ## Status
3352
3661
 
@@ -3354,27 +3663,30 @@ Proposed
3354
3663
 
3355
3664
  ## Context
3356
3665
 
3357
- Next.js offers the App Router and the legacy Pages Router.
3666
+ The application needs a relational database and a schema workflow.
3358
3667
 
3359
3668
  ## Decision
3360
3669
 
3361
- Consider the App Router with Server Components, awaiting human acceptance.
3670
+ Consider PostgreSQL (MySQL as the alternative) accessed through Eloquent and versioned migrations,
3671
+ awaiting human acceptance.
3362
3672
 
3363
3673
  ## Alternatives Considered
3364
3674
 
3365
- - The Pages Router.
3366
- - A mix during migration.
3675
+ - MySQL or MariaDB.
3676
+ - The query builder or raw SQL without Eloquent.
3367
3677
 
3368
3678
  ## Consequences
3369
3679
 
3370
- - Server Components and nested layouts.
3371
- - Requires understanding server and client boundaries.
3372
- `
3680
+ - Expressive models, relationships, and reproducible schema migrations.
3681
+ - Requires discipline against N+1 queries and unbounded result sets.
3682
+
3683
+ ${related(presetId, "`docs/50-quality/TESTING_STRATEGY.md` \u2014 how database tests use factories and a disposable database.")}`
3373
3684
  ),
3374
- proposedAdr3(
3375
- "data-layer",
3376
- "Use a typed data layer with PostgreSQL",
3377
- `# Proposed ADR: Use a typed data layer with PostgreSQL
3685
+ adr(
3686
+ presetId,
3687
+ "auth-sanctum",
3688
+ "Use Laravel Sanctum for authentication",
3689
+ `# Proposed ADR: Use Laravel Sanctum for authentication
3378
3690
 
3379
3691
  ## Status
3380
3692
 
@@ -3382,27 +3694,30 @@ Proposed
3382
3694
 
3383
3695
  ## Context
3384
3696
 
3385
- The app needs a database and a typed access layer.
3697
+ The application needs authentication for first-party clients (${variant === "api" ? "a separate SPA and mobile apps" : "an Inertia SPA, and possibly mobile apps"}).
3386
3698
 
3387
3699
  ## Decision
3388
3700
 
3389
- Consider Drizzle or Prisma with PostgreSQL, awaiting human acceptance.
3701
+ Consider Laravel Sanctum: the cookie-based guard for first-party SPAs and API tokens for mobile or
3702
+ scripted clients. This is not accepted until a human reviews and accepts it.
3390
3703
 
3391
3704
  ## Alternatives Considered
3392
3705
 
3393
- - A hosted backend or BaaS.
3394
- - Raw SQL.
3706
+ - Laravel Passport for full OAuth2 (third-party delegated access).
3707
+ - A managed identity provider.
3395
3708
 
3396
3709
  ## Consequences
3397
3710
 
3398
- - Type-safe queries and migrations.
3399
- - Adds an ORM and schema workflow.
3400
- `
3711
+ - Lightweight first-party auth without standing up a full OAuth2 server.
3712
+ - If third-party delegated access is ever required, revisit with Passport.
3713
+
3714
+ ${related(presetId, "`docs/20-security/SECURITY_MODEL.md` \u2014 record the accepted auth and session model here.")}`
3401
3715
  ),
3402
- proposedAdr3(
3403
- "styling-tailwind",
3404
- "Use Tailwind CSS",
3405
- `# Proposed ADR: Use Tailwind CSS
3716
+ adr(
3717
+ presetId,
3718
+ "validation-authorization",
3719
+ "Validate with Form Requests and authorize with Policies",
3720
+ `# Proposed ADR: Validate with Form Requests and authorize with Policies
3406
3721
 
3407
3722
  ## Status
3408
3723
 
@@ -3410,27 +3725,31 @@ Proposed
3410
3725
 
3411
3726
  ## Context
3412
3727
 
3413
- The app needs a styling approach.
3728
+ Input validation and authorization must be consistent and centralized, not scattered across
3729
+ controllers.
3414
3730
 
3415
3731
  ## Decision
3416
3732
 
3417
- Consider Tailwind CSS for styling, awaiting human acceptance.
3733
+ Consider Form Requests for validation (and request-level authorization) plus Policies and Gates for
3734
+ per-model and per-action permission checks, awaiting human acceptance.
3418
3735
 
3419
3736
  ## Alternatives Considered
3420
3737
 
3421
- - CSS Modules.
3422
- - A component library with its own styling.
3738
+ - Inline validation and authorization in controllers.
3739
+ - A third-party permissions package layered on top.
3423
3740
 
3424
3741
  ## Consequences
3425
3742
 
3426
- - Fast, consistent utility-based styling.
3427
- - Markup includes utility classes that teams must standardize.
3428
- `
3743
+ - Controllers stay thin; validation and authorization are testable in isolation.
3744
+ - Every state-changing action must have an explicit authorization path.
3745
+
3746
+ ${related(presetId)}`
3429
3747
  ),
3430
- proposedAdr3(
3431
- "testing",
3432
- "Use Vitest and Playwright",
3433
- `# Proposed ADR: Use Vitest and Playwright
3748
+ adr(
3749
+ presetId,
3750
+ "application-structure",
3751
+ "Keep controllers thin with Action and Service classes",
3752
+ `# Proposed ADR: Keep controllers thin with Action and Service classes
3434
3753
 
3435
3754
  ## Status
3436
3755
 
@@ -3438,93 +3757,278 @@ Proposed
3438
3757
 
3439
3758
  ## Context
3440
3759
 
3441
- The app needs unit, component, and end-to-end testing.
3760
+ Business logic tends to accumulate in controllers and models, which makes it hard to test and reuse.
3442
3761
 
3443
3762
  ## Decision
3444
3763
 
3445
- Consider Vitest with Testing Library and Playwright, awaiting human acceptance.
3764
+ Consider thin controllers that delegate to single-purpose Action or Service classes, with outbound
3765
+ payloads shaped by API Resources (or typed Inertia props). This is not accepted until a human accepts
3766
+ it.
3446
3767
 
3447
3768
  ## Alternatives Considered
3448
3769
 
3449
- - Jest.
3450
- - Cypress for end-to-end tests.
3770
+ - Fat controllers.
3771
+ - Fat models holding business logic.
3451
3772
 
3452
3773
  ## Consequences
3453
3774
 
3454
- - Fast unit and component tests plus reliable end-to-end coverage.
3455
- - Teams maintain two test toolchains.
3456
- `
3775
+ - Reusable, unit-testable business logic and consistent response shapes.
3776
+ - More classes and a convention the team must follow.
3777
+
3778
+ ${related(presetId)}`
3779
+ ),
3780
+ adr(
3781
+ presetId,
3782
+ "queues-horizon",
3783
+ "Run slow work on queues",
3784
+ `# Proposed ADR: Run slow work on queues
3785
+
3786
+ ## Status
3787
+
3788
+ Proposed
3789
+
3790
+ ## Context
3791
+
3792
+ Email, exports, and third-party calls slow down requests and can fail independently.
3793
+
3794
+ ## Decision
3795
+
3796
+ Consider queued jobs for slow or failure-prone work, using the database driver early and Redis with
3797
+ Laravel Horizon as throughput grows. This is not accepted until a human reviews and accepts it.
3798
+
3799
+ ## Alternatives Considered
3800
+
3801
+ - Doing the work synchronously in the request.
3802
+ - An external task queue or serverless functions.
3803
+
3804
+ ## Consequences
3805
+
3806
+ - Faster responses and isolated, retryable background work.
3807
+ - Adds a worker process and queue infrastructure to operate and monitor.
3808
+
3809
+ ${related(presetId)}`
3810
+ ),
3811
+ adr(
3812
+ presetId,
3813
+ "testing-pest",
3814
+ "Use Pest for testing",
3815
+ `# Proposed ADR: Use Pest for testing
3816
+
3817
+ ## Status
3818
+
3819
+ Proposed
3820
+
3821
+ ## Context
3822
+
3823
+ The application needs a fast, readable testing workflow.
3824
+
3825
+ ## Decision
3826
+
3827
+ Consider Pest with model factories and feature tests that exercise real routes against a disposable
3828
+ database, awaiting human acceptance.
3829
+
3830
+ ## Alternatives Considered
3831
+
3832
+ - PHPUnit directly.
3833
+ - A thinner test suite focused only on unit tests.
3834
+
3835
+ ## Consequences
3836
+
3837
+ - Concise, expressive tests that cover routes, validation, and authorization.
3838
+ - The team standardizes on Pest's syntax and plugins.
3839
+
3840
+ ${related(presetId, "`docs/50-quality/TESTING_STRATEGY.md` \u2014 record the accepted testing approach here.")}`
3457
3841
  )
3458
- ]
3842
+ ];
3843
+ return [...base, frontendDecision(variant)];
3844
+ }
3845
+ function frontendDecision(variant) {
3846
+ const presetId = VARIANTS[variant].id;
3847
+ if (variant === "api") {
3848
+ return adr(
3849
+ presetId,
3850
+ "api-design-rest",
3851
+ "Expose a versioned REST API with API Resources",
3852
+ `# Proposed ADR: Expose a versioned REST API with API Resources
3853
+
3854
+ ## Status
3855
+
3856
+ Proposed
3857
+
3858
+ ## Context
3859
+
3860
+ A decoupled SPA or mobile client consumes Laravel over HTTP, so the API contract must be stable and
3861
+ explicit.
3862
+
3863
+ ## Decision
3864
+
3865
+ Consider a versioned REST API (for example \`/api/v1\`) whose responses are shaped by API Resources,
3866
+ authenticated with Sanctum, and documented (OpenAPI). This is not accepted until a human accepts it.
3867
+
3868
+ ## Alternatives Considered
3869
+
3870
+ - GraphQL.
3871
+ - Server-driven Inertia pages instead of a decoupled API.
3872
+
3873
+ ## Consequences
3874
+
3875
+ - A stable, documented contract that multiple clients can rely on.
3876
+ - Versioning and serialization become an explicit, maintained concern.
3877
+
3878
+ ${related(presetId, "`docs/20-security/SECURITY_MODEL.md` \u2014 record token scopes and the SPA cookie guard here.")}`
3879
+ );
3880
+ }
3881
+ const framework = variant === "react" ? "React 19" : "Vue 3";
3882
+ const components = variant === "react" ? "shadcn/ui components on Tailwind" : "Tailwind with single-file components (script setup)";
3883
+ return adr(
3884
+ presetId,
3885
+ `frontend-inertia-${variant}`,
3886
+ `Use Inertia with ${framework}`,
3887
+ `# Proposed ADR: Use Inertia with ${framework}
3888
+
3889
+ ## Status
3890
+
3891
+ Proposed
3892
+
3893
+ ## Context
3894
+
3895
+ The application needs a modern SPA experience without standing up and securing a separate API for
3896
+ first-party screens.
3897
+
3898
+ ## Decision
3899
+
3900
+ Consider Inertia 2 with ${framework} and TypeScript, built with Vite, using ${components}. Controllers
3901
+ return Inertia responses with typed props. This is not accepted until a human reviews and accepts it.
3902
+
3903
+ ## Alternatives Considered
3904
+
3905
+ - A decoupled REST or GraphQL API with a standalone SPA.
3906
+ - Blade with Livewire.
3907
+
3908
+ ## Consequences
3909
+
3910
+ - Server-driven routing and auth with a reactive ${framework} frontend and no duplicate API layer.
3911
+ - Couples the frontend to Inertia's model and the ${framework} ecosystem.
3912
+
3913
+ ${related(presetId)}`
3914
+ );
3915
+ }
3916
+
3917
+ // src/presets/laravel-api/preset.ts
3918
+ var laravelApiPreset = {
3919
+ id: "laravel-api",
3920
+ name: "Laravel API",
3921
+ description: "Opinionated Laravel API-only opinion pack with proposed decisions only.",
3922
+ templates: [
3923
+ {
3924
+ destination: "docs/ai/presets/laravel-api-guidance.md",
3925
+ description: "Laravel API-only guidance that remains proposed until accepted.",
3926
+ content: laravelGuidance("api")
3927
+ }
3928
+ ],
3929
+ guidance: laravelGuidanceItems(),
3930
+ proposedDecisions: laravelProposedDecisions("api")
3459
3931
  };
3460
3932
 
3461
- // src/presets/python-fastapi/preset.ts
3462
- var guidance4 = `# Python FastAPI Preset Guidance
3933
+ // src/presets/laravel-react/preset.ts
3934
+ var laravelReactPreset = {
3935
+ id: "laravel-react",
3936
+ name: "Laravel + React",
3937
+ description: "Opinionated Laravel + Inertia + React opinion pack with proposed decisions only.",
3938
+ templates: [
3939
+ {
3940
+ destination: "docs/ai/presets/laravel-react-guidance.md",
3941
+ description: "Laravel + React (Inertia) guidance that remains proposed until accepted.",
3942
+ content: laravelGuidance("react")
3943
+ }
3944
+ ],
3945
+ guidance: laravelGuidanceItems(),
3946
+ proposedDecisions: laravelProposedDecisions("react")
3947
+ };
3463
3948
 
3464
- This guidance is proposed, not accepted. Convert any choice you adopt into an accepted ADR in
3465
- repository memory. Until then, treat everything here as a recommendation awaiting human review.
3949
+ // src/presets/laravel-vue/preset.ts
3950
+ var laravelVuePreset = {
3951
+ id: "laravel-vue",
3952
+ name: "Laravel + Vue",
3953
+ description: "Opinionated Laravel + Inertia + Vue opinion pack with proposed decisions only.",
3954
+ templates: [
3955
+ {
3956
+ destination: "docs/ai/presets/laravel-vue-guidance.md",
3957
+ description: "Laravel + Vue (Inertia) guidance that remains proposed until accepted.",
3958
+ content: laravelGuidance("vue")
3959
+ }
3960
+ ],
3961
+ guidance: laravelGuidanceItems(),
3962
+ proposedDecisions: laravelProposedDecisions("vue")
3963
+ };
3964
+
3965
+ // src/presets/nextjs/preset.ts
3966
+ var guidance3 = `# Next.js Preset Guidance
3967
+
3968
+ This is proposed guidance, not accepted. Convert any architecture choice into a proposed ADR, then an
3969
+ accepted ADR, before treating it as repository truth.
3466
3970
 
3467
3971
  ## Decision forks this stack forces
3468
3972
 
3469
- - Web framework: FastAPI vs Flask vs Django REST.
3470
- - Database and access: PostgreSQL with SQLAlchemy and Alembic vs an async ORM vs raw SQL.
3471
- - Validation and settings: Pydantic v2 models and settings.
3472
- - Testing: pytest with httpx vs unittest.
3473
- - Background work and caching: Redis, task queues, and async workers.
3973
+ - Routing: App Router vs Pages Router.
3974
+ - Rendering: Server Components and server actions vs client-heavy rendering.
3975
+ - Data layer: Drizzle or Prisma with PostgreSQL vs a hosted backend.
3976
+ - Styling: Tailwind CSS vs CSS Modules vs a component library.
3977
+ - Testing: Vitest with Testing Library and Playwright vs other runners.
3474
3978
 
3475
3979
  ## Recommended structure (proposed)
3476
3980
 
3477
- - A layered layout: \`api/\` routers, \`services/\` logic, \`repositories/\` data access, \`models/\` schemas.
3478
- - Dependency injection through FastAPI dependencies.
3479
- - Configuration via Pydantic settings loaded from environment variables.
3981
+ - The App Router with route groups and colocated server components.
3982
+ - A typed data layer isolated from UI components.
3983
+ - Shared UI primitives and a consistent styling system.
3480
3984
 
3481
3985
  ## Testing (proposed)
3482
3986
 
3483
- - pytest with the FastAPI test client or httpx \`AsyncClient\`.
3484
- - A disposable test database and transactional fixtures.
3485
- - Contract tests for request and response schemas.
3987
+ - Unit and component tests with Vitest and Testing Library.
3988
+ - End-to-end tests with Playwright for critical flows.
3989
+ - Type-safe data access tested against a disposable database.
3486
3990
 
3487
3991
  ## Security considerations (proposed)
3488
3992
 
3489
- - Load secrets from environment or a secret manager, never from source.
3490
- - Validate and constrain all input with Pydantic models.
3491
- - Scope authentication and authorization at the dependency layer.
3993
+ - Keep secrets in server-only environment variables, never in client bundles.
3994
+ - Validate input on the server, including server actions and route handlers.
3995
+ - Scope authentication and authorization on the server, not the client.
3492
3996
  `;
3493
- function proposedAdr4(topic, title, body) {
3997
+ function proposedAdr3(topic, title, body) {
3494
3998
  return {
3495
- id: `python-fastapi-${topic}`,
3999
+ id: `nextjs-${topic}`,
3496
4000
  title,
3497
4001
  status: "proposed",
3498
- destination: `docs/adrs/proposed/ADR-PROPOSED-python-fastapi-${topic}.md`,
4002
+ destination: `docs/adrs/proposed/ADR-PROPOSED-nextjs-${topic}.md`,
3499
4003
  body
3500
4004
  };
3501
4005
  }
3502
- var pythonFastapiPreset = {
3503
- id: "python-fastapi",
3504
- name: "Python FastAPI",
3505
- description: "Opinionated Python FastAPI opinion pack with proposed decisions only.",
4006
+ var nextjsPreset = {
4007
+ id: "nextjs",
4008
+ name: "Next.js",
4009
+ description: "Opinionated Next.js opinion pack with proposed decisions only.",
3506
4010
  templates: [
3507
4011
  {
3508
- destination: "docs/ai/presets/python-fastapi-guidance.md",
3509
- description: "Python FastAPI guidance that remains proposed until accepted.",
3510
- content: guidance4
4012
+ destination: "docs/ai/presets/nextjs-guidance.md",
4013
+ description: "Next.js guidance that remains proposed until accepted.",
4014
+ content: guidance3
3511
4015
  }
3512
4016
  ],
3513
4017
  guidance: [
3514
4018
  {
3515
- title: "Keep framework and data choices proposed",
3516
- body: "FastAPI, the database and ORM, validation, and testing choices must remain proposed until accepted in repository memory."
4019
+ title: "Keep framework choices proposed",
4020
+ body: "Routing, rendering, data layer, styling, and testing choices must remain proposed until accepted in repository memory."
3517
4021
  },
3518
4022
  {
3519
- title: "Validate all input",
3520
- body: "Use Pydantic models at the boundary, but record the validation approach as a proposed decision before treating it as accepted."
4023
+ title: "Keep secrets server-side",
4024
+ body: "Never expose secrets to the client bundle, and record the data and auth approach as proposed decisions before acceptance."
3521
4025
  }
3522
4026
  ],
3523
4027
  proposedDecisions: [
3524
- proposedAdr4(
4028
+ proposedAdr3(
3525
4029
  "framework",
3526
- "Use FastAPI",
3527
- `# Proposed ADR: Use FastAPI
4030
+ "Use Next.js",
4031
+ `# Proposed ADR: Use Next.js
3528
4032
 
3529
4033
  ## Status
3530
4034
 
@@ -3532,27 +4036,28 @@ Proposed
3532
4036
 
3533
4037
  ## Context
3534
4038
 
3535
- The service needs a Python web framework for an async API.
4039
+ The team needs a React framework for a production web application.
3536
4040
 
3537
4041
  ## Decision
3538
4042
 
3539
- Consider FastAPI as the web framework, awaiting human acceptance.
4043
+ Consider Next.js as the application framework. This is not accepted until a human reviews and accepts
4044
+ it.
3540
4045
 
3541
4046
  ## Alternatives Considered
3542
4047
 
3543
- - Flask.
3544
- - Django REST Framework.
4048
+ - A Vite single-page app with a separate API.
4049
+ - Remix or another framework.
3545
4050
 
3546
4051
  ## Consequences
3547
4052
 
3548
- - Async support and automatic OpenAPI docs.
3549
- - Requires comfort with type hints and dependency injection.
4053
+ - Server rendering, routing, and a large ecosystem.
4054
+ - Couples the app to Next.js conventions.
3550
4055
  `
3551
4056
  ),
3552
- proposedAdr4(
3553
- "database-postgres",
3554
- "Use PostgreSQL with SQLAlchemy and Alembic",
3555
- `# Proposed ADR: Use PostgreSQL with SQLAlchemy
4057
+ proposedAdr3(
4058
+ "routing-app-router",
4059
+ "Use the App Router",
4060
+ `# Proposed ADR: Use the App Router
3556
4061
 
3557
4062
  ## Status
3558
4063
 
@@ -3560,27 +4065,27 @@ Proposed
3560
4065
 
3561
4066
  ## Context
3562
4067
 
3563
- The service needs a relational database and a migration strategy.
4068
+ Next.js offers the App Router and the legacy Pages Router.
3564
4069
 
3565
4070
  ## Decision
3566
4071
 
3567
- Consider PostgreSQL with SQLAlchemy and Alembic migrations, awaiting human acceptance.
4072
+ Consider the App Router with Server Components, awaiting human acceptance.
3568
4073
 
3569
4074
  ## Alternatives Considered
3570
4075
 
3571
- - An async ORM such as Tortoise or SQLModel only.
3572
- - Raw SQL with a lightweight driver.
4076
+ - The Pages Router.
4077
+ - A mix during migration.
3573
4078
 
3574
4079
  ## Consequences
3575
4080
 
3576
- - Mature tooling and explicit migrations.
3577
- - Requires session and connection management discipline.
4081
+ - Server Components and nested layouts.
4082
+ - Requires understanding server and client boundaries.
3578
4083
  `
3579
4084
  ),
3580
- proposedAdr4(
3581
- "validation-pydantic",
3582
- "Use Pydantic for validation and settings",
3583
- `# Proposed ADR: Use Pydantic
4085
+ proposedAdr3(
4086
+ "data-layer",
4087
+ "Use a typed data layer with PostgreSQL",
4088
+ `# Proposed ADR: Use a typed data layer with PostgreSQL
3584
4089
 
3585
4090
  ## Status
3586
4091
 
@@ -3588,22 +4093,228 @@ Proposed
3588
4093
 
3589
4094
  ## Context
3590
4095
 
3591
- The service needs request validation and typed configuration.
4096
+ The app needs a database and a typed access layer.
3592
4097
 
3593
4098
  ## Decision
3594
4099
 
3595
- Consider Pydantic v2 models for validation and settings, awaiting human acceptance.
4100
+ Consider Drizzle or Prisma with PostgreSQL, awaiting human acceptance.
3596
4101
 
3597
4102
  ## Alternatives Considered
3598
4103
 
3599
- - Marshmallow.
3600
- - Hand-written validation.
4104
+ - A hosted backend or BaaS.
4105
+ - Raw SQL.
3601
4106
 
3602
4107
  ## Consequences
3603
4108
 
3604
- - Strong typing at the boundary and clear settings.
3605
- - Adds Pydantic to the dependency surface.
3606
- `
4109
+ - Type-safe queries and migrations.
4110
+ - Adds an ORM and schema workflow.
4111
+ `
4112
+ ),
4113
+ proposedAdr3(
4114
+ "styling-tailwind",
4115
+ "Use Tailwind CSS",
4116
+ `# Proposed ADR: Use Tailwind CSS
4117
+
4118
+ ## Status
4119
+
4120
+ Proposed
4121
+
4122
+ ## Context
4123
+
4124
+ The app needs a styling approach.
4125
+
4126
+ ## Decision
4127
+
4128
+ Consider Tailwind CSS for styling, awaiting human acceptance.
4129
+
4130
+ ## Alternatives Considered
4131
+
4132
+ - CSS Modules.
4133
+ - A component library with its own styling.
4134
+
4135
+ ## Consequences
4136
+
4137
+ - Fast, consistent utility-based styling.
4138
+ - Markup includes utility classes that teams must standardize.
4139
+ `
4140
+ ),
4141
+ proposedAdr3(
4142
+ "testing",
4143
+ "Use Vitest and Playwright",
4144
+ `# Proposed ADR: Use Vitest and Playwright
4145
+
4146
+ ## Status
4147
+
4148
+ Proposed
4149
+
4150
+ ## Context
4151
+
4152
+ The app needs unit, component, and end-to-end testing.
4153
+
4154
+ ## Decision
4155
+
4156
+ Consider Vitest with Testing Library and Playwright, awaiting human acceptance.
4157
+
4158
+ ## Alternatives Considered
4159
+
4160
+ - Jest.
4161
+ - Cypress for end-to-end tests.
4162
+
4163
+ ## Consequences
4164
+
4165
+ - Fast unit and component tests plus reliable end-to-end coverage.
4166
+ - Teams maintain two test toolchains.
4167
+ `
4168
+ )
4169
+ ]
4170
+ };
4171
+
4172
+ // src/presets/python-fastapi/preset.ts
4173
+ var guidance4 = `# Python FastAPI Preset Guidance
4174
+
4175
+ This guidance is proposed, not accepted. Convert any choice you adopt into an accepted ADR in
4176
+ repository memory. Until then, treat everything here as a recommendation awaiting human review.
4177
+
4178
+ ## Decision forks this stack forces
4179
+
4180
+ - Web framework: FastAPI vs Flask vs Django REST.
4181
+ - Database and access: PostgreSQL with SQLAlchemy and Alembic vs an async ORM vs raw SQL.
4182
+ - Validation and settings: Pydantic v2 models and settings.
4183
+ - Testing: pytest with httpx vs unittest.
4184
+ - Background work and caching: Redis, task queues, and async workers.
4185
+
4186
+ ## Recommended structure (proposed)
4187
+
4188
+ - A layered layout: \`api/\` routers, \`services/\` logic, \`repositories/\` data access, \`models/\` schemas.
4189
+ - Dependency injection through FastAPI dependencies.
4190
+ - Configuration via Pydantic settings loaded from environment variables.
4191
+
4192
+ ## Testing (proposed)
4193
+
4194
+ - pytest with the FastAPI test client or httpx \`AsyncClient\`.
4195
+ - A disposable test database and transactional fixtures.
4196
+ - Contract tests for request and response schemas.
4197
+
4198
+ ## Security considerations (proposed)
4199
+
4200
+ - Load secrets from environment or a secret manager, never from source.
4201
+ - Validate and constrain all input with Pydantic models.
4202
+ - Scope authentication and authorization at the dependency layer.
4203
+ `;
4204
+ function proposedAdr4(topic, title, body) {
4205
+ return {
4206
+ id: `python-fastapi-${topic}`,
4207
+ title,
4208
+ status: "proposed",
4209
+ destination: `docs/adrs/proposed/ADR-PROPOSED-python-fastapi-${topic}.md`,
4210
+ body
4211
+ };
4212
+ }
4213
+ var pythonFastapiPreset = {
4214
+ id: "python-fastapi",
4215
+ name: "Python FastAPI",
4216
+ description: "Opinionated Python FastAPI opinion pack with proposed decisions only.",
4217
+ templates: [
4218
+ {
4219
+ destination: "docs/ai/presets/python-fastapi-guidance.md",
4220
+ description: "Python FastAPI guidance that remains proposed until accepted.",
4221
+ content: guidance4
4222
+ }
4223
+ ],
4224
+ guidance: [
4225
+ {
4226
+ title: "Keep framework and data choices proposed",
4227
+ body: "FastAPI, the database and ORM, validation, and testing choices must remain proposed until accepted in repository memory."
4228
+ },
4229
+ {
4230
+ title: "Validate all input",
4231
+ body: "Use Pydantic models at the boundary, but record the validation approach as a proposed decision before treating it as accepted."
4232
+ }
4233
+ ],
4234
+ proposedDecisions: [
4235
+ proposedAdr4(
4236
+ "framework",
4237
+ "Use FastAPI",
4238
+ `# Proposed ADR: Use FastAPI
4239
+
4240
+ ## Status
4241
+
4242
+ Proposed
4243
+
4244
+ ## Context
4245
+
4246
+ The service needs a Python web framework for an async API.
4247
+
4248
+ ## Decision
4249
+
4250
+ Consider FastAPI as the web framework, awaiting human acceptance.
4251
+
4252
+ ## Alternatives Considered
4253
+
4254
+ - Flask.
4255
+ - Django REST Framework.
4256
+
4257
+ ## Consequences
4258
+
4259
+ - Async support and automatic OpenAPI docs.
4260
+ - Requires comfort with type hints and dependency injection.
4261
+ `
4262
+ ),
4263
+ proposedAdr4(
4264
+ "database-postgres",
4265
+ "Use PostgreSQL with SQLAlchemy and Alembic",
4266
+ `# Proposed ADR: Use PostgreSQL with SQLAlchemy
4267
+
4268
+ ## Status
4269
+
4270
+ Proposed
4271
+
4272
+ ## Context
4273
+
4274
+ The service needs a relational database and a migration strategy.
4275
+
4276
+ ## Decision
4277
+
4278
+ Consider PostgreSQL with SQLAlchemy and Alembic migrations, awaiting human acceptance.
4279
+
4280
+ ## Alternatives Considered
4281
+
4282
+ - An async ORM such as Tortoise or SQLModel only.
4283
+ - Raw SQL with a lightweight driver.
4284
+
4285
+ ## Consequences
4286
+
4287
+ - Mature tooling and explicit migrations.
4288
+ - Requires session and connection management discipline.
4289
+ `
4290
+ ),
4291
+ proposedAdr4(
4292
+ "validation-pydantic",
4293
+ "Use Pydantic for validation and settings",
4294
+ `# Proposed ADR: Use Pydantic
4295
+
4296
+ ## Status
4297
+
4298
+ Proposed
4299
+
4300
+ ## Context
4301
+
4302
+ The service needs request validation and typed configuration.
4303
+
4304
+ ## Decision
4305
+
4306
+ Consider Pydantic v2 models for validation and settings, awaiting human acceptance.
4307
+
4308
+ ## Alternatives Considered
4309
+
4310
+ - Marshmallow.
4311
+ - Hand-written validation.
4312
+
4313
+ ## Consequences
4314
+
4315
+ - Strong typing at the boundary and clear settings.
4316
+ - Adds Pydantic to the dependency surface.
4317
+ `
3607
4318
  ),
3608
4319
  proposedAdr4(
3609
4320
  "testing-pytest",
@@ -3743,8 +4454,8 @@ function parsePreset(value) {
3743
4454
  if (!result.success) {
3744
4455
  throw new PresetValidationError(
3745
4456
  result.error.issues.map((issue) => {
3746
- const path16 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
3747
- return `${path16}${issue.message}`;
4457
+ const path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
4458
+ return `${path17}${issue.message}`;
3748
4459
  })
3749
4460
  );
3750
4461
  }
@@ -3770,6 +4481,9 @@ var builtInPresets = validatePresetRegistry([
3770
4481
  genericPreset,
3771
4482
  iosSwiftPreset,
3772
4483
  kotlinAndroidPreset,
4484
+ laravelApiPreset,
4485
+ laravelReactPreset,
4486
+ laravelVuePreset,
3773
4487
  nextjsPreset,
3774
4488
  pythonFastapiPreset
3775
4489
  ]);
@@ -3780,302 +4494,52 @@ function getPreset(id) {
3780
4494
  return builtInPresets.find((preset) => preset.id === id);
3781
4495
  }
3782
4496
 
3783
- // src/commands/init.ts
3784
- var InitError = class extends Error {
3785
- code;
3786
- details;
3787
- constructor(code, message, details = []) {
3788
- super(message);
3789
- this.name = "InitError";
3790
- this.code = code;
3791
- this.details = details;
3792
- }
3793
- };
3794
- async function initProject(options) {
3795
- if (options.force === true && options.reinit !== true && existsSync5(path14.join(options.rootDir, CONFIG_PATH))) {
3796
- throw new InitError(
3797
- "EXISTING_INSTALLATION",
3798
- "Refusing to re-initialize an existing Recall OS installation.",
3799
- [
3800
- "An existing .recall/config.json was found in this directory.",
3801
- "Running init --force here would overwrite existing repository memory.",
3802
- "Pass --reinit together with --force to overwrite an existing installation."
3803
- ]
3804
- );
3805
- }
3806
- const preset = resolvePreset(options.preset);
3807
- const preCommitGates = await detectPreCommitGates(options.rootDir);
3808
- const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
3809
- const files = createInitWriteFiles(options.rootDir, config, preset);
3810
- const plan = createWritePlan({
3811
- rootDir: options.rootDir,
3812
- files,
3813
- force: options.force
3814
- });
3815
- if (plan.hasErrors) {
3816
- throw new InitError(
3817
- "WRITE_PLAN_ERROR",
3818
- "Recall OS init write plan contains errors.",
3819
- plan.entries.filter((entry) => entry.action === "error").map((entry) => `${entry.path}: ${entry.reason}`)
3820
- );
3821
- }
3822
- const writeResult = await executeWritePlan(plan, { dryRun: options.dryRun });
3823
- return {
3824
- preset: preset?.id ?? null,
3825
- dryRun: options.dryRun ?? false,
3826
- plan,
3827
- writeResult
3828
- };
3829
- }
3830
- function formatInitResult(result) {
3831
- const lines = [
3832
- result.dryRun ? "Recall OS init dry run complete." : "Recall OS init complete.",
3833
- `Preset: ${result.preset ?? "none"}`
3834
- ];
3835
- appendWriteSummary(lines, {
3836
- dryRun: result.dryRun,
3837
- writeResult: result.writeResult
3838
- });
3839
- const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
3840
- if (hookWritten) {
3841
- lines.push("");
3842
- lines.push(
3843
- result.dryRun ? "Pre-commit hook will be written to .recall/hooks/pre-commit." : "Pre-commit hook written to .recall/hooks/pre-commit."
3844
- );
3845
- lines.push(`Enable it once per clone: ${HOOKS_PATH_ACTIVATION_COMMAND}`);
3846
- }
3847
- if (!result.dryRun) {
3848
- appendNextSteps(lines, [
3849
- "Read CLAUDE.md and AGENTS.md, then the docs/ memory they point to.",
3850
- "Plan your first feature: `recall feature create <name>`.",
3851
- "Record a decision: `recall adr create <title>`.",
3852
- "Check repository memory health anytime: `recall doctor`."
3853
- ]);
3854
- }
3855
- return `${lines.join("\n")}
3856
- `;
3857
- }
3858
- function resolvePreset(presetId) {
3859
- if (presetId === void 0) {
3860
- return null;
3861
- }
3862
- const preset = getPreset(presetId);
3863
- if (preset === void 0) {
3864
- throw new InitError("UNKNOWN_PRESET", `Unknown preset "${presetId}".`);
3865
- }
3866
- return preset;
3867
- }
3868
- function createInitWriteFiles(rootDir, config, preset) {
3869
- return [
3870
- {
3871
- path: CONFIG_PATH,
3872
- content: `${JSON.stringify(config, null, 2)}
3873
- `
3874
- },
3875
- ...generateInitFiles({ rootDir, preset }),
3876
- {
3877
- path: PRE_COMMIT_HOOK_PATH,
3878
- content: renderPreCommitHook(config.preCommitGates),
3879
- executable: true
3880
- }
3881
- ];
3882
- }
3883
-
3884
- // src/core/mcp/known-servers.ts
3885
- var KNOWN_SERVERS = {
3886
- figma: {
3887
- title: "Figma",
3888
- purpose: "Design variables, components, and layout metadata for building consistent UI.",
3889
- dataAccessed: [
3890
- "Design tokens and variables.",
3891
- "Component and layout metadata.",
3892
- "Frames and styles."
3893
- ]
3894
- },
3895
- linear: {
3896
- title: "Linear",
3897
- purpose: "Tickets, project status, and acceptance criteria.",
3898
- dataAccessed: [
3899
- "Issues and tickets.",
3900
- "Project and cycle status.",
3901
- "Acceptance criteria in issues."
3902
- ]
3903
- },
3904
- jira: {
3905
- title: "Jira",
3906
- purpose: "Tickets, sprints, and acceptance criteria for knocking out tasks.",
3907
- dataAccessed: [
3908
- "Issues and tickets.",
3909
- "Sprint and board status.",
3910
- "Acceptance criteria in issues."
3911
- ]
3912
- },
3913
- github: {
3914
- title: "GitHub",
3915
- purpose: "Pull requests, issues, and review comments.",
3916
- dataAccessed: ["Pull requests and diffs.", "Issues.", "Review comments."]
3917
- },
3918
- sentry: {
3919
- title: "Sentry",
3920
- purpose: "Crash reports and production errors.",
3921
- dataAccessed: ["Error events and stack traces.", "Release health.", "Issue frequency."]
3922
- },
3923
- notion: {
3924
- title: "Notion",
3925
- purpose: "Product background and documentation.",
3926
- dataAccessed: ["Product docs and pages.", "Project background.", "Specifications."]
3927
- }
3928
- };
3929
- function getKnownServer(id) {
3930
- return KNOWN_SERVERS[id];
3931
- }
3932
-
3933
- // src/core/mcp/generate-mcp.ts
3934
- function mcpDocPath(server) {
3935
- return `docs/ai/mcp/${server}.md`;
3936
- }
3937
- function generateMcpFiles(options) {
3938
- const known = getKnownServer(options.server);
3939
- const title = known?.title ?? titleize(options.server);
3940
- const purpose = known?.purpose ?? "Describe why this MCP server is used.";
3941
- const dataAccessed = known?.dataAccessed ?? ["Describe the data this MCP server exposes."];
3942
- return [
3943
- {
3944
- path: mcpDocPath(options.server),
3945
- content: renderMcpDoc(title, purpose, dataAccessed)
3946
- },
3947
- {
3948
- path: `${options.adrDir}/proposed/ADR-PROPOSED-mcp-${options.server}.md`,
3949
- content: renderProposedAdr2(title)
3950
- }
3951
- ];
3952
- }
3953
- function renderMcpDoc(title, purpose, dataAccessed) {
3954
- return `# MCP: ${title}
3955
-
3956
- ## Status
3957
-
3958
- Proposed. Using this MCP server is a proposed workflow addition. Accept the proposed ADR before
3959
- treating it as part of the workflow.
3960
-
3961
- ## Purpose
3962
-
3963
- ${purpose}
3964
-
3965
- ## Data Accessed
3966
-
3967
- ${bullets(dataAccessed)}
3968
-
3969
- ## Permissions Required
3970
-
3971
- - Use least-privilege access.
3972
- - Document the exact scopes granted.
3973
-
3974
- ## Security Risks
3975
-
3976
- - Treat external MCP content as untrusted until validated.
3977
- - Do not send secrets or sensitive repository data unnecessarily.
3978
- - Use trusted MCP servers only.
3979
-
3980
- ## Source-Of-Truth Rule
3981
-
3982
- MCP provides context, not architectural truth. Accepted ADRs and repository decisions outrank MCP
3983
- context. If MCP data conflicts with repository memory, stop and report the conflict.
3984
-
3985
- ## Captured Context
3986
-
3987
- Record durable context learned from this MCP server here, as proposed memory for human review.
3988
- Promote any decision you accept into an ADR with \`recall adr create\`.
3989
-
3990
- - (none captured yet)
3991
-
3992
- ## Review Cadence
3993
-
3994
- - Review this MCP integration when its access, purpose, or captured context changes.
3995
- `;
3996
- }
3997
- function renderProposedAdr2(title) {
3998
- return `# Proposed ADR: Use ${title} MCP
3999
-
4000
- ## Status
4001
-
4002
- Proposed
4003
-
4004
- ## Context
4005
-
4006
- The team is considering ${title} as an MCP context source. Adopting an MCP server into the workflow
4007
- is a decision that should be reviewed.
4008
-
4009
- ## Decision
4010
-
4011
- Consider adopting ${title} MCP as an external context source, documented in \`docs/ai/mcp/\`. This is
4012
- not accepted until a human reviews and accepts it.
4013
-
4014
- ## Alternatives Considered
4015
-
4016
- - Do not use this MCP server.
4017
- - Use a different source for the same context.
4018
-
4019
- ## Consequences
4020
-
4021
- - The team gains durable, reviewable context from ${title}.
4022
- - MCP context never overrides accepted repository memory.
4023
- - Captured context remains proposed until promoted to an ADR.
4024
- `;
4025
- }
4026
- function bullets(values) {
4027
- return values.map((value) => `- ${value}`).join("\n");
4028
- }
4029
- function titleize(value) {
4030
- return value.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
4031
- }
4032
-
4033
- // src/core/skills/render-skill.ts
4034
- function renderSkill(skill) {
4035
- const lines = [
4036
- "---",
4037
- `name: ${skill.name}`,
4038
- // JSON-stringify yields a valid double-quoted YAML scalar, so descriptions with any punctuation
4039
- // stay valid Agent Skills frontmatter.
4040
- `description: ${JSON.stringify(skill.description)}`,
4041
- "---",
4042
- "",
4043
- `# Skill: ${skill.title}`,
4044
- "",
4045
- "## Purpose",
4046
- "",
4047
- ...paragraphs(skill.purpose),
4048
- "",
4049
- "## Inputs",
4050
- "",
4051
- ...bullets2(skill.inputs),
4052
- "",
4053
- "## Required Reading",
4054
- "",
4055
- ...bullets2(skill.requiredReading),
4056
- "",
4057
- "## Output Files",
4058
- "",
4059
- ...bullets2(skill.outputFiles),
4060
- "",
4061
- "## Process",
4062
- "",
4063
- ...numbered(skill.process),
4064
- ""
4065
- ];
4066
- for (const section of skill.extraSections ?? []) {
4067
- lines.push(`## ${section.heading}`, "", ...bullets2(section.bullets), "");
4497
+ // src/core/skills/render-skill.ts
4498
+ function renderSkill(skill) {
4499
+ const lines = [
4500
+ "---",
4501
+ `name: ${skill.name}`,
4502
+ // JSON-stringify yields a valid double-quoted YAML scalar, so descriptions with any punctuation
4503
+ // stay valid Agent Skills frontmatter.
4504
+ `description: ${JSON.stringify(skill.description)}`,
4505
+ "---",
4506
+ "",
4507
+ `# Skill: ${skill.title}`,
4508
+ "",
4509
+ "## Purpose",
4510
+ "",
4511
+ ...paragraphs(skill.purpose),
4512
+ "",
4513
+ "## Inputs",
4514
+ "",
4515
+ ...bullets(skill.inputs),
4516
+ "",
4517
+ "## Required Reading",
4518
+ "",
4519
+ ...bullets(skill.requiredReading),
4520
+ "",
4521
+ "## Output Files",
4522
+ "",
4523
+ ...bullets(skill.outputFiles),
4524
+ "",
4525
+ "## Process",
4526
+ "",
4527
+ ...numbered(skill.process),
4528
+ ""
4529
+ ];
4530
+ for (const section of skill.extraSections ?? []) {
4531
+ lines.push(`## ${section.heading}`, "", ...bullets(section.bullets), "");
4068
4532
  }
4069
4533
  lines.push(
4070
4534
  "## Stop Conditions",
4071
4535
  "",
4072
4536
  "Stop and request human decision if:",
4073
4537
  "",
4074
- ...bullets2(skill.stopConditions),
4538
+ ...bullets(skill.stopConditions),
4075
4539
  "",
4076
4540
  "## Quality Bar",
4077
4541
  "",
4078
- ...bullets2(skill.qualityBar)
4542
+ ...bullets(skill.qualityBar)
4079
4543
  );
4080
4544
  return `${lines.join("\n")}
4081
4545
  `;
@@ -4090,7 +4554,7 @@ function paragraphs(values) {
4090
4554
  });
4091
4555
  return out;
4092
4556
  }
4093
- function bullets2(values) {
4557
+ function bullets(values) {
4094
4558
  return values.map((value) => `- ${value}`);
4095
4559
  }
4096
4560
  function numbered(values) {
@@ -4629,6 +5093,9 @@ var SKILL_CATALOG = [
4629
5093
  function getCatalogSkill(name) {
4630
5094
  return SKILL_CATALOG.find((skill) => skill.name === name);
4631
5095
  }
5096
+ function listCatalogSkillNames() {
5097
+ return SKILL_CATALOG.map((skill) => skill.name);
5098
+ }
4632
5099
 
4633
5100
  // src/core/skills/generate-skill.ts
4634
5101
  var SKILL_TARGETS = [".claude/skills", ".agents/skills"];
@@ -4647,7 +5114,7 @@ function generateSkillFiles(name) {
4647
5114
  function skeletonSkill(name) {
4648
5115
  return {
4649
5116
  name,
4650
- title: titleize2(name),
5117
+ title: titleize(name),
4651
5118
  description: `Describe what the ${name} skill does and when to use it. Use when ... (replace this with concrete trigger keywords).`,
4652
5119
  purpose: ["Describe the single job this skill performs."],
4653
5120
  inputs: ["List the inputs this skill needs."],
@@ -4661,10 +5128,287 @@ function skeletonSkill(name) {
4661
5128
  qualityBar: ["State how to tell the skill did its job well."]
4662
5129
  };
4663
5130
  }
4664
- function titleize2(name) {
5131
+ function titleize(name) {
4665
5132
  return name.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
4666
5133
  }
4667
5134
 
5135
+ // src/commands/init.ts
5136
+ var InitError = class extends Error {
5137
+ code;
5138
+ details;
5139
+ constructor(code, message, details = []) {
5140
+ super(message);
5141
+ this.name = "InitError";
5142
+ this.code = code;
5143
+ this.details = details;
5144
+ }
5145
+ };
5146
+ async function initProject(options) {
5147
+ if (options.force === true && options.reinit !== true && existsSync6(path15.join(options.rootDir, CONFIG_PATH))) {
5148
+ throw new InitError(
5149
+ "EXISTING_INSTALLATION",
5150
+ "Refusing to re-initialize an existing Recall OS installation.",
5151
+ [
5152
+ "An existing .recall/config.json was found in this directory.",
5153
+ "Running init --force here would overwrite existing repository memory.",
5154
+ "Pass --reinit together with --force to overwrite an existing installation."
5155
+ ]
5156
+ );
5157
+ }
5158
+ const preset = resolvePreset(options.preset);
5159
+ const preCommitGates = await detectPreCommitGates(options.rootDir);
5160
+ const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
5161
+ const files = createInitWriteFiles(options.rootDir, config, preset);
5162
+ const plan = createWritePlan({
5163
+ rootDir: options.rootDir,
5164
+ files,
5165
+ force: options.force
5166
+ });
5167
+ if (plan.hasErrors) {
5168
+ throw new InitError(
5169
+ "WRITE_PLAN_ERROR",
5170
+ "Recall OS init write plan contains errors.",
5171
+ plan.entries.filter((entry) => entry.action === "error").map((entry) => `${entry.path}: ${entry.reason}`)
5172
+ );
5173
+ }
5174
+ const writeResult = await executeWritePlan(plan, { dryRun: options.dryRun });
5175
+ return {
5176
+ preset: preset?.id ?? null,
5177
+ dryRun: options.dryRun ?? false,
5178
+ plan,
5179
+ writeResult
5180
+ };
5181
+ }
5182
+ function formatInitResult(result) {
5183
+ const lines = [
5184
+ result.dryRun ? "Recall OS init dry run complete." : "Recall OS init complete.",
5185
+ `Preset: ${result.preset ?? "none"}`
5186
+ ];
5187
+ if (!result.dryRun) {
5188
+ lines.push(
5189
+ `Generated repository memory, ${listCatalogSkillNames().length} agent skills, a pre-commit hook, a CI workflow, a Claude SessionStart hook, and a Cursor rule that load memory automatically.`
5190
+ );
5191
+ }
5192
+ appendWriteSummary(lines, {
5193
+ dryRun: result.dryRun,
5194
+ writeResult: result.writeResult
5195
+ });
5196
+ const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
5197
+ if (hookWritten) {
5198
+ lines.push("");
5199
+ lines.push(
5200
+ result.dryRun ? "Pre-commit hook will be written to .recall/hooks/pre-commit." : "Pre-commit hook written to .recall/hooks/pre-commit."
5201
+ );
5202
+ lines.push(`Enable it once per clone: ${HOOKS_PATH_ACTIVATION_COMMAND}`);
5203
+ }
5204
+ if (!result.dryRun) {
5205
+ appendNextSteps(lines, [
5206
+ "Read CLAUDE.md and AGENTS.md, then the docs/ memory they point to.",
5207
+ "AI agent skills are in .claude/skills/ and .agents/skills/ \u2014 restart your AI tool to load them.",
5208
+ "Memory loads automatically per tool: a Claude SessionStart hook (.claude/hooks/session-start.sh), a Cursor rule (.cursor/rules/recall-memory.mdc), and AGENTS.md for Codex.",
5209
+ "CI is wired in .github/workflows/recall.yml; the pre-commit hook is in .recall/hooks/.",
5210
+ "Plan your first feature: `recall feature create <name>`.",
5211
+ "Record a decision: `recall adr create <title>`, then accept it with `recall adr accept`.",
5212
+ "Check repository memory health anytime: `recall doctor`."
5213
+ ]);
5214
+ }
5215
+ return `${lines.join("\n")}
5216
+ `;
5217
+ }
5218
+ function resolvePreset(presetId) {
5219
+ if (presetId === void 0) {
5220
+ return null;
5221
+ }
5222
+ const preset = getPreset(presetId);
5223
+ if (preset === void 0) {
5224
+ throw new InitError("UNKNOWN_PRESET", `Unknown preset "${presetId}".`);
5225
+ }
5226
+ return preset;
5227
+ }
5228
+ function createInitWriteFiles(rootDir, config, preset) {
5229
+ return [
5230
+ {
5231
+ path: CONFIG_PATH,
5232
+ content: `${JSON.stringify(config, null, 2)}
5233
+ `
5234
+ },
5235
+ ...generateInitFiles({ rootDir, preset }),
5236
+ {
5237
+ path: PRE_COMMIT_HOOK_PATH,
5238
+ content: renderPreCommitHook(config.preCommitGates),
5239
+ executable: true
5240
+ },
5241
+ // A Claude Code SessionStart hook that injects a memory map every session, so a fresh agent
5242
+ // reliably loads durable memory, plus the settings that wire it (skipped if settings exist).
5243
+ {
5244
+ path: SESSION_START_HOOK_PATH,
5245
+ content: renderSessionStartHook(),
5246
+ executable: true
5247
+ },
5248
+ {
5249
+ path: CLAUDE_SETTINGS_PATH,
5250
+ content: renderClaudeSettings()
5251
+ },
5252
+ // Generate the agent skill set so a fresh repo has the workflows that guide AI agents,
5253
+ // not just the docs. Written to both the Claude and portable Agent Skills targets.
5254
+ ...listCatalogSkillNames().flatMap((name) => generateSkillFiles(name).files)
5255
+ ];
5256
+ }
5257
+
5258
+ // src/core/mcp/known-servers.ts
5259
+ var KNOWN_SERVERS = {
5260
+ figma: {
5261
+ title: "Figma",
5262
+ purpose: "Design variables, components, and layout metadata for building consistent UI.",
5263
+ dataAccessed: [
5264
+ "Design tokens and variables.",
5265
+ "Component and layout metadata.",
5266
+ "Frames and styles."
5267
+ ]
5268
+ },
5269
+ linear: {
5270
+ title: "Linear",
5271
+ purpose: "Tickets, project status, and acceptance criteria.",
5272
+ dataAccessed: [
5273
+ "Issues and tickets.",
5274
+ "Project and cycle status.",
5275
+ "Acceptance criteria in issues."
5276
+ ]
5277
+ },
5278
+ jira: {
5279
+ title: "Jira",
5280
+ purpose: "Tickets, sprints, and acceptance criteria for knocking out tasks.",
5281
+ dataAccessed: [
5282
+ "Issues and tickets.",
5283
+ "Sprint and board status.",
5284
+ "Acceptance criteria in issues."
5285
+ ]
5286
+ },
5287
+ github: {
5288
+ title: "GitHub",
5289
+ purpose: "Pull requests, issues, and review comments.",
5290
+ dataAccessed: ["Pull requests and diffs.", "Issues.", "Review comments."]
5291
+ },
5292
+ sentry: {
5293
+ title: "Sentry",
5294
+ purpose: "Crash reports and production errors.",
5295
+ dataAccessed: ["Error events and stack traces.", "Release health.", "Issue frequency."]
5296
+ },
5297
+ notion: {
5298
+ title: "Notion",
5299
+ purpose: "Product background and documentation.",
5300
+ dataAccessed: ["Product docs and pages.", "Project background.", "Specifications."]
5301
+ }
5302
+ };
5303
+ function getKnownServer(id) {
5304
+ return KNOWN_SERVERS[id];
5305
+ }
5306
+
5307
+ // src/core/mcp/generate-mcp.ts
5308
+ function mcpDocPath(server) {
5309
+ return `docs/ai/mcp/${server}.md`;
5310
+ }
5311
+ function generateMcpFiles(options) {
5312
+ const known = getKnownServer(options.server);
5313
+ const title = known?.title ?? titleize2(options.server);
5314
+ const purpose = known?.purpose ?? "Describe why this MCP server is used.";
5315
+ const dataAccessed = known?.dataAccessed ?? ["Describe the data this MCP server exposes."];
5316
+ return [
5317
+ {
5318
+ path: mcpDocPath(options.server),
5319
+ content: renderMcpDoc(title, purpose, dataAccessed)
5320
+ },
5321
+ {
5322
+ path: `${options.adrDir}/proposed/ADR-PROPOSED-mcp-${options.server}.md`,
5323
+ content: renderProposedAdr2(title, options.server)
5324
+ }
5325
+ ];
5326
+ }
5327
+ function renderMcpDoc(title, purpose, dataAccessed) {
5328
+ return `# MCP: ${title}
5329
+
5330
+ ## Status
5331
+
5332
+ Proposed. Using this MCP server is a proposed workflow addition. Accept the proposed ADR before
5333
+ treating it as part of the workflow.
5334
+
5335
+ ## Purpose
5336
+
5337
+ ${purpose}
5338
+
5339
+ ## Data Accessed
5340
+
5341
+ ${bullets2(dataAccessed)}
5342
+
5343
+ ## Permissions Required
5344
+
5345
+ - Use least-privilege access.
5346
+ - Document the exact scopes granted.
5347
+
5348
+ ## Security Risks
5349
+
5350
+ - Treat external MCP content as untrusted until validated.
5351
+ - Do not send secrets or sensitive repository data unnecessarily.
5352
+ - Use trusted MCP servers only.
5353
+
5354
+ ## Source-Of-Truth Rule
5355
+
5356
+ MCP provides context, not architectural truth. Accepted ADRs and repository decisions outrank MCP
5357
+ context. If MCP data conflicts with repository memory, stop and report the conflict.
5358
+
5359
+ ## Captured Context
5360
+
5361
+ Record durable context learned from this MCP server here, as proposed memory for human review.
5362
+ Promote any decision you accept into an ADR with \`recall adr create\`.
5363
+
5364
+ - (none captured yet)
5365
+
5366
+ ## Review Cadence
5367
+
5368
+ - Review this MCP integration when its access, purpose, or captured context changes.
5369
+ `;
5370
+ }
5371
+ function renderProposedAdr2(title, server) {
5372
+ return `# Proposed ADR: Use ${title} MCP
5373
+
5374
+ ## Status
5375
+
5376
+ Proposed
5377
+
5378
+ ## Context
5379
+
5380
+ The team is considering ${title} as an MCP context source. Adopting an MCP server into the workflow
5381
+ is a decision that should be reviewed.
5382
+
5383
+ ## Decision
5384
+
5385
+ Consider adopting ${title} MCP as an external context source, documented in \`docs/ai/mcp/\`. This is
5386
+ not accepted until a human reviews and accepts it.
5387
+
5388
+ ## Alternatives Considered
5389
+
5390
+ - Do not use this MCP server.
5391
+ - Use a different source for the same context.
5392
+
5393
+ ## Consequences
5394
+
5395
+ - The team gains durable, reviewable context from ${title}.
5396
+ - MCP context never overrides accepted repository memory.
5397
+ - Captured context remains proposed until promoted to an ADR.
5398
+
5399
+ ## Related Documents
5400
+
5401
+ - \`docs/ai/mcp/${server}.md\` \u2014 captured ${title} MCP context.
5402
+ - \`docs/ai/MCP_STRATEGY.md\` \u2014 how MCP context is captured and ranked.
5403
+ `;
5404
+ }
5405
+ function bullets2(values) {
5406
+ return values.map((value) => `- ${value}`).join("\n");
5407
+ }
5408
+ function titleize2(value) {
5409
+ return value.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
5410
+ }
5411
+
4668
5412
  // src/commands/mcp/add.ts
4669
5413
  var McpAddError = class extends Error {
4670
5414
  code;
@@ -4746,7 +5490,7 @@ async function loadConfigOrDefault2(rootDir) {
4746
5490
  }
4747
5491
 
4748
5492
  // src/core/generator/generate-module.ts
4749
- import path15 from "path";
5493
+ import path16 from "path";
4750
5494
  var moduleTemplates = [
4751
5495
  {
4752
5496
  fileName: "MODULE.md",
@@ -4819,14 +5563,14 @@ Record durable module decisions here.
4819
5563
  ];
4820
5564
  function generateModuleFiles(options) {
4821
5565
  const slug = slugify(options.moduleName);
4822
- const moduleDir = path15.posix.join(options.modulesDir, slug);
5566
+ const moduleDir = path16.posix.join(options.modulesDir, slug);
4823
5567
  const title = titleizeModuleName(options.moduleName);
4824
5568
  const context = createTemplateContext({
4825
5569
  slug,
4826
5570
  title
4827
5571
  });
4828
5572
  return moduleTemplates.map((template) => ({
4829
- path: path15.posix.join(moduleDir, template.fileName),
5573
+ path: path16.posix.join(moduleDir, template.fileName),
4830
5574
  content: renderTemplate(template.content, context)
4831
5575
  }));
4832
5576
  }