recall-os 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -32
- package/dist/cli.js +451 -81
- package/dist/cli.js.map +1 -1
- package/dist/index.js +451 -81
- package/dist/index.js.map +1 -1
- package/examples/generated-flutter/AGENTS.md +28 -0
- package/examples/generated-flutter/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-flutter/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-generic/AGENTS.md +28 -0
- package/examples/generated-generic/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-generic/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-ios-swift/AGENTS.md +28 -0
- package/examples/generated-ios-swift/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-ios-swift/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-kotlin-android/AGENTS.md +28 -0
- package/examples/generated-kotlin-android/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-kotlin-android/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-api/AGENTS.md +28 -0
- package/examples/generated-laravel-api/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-api/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-react/AGENTS.md +28 -0
- package/examples/generated-laravel-react/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-react/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-vue/AGENTS.md +28 -0
- package/examples/generated-laravel-vue/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-vue/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-nextjs/AGENTS.md +28 -0
- package/examples/generated-nextjs/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-nextjs/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-python-fastapi/AGENTS.md +28 -0
- package/examples/generated-python-fastapi/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-python-fastapi/docs/ai/RECALL_COMMANDS.md +13 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -134,8 +134,8 @@ function parseConfig(value) {
|
|
|
134
134
|
if (!result.success) {
|
|
135
135
|
throw new ConfigValidationError(
|
|
136
136
|
result.error.issues.map((issue) => {
|
|
137
|
-
const
|
|
138
|
-
return `${
|
|
137
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
138
|
+
return `${path19}${issue.message}`;
|
|
139
139
|
})
|
|
140
140
|
);
|
|
141
141
|
}
|
|
@@ -800,6 +800,54 @@ function generateAdrFile(options) {
|
|
|
800
800
|
}
|
|
801
801
|
];
|
|
802
802
|
}
|
|
803
|
+
var supersedingAdrTemplate = `# {{adrId}}: {{title}}
|
|
804
|
+
|
|
805
|
+
## Status
|
|
806
|
+
|
|
807
|
+
Accepted
|
|
808
|
+
|
|
809
|
+
## Supersedes
|
|
810
|
+
|
|
811
|
+
- {{supersedesRef}}
|
|
812
|
+
|
|
813
|
+
## Context
|
|
814
|
+
|
|
815
|
+
What changed, and why the previous decision no longer holds?
|
|
816
|
+
|
|
817
|
+
## Decision
|
|
818
|
+
|
|
819
|
+
What is the new decision?
|
|
820
|
+
|
|
821
|
+
## Alternatives Considered
|
|
822
|
+
|
|
823
|
+
What other options were considered?
|
|
824
|
+
|
|
825
|
+
## Consequences
|
|
826
|
+
|
|
827
|
+
What improves, what worsens, and what risks remain?
|
|
828
|
+
|
|
829
|
+
## Related Documents
|
|
830
|
+
|
|
831
|
+
- PRD:
|
|
832
|
+
- Architecture:
|
|
833
|
+
- Security:
|
|
834
|
+
- Feature:
|
|
835
|
+
`;
|
|
836
|
+
function generateSupersedingAdr(options) {
|
|
837
|
+
const slug = slugify(options.title);
|
|
838
|
+
const title = titleizeAdrTitle(options.title);
|
|
839
|
+
const context = createTemplateContext({
|
|
840
|
+
adrId: options.adrId,
|
|
841
|
+
slug,
|
|
842
|
+
title,
|
|
843
|
+
supersedesRef: options.supersedesRef
|
|
844
|
+
});
|
|
845
|
+
return {
|
|
846
|
+
path: path4.posix.join(options.adrDir, `${options.adrId}-${slug}.md`),
|
|
847
|
+
content: renderTemplate(supersedingAdrTemplate, context),
|
|
848
|
+
slug
|
|
849
|
+
};
|
|
850
|
+
}
|
|
803
851
|
function titleizeAdrTitle(title) {
|
|
804
852
|
return title.trim().replace(/[-_]+/gu, " ").replace(/\s+/gu, " ").replace(/\b\w/gu, (character) => character.toUpperCase());
|
|
805
853
|
}
|
|
@@ -890,8 +938,148 @@ async function loadRequiredConfig2(rootDir) {
|
|
|
890
938
|
}
|
|
891
939
|
}
|
|
892
940
|
|
|
893
|
-
// src/
|
|
941
|
+
// src/commands/adr/supersede.ts
|
|
942
|
+
import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
894
943
|
import path5 from "path";
|
|
944
|
+
var AdrSupersedeError = class extends Error {
|
|
945
|
+
code;
|
|
946
|
+
details;
|
|
947
|
+
constructor(code, message, details = []) {
|
|
948
|
+
super(message);
|
|
949
|
+
this.name = "AdrSupersedeError";
|
|
950
|
+
this.code = code;
|
|
951
|
+
this.details = details;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
async function supersedeAdr(options) {
|
|
955
|
+
const oldSlug = createSlug2(options.oldName, "oldName");
|
|
956
|
+
createSlug2(options.newTitle, "newTitle");
|
|
957
|
+
const config = await loadRequiredConfig3(options.rootDir);
|
|
958
|
+
const adrDirAbsolute = resolveSafePath(options.rootDir, config.adrDir).absolutePath;
|
|
959
|
+
const old = await findAcceptedAdr(adrDirAbsolute, oldSlug);
|
|
960
|
+
const next = await getNextAdrNumber(adrDirAbsolute);
|
|
961
|
+
const oldRef = old.fileName.replace(/\.md$/u, "");
|
|
962
|
+
const superseding = generateSupersedingAdr({
|
|
963
|
+
adrDir: config.adrDir,
|
|
964
|
+
adrId: next.id,
|
|
965
|
+
title: options.newTitle,
|
|
966
|
+
supersedesRef: oldRef
|
|
967
|
+
});
|
|
968
|
+
const newRef = `${next.id}-${superseding.slug}`;
|
|
969
|
+
const markedOld = old.content.replace(
|
|
970
|
+
/(##\s+Status\r?\n\r?\n)Accepted[^\n]*/u,
|
|
971
|
+
`$1Accepted \u2014 superseded by ${newRef}`
|
|
972
|
+
);
|
|
973
|
+
const writeNew = await write2(options, superseding.path, superseding.content, options.force);
|
|
974
|
+
const oldRelative = `${config.adrDir}/${old.fileName}`;
|
|
975
|
+
const writeOld = await write2(options, oldRelative, markedOld, true);
|
|
976
|
+
return {
|
|
977
|
+
oldRef,
|
|
978
|
+
oldPath: oldRelative,
|
|
979
|
+
newRef,
|
|
980
|
+
newPath: superseding.path,
|
|
981
|
+
dryRun: options.dryRun ?? false,
|
|
982
|
+
writeResult: {
|
|
983
|
+
created: [...writeNew.created, ...writeOld.created],
|
|
984
|
+
overwritten: [...writeNew.overwritten, ...writeOld.overwritten],
|
|
985
|
+
skipped: [...writeNew.skipped, ...writeOld.skipped],
|
|
986
|
+
dryRun: options.dryRun ?? false
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async function findAcceptedAdr(adrDirAbsolute, slug) {
|
|
991
|
+
const pattern = new RegExp(`^ADR-\\d{4,}-${escapeRegExp2(slug)}\\.md$`, "u");
|
|
992
|
+
let entries;
|
|
993
|
+
try {
|
|
994
|
+
entries = await readdir3(adrDirAbsolute, { withFileTypes: true });
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const nodeError = error;
|
|
997
|
+
if (nodeError.code === "ENOENT") {
|
|
998
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`);
|
|
999
|
+
}
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
const match = entries.find((entry) => entry.isFile() && pattern.test(entry.name));
|
|
1003
|
+
if (match === void 0) {
|
|
1004
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`, [
|
|
1005
|
+
`Looked for an ADR-####-${slug}.md in the ADR directory.`
|
|
1006
|
+
]);
|
|
1007
|
+
}
|
|
1008
|
+
const content = await readFile3(path5.join(adrDirAbsolute, match.name), "utf8");
|
|
1009
|
+
if (!/(##\s+Status\r?\n\r?\n)Accepted\b/u.test(content)) {
|
|
1010
|
+
throw new AdrSupersedeError(
|
|
1011
|
+
"NOT_ACCEPTED",
|
|
1012
|
+
`ADR ${match.name} is not Accepted, so there is nothing to supersede.`,
|
|
1013
|
+
["Only an accepted decision can be superseded. Accept it first with `recall adr accept`."]
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return { fileName: match.name, content };
|
|
1017
|
+
}
|
|
1018
|
+
async function write2(options, relativePath, content, force) {
|
|
1019
|
+
const plan = createWritePlan({
|
|
1020
|
+
rootDir: options.rootDir,
|
|
1021
|
+
files: [{ path: relativePath, content }],
|
|
1022
|
+
force
|
|
1023
|
+
});
|
|
1024
|
+
if (plan.hasErrors) {
|
|
1025
|
+
throw new AdrSupersedeError(
|
|
1026
|
+
"WRITE_PLAN_ERROR",
|
|
1027
|
+
"ADR supersede write plan contains errors.",
|
|
1028
|
+
plan.entries.filter(
|
|
1029
|
+
(entry) => entry.action === "error"
|
|
1030
|
+
).map((entry) => `${entry.path}: ${entry.reason}`)
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
return executeWritePlan(plan, { dryRun: options.dryRun });
|
|
1034
|
+
}
|
|
1035
|
+
function formatAdrSupersedeResult(result) {
|
|
1036
|
+
const lines = [
|
|
1037
|
+
result.dryRun ? "Recall OS ADR supersede dry run complete." : "Recall OS ADR supersede complete.",
|
|
1038
|
+
`Superseded: ${result.oldPath} (now marked superseded by ${result.newRef})`,
|
|
1039
|
+
`New decision: ${result.newPath}`
|
|
1040
|
+
];
|
|
1041
|
+
appendWriteSummary(lines, { dryRun: result.dryRun, writeResult: result.writeResult });
|
|
1042
|
+
if (!result.dryRun) {
|
|
1043
|
+
appendNextSteps(lines, [
|
|
1044
|
+
`Fill ${result.newPath}: Context (what changed), Decision, Alternatives, Consequences.`,
|
|
1045
|
+
`${result.oldRef} stays in history as superseded; update any memory that still relies on it.`,
|
|
1046
|
+
"Run `recall doctor` \u2014 it flags memory that still references the superseded decision."
|
|
1047
|
+
]);
|
|
1048
|
+
}
|
|
1049
|
+
return `${lines.join("\n")}
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
function createSlug2(name, field) {
|
|
1053
|
+
const withoutPrefix = name.replace(/^ADR-(?:PROPOSED-|\d{4,}-)/iu, "");
|
|
1054
|
+
try {
|
|
1055
|
+
return slugify(withoutPrefix);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error instanceof SlugifyError) {
|
|
1058
|
+
throw new AdrSupersedeError("INVALID_ADR_NAME", `Invalid ${field}: ${error.message}`);
|
|
1059
|
+
}
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function escapeRegExp2(value) {
|
|
1064
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1065
|
+
}
|
|
1066
|
+
async function loadRequiredConfig3(rootDir) {
|
|
1067
|
+
try {
|
|
1068
|
+
return await loadConfig(rootDir);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (error instanceof ConfigLoadError || error instanceof ConfigValidationError) {
|
|
1071
|
+
throw new AdrSupersedeError(
|
|
1072
|
+
"CONFIG_REQUIRED",
|
|
1073
|
+
"Recall OS config not found or invalid. Run `recall init` first.",
|
|
1074
|
+
[error.message]
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/core/generator/generate-feature.ts
|
|
1082
|
+
import path6 from "path";
|
|
895
1083
|
var featureTemplates = [
|
|
896
1084
|
{
|
|
897
1085
|
fileName: "PRD.md",
|
|
@@ -1043,7 +1231,7 @@ Pending.
|
|
|
1043
1231
|
];
|
|
1044
1232
|
function generateFeatureFiles(options) {
|
|
1045
1233
|
const slug = slugify(options.featureName);
|
|
1046
|
-
const featureDir =
|
|
1234
|
+
const featureDir = path6.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
|
|
1047
1235
|
const title = titleizeFeatureName(options.featureName);
|
|
1048
1236
|
const context = createTemplateContext({
|
|
1049
1237
|
featureId: options.featureId,
|
|
@@ -1051,7 +1239,7 @@ function generateFeatureFiles(options) {
|
|
|
1051
1239
|
title
|
|
1052
1240
|
});
|
|
1053
1241
|
return featureTemplates.map((template) => ({
|
|
1054
|
-
path:
|
|
1242
|
+
path: path6.posix.join(featureDir, template.fileName),
|
|
1055
1243
|
content: renderTemplate(template.content, context)
|
|
1056
1244
|
}));
|
|
1057
1245
|
}
|
|
@@ -1060,7 +1248,7 @@ function titleizeFeatureName(featureName) {
|
|
|
1060
1248
|
}
|
|
1061
1249
|
|
|
1062
1250
|
// src/core/naming/feature-number.ts
|
|
1063
|
-
import { readdir as
|
|
1251
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1064
1252
|
var FEATURE_FOLDER_PATTERN = /^F-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/u;
|
|
1065
1253
|
async function getFeatureFolderForSlug(featuresDirAbsolutePath, slug) {
|
|
1066
1254
|
const existingFolders = await readExistingFeatureFolders(featuresDirAbsolutePath);
|
|
@@ -1093,7 +1281,7 @@ function formatFeatureNumber(featureNumber) {
|
|
|
1093
1281
|
async function readExistingFeatureFolders(featuresDirAbsolutePath) {
|
|
1094
1282
|
let entries;
|
|
1095
1283
|
try {
|
|
1096
|
-
entries = await
|
|
1284
|
+
entries = await readdir4(featuresDirAbsolutePath, { withFileTypes: true });
|
|
1097
1285
|
} catch (error) {
|
|
1098
1286
|
const nodeError = error;
|
|
1099
1287
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1132,7 +1320,7 @@ var FeatureCreateError = class extends Error {
|
|
|
1132
1320
|
};
|
|
1133
1321
|
async function createFeature(options) {
|
|
1134
1322
|
const slug = createFeatureSlug(options.name);
|
|
1135
|
-
const config = await
|
|
1323
|
+
const config = await loadRequiredConfig4(options.rootDir);
|
|
1136
1324
|
const featuresDirPath = resolveSafePath(options.rootDir, config.featuresDir);
|
|
1137
1325
|
const featureFolder = await getFeatureFolderForSlug(featuresDirPath.absolutePath, slug);
|
|
1138
1326
|
const files = generateFeatureFiles({
|
|
@@ -1191,7 +1379,7 @@ function createFeatureSlug(name) {
|
|
|
1191
1379
|
throw error;
|
|
1192
1380
|
}
|
|
1193
1381
|
}
|
|
1194
|
-
async function
|
|
1382
|
+
async function loadRequiredConfig4(rootDir) {
|
|
1195
1383
|
try {
|
|
1196
1384
|
return await loadConfig(rootDir);
|
|
1197
1385
|
} catch (error) {
|
|
@@ -1232,8 +1420,8 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1232
1420
|
|
|
1233
1421
|
// src/core/adopt/inspect-repo.ts
|
|
1234
1422
|
import { existsSync as existsSync3 } from "fs";
|
|
1235
|
-
import { readFile as
|
|
1236
|
-
import
|
|
1423
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1424
|
+
import path7 from "path";
|
|
1237
1425
|
var FRAMEWORK_SOURCES = {
|
|
1238
1426
|
"Next.js": "package.json",
|
|
1239
1427
|
React: "package.json",
|
|
@@ -1256,7 +1444,7 @@ var FRAMEWORK_SOURCES = {
|
|
|
1256
1444
|
Flutter: "pubspec.yaml"
|
|
1257
1445
|
};
|
|
1258
1446
|
async function inspectRepo(rootDir) {
|
|
1259
|
-
const has = (relativePath) => existsSync3(
|
|
1447
|
+
const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
|
|
1260
1448
|
const languages = /* @__PURE__ */ new Set();
|
|
1261
1449
|
const frameworks = /* @__PURE__ */ new Set();
|
|
1262
1450
|
const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
|
|
@@ -1470,7 +1658,7 @@ async function findTestFile(rootDir) {
|
|
|
1470
1658
|
}
|
|
1471
1659
|
let entries;
|
|
1472
1660
|
try {
|
|
1473
|
-
entries = await
|
|
1661
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1474
1662
|
} catch {
|
|
1475
1663
|
continue;
|
|
1476
1664
|
}
|
|
@@ -1481,10 +1669,10 @@ async function findTestFile(rootDir) {
|
|
|
1481
1669
|
}
|
|
1482
1670
|
if (entry.isDirectory()) {
|
|
1483
1671
|
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1484
|
-
stack.push(
|
|
1672
|
+
stack.push(path7.join(dir, entry.name));
|
|
1485
1673
|
}
|
|
1486
1674
|
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1487
|
-
return
|
|
1675
|
+
return path7.relative(rootDir, path7.join(dir, entry.name));
|
|
1488
1676
|
}
|
|
1489
1677
|
}
|
|
1490
1678
|
}
|
|
@@ -1508,7 +1696,7 @@ async function readJson(rootDir, relativePath) {
|
|
|
1508
1696
|
}
|
|
1509
1697
|
async function readText(rootDir, relativePath) {
|
|
1510
1698
|
try {
|
|
1511
|
-
return await
|
|
1699
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1512
1700
|
} catch {
|
|
1513
1701
|
return "";
|
|
1514
1702
|
}
|
|
@@ -1684,8 +1872,8 @@ function formatList2(values) {
|
|
|
1684
1872
|
|
|
1685
1873
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1686
1874
|
import { existsSync as existsSync4 } from "fs";
|
|
1687
|
-
import { readFile as
|
|
1688
|
-
import
|
|
1875
|
+
import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
|
|
1876
|
+
import path8 from "path";
|
|
1689
1877
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1690
1878
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1691
1879
|
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
@@ -1702,7 +1890,7 @@ async function checkCodeReferences(context) {
|
|
|
1702
1890
|
continue;
|
|
1703
1891
|
}
|
|
1704
1892
|
for (const doc of FEATURE_DOCS) {
|
|
1705
|
-
const relativePath =
|
|
1893
|
+
const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1706
1894
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1707
1895
|
}
|
|
1708
1896
|
}
|
|
@@ -1712,7 +1900,7 @@ async function checkCodeReferences(context) {
|
|
|
1712
1900
|
continue;
|
|
1713
1901
|
}
|
|
1714
1902
|
for (const doc of MODULE_DOCS) {
|
|
1715
|
-
const relativePath =
|
|
1903
|
+
const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1716
1904
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1717
1905
|
}
|
|
1718
1906
|
}
|
|
@@ -1731,7 +1919,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1731
1919
|
continue;
|
|
1732
1920
|
}
|
|
1733
1921
|
seen.add(reference);
|
|
1734
|
-
if (!existsSync4(
|
|
1922
|
+
if (!existsSync4(path8.join(rootDir, reference))) {
|
|
1735
1923
|
findings.push({
|
|
1736
1924
|
severity: "warning",
|
|
1737
1925
|
check: "drift-code-reference",
|
|
@@ -1744,7 +1932,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1744
1932
|
}
|
|
1745
1933
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1746
1934
|
try {
|
|
1747
|
-
return await
|
|
1935
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1748
1936
|
} catch (error) {
|
|
1749
1937
|
const nodeError = error;
|
|
1750
1938
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1755,7 +1943,7 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1755
1943
|
}
|
|
1756
1944
|
async function readFileIfExists(rootDir, relativePath) {
|
|
1757
1945
|
try {
|
|
1758
|
-
return await
|
|
1946
|
+
return await readFile5(path8.join(rootDir, relativePath), "utf8");
|
|
1759
1947
|
} catch (error) {
|
|
1760
1948
|
const nodeError = error;
|
|
1761
1949
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1766,12 +1954,12 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1766
1954
|
}
|
|
1767
1955
|
|
|
1768
1956
|
// src/core/doctor/checks/config-check.ts
|
|
1769
|
-
import { readFile as
|
|
1957
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1770
1958
|
async function checkConfig(rootDir) {
|
|
1771
1959
|
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1772
1960
|
let rawConfig;
|
|
1773
1961
|
try {
|
|
1774
|
-
rawConfig = await
|
|
1962
|
+
rawConfig = await readFile6(configPath.absolutePath, "utf8");
|
|
1775
1963
|
} catch (error) {
|
|
1776
1964
|
const nodeError = error;
|
|
1777
1965
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1834,8 +2022,8 @@ async function checkConfig(rootDir) {
|
|
|
1834
2022
|
}
|
|
1835
2023
|
|
|
1836
2024
|
// src/core/doctor/checks/content-check.ts
|
|
1837
|
-
import { readFile as
|
|
1838
|
-
import
|
|
2025
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
2026
|
+
import path9 from "path";
|
|
1839
2027
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1840
2028
|
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1841
2029
|
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
@@ -1850,7 +2038,7 @@ async function checkContent(context) {
|
|
|
1850
2038
|
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1851
2039
|
);
|
|
1852
2040
|
for (const folder of featureFolders) {
|
|
1853
|
-
const prdPath =
|
|
2041
|
+
const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1854
2042
|
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1855
2043
|
if (prd === void 0) {
|
|
1856
2044
|
continue;
|
|
@@ -1883,7 +2071,7 @@ async function checkContent(context) {
|
|
|
1883
2071
|
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
1884
2072
|
}
|
|
1885
2073
|
for (const folder of moduleFolders) {
|
|
1886
|
-
const modulePath =
|
|
2074
|
+
const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1887
2075
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1888
2076
|
if (moduleDoc === void 0) {
|
|
1889
2077
|
continue;
|
|
@@ -1961,7 +2149,7 @@ function getSection(content, heading) {
|
|
|
1961
2149
|
}
|
|
1962
2150
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1963
2151
|
try {
|
|
1964
|
-
return await
|
|
2152
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1965
2153
|
} catch (error) {
|
|
1966
2154
|
const nodeError = error;
|
|
1967
2155
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1972,7 +2160,7 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1972
2160
|
}
|
|
1973
2161
|
async function readFileIfExists2(rootDir, relativePath) {
|
|
1974
2162
|
try {
|
|
1975
|
-
return await
|
|
2163
|
+
return await readFile7(path9.join(rootDir, relativePath), "utf8");
|
|
1976
2164
|
} catch (error) {
|
|
1977
2165
|
const nodeError = error;
|
|
1978
2166
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1983,8 +2171,8 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1983
2171
|
}
|
|
1984
2172
|
|
|
1985
2173
|
// src/core/doctor/checks/drift-check.ts
|
|
1986
|
-
import { readFile as
|
|
1987
|
-
import
|
|
2174
|
+
import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2175
|
+
import path10 from "path";
|
|
1988
2176
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1989
2177
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1990
2178
|
async function checkDrift(context) {
|
|
@@ -2001,12 +2189,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
2001
2189
|
const known = /* @__PURE__ */ new Map();
|
|
2002
2190
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
2003
2191
|
for (const file of files) {
|
|
2004
|
-
const match = adrFilePattern.exec(
|
|
2192
|
+
const match = adrFilePattern.exec(path10.basename(file));
|
|
2005
2193
|
if (match === null) {
|
|
2006
2194
|
continue;
|
|
2007
2195
|
}
|
|
2008
2196
|
const id = `ADR-${match[1]}`;
|
|
2009
|
-
const content = await
|
|
2197
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2010
2198
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
2011
2199
|
const existing = known.get(id);
|
|
2012
2200
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -2019,7 +2207,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
2019
2207
|
const findings = [];
|
|
2020
2208
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
2021
2209
|
for (const file of files) {
|
|
2022
|
-
const content = await
|
|
2210
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2023
2211
|
const referenced = /* @__PURE__ */ new Set();
|
|
2024
2212
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
2025
2213
|
referenced.add(match[0].toUpperCase());
|
|
@@ -2054,7 +2242,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
|
|
|
2054
2242
|
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
2055
2243
|
const files = [];
|
|
2056
2244
|
for (const entry of entries) {
|
|
2057
|
-
const childRelative =
|
|
2245
|
+
const childRelative = path10.posix.join(relativeDir, entry.name);
|
|
2058
2246
|
if (entry.isDirectory()) {
|
|
2059
2247
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
2060
2248
|
continue;
|
|
@@ -2087,7 +2275,7 @@ function getSection2(content, heading) {
|
|
|
2087
2275
|
}
|
|
2088
2276
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
2089
2277
|
try {
|
|
2090
|
-
return await
|
|
2278
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
2091
2279
|
} catch (error) {
|
|
2092
2280
|
const nodeError = error;
|
|
2093
2281
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2098,8 +2286,8 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
2098
2286
|
}
|
|
2099
2287
|
|
|
2100
2288
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
2101
|
-
import { lstat as lstat2, readFile as
|
|
2102
|
-
import
|
|
2289
|
+
import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2290
|
+
import path11 from "path";
|
|
2103
2291
|
|
|
2104
2292
|
// src/core/adr/adr-sections.ts
|
|
2105
2293
|
var REQUIRED_ADR_SECTIONS = [
|
|
@@ -2163,7 +2351,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
2163
2351
|
);
|
|
2164
2352
|
for (const featureFolder of featureFolders) {
|
|
2165
2353
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
2166
|
-
const filePath =
|
|
2354
|
+
const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
2167
2355
|
if (!await isFile(rootDir, filePath)) {
|
|
2168
2356
|
findings.push({
|
|
2169
2357
|
severity: "error",
|
|
@@ -2187,7 +2375,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
2187
2375
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
2188
2376
|
for (const moduleFolder of moduleFolders) {
|
|
2189
2377
|
for (const requiredDoc of requiredModuleDocs) {
|
|
2190
|
-
const filePath =
|
|
2378
|
+
const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
2191
2379
|
if (!await isFile(rootDir, filePath)) {
|
|
2192
2380
|
findings.push({
|
|
2193
2381
|
severity: "error",
|
|
@@ -2210,8 +2398,8 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2210
2398
|
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
2211
2399
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
2212
2400
|
for (const adrFile of adrFiles) {
|
|
2213
|
-
const filePath =
|
|
2214
|
-
const content = await
|
|
2401
|
+
const filePath = path11.posix.join(adrDir, adrFile.name);
|
|
2402
|
+
const content = await readFile9(path11.join(rootDir, filePath), "utf8");
|
|
2215
2403
|
for (const requiredSection of requiredAdrSections) {
|
|
2216
2404
|
if (!content.includes(requiredSection)) {
|
|
2217
2405
|
findings.push({
|
|
@@ -2232,7 +2420,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2232
2420
|
}
|
|
2233
2421
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2234
2422
|
try {
|
|
2235
|
-
return await
|
|
2423
|
+
return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
|
|
2236
2424
|
} catch (error) {
|
|
2237
2425
|
const nodeError = error;
|
|
2238
2426
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2243,7 +2431,7 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2243
2431
|
}
|
|
2244
2432
|
async function isFile(rootDir, relativePath) {
|
|
2245
2433
|
try {
|
|
2246
|
-
return (await lstat2(
|
|
2434
|
+
return (await lstat2(path11.join(rootDir, relativePath))).isFile();
|
|
2247
2435
|
} catch (error) {
|
|
2248
2436
|
const nodeError = error;
|
|
2249
2437
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2255,7 +2443,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
2255
2443
|
|
|
2256
2444
|
// src/core/doctor/checks/required-files-check.ts
|
|
2257
2445
|
import { lstat as lstat3 } from "fs/promises";
|
|
2258
|
-
import
|
|
2446
|
+
import path12 from "path";
|
|
2259
2447
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
2260
2448
|
var requiredDocs = [
|
|
2261
2449
|
"00-product/PRD.md",
|
|
@@ -2282,12 +2470,12 @@ async function checkRequiredFiles(context) {
|
|
|
2282
2470
|
}
|
|
2283
2471
|
}
|
|
2284
2472
|
for (const relativeDocPath of requiredDocs) {
|
|
2285
|
-
const filePath =
|
|
2473
|
+
const filePath = path12.posix.join(docsDir, relativeDocPath);
|
|
2286
2474
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
2287
2475
|
findings.push(missingFile(filePath, "required-docs"));
|
|
2288
2476
|
}
|
|
2289
2477
|
}
|
|
2290
|
-
const adrIndexPath =
|
|
2478
|
+
const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
2291
2479
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
2292
2480
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
2293
2481
|
}
|
|
@@ -2313,7 +2501,7 @@ async function checkRequiredFiles(context) {
|
|
|
2313
2501
|
}
|
|
2314
2502
|
async function isFile2(rootDir, relativePath) {
|
|
2315
2503
|
try {
|
|
2316
|
-
return (await lstat3(
|
|
2504
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isFile();
|
|
2317
2505
|
} catch (error) {
|
|
2318
2506
|
const nodeError = error;
|
|
2319
2507
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2324,7 +2512,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
2324
2512
|
}
|
|
2325
2513
|
async function isDirectory(rootDir, relativePath) {
|
|
2326
2514
|
try {
|
|
2327
|
-
return (await lstat3(
|
|
2515
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
|
|
2328
2516
|
} catch (error) {
|
|
2329
2517
|
const nodeError = error;
|
|
2330
2518
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2343,8 +2531,8 @@ function missingFile(pathValue, check) {
|
|
|
2343
2531
|
}
|
|
2344
2532
|
|
|
2345
2533
|
// src/core/doctor/checks/standards-check.ts
|
|
2346
|
-
import { lstat as lstat4, readFile as
|
|
2347
|
-
import
|
|
2534
|
+
import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
|
|
2535
|
+
import path13 from "path";
|
|
2348
2536
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2349
2537
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2350
2538
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
@@ -2364,10 +2552,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2364
2552
|
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2365
2553
|
);
|
|
2366
2554
|
for (const featureFolder of featureFolders) {
|
|
2367
|
-
const featureDir =
|
|
2368
|
-
const completionReportPath =
|
|
2369
|
-
const reviewPath =
|
|
2370
|
-
const architectureImpactPath =
|
|
2555
|
+
const featureDir = path13.posix.join(featuresDir, featureFolder.name);
|
|
2556
|
+
const completionReportPath = path13.posix.join(featureDir, "COMPLETION_REPORT.md");
|
|
2557
|
+
const reviewPath = path13.posix.join(featureDir, "REVIEW.md");
|
|
2558
|
+
const architectureImpactPath = path13.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
|
|
2371
2559
|
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2372
2560
|
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2373
2561
|
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
@@ -2417,8 +2605,8 @@ async function checkAdrStandards(rootDir, adrDir) {
|
|
|
2417
2605
|
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2418
2606
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2419
2607
|
for (const adrFile of adrFiles) {
|
|
2420
|
-
const adrPath =
|
|
2421
|
-
const content = await
|
|
2608
|
+
const adrPath = path13.posix.join(adrDir, adrFile.name);
|
|
2609
|
+
const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
|
|
2422
2610
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2423
2611
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2424
2612
|
findings.push({
|
|
@@ -2502,7 +2690,7 @@ function isPlaceholder(value) {
|
|
|
2502
2690
|
}
|
|
2503
2691
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2504
2692
|
try {
|
|
2505
|
-
return await
|
|
2693
|
+
return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
|
|
2506
2694
|
} catch (error) {
|
|
2507
2695
|
const nodeError = error;
|
|
2508
2696
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2516,7 +2704,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2516
2704
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2517
2705
|
return void 0;
|
|
2518
2706
|
}
|
|
2519
|
-
return await
|
|
2707
|
+
return await readFile10(path13.join(rootDir, relativePath), "utf8");
|
|
2520
2708
|
} catch (error) {
|
|
2521
2709
|
const nodeError = error;
|
|
2522
2710
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2527,7 +2715,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2527
2715
|
}
|
|
2528
2716
|
async function isFile3(rootDir, relativePath) {
|
|
2529
2717
|
try {
|
|
2530
|
-
return (await lstat4(
|
|
2718
|
+
return (await lstat4(path13.join(rootDir, relativePath))).isFile();
|
|
2531
2719
|
} catch (error) {
|
|
2532
2720
|
const nodeError = error;
|
|
2533
2721
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2537,6 +2725,110 @@ async function isFile3(rootDir, relativePath) {
|
|
|
2537
2725
|
}
|
|
2538
2726
|
}
|
|
2539
2727
|
|
|
2728
|
+
// src/core/doctor/checks/superseded-check.ts
|
|
2729
|
+
import { readFile as readFile11, readdir as readdir11 } from "fs/promises";
|
|
2730
|
+
import path14 from "path";
|
|
2731
|
+
var adrFilePattern4 = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
2732
|
+
var adrReferencePattern2 = /ADR-\d{4,}/giu;
|
|
2733
|
+
async function checkSuperseded(context) {
|
|
2734
|
+
if (context.config === void 0) {
|
|
2735
|
+
return [];
|
|
2736
|
+
}
|
|
2737
|
+
const supersededIds = await loadSupersededAdrIds(context.rootDir, context.config.adrDir);
|
|
2738
|
+
if (supersededIds.size === 0) {
|
|
2739
|
+
return [];
|
|
2740
|
+
}
|
|
2741
|
+
const findings = [];
|
|
2742
|
+
findings.push(
|
|
2743
|
+
...await checkReferences2(context.rootDir, context.config.featuresDir, supersededIds)
|
|
2744
|
+
);
|
|
2745
|
+
findings.push(
|
|
2746
|
+
...await checkReferences2(context.rootDir, context.config.modulesDir, supersededIds)
|
|
2747
|
+
);
|
|
2748
|
+
return findings;
|
|
2749
|
+
}
|
|
2750
|
+
async function loadSupersededAdrIds(rootDir, adrDir) {
|
|
2751
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2752
|
+
const files = await readMarkdownFiles2(rootDir, adrDir);
|
|
2753
|
+
for (const file of files) {
|
|
2754
|
+
const match = adrFilePattern4.exec(path14.basename(file));
|
|
2755
|
+
if (match === null) {
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2759
|
+
if (statusContains(content, /superseded\s+by/iu)) {
|
|
2760
|
+
superseded.add(`ADR-${match[1]}`.toUpperCase());
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return superseded;
|
|
2764
|
+
}
|
|
2765
|
+
async function checkReferences2(rootDir, referenceDir, supersededIds) {
|
|
2766
|
+
const findings = [];
|
|
2767
|
+
const files = await readMarkdownFiles2(rootDir, referenceDir);
|
|
2768
|
+
for (const file of files) {
|
|
2769
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2770
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2771
|
+
for (const match of stripCode2(content).matchAll(adrReferencePattern2)) {
|
|
2772
|
+
referenced.add(match[0].toUpperCase());
|
|
2773
|
+
}
|
|
2774
|
+
for (const id of referenced) {
|
|
2775
|
+
if (supersededIds.has(id)) {
|
|
2776
|
+
findings.push({
|
|
2777
|
+
severity: "warning",
|
|
2778
|
+
check: "superseded-reference",
|
|
2779
|
+
message: `Repository memory references ${id}, which has been superseded \u2014 update it to the current decision.`,
|
|
2780
|
+
path: file
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return findings;
|
|
2786
|
+
}
|
|
2787
|
+
function statusContains(content, pattern) {
|
|
2788
|
+
const lines = content.split(/\r?\n/u);
|
|
2789
|
+
const startIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## status");
|
|
2790
|
+
if (startIndex === -1) {
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
const body = [];
|
|
2794
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
2795
|
+
if (/^##\s+/u.test(lines[index])) {
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
body.push(lines[index]);
|
|
2799
|
+
}
|
|
2800
|
+
return pattern.test(body.join("\n"));
|
|
2801
|
+
}
|
|
2802
|
+
function stripCode2(content) {
|
|
2803
|
+
return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
|
|
2804
|
+
}
|
|
2805
|
+
async function readMarkdownFiles2(rootDir, relativeDir) {
|
|
2806
|
+
const entries = await readDirIfExists6(rootDir, relativeDir);
|
|
2807
|
+
const files = [];
|
|
2808
|
+
for (const entry of entries) {
|
|
2809
|
+
const childRelative = path14.posix.join(relativeDir, entry.name);
|
|
2810
|
+
if (entry.isDirectory()) {
|
|
2811
|
+
files.push(...await readMarkdownFiles2(rootDir, childRelative));
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
2815
|
+
files.push(childRelative);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return files;
|
|
2819
|
+
}
|
|
2820
|
+
async function readDirIfExists6(rootDir, relativePath) {
|
|
2821
|
+
try {
|
|
2822
|
+
return await readdir11(path14.join(rootDir, relativePath), { withFileTypes: true });
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
const nodeError = error;
|
|
2825
|
+
if (nodeError.code === "ENOENT") {
|
|
2826
|
+
return [];
|
|
2827
|
+
}
|
|
2828
|
+
throw error;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2540
2832
|
// src/core/doctor/doctor-check.ts
|
|
2541
2833
|
async function runDoctor(rootDir) {
|
|
2542
2834
|
const findings = [];
|
|
@@ -2553,6 +2845,7 @@ async function runDoctor(rootDir) {
|
|
|
2553
2845
|
findings.push(...await checkDrift(context));
|
|
2554
2846
|
findings.push(...await checkContent(context));
|
|
2555
2847
|
findings.push(...await checkCodeReferences(context));
|
|
2848
|
+
findings.push(...await checkSuperseded(context));
|
|
2556
2849
|
}
|
|
2557
2850
|
return createDoctorReport(findings);
|
|
2558
2851
|
}
|
|
@@ -2630,10 +2923,10 @@ function formatDoctorResult(result) {
|
|
|
2630
2923
|
|
|
2631
2924
|
// src/commands/init.ts
|
|
2632
2925
|
import { existsSync as existsSync6 } from "fs";
|
|
2633
|
-
import
|
|
2926
|
+
import path17 from "path";
|
|
2634
2927
|
|
|
2635
2928
|
// src/core/generator/generate-init.ts
|
|
2636
|
-
import
|
|
2929
|
+
import path15 from "path";
|
|
2637
2930
|
var neutralTemplates = [
|
|
2638
2931
|
{
|
|
2639
2932
|
path: "AGENTS.md",
|
|
@@ -2652,6 +2945,32 @@ Required reading:
|
|
|
2652
2945
|
- \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
|
|
2653
2946
|
|
|
2654
2947
|
Repository rules override model preferences. If instructions conflict, stop and report the conflict.
|
|
2948
|
+
|
|
2949
|
+
## Recall OS commands
|
|
2950
|
+
|
|
2951
|
+
This repository is maintained with the Recall OS CLI. Use these commands yourself as you work \u2014 do not
|
|
2952
|
+
ask the human to run them, and do not search the web for them (this is a project-local tool):
|
|
2953
|
+
|
|
2954
|
+
- \`recall doctor\` \u2014 validate repository memory; run it before claiming any work is complete.
|
|
2955
|
+
- \`recall feature create <name>\` \u2014 scaffold feature memory before non-trivial feature work.
|
|
2956
|
+
- \`recall adr create <title>\` \u2014 propose a decision; \`recall adr accept <name>\` accepts it.
|
|
2957
|
+
- \`recall adr supersede <old> <new-title>\` \u2014 record a changed decision (never overwrite an accepted ADR).
|
|
2958
|
+
- \`recall module create <name>\` \u2014 scaffold module memory for a new responsibility boundary.
|
|
2959
|
+
- \`recall mcp add <server>\` \u2014 capture an MCP tool's context into memory, offline.
|
|
2960
|
+
|
|
2961
|
+
Full command reference: \`docs/ai/RECALL_COMMANDS.md\`.
|
|
2962
|
+
|
|
2963
|
+
## Changing an accepted decision
|
|
2964
|
+
|
|
2965
|
+
Before changing anything an accepted ADR governs (framework, database, auth, API shape, and similar):
|
|
2966
|
+
|
|
2967
|
+
1. Check \`docs/adrs/\` for an accepted ADR that covers it.
|
|
2968
|
+
2. If your change contradicts one, stop and confirm with a human first \u2014 do not silently change the
|
|
2969
|
+
code and leave the ADR saying the opposite.
|
|
2970
|
+
3. Record the change as a new decision with \`recall adr supersede <old> <new-title>\`. That supersedes
|
|
2971
|
+
the old ADR instead of overwriting history, so the reasoning stays auditable.
|
|
2972
|
+
|
|
2973
|
+
Repository memory is only trustworthy if decisions change through this trail, not silently.
|
|
2655
2974
|
`
|
|
2656
2975
|
},
|
|
2657
2976
|
{
|
|
@@ -2688,10 +3007,17 @@ Before non-trivial work:
|
|
|
2688
3007
|
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2689
3008
|
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2690
3009
|
- If an instruction conflicts with accepted repository memory, stop and report the conflict.
|
|
3010
|
+
- Before changing what an accepted ADR governs, confirm with a human and record it with
|
|
3011
|
+
\`recall adr supersede <old> <new-title>\` \u2014 never silently contradict an accepted decision.
|
|
2691
3012
|
|
|
2692
3013
|
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2693
3014
|
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
2694
3015
|
|
|
3016
|
+
Recall OS commands \u2014 use these yourself (do not web-search this project-local CLI): \`recall doctor\`,
|
|
3017
|
+
\`recall feature create <name>\`, \`recall adr create <title>\` then \`recall adr accept <name>\`,
|
|
3018
|
+
\`recall adr supersede <old> <new-title>\`, \`recall module create <name>\`, \`recall mcp add <server>\`.
|
|
3019
|
+
Full reference: \`docs/ai/RECALL_COMMANDS.md\`.
|
|
3020
|
+
|
|
2695
3021
|
Before claiming work is complete, run \`recall doctor\` and fix reported errors.
|
|
2696
3022
|
`
|
|
2697
3023
|
},
|
|
@@ -2896,6 +3222,15 @@ Baseline rules:
|
|
|
2896
3222
|
AI agents must follow repository memory over model preference.
|
|
2897
3223
|
|
|
2898
3224
|
If a request conflicts with accepted repository memory or engineering standards, stop and report the conflict.
|
|
3225
|
+
|
|
3226
|
+
## Changing an accepted decision
|
|
3227
|
+
|
|
3228
|
+
When work would change something an accepted ADR governs:
|
|
3229
|
+
|
|
3230
|
+
1. Find the accepted ADR in \`docs/adrs/\` that covers it.
|
|
3231
|
+
2. If the change contradicts it, stop and confirm with a human before changing the code.
|
|
3232
|
+
3. Record the new decision with \`recall adr supersede <old> <new-title>\` so the old ADR is marked
|
|
3233
|
+
superseded and the reasoning is preserved, instead of silently editing or contradicting it.
|
|
2899
3234
|
`
|
|
2900
3235
|
},
|
|
2901
3236
|
{
|
|
@@ -3029,6 +3364,17 @@ Options:
|
|
|
3029
3364
|
- \`--dry-run\`: show planned writes without writing files.
|
|
3030
3365
|
- \`--force\`: overwrite existing files explicitly.
|
|
3031
3366
|
|
|
3367
|
+
### \`recall adr supersede <old> <new-title>\`
|
|
3368
|
+
|
|
3369
|
+
Record a changed decision. Marks an accepted ADR as \`Accepted \u2014 superseded by ADR-####\` and creates a
|
|
3370
|
+
new accepted ADR that declares what it supersedes, so the reasoning trail stays auditable instead of
|
|
3371
|
+
being overwritten. Doctor then warns about any memory still referencing the superseded decision.
|
|
3372
|
+
|
|
3373
|
+
Options:
|
|
3374
|
+
|
|
3375
|
+
- \`--dry-run\`: show planned writes without writing files.
|
|
3376
|
+
- \`--force\`: overwrite existing files explicitly.
|
|
3377
|
+
|
|
3032
3378
|
### \`recall module create <name>\`
|
|
3033
3379
|
|
|
3034
3380
|
Create module memory docs under the configured modules directory.
|
|
@@ -3044,7 +3390,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
|
|
|
3044
3390
|
engineering evidence is present, and whether memory references decisions that exist and are accepted.
|
|
3045
3391
|
|
|
3046
3392
|
Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
|
|
3047
|
-
is an error,
|
|
3393
|
+
is an error, memory that references a not-yet-accepted ADR is a warning, and memory that still
|
|
3394
|
+
references a superseded decision is a warning.
|
|
3048
3395
|
|
|
3049
3396
|
Exit codes:
|
|
3050
3397
|
|
|
@@ -3131,7 +3478,7 @@ jobs:
|
|
|
3131
3478
|
}
|
|
3132
3479
|
];
|
|
3133
3480
|
function generateInitFiles(options) {
|
|
3134
|
-
const repositoryName =
|
|
3481
|
+
const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
|
|
3135
3482
|
const context = createTemplateContext({ repositoryName });
|
|
3136
3483
|
const files = neutralTemplates.map((template) => ({
|
|
3137
3484
|
path: template.path,
|
|
@@ -3158,17 +3505,17 @@ function generatePresetFiles(preset) {
|
|
|
3158
3505
|
|
|
3159
3506
|
// src/core/hooks/detect-gates.ts
|
|
3160
3507
|
import { existsSync as existsSync5 } from "fs";
|
|
3161
|
-
import { readFile as
|
|
3162
|
-
import
|
|
3508
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3509
|
+
import path16 from "path";
|
|
3163
3510
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
3164
3511
|
async function detectPreCommitGates(rootDir) {
|
|
3165
|
-
const packageJsonPath =
|
|
3512
|
+
const packageJsonPath = path16.join(rootDir, "package.json");
|
|
3166
3513
|
if (!existsSync5(packageJsonPath)) {
|
|
3167
3514
|
return [];
|
|
3168
3515
|
}
|
|
3169
3516
|
let scripts;
|
|
3170
3517
|
try {
|
|
3171
|
-
const raw = await
|
|
3518
|
+
const raw = await readFile12(packageJsonPath, "utf8");
|
|
3172
3519
|
const parsed = JSON.parse(raw);
|
|
3173
3520
|
scripts = parsed.scripts ?? {};
|
|
3174
3521
|
} catch {
|
|
@@ -3183,10 +3530,10 @@ async function detectPreCommitGates(rootDir) {
|
|
|
3183
3530
|
);
|
|
3184
3531
|
}
|
|
3185
3532
|
function detectPackageManager2(rootDir) {
|
|
3186
|
-
if (existsSync5(
|
|
3533
|
+
if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
|
|
3187
3534
|
return "pnpm";
|
|
3188
3535
|
}
|
|
3189
|
-
if (existsSync5(
|
|
3536
|
+
if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
|
|
3190
3537
|
return "yarn";
|
|
3191
3538
|
}
|
|
3192
3539
|
return "npm";
|
|
@@ -3206,7 +3553,7 @@ function renderSessionStartHook() {
|
|
|
3206
3553
|
adrs=$(ls docs/adrs/ADR-*.md 2>/dev/null | sed 's|.*/||;s|\\.md$||' | tr '\\n' ' ')
|
|
3207
3554
|
modules=$(ls -d docs/30-modules/*/ 2>/dev/null | sed 's|docs/30-modules/||;s|/$||' | tr '\\n' ' ')
|
|
3208
3555
|
|
|
3209
|
-
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."
|
|
3556
|
+
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}. Use the Recall OS CLI commands listed in AGENTS.md (recall feature/adr/module create, recall adr accept and supersede, recall doctor) yourself; do not web-search them. Run 'recall doctor' before claiming work complete."
|
|
3210
3557
|
|
|
3211
3558
|
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$context"
|
|
3212
3559
|
`;
|
|
@@ -4691,8 +5038,8 @@ function parsePreset(value) {
|
|
|
4691
5038
|
if (!result.success) {
|
|
4692
5039
|
throw new PresetValidationError(
|
|
4693
5040
|
result.error.issues.map((issue) => {
|
|
4694
|
-
const
|
|
4695
|
-
return `${
|
|
5041
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
5042
|
+
return `${path19}${issue.message}`;
|
|
4696
5043
|
})
|
|
4697
5044
|
);
|
|
4698
5045
|
}
|
|
@@ -5381,7 +5728,7 @@ var InitError = class extends Error {
|
|
|
5381
5728
|
}
|
|
5382
5729
|
};
|
|
5383
5730
|
async function initProject(options) {
|
|
5384
|
-
if (options.force === true && options.reinit !== true && existsSync6(
|
|
5731
|
+
if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
|
|
5385
5732
|
throw new InitError(
|
|
5386
5733
|
"EXISTING_INSTALLATION",
|
|
5387
5734
|
"Refusing to re-initialize an existing Recall OS installation.",
|
|
@@ -5745,7 +6092,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
5745
6092
|
}
|
|
5746
6093
|
|
|
5747
6094
|
// src/core/generator/generate-module.ts
|
|
5748
|
-
import
|
|
6095
|
+
import path18 from "path";
|
|
5749
6096
|
var moduleTemplates = [
|
|
5750
6097
|
{
|
|
5751
6098
|
fileName: "MODULE.md",
|
|
@@ -5818,14 +6165,14 @@ Record durable module decisions here.
|
|
|
5818
6165
|
];
|
|
5819
6166
|
function generateModuleFiles(options) {
|
|
5820
6167
|
const slug = slugify(options.moduleName);
|
|
5821
|
-
const moduleDir =
|
|
6168
|
+
const moduleDir = path18.posix.join(options.modulesDir, slug);
|
|
5822
6169
|
const title = titleizeModuleName(options.moduleName);
|
|
5823
6170
|
const context = createTemplateContext({
|
|
5824
6171
|
slug,
|
|
5825
6172
|
title
|
|
5826
6173
|
});
|
|
5827
6174
|
return moduleTemplates.map((template) => ({
|
|
5828
|
-
path:
|
|
6175
|
+
path: path18.posix.join(moduleDir, template.fileName),
|
|
5829
6176
|
content: renderTemplate(template.content, context)
|
|
5830
6177
|
}));
|
|
5831
6178
|
}
|
|
@@ -5846,7 +6193,7 @@ var ModuleCreateError = class extends Error {
|
|
|
5846
6193
|
};
|
|
5847
6194
|
async function createModule(options) {
|
|
5848
6195
|
const slug = createModuleSlug(options.name);
|
|
5849
|
-
const config = await
|
|
6196
|
+
const config = await loadRequiredConfig5(options.rootDir);
|
|
5850
6197
|
const files = generateModuleFiles({
|
|
5851
6198
|
modulesDir: config.modulesDir,
|
|
5852
6199
|
moduleName: options.name
|
|
@@ -5900,7 +6247,7 @@ function createModuleSlug(name) {
|
|
|
5900
6247
|
throw error;
|
|
5901
6248
|
}
|
|
5902
6249
|
}
|
|
5903
|
-
async function
|
|
6250
|
+
async function loadRequiredConfig5(rootDir) {
|
|
5904
6251
|
try {
|
|
5905
6252
|
return await loadConfig(rootDir);
|
|
5906
6253
|
} catch (error) {
|
|
@@ -6074,6 +6421,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
|
|
|
6074
6421
|
});
|
|
6075
6422
|
stdout.write(formatAdrAcceptResult(result));
|
|
6076
6423
|
});
|
|
6424
|
+
adrCommand.command("supersede").description(
|
|
6425
|
+
"Record a changed decision: mark an accepted ADR superseded by a new accepted ADR."
|
|
6426
|
+
).argument("<old>", "Accepted ADR name or slug being superseded, e.g. database-postgres.").argument("<new-title>", "Title of the new decision that replaces it.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(
|
|
6427
|
+
async (oldName, newTitle, options) => {
|
|
6428
|
+
const result = await supersedeAdr({
|
|
6429
|
+
rootDir: cwd,
|
|
6430
|
+
oldName,
|
|
6431
|
+
newTitle,
|
|
6432
|
+
dryRun: options.dryRun,
|
|
6433
|
+
force: options.force
|
|
6434
|
+
});
|
|
6435
|
+
stdout.write(formatAdrSupersedeResult(result));
|
|
6436
|
+
}
|
|
6437
|
+
);
|
|
6077
6438
|
const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
|
|
6078
6439
|
moduleCommand.command("create").description("Create module memory docs.").argument("<name>", "Module name.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(async (name, options) => {
|
|
6079
6440
|
const result = await createModule({
|
|
@@ -6167,6 +6528,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
6167
6528
|
`);
|
|
6168
6529
|
for (const detail of error.details) {
|
|
6169
6530
|
stderr.write(`- ${detail}
|
|
6531
|
+
`);
|
|
6532
|
+
}
|
|
6533
|
+
return 1;
|
|
6534
|
+
}
|
|
6535
|
+
if (error instanceof AdrSupersedeError) {
|
|
6536
|
+
stderr.write(`${error.message}
|
|
6537
|
+
`);
|
|
6538
|
+
for (const detail of error.details) {
|
|
6539
|
+
stderr.write(`- ${detail}
|
|
6170
6540
|
`);
|
|
6171
6541
|
}
|
|
6172
6542
|
return 1;
|