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/index.js
CHANGED
|
@@ -132,8 +132,8 @@ function parseConfig(value) {
|
|
|
132
132
|
if (!result.success) {
|
|
133
133
|
throw new ConfigValidationError(
|
|
134
134
|
result.error.issues.map((issue) => {
|
|
135
|
-
const
|
|
136
|
-
return `${
|
|
135
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
136
|
+
return `${path19}${issue.message}`;
|
|
137
137
|
})
|
|
138
138
|
);
|
|
139
139
|
}
|
|
@@ -798,6 +798,54 @@ function generateAdrFile(options) {
|
|
|
798
798
|
}
|
|
799
799
|
];
|
|
800
800
|
}
|
|
801
|
+
var supersedingAdrTemplate = `# {{adrId}}: {{title}}
|
|
802
|
+
|
|
803
|
+
## Status
|
|
804
|
+
|
|
805
|
+
Accepted
|
|
806
|
+
|
|
807
|
+
## Supersedes
|
|
808
|
+
|
|
809
|
+
- {{supersedesRef}}
|
|
810
|
+
|
|
811
|
+
## Context
|
|
812
|
+
|
|
813
|
+
What changed, and why the previous decision no longer holds?
|
|
814
|
+
|
|
815
|
+
## Decision
|
|
816
|
+
|
|
817
|
+
What is the new decision?
|
|
818
|
+
|
|
819
|
+
## Alternatives Considered
|
|
820
|
+
|
|
821
|
+
What other options were considered?
|
|
822
|
+
|
|
823
|
+
## Consequences
|
|
824
|
+
|
|
825
|
+
What improves, what worsens, and what risks remain?
|
|
826
|
+
|
|
827
|
+
## Related Documents
|
|
828
|
+
|
|
829
|
+
- PRD:
|
|
830
|
+
- Architecture:
|
|
831
|
+
- Security:
|
|
832
|
+
- Feature:
|
|
833
|
+
`;
|
|
834
|
+
function generateSupersedingAdr(options) {
|
|
835
|
+
const slug = slugify(options.title);
|
|
836
|
+
const title = titleizeAdrTitle(options.title);
|
|
837
|
+
const context = createTemplateContext({
|
|
838
|
+
adrId: options.adrId,
|
|
839
|
+
slug,
|
|
840
|
+
title,
|
|
841
|
+
supersedesRef: options.supersedesRef
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
path: path4.posix.join(options.adrDir, `${options.adrId}-${slug}.md`),
|
|
845
|
+
content: renderTemplate(supersedingAdrTemplate, context),
|
|
846
|
+
slug
|
|
847
|
+
};
|
|
848
|
+
}
|
|
801
849
|
function titleizeAdrTitle(title) {
|
|
802
850
|
return title.trim().replace(/[-_]+/gu, " ").replace(/\s+/gu, " ").replace(/\b\w/gu, (character) => character.toUpperCase());
|
|
803
851
|
}
|
|
@@ -888,8 +936,148 @@ async function loadRequiredConfig2(rootDir) {
|
|
|
888
936
|
}
|
|
889
937
|
}
|
|
890
938
|
|
|
891
|
-
// src/
|
|
939
|
+
// src/commands/adr/supersede.ts
|
|
940
|
+
import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
892
941
|
import path5 from "path";
|
|
942
|
+
var AdrSupersedeError = class extends Error {
|
|
943
|
+
code;
|
|
944
|
+
details;
|
|
945
|
+
constructor(code, message, details = []) {
|
|
946
|
+
super(message);
|
|
947
|
+
this.name = "AdrSupersedeError";
|
|
948
|
+
this.code = code;
|
|
949
|
+
this.details = details;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
async function supersedeAdr(options) {
|
|
953
|
+
const oldSlug = createSlug2(options.oldName, "oldName");
|
|
954
|
+
createSlug2(options.newTitle, "newTitle");
|
|
955
|
+
const config = await loadRequiredConfig3(options.rootDir);
|
|
956
|
+
const adrDirAbsolute = resolveSafePath(options.rootDir, config.adrDir).absolutePath;
|
|
957
|
+
const old = await findAcceptedAdr(adrDirAbsolute, oldSlug);
|
|
958
|
+
const next = await getNextAdrNumber(adrDirAbsolute);
|
|
959
|
+
const oldRef = old.fileName.replace(/\.md$/u, "");
|
|
960
|
+
const superseding = generateSupersedingAdr({
|
|
961
|
+
adrDir: config.adrDir,
|
|
962
|
+
adrId: next.id,
|
|
963
|
+
title: options.newTitle,
|
|
964
|
+
supersedesRef: oldRef
|
|
965
|
+
});
|
|
966
|
+
const newRef = `${next.id}-${superseding.slug}`;
|
|
967
|
+
const markedOld = old.content.replace(
|
|
968
|
+
/(##\s+Status\r?\n\r?\n)Accepted[^\n]*/u,
|
|
969
|
+
`$1Accepted \u2014 superseded by ${newRef}`
|
|
970
|
+
);
|
|
971
|
+
const writeNew = await write2(options, superseding.path, superseding.content, options.force);
|
|
972
|
+
const oldRelative = `${config.adrDir}/${old.fileName}`;
|
|
973
|
+
const writeOld = await write2(options, oldRelative, markedOld, true);
|
|
974
|
+
return {
|
|
975
|
+
oldRef,
|
|
976
|
+
oldPath: oldRelative,
|
|
977
|
+
newRef,
|
|
978
|
+
newPath: superseding.path,
|
|
979
|
+
dryRun: options.dryRun ?? false,
|
|
980
|
+
writeResult: {
|
|
981
|
+
created: [...writeNew.created, ...writeOld.created],
|
|
982
|
+
overwritten: [...writeNew.overwritten, ...writeOld.overwritten],
|
|
983
|
+
skipped: [...writeNew.skipped, ...writeOld.skipped],
|
|
984
|
+
dryRun: options.dryRun ?? false
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async function findAcceptedAdr(adrDirAbsolute, slug) {
|
|
989
|
+
const pattern = new RegExp(`^ADR-\\d{4,}-${escapeRegExp2(slug)}\\.md$`, "u");
|
|
990
|
+
let entries;
|
|
991
|
+
try {
|
|
992
|
+
entries = await readdir3(adrDirAbsolute, { withFileTypes: true });
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const nodeError = error;
|
|
995
|
+
if (nodeError.code === "ENOENT") {
|
|
996
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`);
|
|
997
|
+
}
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
const match = entries.find((entry) => entry.isFile() && pattern.test(entry.name));
|
|
1001
|
+
if (match === void 0) {
|
|
1002
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`, [
|
|
1003
|
+
`Looked for an ADR-####-${slug}.md in the ADR directory.`
|
|
1004
|
+
]);
|
|
1005
|
+
}
|
|
1006
|
+
const content = await readFile3(path5.join(adrDirAbsolute, match.name), "utf8");
|
|
1007
|
+
if (!/(##\s+Status\r?\n\r?\n)Accepted\b/u.test(content)) {
|
|
1008
|
+
throw new AdrSupersedeError(
|
|
1009
|
+
"NOT_ACCEPTED",
|
|
1010
|
+
`ADR ${match.name} is not Accepted, so there is nothing to supersede.`,
|
|
1011
|
+
["Only an accepted decision can be superseded. Accept it first with `recall adr accept`."]
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return { fileName: match.name, content };
|
|
1015
|
+
}
|
|
1016
|
+
async function write2(options, relativePath, content, force) {
|
|
1017
|
+
const plan = createWritePlan({
|
|
1018
|
+
rootDir: options.rootDir,
|
|
1019
|
+
files: [{ path: relativePath, content }],
|
|
1020
|
+
force
|
|
1021
|
+
});
|
|
1022
|
+
if (plan.hasErrors) {
|
|
1023
|
+
throw new AdrSupersedeError(
|
|
1024
|
+
"WRITE_PLAN_ERROR",
|
|
1025
|
+
"ADR supersede write plan contains errors.",
|
|
1026
|
+
plan.entries.filter(
|
|
1027
|
+
(entry) => entry.action === "error"
|
|
1028
|
+
).map((entry) => `${entry.path}: ${entry.reason}`)
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
return executeWritePlan(plan, { dryRun: options.dryRun });
|
|
1032
|
+
}
|
|
1033
|
+
function formatAdrSupersedeResult(result) {
|
|
1034
|
+
const lines = [
|
|
1035
|
+
result.dryRun ? "Recall OS ADR supersede dry run complete." : "Recall OS ADR supersede complete.",
|
|
1036
|
+
`Superseded: ${result.oldPath} (now marked superseded by ${result.newRef})`,
|
|
1037
|
+
`New decision: ${result.newPath}`
|
|
1038
|
+
];
|
|
1039
|
+
appendWriteSummary(lines, { dryRun: result.dryRun, writeResult: result.writeResult });
|
|
1040
|
+
if (!result.dryRun) {
|
|
1041
|
+
appendNextSteps(lines, [
|
|
1042
|
+
`Fill ${result.newPath}: Context (what changed), Decision, Alternatives, Consequences.`,
|
|
1043
|
+
`${result.oldRef} stays in history as superseded; update any memory that still relies on it.`,
|
|
1044
|
+
"Run `recall doctor` \u2014 it flags memory that still references the superseded decision."
|
|
1045
|
+
]);
|
|
1046
|
+
}
|
|
1047
|
+
return `${lines.join("\n")}
|
|
1048
|
+
`;
|
|
1049
|
+
}
|
|
1050
|
+
function createSlug2(name, field) {
|
|
1051
|
+
const withoutPrefix = name.replace(/^ADR-(?:PROPOSED-|\d{4,}-)/iu, "");
|
|
1052
|
+
try {
|
|
1053
|
+
return slugify(withoutPrefix);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (error instanceof SlugifyError) {
|
|
1056
|
+
throw new AdrSupersedeError("INVALID_ADR_NAME", `Invalid ${field}: ${error.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function escapeRegExp2(value) {
|
|
1062
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1063
|
+
}
|
|
1064
|
+
async function loadRequiredConfig3(rootDir) {
|
|
1065
|
+
try {
|
|
1066
|
+
return await loadConfig(rootDir);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error instanceof ConfigLoadError || error instanceof ConfigValidationError) {
|
|
1069
|
+
throw new AdrSupersedeError(
|
|
1070
|
+
"CONFIG_REQUIRED",
|
|
1071
|
+
"Recall OS config not found or invalid. Run `recall init` first.",
|
|
1072
|
+
[error.message]
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/core/generator/generate-feature.ts
|
|
1080
|
+
import path6 from "path";
|
|
893
1081
|
var featureTemplates = [
|
|
894
1082
|
{
|
|
895
1083
|
fileName: "PRD.md",
|
|
@@ -1041,7 +1229,7 @@ Pending.
|
|
|
1041
1229
|
];
|
|
1042
1230
|
function generateFeatureFiles(options) {
|
|
1043
1231
|
const slug = slugify(options.featureName);
|
|
1044
|
-
const featureDir =
|
|
1232
|
+
const featureDir = path6.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
|
|
1045
1233
|
const title = titleizeFeatureName(options.featureName);
|
|
1046
1234
|
const context = createTemplateContext({
|
|
1047
1235
|
featureId: options.featureId,
|
|
@@ -1049,7 +1237,7 @@ function generateFeatureFiles(options) {
|
|
|
1049
1237
|
title
|
|
1050
1238
|
});
|
|
1051
1239
|
return featureTemplates.map((template) => ({
|
|
1052
|
-
path:
|
|
1240
|
+
path: path6.posix.join(featureDir, template.fileName),
|
|
1053
1241
|
content: renderTemplate(template.content, context)
|
|
1054
1242
|
}));
|
|
1055
1243
|
}
|
|
@@ -1058,7 +1246,7 @@ function titleizeFeatureName(featureName) {
|
|
|
1058
1246
|
}
|
|
1059
1247
|
|
|
1060
1248
|
// src/core/naming/feature-number.ts
|
|
1061
|
-
import { readdir as
|
|
1249
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1062
1250
|
var FEATURE_FOLDER_PATTERN = /^F-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/u;
|
|
1063
1251
|
async function getFeatureFolderForSlug(featuresDirAbsolutePath, slug) {
|
|
1064
1252
|
const existingFolders = await readExistingFeatureFolders(featuresDirAbsolutePath);
|
|
@@ -1091,7 +1279,7 @@ function formatFeatureNumber(featureNumber) {
|
|
|
1091
1279
|
async function readExistingFeatureFolders(featuresDirAbsolutePath) {
|
|
1092
1280
|
let entries;
|
|
1093
1281
|
try {
|
|
1094
|
-
entries = await
|
|
1282
|
+
entries = await readdir4(featuresDirAbsolutePath, { withFileTypes: true });
|
|
1095
1283
|
} catch (error) {
|
|
1096
1284
|
const nodeError = error;
|
|
1097
1285
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1130,7 +1318,7 @@ var FeatureCreateError = class extends Error {
|
|
|
1130
1318
|
};
|
|
1131
1319
|
async function createFeature(options) {
|
|
1132
1320
|
const slug = createFeatureSlug(options.name);
|
|
1133
|
-
const config = await
|
|
1321
|
+
const config = await loadRequiredConfig4(options.rootDir);
|
|
1134
1322
|
const featuresDirPath = resolveSafePath(options.rootDir, config.featuresDir);
|
|
1135
1323
|
const featureFolder = await getFeatureFolderForSlug(featuresDirPath.absolutePath, slug);
|
|
1136
1324
|
const files = generateFeatureFiles({
|
|
@@ -1189,7 +1377,7 @@ function createFeatureSlug(name) {
|
|
|
1189
1377
|
throw error;
|
|
1190
1378
|
}
|
|
1191
1379
|
}
|
|
1192
|
-
async function
|
|
1380
|
+
async function loadRequiredConfig4(rootDir) {
|
|
1193
1381
|
try {
|
|
1194
1382
|
return await loadConfig(rootDir);
|
|
1195
1383
|
} catch (error) {
|
|
@@ -1230,8 +1418,8 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1230
1418
|
|
|
1231
1419
|
// src/core/adopt/inspect-repo.ts
|
|
1232
1420
|
import { existsSync as existsSync3 } from "fs";
|
|
1233
|
-
import { readFile as
|
|
1234
|
-
import
|
|
1421
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1422
|
+
import path7 from "path";
|
|
1235
1423
|
var FRAMEWORK_SOURCES = {
|
|
1236
1424
|
"Next.js": "package.json",
|
|
1237
1425
|
React: "package.json",
|
|
@@ -1254,7 +1442,7 @@ var FRAMEWORK_SOURCES = {
|
|
|
1254
1442
|
Flutter: "pubspec.yaml"
|
|
1255
1443
|
};
|
|
1256
1444
|
async function inspectRepo(rootDir) {
|
|
1257
|
-
const has = (relativePath) => existsSync3(
|
|
1445
|
+
const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
|
|
1258
1446
|
const languages = /* @__PURE__ */ new Set();
|
|
1259
1447
|
const frameworks = /* @__PURE__ */ new Set();
|
|
1260
1448
|
const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
|
|
@@ -1468,7 +1656,7 @@ async function findTestFile(rootDir) {
|
|
|
1468
1656
|
}
|
|
1469
1657
|
let entries;
|
|
1470
1658
|
try {
|
|
1471
|
-
entries = await
|
|
1659
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1472
1660
|
} catch {
|
|
1473
1661
|
continue;
|
|
1474
1662
|
}
|
|
@@ -1479,10 +1667,10 @@ async function findTestFile(rootDir) {
|
|
|
1479
1667
|
}
|
|
1480
1668
|
if (entry.isDirectory()) {
|
|
1481
1669
|
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1482
|
-
stack.push(
|
|
1670
|
+
stack.push(path7.join(dir, entry.name));
|
|
1483
1671
|
}
|
|
1484
1672
|
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1485
|
-
return
|
|
1673
|
+
return path7.relative(rootDir, path7.join(dir, entry.name));
|
|
1486
1674
|
}
|
|
1487
1675
|
}
|
|
1488
1676
|
}
|
|
@@ -1506,7 +1694,7 @@ async function readJson(rootDir, relativePath) {
|
|
|
1506
1694
|
}
|
|
1507
1695
|
async function readText(rootDir, relativePath) {
|
|
1508
1696
|
try {
|
|
1509
|
-
return await
|
|
1697
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1510
1698
|
} catch {
|
|
1511
1699
|
return "";
|
|
1512
1700
|
}
|
|
@@ -1682,8 +1870,8 @@ function formatList2(values) {
|
|
|
1682
1870
|
|
|
1683
1871
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1684
1872
|
import { existsSync as existsSync4 } from "fs";
|
|
1685
|
-
import { readFile as
|
|
1686
|
-
import
|
|
1873
|
+
import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
|
|
1874
|
+
import path8 from "path";
|
|
1687
1875
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1688
1876
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1689
1877
|
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
@@ -1700,7 +1888,7 @@ async function checkCodeReferences(context) {
|
|
|
1700
1888
|
continue;
|
|
1701
1889
|
}
|
|
1702
1890
|
for (const doc of FEATURE_DOCS) {
|
|
1703
|
-
const relativePath =
|
|
1891
|
+
const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1704
1892
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1705
1893
|
}
|
|
1706
1894
|
}
|
|
@@ -1710,7 +1898,7 @@ async function checkCodeReferences(context) {
|
|
|
1710
1898
|
continue;
|
|
1711
1899
|
}
|
|
1712
1900
|
for (const doc of MODULE_DOCS) {
|
|
1713
|
-
const relativePath =
|
|
1901
|
+
const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1714
1902
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1715
1903
|
}
|
|
1716
1904
|
}
|
|
@@ -1729,7 +1917,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1729
1917
|
continue;
|
|
1730
1918
|
}
|
|
1731
1919
|
seen.add(reference);
|
|
1732
|
-
if (!existsSync4(
|
|
1920
|
+
if (!existsSync4(path8.join(rootDir, reference))) {
|
|
1733
1921
|
findings.push({
|
|
1734
1922
|
severity: "warning",
|
|
1735
1923
|
check: "drift-code-reference",
|
|
@@ -1742,7 +1930,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1742
1930
|
}
|
|
1743
1931
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1744
1932
|
try {
|
|
1745
|
-
return await
|
|
1933
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1746
1934
|
} catch (error) {
|
|
1747
1935
|
const nodeError = error;
|
|
1748
1936
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1753,7 +1941,7 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1753
1941
|
}
|
|
1754
1942
|
async function readFileIfExists(rootDir, relativePath) {
|
|
1755
1943
|
try {
|
|
1756
|
-
return await
|
|
1944
|
+
return await readFile5(path8.join(rootDir, relativePath), "utf8");
|
|
1757
1945
|
} catch (error) {
|
|
1758
1946
|
const nodeError = error;
|
|
1759
1947
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1764,12 +1952,12 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1764
1952
|
}
|
|
1765
1953
|
|
|
1766
1954
|
// src/core/doctor/checks/config-check.ts
|
|
1767
|
-
import { readFile as
|
|
1955
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1768
1956
|
async function checkConfig(rootDir) {
|
|
1769
1957
|
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1770
1958
|
let rawConfig;
|
|
1771
1959
|
try {
|
|
1772
|
-
rawConfig = await
|
|
1960
|
+
rawConfig = await readFile6(configPath.absolutePath, "utf8");
|
|
1773
1961
|
} catch (error) {
|
|
1774
1962
|
const nodeError = error;
|
|
1775
1963
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1832,8 +2020,8 @@ async function checkConfig(rootDir) {
|
|
|
1832
2020
|
}
|
|
1833
2021
|
|
|
1834
2022
|
// src/core/doctor/checks/content-check.ts
|
|
1835
|
-
import { readFile as
|
|
1836
|
-
import
|
|
2023
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
2024
|
+
import path9 from "path";
|
|
1837
2025
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1838
2026
|
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1839
2027
|
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
@@ -1848,7 +2036,7 @@ async function checkContent(context) {
|
|
|
1848
2036
|
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1849
2037
|
);
|
|
1850
2038
|
for (const folder of featureFolders) {
|
|
1851
|
-
const prdPath =
|
|
2039
|
+
const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1852
2040
|
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1853
2041
|
if (prd === void 0) {
|
|
1854
2042
|
continue;
|
|
@@ -1881,7 +2069,7 @@ async function checkContent(context) {
|
|
|
1881
2069
|
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
1882
2070
|
}
|
|
1883
2071
|
for (const folder of moduleFolders) {
|
|
1884
|
-
const modulePath =
|
|
2072
|
+
const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1885
2073
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1886
2074
|
if (moduleDoc === void 0) {
|
|
1887
2075
|
continue;
|
|
@@ -1959,7 +2147,7 @@ function getSection(content, heading) {
|
|
|
1959
2147
|
}
|
|
1960
2148
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1961
2149
|
try {
|
|
1962
|
-
return await
|
|
2150
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1963
2151
|
} catch (error) {
|
|
1964
2152
|
const nodeError = error;
|
|
1965
2153
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1970,7 +2158,7 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1970
2158
|
}
|
|
1971
2159
|
async function readFileIfExists2(rootDir, relativePath) {
|
|
1972
2160
|
try {
|
|
1973
|
-
return await
|
|
2161
|
+
return await readFile7(path9.join(rootDir, relativePath), "utf8");
|
|
1974
2162
|
} catch (error) {
|
|
1975
2163
|
const nodeError = error;
|
|
1976
2164
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1981,8 +2169,8 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1981
2169
|
}
|
|
1982
2170
|
|
|
1983
2171
|
// src/core/doctor/checks/drift-check.ts
|
|
1984
|
-
import { readFile as
|
|
1985
|
-
import
|
|
2172
|
+
import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2173
|
+
import path10 from "path";
|
|
1986
2174
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1987
2175
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1988
2176
|
async function checkDrift(context) {
|
|
@@ -1999,12 +2187,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
1999
2187
|
const known = /* @__PURE__ */ new Map();
|
|
2000
2188
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
2001
2189
|
for (const file of files) {
|
|
2002
|
-
const match = adrFilePattern.exec(
|
|
2190
|
+
const match = adrFilePattern.exec(path10.basename(file));
|
|
2003
2191
|
if (match === null) {
|
|
2004
2192
|
continue;
|
|
2005
2193
|
}
|
|
2006
2194
|
const id = `ADR-${match[1]}`;
|
|
2007
|
-
const content = await
|
|
2195
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2008
2196
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
2009
2197
|
const existing = known.get(id);
|
|
2010
2198
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -2017,7 +2205,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
2017
2205
|
const findings = [];
|
|
2018
2206
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
2019
2207
|
for (const file of files) {
|
|
2020
|
-
const content = await
|
|
2208
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2021
2209
|
const referenced = /* @__PURE__ */ new Set();
|
|
2022
2210
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
2023
2211
|
referenced.add(match[0].toUpperCase());
|
|
@@ -2052,7 +2240,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
|
|
|
2052
2240
|
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
2053
2241
|
const files = [];
|
|
2054
2242
|
for (const entry of entries) {
|
|
2055
|
-
const childRelative =
|
|
2243
|
+
const childRelative = path10.posix.join(relativeDir, entry.name);
|
|
2056
2244
|
if (entry.isDirectory()) {
|
|
2057
2245
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
2058
2246
|
continue;
|
|
@@ -2085,7 +2273,7 @@ function getSection2(content, heading) {
|
|
|
2085
2273
|
}
|
|
2086
2274
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
2087
2275
|
try {
|
|
2088
|
-
return await
|
|
2276
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
2089
2277
|
} catch (error) {
|
|
2090
2278
|
const nodeError = error;
|
|
2091
2279
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2096,8 +2284,8 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
2096
2284
|
}
|
|
2097
2285
|
|
|
2098
2286
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
2099
|
-
import { lstat as lstat2, readFile as
|
|
2100
|
-
import
|
|
2287
|
+
import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2288
|
+
import path11 from "path";
|
|
2101
2289
|
|
|
2102
2290
|
// src/core/adr/adr-sections.ts
|
|
2103
2291
|
var REQUIRED_ADR_SECTIONS = [
|
|
@@ -2161,7 +2349,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
2161
2349
|
);
|
|
2162
2350
|
for (const featureFolder of featureFolders) {
|
|
2163
2351
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
2164
|
-
const filePath =
|
|
2352
|
+
const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
2165
2353
|
if (!await isFile(rootDir, filePath)) {
|
|
2166
2354
|
findings.push({
|
|
2167
2355
|
severity: "error",
|
|
@@ -2185,7 +2373,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
2185
2373
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
2186
2374
|
for (const moduleFolder of moduleFolders) {
|
|
2187
2375
|
for (const requiredDoc of requiredModuleDocs) {
|
|
2188
|
-
const filePath =
|
|
2376
|
+
const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
2189
2377
|
if (!await isFile(rootDir, filePath)) {
|
|
2190
2378
|
findings.push({
|
|
2191
2379
|
severity: "error",
|
|
@@ -2208,8 +2396,8 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2208
2396
|
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
2209
2397
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
2210
2398
|
for (const adrFile of adrFiles) {
|
|
2211
|
-
const filePath =
|
|
2212
|
-
const content = await
|
|
2399
|
+
const filePath = path11.posix.join(adrDir, adrFile.name);
|
|
2400
|
+
const content = await readFile9(path11.join(rootDir, filePath), "utf8");
|
|
2213
2401
|
for (const requiredSection of requiredAdrSections) {
|
|
2214
2402
|
if (!content.includes(requiredSection)) {
|
|
2215
2403
|
findings.push({
|
|
@@ -2230,7 +2418,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2230
2418
|
}
|
|
2231
2419
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2232
2420
|
try {
|
|
2233
|
-
return await
|
|
2421
|
+
return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
|
|
2234
2422
|
} catch (error) {
|
|
2235
2423
|
const nodeError = error;
|
|
2236
2424
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2241,7 +2429,7 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2241
2429
|
}
|
|
2242
2430
|
async function isFile(rootDir, relativePath) {
|
|
2243
2431
|
try {
|
|
2244
|
-
return (await lstat2(
|
|
2432
|
+
return (await lstat2(path11.join(rootDir, relativePath))).isFile();
|
|
2245
2433
|
} catch (error) {
|
|
2246
2434
|
const nodeError = error;
|
|
2247
2435
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2253,7 +2441,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
2253
2441
|
|
|
2254
2442
|
// src/core/doctor/checks/required-files-check.ts
|
|
2255
2443
|
import { lstat as lstat3 } from "fs/promises";
|
|
2256
|
-
import
|
|
2444
|
+
import path12 from "path";
|
|
2257
2445
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
2258
2446
|
var requiredDocs = [
|
|
2259
2447
|
"00-product/PRD.md",
|
|
@@ -2280,12 +2468,12 @@ async function checkRequiredFiles(context) {
|
|
|
2280
2468
|
}
|
|
2281
2469
|
}
|
|
2282
2470
|
for (const relativeDocPath of requiredDocs) {
|
|
2283
|
-
const filePath =
|
|
2471
|
+
const filePath = path12.posix.join(docsDir, relativeDocPath);
|
|
2284
2472
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
2285
2473
|
findings.push(missingFile(filePath, "required-docs"));
|
|
2286
2474
|
}
|
|
2287
2475
|
}
|
|
2288
|
-
const adrIndexPath =
|
|
2476
|
+
const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
2289
2477
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
2290
2478
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
2291
2479
|
}
|
|
@@ -2311,7 +2499,7 @@ async function checkRequiredFiles(context) {
|
|
|
2311
2499
|
}
|
|
2312
2500
|
async function isFile2(rootDir, relativePath) {
|
|
2313
2501
|
try {
|
|
2314
|
-
return (await lstat3(
|
|
2502
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isFile();
|
|
2315
2503
|
} catch (error) {
|
|
2316
2504
|
const nodeError = error;
|
|
2317
2505
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2322,7 +2510,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
2322
2510
|
}
|
|
2323
2511
|
async function isDirectory(rootDir, relativePath) {
|
|
2324
2512
|
try {
|
|
2325
|
-
return (await lstat3(
|
|
2513
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
|
|
2326
2514
|
} catch (error) {
|
|
2327
2515
|
const nodeError = error;
|
|
2328
2516
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2341,8 +2529,8 @@ function missingFile(pathValue, check) {
|
|
|
2341
2529
|
}
|
|
2342
2530
|
|
|
2343
2531
|
// src/core/doctor/checks/standards-check.ts
|
|
2344
|
-
import { lstat as lstat4, readFile as
|
|
2345
|
-
import
|
|
2532
|
+
import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
|
|
2533
|
+
import path13 from "path";
|
|
2346
2534
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2347
2535
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2348
2536
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
@@ -2362,10 +2550,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2362
2550
|
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2363
2551
|
);
|
|
2364
2552
|
for (const featureFolder of featureFolders) {
|
|
2365
|
-
const featureDir =
|
|
2366
|
-
const completionReportPath =
|
|
2367
|
-
const reviewPath =
|
|
2368
|
-
const architectureImpactPath =
|
|
2553
|
+
const featureDir = path13.posix.join(featuresDir, featureFolder.name);
|
|
2554
|
+
const completionReportPath = path13.posix.join(featureDir, "COMPLETION_REPORT.md");
|
|
2555
|
+
const reviewPath = path13.posix.join(featureDir, "REVIEW.md");
|
|
2556
|
+
const architectureImpactPath = path13.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
|
|
2369
2557
|
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2370
2558
|
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2371
2559
|
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
@@ -2415,8 +2603,8 @@ async function checkAdrStandards(rootDir, adrDir) {
|
|
|
2415
2603
|
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2416
2604
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2417
2605
|
for (const adrFile of adrFiles) {
|
|
2418
|
-
const adrPath =
|
|
2419
|
-
const content = await
|
|
2606
|
+
const adrPath = path13.posix.join(adrDir, adrFile.name);
|
|
2607
|
+
const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
|
|
2420
2608
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2421
2609
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2422
2610
|
findings.push({
|
|
@@ -2500,7 +2688,7 @@ function isPlaceholder(value) {
|
|
|
2500
2688
|
}
|
|
2501
2689
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2502
2690
|
try {
|
|
2503
|
-
return await
|
|
2691
|
+
return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
|
|
2504
2692
|
} catch (error) {
|
|
2505
2693
|
const nodeError = error;
|
|
2506
2694
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2514,7 +2702,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2514
2702
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2515
2703
|
return void 0;
|
|
2516
2704
|
}
|
|
2517
|
-
return await
|
|
2705
|
+
return await readFile10(path13.join(rootDir, relativePath), "utf8");
|
|
2518
2706
|
} catch (error) {
|
|
2519
2707
|
const nodeError = error;
|
|
2520
2708
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2525,7 +2713,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2525
2713
|
}
|
|
2526
2714
|
async function isFile3(rootDir, relativePath) {
|
|
2527
2715
|
try {
|
|
2528
|
-
return (await lstat4(
|
|
2716
|
+
return (await lstat4(path13.join(rootDir, relativePath))).isFile();
|
|
2529
2717
|
} catch (error) {
|
|
2530
2718
|
const nodeError = error;
|
|
2531
2719
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2535,6 +2723,110 @@ async function isFile3(rootDir, relativePath) {
|
|
|
2535
2723
|
}
|
|
2536
2724
|
}
|
|
2537
2725
|
|
|
2726
|
+
// src/core/doctor/checks/superseded-check.ts
|
|
2727
|
+
import { readFile as readFile11, readdir as readdir11 } from "fs/promises";
|
|
2728
|
+
import path14 from "path";
|
|
2729
|
+
var adrFilePattern4 = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
2730
|
+
var adrReferencePattern2 = /ADR-\d{4,}/giu;
|
|
2731
|
+
async function checkSuperseded(context) {
|
|
2732
|
+
if (context.config === void 0) {
|
|
2733
|
+
return [];
|
|
2734
|
+
}
|
|
2735
|
+
const supersededIds = await loadSupersededAdrIds(context.rootDir, context.config.adrDir);
|
|
2736
|
+
if (supersededIds.size === 0) {
|
|
2737
|
+
return [];
|
|
2738
|
+
}
|
|
2739
|
+
const findings = [];
|
|
2740
|
+
findings.push(
|
|
2741
|
+
...await checkReferences2(context.rootDir, context.config.featuresDir, supersededIds)
|
|
2742
|
+
);
|
|
2743
|
+
findings.push(
|
|
2744
|
+
...await checkReferences2(context.rootDir, context.config.modulesDir, supersededIds)
|
|
2745
|
+
);
|
|
2746
|
+
return findings;
|
|
2747
|
+
}
|
|
2748
|
+
async function loadSupersededAdrIds(rootDir, adrDir) {
|
|
2749
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2750
|
+
const files = await readMarkdownFiles2(rootDir, adrDir);
|
|
2751
|
+
for (const file of files) {
|
|
2752
|
+
const match = adrFilePattern4.exec(path14.basename(file));
|
|
2753
|
+
if (match === null) {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2757
|
+
if (statusContains(content, /superseded\s+by/iu)) {
|
|
2758
|
+
superseded.add(`ADR-${match[1]}`.toUpperCase());
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return superseded;
|
|
2762
|
+
}
|
|
2763
|
+
async function checkReferences2(rootDir, referenceDir, supersededIds) {
|
|
2764
|
+
const findings = [];
|
|
2765
|
+
const files = await readMarkdownFiles2(rootDir, referenceDir);
|
|
2766
|
+
for (const file of files) {
|
|
2767
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2768
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2769
|
+
for (const match of stripCode2(content).matchAll(adrReferencePattern2)) {
|
|
2770
|
+
referenced.add(match[0].toUpperCase());
|
|
2771
|
+
}
|
|
2772
|
+
for (const id of referenced) {
|
|
2773
|
+
if (supersededIds.has(id)) {
|
|
2774
|
+
findings.push({
|
|
2775
|
+
severity: "warning",
|
|
2776
|
+
check: "superseded-reference",
|
|
2777
|
+
message: `Repository memory references ${id}, which has been superseded \u2014 update it to the current decision.`,
|
|
2778
|
+
path: file
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return findings;
|
|
2784
|
+
}
|
|
2785
|
+
function statusContains(content, pattern) {
|
|
2786
|
+
const lines = content.split(/\r?\n/u);
|
|
2787
|
+
const startIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## status");
|
|
2788
|
+
if (startIndex === -1) {
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
const body = [];
|
|
2792
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
2793
|
+
if (/^##\s+/u.test(lines[index])) {
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
body.push(lines[index]);
|
|
2797
|
+
}
|
|
2798
|
+
return pattern.test(body.join("\n"));
|
|
2799
|
+
}
|
|
2800
|
+
function stripCode2(content) {
|
|
2801
|
+
return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
|
|
2802
|
+
}
|
|
2803
|
+
async function readMarkdownFiles2(rootDir, relativeDir) {
|
|
2804
|
+
const entries = await readDirIfExists6(rootDir, relativeDir);
|
|
2805
|
+
const files = [];
|
|
2806
|
+
for (const entry of entries) {
|
|
2807
|
+
const childRelative = path14.posix.join(relativeDir, entry.name);
|
|
2808
|
+
if (entry.isDirectory()) {
|
|
2809
|
+
files.push(...await readMarkdownFiles2(rootDir, childRelative));
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
2813
|
+
files.push(childRelative);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return files;
|
|
2817
|
+
}
|
|
2818
|
+
async function readDirIfExists6(rootDir, relativePath) {
|
|
2819
|
+
try {
|
|
2820
|
+
return await readdir11(path14.join(rootDir, relativePath), { withFileTypes: true });
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
const nodeError = error;
|
|
2823
|
+
if (nodeError.code === "ENOENT") {
|
|
2824
|
+
return [];
|
|
2825
|
+
}
|
|
2826
|
+
throw error;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2538
2830
|
// src/core/doctor/doctor-check.ts
|
|
2539
2831
|
async function runDoctor(rootDir) {
|
|
2540
2832
|
const findings = [];
|
|
@@ -2551,6 +2843,7 @@ async function runDoctor(rootDir) {
|
|
|
2551
2843
|
findings.push(...await checkDrift(context));
|
|
2552
2844
|
findings.push(...await checkContent(context));
|
|
2553
2845
|
findings.push(...await checkCodeReferences(context));
|
|
2846
|
+
findings.push(...await checkSuperseded(context));
|
|
2554
2847
|
}
|
|
2555
2848
|
return createDoctorReport(findings);
|
|
2556
2849
|
}
|
|
@@ -2628,10 +2921,10 @@ function formatDoctorResult(result) {
|
|
|
2628
2921
|
|
|
2629
2922
|
// src/commands/init.ts
|
|
2630
2923
|
import { existsSync as existsSync6 } from "fs";
|
|
2631
|
-
import
|
|
2924
|
+
import path17 from "path";
|
|
2632
2925
|
|
|
2633
2926
|
// src/core/generator/generate-init.ts
|
|
2634
|
-
import
|
|
2927
|
+
import path15 from "path";
|
|
2635
2928
|
var neutralTemplates = [
|
|
2636
2929
|
{
|
|
2637
2930
|
path: "AGENTS.md",
|
|
@@ -2650,6 +2943,32 @@ Required reading:
|
|
|
2650
2943
|
- \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
|
|
2651
2944
|
|
|
2652
2945
|
Repository rules override model preferences. If instructions conflict, stop and report the conflict.
|
|
2946
|
+
|
|
2947
|
+
## Recall OS commands
|
|
2948
|
+
|
|
2949
|
+
This repository is maintained with the Recall OS CLI. Use these commands yourself as you work \u2014 do not
|
|
2950
|
+
ask the human to run them, and do not search the web for them (this is a project-local tool):
|
|
2951
|
+
|
|
2952
|
+
- \`recall doctor\` \u2014 validate repository memory; run it before claiming any work is complete.
|
|
2953
|
+
- \`recall feature create <name>\` \u2014 scaffold feature memory before non-trivial feature work.
|
|
2954
|
+
- \`recall adr create <title>\` \u2014 propose a decision; \`recall adr accept <name>\` accepts it.
|
|
2955
|
+
- \`recall adr supersede <old> <new-title>\` \u2014 record a changed decision (never overwrite an accepted ADR).
|
|
2956
|
+
- \`recall module create <name>\` \u2014 scaffold module memory for a new responsibility boundary.
|
|
2957
|
+
- \`recall mcp add <server>\` \u2014 capture an MCP tool's context into memory, offline.
|
|
2958
|
+
|
|
2959
|
+
Full command reference: \`docs/ai/RECALL_COMMANDS.md\`.
|
|
2960
|
+
|
|
2961
|
+
## Changing an accepted decision
|
|
2962
|
+
|
|
2963
|
+
Before changing anything an accepted ADR governs (framework, database, auth, API shape, and similar):
|
|
2964
|
+
|
|
2965
|
+
1. Check \`docs/adrs/\` for an accepted ADR that covers it.
|
|
2966
|
+
2. If your change contradicts one, stop and confirm with a human first \u2014 do not silently change the
|
|
2967
|
+
code and leave the ADR saying the opposite.
|
|
2968
|
+
3. Record the change as a new decision with \`recall adr supersede <old> <new-title>\`. That supersedes
|
|
2969
|
+
the old ADR instead of overwriting history, so the reasoning stays auditable.
|
|
2970
|
+
|
|
2971
|
+
Repository memory is only trustworthy if decisions change through this trail, not silently.
|
|
2653
2972
|
`
|
|
2654
2973
|
},
|
|
2655
2974
|
{
|
|
@@ -2686,10 +3005,17 @@ Before non-trivial work:
|
|
|
2686
3005
|
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2687
3006
|
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2688
3007
|
- If an instruction conflicts with accepted repository memory, stop and report the conflict.
|
|
3008
|
+
- Before changing what an accepted ADR governs, confirm with a human and record it with
|
|
3009
|
+
\`recall adr supersede <old> <new-title>\` \u2014 never silently contradict an accepted decision.
|
|
2689
3010
|
|
|
2690
3011
|
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2691
3012
|
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
2692
3013
|
|
|
3014
|
+
Recall OS commands \u2014 use these yourself (do not web-search this project-local CLI): \`recall doctor\`,
|
|
3015
|
+
\`recall feature create <name>\`, \`recall adr create <title>\` then \`recall adr accept <name>\`,
|
|
3016
|
+
\`recall adr supersede <old> <new-title>\`, \`recall module create <name>\`, \`recall mcp add <server>\`.
|
|
3017
|
+
Full reference: \`docs/ai/RECALL_COMMANDS.md\`.
|
|
3018
|
+
|
|
2693
3019
|
Before claiming work is complete, run \`recall doctor\` and fix reported errors.
|
|
2694
3020
|
`
|
|
2695
3021
|
},
|
|
@@ -2894,6 +3220,15 @@ Baseline rules:
|
|
|
2894
3220
|
AI agents must follow repository memory over model preference.
|
|
2895
3221
|
|
|
2896
3222
|
If a request conflicts with accepted repository memory or engineering standards, stop and report the conflict.
|
|
3223
|
+
|
|
3224
|
+
## Changing an accepted decision
|
|
3225
|
+
|
|
3226
|
+
When work would change something an accepted ADR governs:
|
|
3227
|
+
|
|
3228
|
+
1. Find the accepted ADR in \`docs/adrs/\` that covers it.
|
|
3229
|
+
2. If the change contradicts it, stop and confirm with a human before changing the code.
|
|
3230
|
+
3. Record the new decision with \`recall adr supersede <old> <new-title>\` so the old ADR is marked
|
|
3231
|
+
superseded and the reasoning is preserved, instead of silently editing or contradicting it.
|
|
2897
3232
|
`
|
|
2898
3233
|
},
|
|
2899
3234
|
{
|
|
@@ -3027,6 +3362,17 @@ Options:
|
|
|
3027
3362
|
- \`--dry-run\`: show planned writes without writing files.
|
|
3028
3363
|
- \`--force\`: overwrite existing files explicitly.
|
|
3029
3364
|
|
|
3365
|
+
### \`recall adr supersede <old> <new-title>\`
|
|
3366
|
+
|
|
3367
|
+
Record a changed decision. Marks an accepted ADR as \`Accepted \u2014 superseded by ADR-####\` and creates a
|
|
3368
|
+
new accepted ADR that declares what it supersedes, so the reasoning trail stays auditable instead of
|
|
3369
|
+
being overwritten. Doctor then warns about any memory still referencing the superseded decision.
|
|
3370
|
+
|
|
3371
|
+
Options:
|
|
3372
|
+
|
|
3373
|
+
- \`--dry-run\`: show planned writes without writing files.
|
|
3374
|
+
- \`--force\`: overwrite existing files explicitly.
|
|
3375
|
+
|
|
3030
3376
|
### \`recall module create <name>\`
|
|
3031
3377
|
|
|
3032
3378
|
Create module memory docs under the configured modules directory.
|
|
@@ -3042,7 +3388,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
|
|
|
3042
3388
|
engineering evidence is present, and whether memory references decisions that exist and are accepted.
|
|
3043
3389
|
|
|
3044
3390
|
Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
|
|
3045
|
-
is an error,
|
|
3391
|
+
is an error, memory that references a not-yet-accepted ADR is a warning, and memory that still
|
|
3392
|
+
references a superseded decision is a warning.
|
|
3046
3393
|
|
|
3047
3394
|
Exit codes:
|
|
3048
3395
|
|
|
@@ -3129,7 +3476,7 @@ jobs:
|
|
|
3129
3476
|
}
|
|
3130
3477
|
];
|
|
3131
3478
|
function generateInitFiles(options) {
|
|
3132
|
-
const repositoryName =
|
|
3479
|
+
const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
|
|
3133
3480
|
const context = createTemplateContext({ repositoryName });
|
|
3134
3481
|
const files = neutralTemplates.map((template) => ({
|
|
3135
3482
|
path: template.path,
|
|
@@ -3156,17 +3503,17 @@ function generatePresetFiles(preset) {
|
|
|
3156
3503
|
|
|
3157
3504
|
// src/core/hooks/detect-gates.ts
|
|
3158
3505
|
import { existsSync as existsSync5 } from "fs";
|
|
3159
|
-
import { readFile as
|
|
3160
|
-
import
|
|
3506
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3507
|
+
import path16 from "path";
|
|
3161
3508
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
3162
3509
|
async function detectPreCommitGates(rootDir) {
|
|
3163
|
-
const packageJsonPath =
|
|
3510
|
+
const packageJsonPath = path16.join(rootDir, "package.json");
|
|
3164
3511
|
if (!existsSync5(packageJsonPath)) {
|
|
3165
3512
|
return [];
|
|
3166
3513
|
}
|
|
3167
3514
|
let scripts;
|
|
3168
3515
|
try {
|
|
3169
|
-
const raw = await
|
|
3516
|
+
const raw = await readFile12(packageJsonPath, "utf8");
|
|
3170
3517
|
const parsed = JSON.parse(raw);
|
|
3171
3518
|
scripts = parsed.scripts ?? {};
|
|
3172
3519
|
} catch {
|
|
@@ -3181,10 +3528,10 @@ async function detectPreCommitGates(rootDir) {
|
|
|
3181
3528
|
);
|
|
3182
3529
|
}
|
|
3183
3530
|
function detectPackageManager2(rootDir) {
|
|
3184
|
-
if (existsSync5(
|
|
3531
|
+
if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
|
|
3185
3532
|
return "pnpm";
|
|
3186
3533
|
}
|
|
3187
|
-
if (existsSync5(
|
|
3534
|
+
if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
|
|
3188
3535
|
return "yarn";
|
|
3189
3536
|
}
|
|
3190
3537
|
return "npm";
|
|
@@ -3204,7 +3551,7 @@ function renderSessionStartHook() {
|
|
|
3204
3551
|
adrs=$(ls docs/adrs/ADR-*.md 2>/dev/null | sed 's|.*/||;s|\\.md$||' | tr '\\n' ' ')
|
|
3205
3552
|
modules=$(ls -d docs/30-modules/*/ 2>/dev/null | sed 's|docs/30-modules/||;s|/$||' | tr '\\n' ' ')
|
|
3206
3553
|
|
|
3207
|
-
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."
|
|
3554
|
+
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."
|
|
3208
3555
|
|
|
3209
3556
|
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$context"
|
|
3210
3557
|
`;
|
|
@@ -4689,8 +5036,8 @@ function parsePreset(value) {
|
|
|
4689
5036
|
if (!result.success) {
|
|
4690
5037
|
throw new PresetValidationError(
|
|
4691
5038
|
result.error.issues.map((issue) => {
|
|
4692
|
-
const
|
|
4693
|
-
return `${
|
|
5039
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
5040
|
+
return `${path19}${issue.message}`;
|
|
4694
5041
|
})
|
|
4695
5042
|
);
|
|
4696
5043
|
}
|
|
@@ -5379,7 +5726,7 @@ var InitError = class extends Error {
|
|
|
5379
5726
|
}
|
|
5380
5727
|
};
|
|
5381
5728
|
async function initProject(options) {
|
|
5382
|
-
if (options.force === true && options.reinit !== true && existsSync6(
|
|
5729
|
+
if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
|
|
5383
5730
|
throw new InitError(
|
|
5384
5731
|
"EXISTING_INSTALLATION",
|
|
5385
5732
|
"Refusing to re-initialize an existing Recall OS installation.",
|
|
@@ -5743,7 +6090,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
5743
6090
|
}
|
|
5744
6091
|
|
|
5745
6092
|
// src/core/generator/generate-module.ts
|
|
5746
|
-
import
|
|
6093
|
+
import path18 from "path";
|
|
5747
6094
|
var moduleTemplates = [
|
|
5748
6095
|
{
|
|
5749
6096
|
fileName: "MODULE.md",
|
|
@@ -5816,14 +6163,14 @@ Record durable module decisions here.
|
|
|
5816
6163
|
];
|
|
5817
6164
|
function generateModuleFiles(options) {
|
|
5818
6165
|
const slug = slugify(options.moduleName);
|
|
5819
|
-
const moduleDir =
|
|
6166
|
+
const moduleDir = path18.posix.join(options.modulesDir, slug);
|
|
5820
6167
|
const title = titleizeModuleName(options.moduleName);
|
|
5821
6168
|
const context = createTemplateContext({
|
|
5822
6169
|
slug,
|
|
5823
6170
|
title
|
|
5824
6171
|
});
|
|
5825
6172
|
return moduleTemplates.map((template) => ({
|
|
5826
|
-
path:
|
|
6173
|
+
path: path18.posix.join(moduleDir, template.fileName),
|
|
5827
6174
|
content: renderTemplate(template.content, context)
|
|
5828
6175
|
}));
|
|
5829
6176
|
}
|
|
@@ -5844,7 +6191,7 @@ var ModuleCreateError = class extends Error {
|
|
|
5844
6191
|
};
|
|
5845
6192
|
async function createModule(options) {
|
|
5846
6193
|
const slug = createModuleSlug(options.name);
|
|
5847
|
-
const config = await
|
|
6194
|
+
const config = await loadRequiredConfig5(options.rootDir);
|
|
5848
6195
|
const files = generateModuleFiles({
|
|
5849
6196
|
modulesDir: config.modulesDir,
|
|
5850
6197
|
moduleName: options.name
|
|
@@ -5898,7 +6245,7 @@ function createModuleSlug(name) {
|
|
|
5898
6245
|
throw error;
|
|
5899
6246
|
}
|
|
5900
6247
|
}
|
|
5901
|
-
async function
|
|
6248
|
+
async function loadRequiredConfig5(rootDir) {
|
|
5902
6249
|
try {
|
|
5903
6250
|
return await loadConfig(rootDir);
|
|
5904
6251
|
} catch (error) {
|
|
@@ -6072,6 +6419,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
|
|
|
6072
6419
|
});
|
|
6073
6420
|
stdout.write(formatAdrAcceptResult(result));
|
|
6074
6421
|
});
|
|
6422
|
+
adrCommand.command("supersede").description(
|
|
6423
|
+
"Record a changed decision: mark an accepted ADR superseded by a new accepted ADR."
|
|
6424
|
+
).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(
|
|
6425
|
+
async (oldName, newTitle, options) => {
|
|
6426
|
+
const result = await supersedeAdr({
|
|
6427
|
+
rootDir: cwd,
|
|
6428
|
+
oldName,
|
|
6429
|
+
newTitle,
|
|
6430
|
+
dryRun: options.dryRun,
|
|
6431
|
+
force: options.force
|
|
6432
|
+
});
|
|
6433
|
+
stdout.write(formatAdrSupersedeResult(result));
|
|
6434
|
+
}
|
|
6435
|
+
);
|
|
6075
6436
|
const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
|
|
6076
6437
|
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) => {
|
|
6077
6438
|
const result = await createModule({
|
|
@@ -6165,6 +6526,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
6165
6526
|
`);
|
|
6166
6527
|
for (const detail of error.details) {
|
|
6167
6528
|
stderr.write(`- ${detail}
|
|
6529
|
+
`);
|
|
6530
|
+
}
|
|
6531
|
+
return 1;
|
|
6532
|
+
}
|
|
6533
|
+
if (error instanceof AdrSupersedeError) {
|
|
6534
|
+
stderr.write(`${error.message}
|
|
6535
|
+
`);
|
|
6536
|
+
for (const detail of error.details) {
|
|
6537
|
+
stderr.write(`- ${detail}
|
|
6168
6538
|
`);
|
|
6169
6539
|
}
|
|
6170
6540
|
return 1;
|