recall-os 0.1.1 → 0.2.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 +43 -20
- package/dist/cli.js +1791 -792
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1791 -792
- package/dist/index.js.map +1 -1
- package/examples/generated-flutter/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-flutter/docs/20-security/THREAT_MODEL.md +35 -3
- package/examples/generated-generic/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-generic/docs/20-security/THREAT_MODEL.md +35 -3
- package/examples/generated-ios-swift/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-ios-swift/docs/20-security/THREAT_MODEL.md +35 -3
- package/examples/generated-kotlin-android/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-kotlin-android/docs/20-security/THREAT_MODEL.md +35 -3
- package/examples/generated-laravel-api/.recall/config.json +17 -0
- package/examples/generated-laravel-api/.recall/hooks/pre-commit +9 -0
- package/examples/generated-laravel-api/AGENTS.md +15 -0
- package/examples/generated-laravel-api/CLAUDE.md +9 -0
- package/examples/generated-laravel-api/README.md +11 -0
- package/examples/generated-laravel-api/docs/00-product/BRD.md +9 -0
- package/examples/generated-laravel-api/docs/00-product/PRD.md +13 -0
- package/examples/generated-laravel-api/docs/10-architecture/ARCHITECTURE.md +11 -0
- package/examples/generated-laravel-api/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
- package/examples/generated-laravel-api/docs/10-architecture/MEMORY_ENGINE.md +16 -0
- package/examples/generated-laravel-api/docs/20-security/SECURITY_MODEL.md +32 -0
- package/examples/generated-laravel-api/docs/20-security/THREAT_MODEL.md +39 -0
- package/examples/generated-laravel-api/docs/30-modules/README.md +17 -0
- package/examples/generated-laravel-api/docs/40-features/README.md +22 -0
- package/examples/generated-laravel-api/docs/50-quality/QUALITY_GATES.md +11 -0
- package/examples/generated-laravel-api/docs/50-quality/TESTING_STRATEGY.md +5 -0
- package/examples/generated-laravel-api/docs/60-engineering/AI_AGENT_RULES.md +6 -0
- package/examples/generated-laravel-api/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
- package/examples/generated-laravel-api/docs/adrs/README.md +9 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-api-design-rest.md +31 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-application-structure.md +30 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-auth-sanctum.md +30 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-database-eloquent.md +31 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-framework.md +29 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-queues-horizon.md +29 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-testing-pest.md +30 -0
- package/examples/generated-laravel-api/docs/adrs/proposed/ADR-PROPOSED-laravel-api-validation-authorization.md +30 -0
- package/examples/generated-laravel-api/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
- package/examples/generated-laravel-api/docs/ai/MCP_STRATEGY.md +6 -0
- package/examples/generated-laravel-api/docs/ai/RECALL_COMMANDS.md +133 -0
- package/examples/generated-laravel-api/docs/ai/presets/laravel-api-guidance.md +62 -0
- package/examples/generated-laravel-react/.recall/config.json +17 -0
- package/examples/generated-laravel-react/.recall/hooks/pre-commit +9 -0
- package/examples/generated-laravel-react/AGENTS.md +15 -0
- package/examples/generated-laravel-react/CLAUDE.md +9 -0
- package/examples/generated-laravel-react/README.md +11 -0
- package/examples/generated-laravel-react/docs/00-product/BRD.md +9 -0
- package/examples/generated-laravel-react/docs/00-product/PRD.md +13 -0
- package/examples/generated-laravel-react/docs/10-architecture/ARCHITECTURE.md +11 -0
- package/examples/generated-laravel-react/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
- package/examples/generated-laravel-react/docs/10-architecture/MEMORY_ENGINE.md +16 -0
- package/examples/generated-laravel-react/docs/20-security/SECURITY_MODEL.md +32 -0
- package/examples/generated-laravel-react/docs/20-security/THREAT_MODEL.md +39 -0
- package/examples/generated-laravel-react/docs/30-modules/README.md +17 -0
- package/examples/generated-laravel-react/docs/40-features/README.md +22 -0
- package/examples/generated-laravel-react/docs/50-quality/QUALITY_GATES.md +11 -0
- package/examples/generated-laravel-react/docs/50-quality/TESTING_STRATEGY.md +5 -0
- package/examples/generated-laravel-react/docs/60-engineering/AI_AGENT_RULES.md +6 -0
- package/examples/generated-laravel-react/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
- package/examples/generated-laravel-react/docs/adrs/README.md +9 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-application-structure.md +30 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-auth-sanctum.md +31 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-database-eloquent.md +31 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-framework.md +29 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-frontend-inertia-react.md +31 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-queues-horizon.md +29 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-testing-pest.md +30 -0
- package/examples/generated-laravel-react/docs/adrs/proposed/ADR-PROPOSED-laravel-react-validation-authorization.md +30 -0
- package/examples/generated-laravel-react/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
- package/examples/generated-laravel-react/docs/ai/MCP_STRATEGY.md +6 -0
- package/examples/generated-laravel-react/docs/ai/RECALL_COMMANDS.md +133 -0
- package/examples/generated-laravel-react/docs/ai/presets/laravel-react-guidance.md +64 -0
- package/examples/generated-laravel-vue/.recall/config.json +17 -0
- package/examples/generated-laravel-vue/.recall/hooks/pre-commit +9 -0
- package/examples/generated-laravel-vue/AGENTS.md +15 -0
- package/examples/generated-laravel-vue/CLAUDE.md +9 -0
- package/examples/generated-laravel-vue/README.md +11 -0
- package/examples/generated-laravel-vue/docs/00-product/BRD.md +9 -0
- package/examples/generated-laravel-vue/docs/00-product/PRD.md +13 -0
- package/examples/generated-laravel-vue/docs/10-architecture/ARCHITECTURE.md +11 -0
- package/examples/generated-laravel-vue/docs/10-architecture/FILE_WRITE_POLICY.md +8 -0
- package/examples/generated-laravel-vue/docs/10-architecture/MEMORY_ENGINE.md +16 -0
- package/examples/generated-laravel-vue/docs/20-security/SECURITY_MODEL.md +32 -0
- package/examples/generated-laravel-vue/docs/20-security/THREAT_MODEL.md +39 -0
- package/examples/generated-laravel-vue/docs/30-modules/README.md +17 -0
- package/examples/generated-laravel-vue/docs/40-features/README.md +22 -0
- package/examples/generated-laravel-vue/docs/50-quality/QUALITY_GATES.md +11 -0
- package/examples/generated-laravel-vue/docs/50-quality/TESTING_STRATEGY.md +5 -0
- package/examples/generated-laravel-vue/docs/60-engineering/AI_AGENT_RULES.md +6 -0
- package/examples/generated-laravel-vue/docs/60-engineering/ENGINEERING_STANDARDS.md +11 -0
- package/examples/generated-laravel-vue/docs/adrs/README.md +9 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-application-structure.md +30 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-auth-sanctum.md +31 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-database-eloquent.md +31 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-framework.md +29 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-frontend-inertia-vue.md +31 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-queues-horizon.md +29 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-testing-pest.md +30 -0
- package/examples/generated-laravel-vue/docs/adrs/proposed/ADR-PROPOSED-laravel-vue-validation-authorization.md +30 -0
- package/examples/generated-laravel-vue/docs/ai/AI_AGENTS_SKILLS_MCP_STRATEGY.md +7 -0
- package/examples/generated-laravel-vue/docs/ai/MCP_STRATEGY.md +6 -0
- package/examples/generated-laravel-vue/docs/ai/RECALL_COMMANDS.md +133 -0
- package/examples/generated-laravel-vue/docs/ai/presets/laravel-vue-guidance.md +64 -0
- package/examples/generated-nextjs/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-nextjs/docs/20-security/THREAT_MODEL.md +35 -3
- package/examples/generated-python-fastapi/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-python-fastapi/docs/20-security/THREAT_MODEL.md +35 -3
- package/package.json +1 -1
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 path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
136
|
+
return `${path17}${issue.message}`;
|
|
137
137
|
})
|
|
138
138
|
);
|
|
139
139
|
}
|
|
@@ -1228,106 +1228,31 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1228
1228
|
});
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
-
// src/core/adopt/generate-adoption.ts
|
|
1232
|
-
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1233
|
-
function generateAdoptionFiles(options) {
|
|
1234
|
-
const files = [
|
|
1235
|
-
{
|
|
1236
|
-
path: ADOPTION_REPORT_PATH,
|
|
1237
|
-
content: renderReport(options.adrDir, options.signals)
|
|
1238
|
-
}
|
|
1239
|
-
];
|
|
1240
|
-
for (const framework of options.signals.frameworks) {
|
|
1241
|
-
files.push({
|
|
1242
|
-
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1243
|
-
content: renderProposedAdr(framework)
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
return files;
|
|
1247
|
-
}
|
|
1248
|
-
function renderReport(adrDir, signals) {
|
|
1249
|
-
return `# Adoption Report
|
|
1250
|
-
|
|
1251
|
-
## Status
|
|
1252
|
-
|
|
1253
|
-
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1254
|
-
is accepted repository memory until you accept it.
|
|
1255
|
-
|
|
1256
|
-
## Detected Signals
|
|
1257
|
-
|
|
1258
|
-
- Languages: ${formatList(signals.languages)}
|
|
1259
|
-
- Package manager: ${signals.packageManager ?? "none detected"}
|
|
1260
|
-
- Frameworks: ${formatList(signals.frameworks)}
|
|
1261
|
-
- Tests present: ${formatBool(signals.hasTests)}
|
|
1262
|
-
- README present: ${formatBool(signals.hasReadme)}
|
|
1263
|
-
- Docs folder present: ${formatBool(signals.hasDocs)}
|
|
1264
|
-
|
|
1265
|
-
## Proposed Decisions
|
|
1266
|
-
|
|
1267
|
-
${renderProposedDecisions(adrDir, signals)}
|
|
1268
|
-
|
|
1269
|
-
## Review Checklist
|
|
1270
|
-
|
|
1271
|
-
- [ ] Confirm the detected languages and package manager.
|
|
1272
|
-
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1273
|
-
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1274
|
-
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1275
|
-
|
|
1276
|
-
## Notes
|
|
1277
|
-
|
|
1278
|
-
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1279
|
-
files. No repository code was executed and no decision was accepted automatically.
|
|
1280
|
-
`;
|
|
1281
|
-
}
|
|
1282
|
-
function renderProposedDecisions(adrDir, signals) {
|
|
1283
|
-
if (signals.frameworks.length === 0) {
|
|
1284
|
-
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1285
|
-
}
|
|
1286
|
-
return signals.frameworks.map(
|
|
1287
|
-
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1288
|
-
).join("\n");
|
|
1289
|
-
}
|
|
1290
|
-
function renderProposedAdr(framework) {
|
|
1291
|
-
return `# Proposed ADR: Use ${framework}
|
|
1292
|
-
|
|
1293
|
-
## Status
|
|
1294
|
-
|
|
1295
|
-
Proposed
|
|
1296
|
-
|
|
1297
|
-
## Context
|
|
1298
|
-
|
|
1299
|
-
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1300
|
-
|
|
1301
|
-
## Decision
|
|
1302
|
-
|
|
1303
|
-
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1304
|
-
and is not accepted until a human reviews and accepts it.
|
|
1305
|
-
|
|
1306
|
-
## Alternatives Considered
|
|
1307
|
-
|
|
1308
|
-
- Record a different framework.
|
|
1309
|
-
- Leave the decision unrecorded for now.
|
|
1310
|
-
|
|
1311
|
-
## Consequences
|
|
1312
|
-
|
|
1313
|
-
- Captures a framework already in use as reviewable repository memory.
|
|
1314
|
-
- Requires explicit human acceptance before it becomes repository truth.
|
|
1315
|
-
`;
|
|
1316
|
-
}
|
|
1317
|
-
function frameworkSlug(framework) {
|
|
1318
|
-
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1319
|
-
}
|
|
1320
|
-
function formatList(values) {
|
|
1321
|
-
return values.length > 0 ? values.join(", ") : "none detected";
|
|
1322
|
-
}
|
|
1323
|
-
function formatBool(value) {
|
|
1324
|
-
return value ? "yes" : "no";
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
1231
|
// src/core/adopt/inspect-repo.ts
|
|
1328
1232
|
import { existsSync as existsSync3 } from "fs";
|
|
1329
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1233
|
+
import { readFile as readFile3, readdir as readdir4 } from "fs/promises";
|
|
1330
1234
|
import path6 from "path";
|
|
1235
|
+
var FRAMEWORK_SOURCES = {
|
|
1236
|
+
"Next.js": "package.json",
|
|
1237
|
+
React: "package.json",
|
|
1238
|
+
NestJS: "package.json",
|
|
1239
|
+
Express: "package.json",
|
|
1240
|
+
FastAPI: "pyproject.toml / requirements.txt",
|
|
1241
|
+
Flask: "pyproject.toml / requirements.txt",
|
|
1242
|
+
Django: "pyproject.toml / requirements.txt",
|
|
1243
|
+
Gin: "go.mod",
|
|
1244
|
+
Echo: "go.mod",
|
|
1245
|
+
Fiber: "go.mod",
|
|
1246
|
+
Chi: "go.mod",
|
|
1247
|
+
"Spring Boot": "pom.xml / build.gradle",
|
|
1248
|
+
"Actix Web": "Cargo.toml",
|
|
1249
|
+
Axum: "Cargo.toml",
|
|
1250
|
+
Rocket: "Cargo.toml",
|
|
1251
|
+
Laravel: "composer.json",
|
|
1252
|
+
Symfony: "composer.json",
|
|
1253
|
+
"Ruby on Rails": "Gemfile",
|
|
1254
|
+
Flutter: "pubspec.yaml"
|
|
1255
|
+
};
|
|
1331
1256
|
async function inspectRepo(rootDir) {
|
|
1332
1257
|
const has = (relativePath) => existsSync3(path6.join(rootDir, relativePath));
|
|
1333
1258
|
const languages = /* @__PURE__ */ new Set();
|
|
@@ -1361,6 +1286,22 @@ async function inspectRepo(rootDir) {
|
|
|
1361
1286
|
languages.add("Dart");
|
|
1362
1287
|
frameworks.add("Flutter");
|
|
1363
1288
|
}
|
|
1289
|
+
if (has("composer.json")) {
|
|
1290
|
+
languages.add("PHP");
|
|
1291
|
+
const composer = (await readText(rootDir, "composer.json")).toLowerCase();
|
|
1292
|
+
if (composer.includes("laravel/framework")) {
|
|
1293
|
+
frameworks.add("Laravel");
|
|
1294
|
+
} else if (composer.includes("symfony/")) {
|
|
1295
|
+
frameworks.add("Symfony");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (has("Gemfile")) {
|
|
1299
|
+
languages.add("Ruby");
|
|
1300
|
+
const gemfile = (await readText(rootDir, "Gemfile")).toLowerCase();
|
|
1301
|
+
if (gemfile.includes("rails")) {
|
|
1302
|
+
frameworks.add("Ruby on Rails");
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1364
1305
|
const deps = collectDependencies(pkg);
|
|
1365
1306
|
if ("next" in deps) {
|
|
1366
1307
|
frameworks.add("Next.js");
|
|
@@ -1380,25 +1321,173 @@ async function inspectRepo(rootDir) {
|
|
|
1380
1321
|
} else if (python.includes("django")) {
|
|
1381
1322
|
frameworks.add("Django");
|
|
1382
1323
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1324
|
+
if (has("go.mod")) {
|
|
1325
|
+
const goModules = `${await readText(rootDir, "go.mod")}${await readText(rootDir, "go.sum")}`;
|
|
1326
|
+
if (goModules.includes("gin-gonic/gin")) {
|
|
1327
|
+
frameworks.add("Gin");
|
|
1328
|
+
} else if (goModules.includes("labstack/echo")) {
|
|
1329
|
+
frameworks.add("Echo");
|
|
1330
|
+
} else if (goModules.includes("gofiber/fiber")) {
|
|
1331
|
+
frameworks.add("Fiber");
|
|
1332
|
+
} else if (goModules.includes("go-chi/chi")) {
|
|
1333
|
+
frameworks.add("Chi");
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const jvmManifest = `${await readText(rootDir, "pom.xml")}${await readText(rootDir, "build.gradle")}${await readText(rootDir, "build.gradle.kts")}`.toLowerCase();
|
|
1337
|
+
if (jvmManifest.includes("spring-boot") || jvmManifest.includes("springframework")) {
|
|
1338
|
+
frameworks.add("Spring Boot");
|
|
1339
|
+
}
|
|
1340
|
+
if (has("Cargo.toml")) {
|
|
1341
|
+
const cargo = (await readText(rootDir, "Cargo.toml")).toLowerCase();
|
|
1342
|
+
if (cargo.includes("actix-web")) {
|
|
1343
|
+
frameworks.add("Actix Web");
|
|
1344
|
+
} else if (cargo.includes("axum")) {
|
|
1345
|
+
frameworks.add("Axum");
|
|
1346
|
+
} else if (cargo.includes("rocket")) {
|
|
1347
|
+
frameworks.add("Rocket");
|
|
1348
|
+
}
|
|
1390
1349
|
}
|
|
1350
|
+
const [packageManager, packageManagerSource] = detectPackageManager(has);
|
|
1391
1351
|
const scripts = pkg !== null && isRecord(pkg.scripts) ? pkg.scripts : {};
|
|
1392
|
-
const
|
|
1352
|
+
const testsEvidence = await detectTestsEvidence(rootDir, has, "test" in scripts, python);
|
|
1393
1353
|
return {
|
|
1394
1354
|
languages: [...languages],
|
|
1395
1355
|
packageManager,
|
|
1356
|
+
packageManagerSource,
|
|
1396
1357
|
frameworks: [...frameworks],
|
|
1397
|
-
hasTests,
|
|
1358
|
+
hasTests: testsEvidence !== null,
|
|
1359
|
+
testsEvidence,
|
|
1398
1360
|
hasReadme: has("README.md") || has("README"),
|
|
1399
1361
|
hasDocs: has("docs")
|
|
1400
1362
|
};
|
|
1401
1363
|
}
|
|
1364
|
+
function summarizeSignals(signals) {
|
|
1365
|
+
const lines = [];
|
|
1366
|
+
lines.push(`- Languages: ${formatList(signals.languages)}`);
|
|
1367
|
+
lines.push(
|
|
1368
|
+
signals.packageManager === null ? "- Package manager: none detected" : `- Package manager: ${signals.packageManager}${signals.packageManagerSource === null ? "" : ` (from \`${signals.packageManagerSource}\`)`}`
|
|
1369
|
+
);
|
|
1370
|
+
if (signals.frameworks.length === 0) {
|
|
1371
|
+
lines.push("- Frameworks: none detected");
|
|
1372
|
+
} else {
|
|
1373
|
+
const withSource = signals.frameworks.map((framework) => {
|
|
1374
|
+
const source = FRAMEWORK_SOURCES[framework];
|
|
1375
|
+
return source === void 0 ? framework : `${framework} (from \`${source}\`)`;
|
|
1376
|
+
});
|
|
1377
|
+
lines.push(`- Frameworks: ${withSource.join(", ")}`);
|
|
1378
|
+
}
|
|
1379
|
+
lines.push(
|
|
1380
|
+
signals.testsEvidence === null ? "- Tests: none detected \u2014 if tests exist, point Recall at them by correcting this report" : `- Tests: detected via ${signals.testsEvidence}`
|
|
1381
|
+
);
|
|
1382
|
+
lines.push(`- README present: ${signals.hasReadme ? "yes" : "no"}`);
|
|
1383
|
+
lines.push(`- Docs folder present: ${signals.hasDocs ? "yes" : "no"}`);
|
|
1384
|
+
return lines;
|
|
1385
|
+
}
|
|
1386
|
+
function formatList(values) {
|
|
1387
|
+
return values.length === 0 ? "none detected" : values.join(", ");
|
|
1388
|
+
}
|
|
1389
|
+
function detectPackageManager(has) {
|
|
1390
|
+
const candidates = [
|
|
1391
|
+
[has("go.mod"), "Go modules", "go.mod"],
|
|
1392
|
+
[has("Cargo.toml"), "Cargo", "Cargo.toml"],
|
|
1393
|
+
[has("pom.xml"), "Maven", "pom.xml"],
|
|
1394
|
+
[has("build.gradle"), "Gradle", "build.gradle"],
|
|
1395
|
+
[has("build.gradle.kts"), "Gradle", "build.gradle.kts"],
|
|
1396
|
+
[has("composer.json"), "Composer", "composer.json"],
|
|
1397
|
+
[has("Gemfile"), "Bundler", "Gemfile"],
|
|
1398
|
+
[has("Package.swift"), "Swift Package Manager", "Package.swift"],
|
|
1399
|
+
[has("pubspec.yaml"), "pub", "pubspec.yaml"],
|
|
1400
|
+
[has("uv.lock"), "uv", "uv.lock"],
|
|
1401
|
+
[has("poetry.lock"), "Poetry", "poetry.lock"],
|
|
1402
|
+
[has("requirements.txt"), "pip", "requirements.txt"],
|
|
1403
|
+
[has("pyproject.toml"), "pip", "pyproject.toml"],
|
|
1404
|
+
[has("pnpm-lock.yaml"), "pnpm", "pnpm-lock.yaml"],
|
|
1405
|
+
[has("yarn.lock"), "yarn", "yarn.lock"],
|
|
1406
|
+
[has("package-lock.json"), "npm", "package-lock.json"],
|
|
1407
|
+
[has("package.json"), "npm", "package.json"]
|
|
1408
|
+
];
|
|
1409
|
+
for (const [present, name, source] of candidates) {
|
|
1410
|
+
if (present) {
|
|
1411
|
+
return [name, source];
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return [null, null];
|
|
1415
|
+
}
|
|
1416
|
+
async function detectTestsEvidence(rootDir, has, hasTestScript, pythonText) {
|
|
1417
|
+
if (has("tests")) {
|
|
1418
|
+
return "`tests/` directory";
|
|
1419
|
+
}
|
|
1420
|
+
if (has("test")) {
|
|
1421
|
+
return "`test/` directory";
|
|
1422
|
+
}
|
|
1423
|
+
if (has("__tests__")) {
|
|
1424
|
+
return "`__tests__/` directory";
|
|
1425
|
+
}
|
|
1426
|
+
if (has("pytest.ini") || pythonText.includes("pytest")) {
|
|
1427
|
+
return "pytest configuration";
|
|
1428
|
+
}
|
|
1429
|
+
if (has("phpunit.xml") || has("phpunit.xml.dist")) {
|
|
1430
|
+
return "PHPUnit configuration";
|
|
1431
|
+
}
|
|
1432
|
+
if (hasTestScript) {
|
|
1433
|
+
return '`"test"` script in package.json';
|
|
1434
|
+
}
|
|
1435
|
+
const testFile = await findTestFile(rootDir);
|
|
1436
|
+
if (testFile !== null) {
|
|
1437
|
+
return `\`${testFile}\``;
|
|
1438
|
+
}
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
var TEST_FILE_PATTERNS = [
|
|
1442
|
+
/_test\.go$/u,
|
|
1443
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/u,
|
|
1444
|
+
/^test_.+\.py$/u,
|
|
1445
|
+
/_test\.py$/u,
|
|
1446
|
+
/.+Tests?\.(java|kt)$/u,
|
|
1447
|
+
/.+Test\.php$/u,
|
|
1448
|
+
/_spec\.rb$/u,
|
|
1449
|
+
/_test\.rb$/u
|
|
1450
|
+
];
|
|
1451
|
+
var TEST_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1452
|
+
"node_modules",
|
|
1453
|
+
"vendor",
|
|
1454
|
+
"dist",
|
|
1455
|
+
"build",
|
|
1456
|
+
"target",
|
|
1457
|
+
"coverage",
|
|
1458
|
+
"Pods",
|
|
1459
|
+
"__pycache__"
|
|
1460
|
+
]);
|
|
1461
|
+
async function findTestFile(rootDir) {
|
|
1462
|
+
let budget = 4e3;
|
|
1463
|
+
const stack = [rootDir];
|
|
1464
|
+
while (stack.length > 0 && budget > 0) {
|
|
1465
|
+
const dir = stack.pop();
|
|
1466
|
+
if (dir === void 0) {
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
let entries;
|
|
1470
|
+
try {
|
|
1471
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
1472
|
+
} catch {
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
for (const entry of entries) {
|
|
1476
|
+
budget -= 1;
|
|
1477
|
+
if (budget <= 0) {
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
if (entry.isDirectory()) {
|
|
1481
|
+
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1482
|
+
stack.push(path6.join(dir, entry.name));
|
|
1483
|
+
}
|
|
1484
|
+
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1485
|
+
return path6.relative(rootDir, path6.join(dir, entry.name));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1402
1491
|
function collectDependencies(pkg) {
|
|
1403
1492
|
if (pkg === null) {
|
|
1404
1493
|
return {};
|
|
@@ -1426,6 +1515,100 @@ function isRecord(value) {
|
|
|
1426
1515
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1427
1516
|
}
|
|
1428
1517
|
|
|
1518
|
+
// src/core/adopt/generate-adoption.ts
|
|
1519
|
+
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1520
|
+
function generateAdoptionFiles(options) {
|
|
1521
|
+
const files = [
|
|
1522
|
+
{
|
|
1523
|
+
path: ADOPTION_REPORT_PATH,
|
|
1524
|
+
content: renderReport(options.adrDir, options.signals)
|
|
1525
|
+
}
|
|
1526
|
+
];
|
|
1527
|
+
for (const framework of options.signals.frameworks) {
|
|
1528
|
+
files.push({
|
|
1529
|
+
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1530
|
+
content: renderProposedAdr(framework)
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return files;
|
|
1534
|
+
}
|
|
1535
|
+
function renderReport(adrDir, signals) {
|
|
1536
|
+
return `# Adoption Report
|
|
1537
|
+
|
|
1538
|
+
## Status
|
|
1539
|
+
|
|
1540
|
+
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1541
|
+
is accepted repository memory until you accept it.
|
|
1542
|
+
|
|
1543
|
+
## Detected Signals
|
|
1544
|
+
|
|
1545
|
+
Each signal notes the file it was inferred from. If one is wrong, correct the source or edit this
|
|
1546
|
+
report \u2014 nothing here is accepted.
|
|
1547
|
+
|
|
1548
|
+
${summarizeSignals(signals).join("\n")}
|
|
1549
|
+
|
|
1550
|
+
## Proposed Decisions
|
|
1551
|
+
|
|
1552
|
+
${renderProposedDecisions(adrDir, signals)}
|
|
1553
|
+
|
|
1554
|
+
## Review Checklist
|
|
1555
|
+
|
|
1556
|
+
- [ ] Confirm the detected languages and package manager (and the source each was read from).
|
|
1557
|
+
- [ ] Confirm where tests were detected, or point Recall at the right location if it is wrong.
|
|
1558
|
+
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1559
|
+
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1560
|
+
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1561
|
+
|
|
1562
|
+
## Notes
|
|
1563
|
+
|
|
1564
|
+
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1565
|
+
files. No repository code was executed and no decision was accepted automatically.
|
|
1566
|
+
`;
|
|
1567
|
+
}
|
|
1568
|
+
function renderProposedDecisions(adrDir, signals) {
|
|
1569
|
+
if (signals.frameworks.length === 0) {
|
|
1570
|
+
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1571
|
+
}
|
|
1572
|
+
return signals.frameworks.map(
|
|
1573
|
+
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1574
|
+
).join("\n");
|
|
1575
|
+
}
|
|
1576
|
+
function renderProposedAdr(framework) {
|
|
1577
|
+
return `# Proposed ADR: Use ${framework}
|
|
1578
|
+
|
|
1579
|
+
## Status
|
|
1580
|
+
|
|
1581
|
+
Proposed
|
|
1582
|
+
|
|
1583
|
+
## Context
|
|
1584
|
+
|
|
1585
|
+
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1586
|
+
|
|
1587
|
+
## Decision
|
|
1588
|
+
|
|
1589
|
+
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1590
|
+
and is not accepted until a human reviews and accepts it.
|
|
1591
|
+
|
|
1592
|
+
## Alternatives Considered
|
|
1593
|
+
|
|
1594
|
+
- Record a different framework.
|
|
1595
|
+
- Leave the decision unrecorded for now.
|
|
1596
|
+
|
|
1597
|
+
## Consequences
|
|
1598
|
+
|
|
1599
|
+
- Captures a framework already in use as reviewable repository memory.
|
|
1600
|
+
- Requires explicit human acceptance before it becomes repository truth.
|
|
1601
|
+
|
|
1602
|
+
## Related Documents
|
|
1603
|
+
|
|
1604
|
+
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1605
|
+
- The adoption report generated alongside this proposal.
|
|
1606
|
+
`;
|
|
1607
|
+
}
|
|
1608
|
+
function frameworkSlug(framework) {
|
|
1609
|
+
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1429
1612
|
// src/commands/adopt.ts
|
|
1430
1613
|
var AdoptError = class extends Error {
|
|
1431
1614
|
code;
|
|
@@ -1497,19 +1680,102 @@ function formatList2(values) {
|
|
|
1497
1680
|
return values.length > 0 ? values.join(", ") : "none detected";
|
|
1498
1681
|
}
|
|
1499
1682
|
|
|
1500
|
-
// src/core/doctor/checks/
|
|
1501
|
-
import {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1683
|
+
// src/core/doctor/checks/code-reference-check.ts
|
|
1684
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1685
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1686
|
+
import path7 from "path";
|
|
1687
|
+
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1688
|
+
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1689
|
+
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
1690
|
+
var codePathPattern = /`((?:src|tests)\/[A-Za-z0-9._/-]+\.[A-Za-z0-9]+)`/gu;
|
|
1691
|
+
var placeholderMarkers = /[<>*]|\.\.\./u;
|
|
1692
|
+
async function checkCodeReferences(context) {
|
|
1693
|
+
if (context.config === void 0) {
|
|
1694
|
+
return [];
|
|
1695
|
+
}
|
|
1696
|
+
const findings = [];
|
|
1697
|
+
const featureEntries = await readDirIfExists(context.rootDir, context.config.featuresDir);
|
|
1698
|
+
for (const folder of featureEntries) {
|
|
1699
|
+
if (!folder.isDirectory() || !featureFolderPattern.test(folder.name)) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
for (const doc of FEATURE_DOCS) {
|
|
1703
|
+
const relativePath = path7.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1704
|
+
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const moduleEntries = await readDirIfExists(context.rootDir, context.config.modulesDir);
|
|
1708
|
+
for (const folder of moduleEntries) {
|
|
1709
|
+
if (!folder.isDirectory()) {
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
for (const doc of MODULE_DOCS) {
|
|
1713
|
+
const relativePath = path7.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1714
|
+
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return findings;
|
|
1718
|
+
}
|
|
1719
|
+
async function checkDoc(rootDir, relativePath) {
|
|
1720
|
+
const content = await readFileIfExists(rootDir, relativePath);
|
|
1721
|
+
if (content === void 0) {
|
|
1722
|
+
return [];
|
|
1723
|
+
}
|
|
1724
|
+
const findings = [];
|
|
1725
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1726
|
+
for (const match of content.matchAll(codePathPattern)) {
|
|
1727
|
+
const reference = match[1];
|
|
1728
|
+
if (placeholderMarkers.test(reference) || seen.has(reference)) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
seen.add(reference);
|
|
1732
|
+
if (!existsSync4(path7.join(rootDir, reference))) {
|
|
1733
|
+
findings.push({
|
|
1734
|
+
severity: "warning",
|
|
1735
|
+
check: "drift-code-reference",
|
|
1736
|
+
message: `Repository memory references ${reference}, which does not exist.`,
|
|
1737
|
+
path: relativePath
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return findings;
|
|
1742
|
+
}
|
|
1743
|
+
async function readDirIfExists(rootDir, relativePath) {
|
|
1505
1744
|
try {
|
|
1506
|
-
|
|
1745
|
+
return await readdir5(path7.join(rootDir, relativePath), { withFileTypes: true });
|
|
1507
1746
|
} catch (error) {
|
|
1508
1747
|
const nodeError = error;
|
|
1509
1748
|
if (nodeError.code === "ENOENT") {
|
|
1510
|
-
return
|
|
1511
|
-
|
|
1512
|
-
|
|
1749
|
+
return [];
|
|
1750
|
+
}
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async function readFileIfExists(rootDir, relativePath) {
|
|
1755
|
+
try {
|
|
1756
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
const nodeError = error;
|
|
1759
|
+
if (nodeError.code === "ENOENT") {
|
|
1760
|
+
return void 0;
|
|
1761
|
+
}
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/core/doctor/checks/config-check.ts
|
|
1767
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1768
|
+
async function checkConfig(rootDir) {
|
|
1769
|
+
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1770
|
+
let rawConfig;
|
|
1771
|
+
try {
|
|
1772
|
+
rawConfig = await readFile5(configPath.absolutePath, "utf8");
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
const nodeError = error;
|
|
1775
|
+
if (nodeError.code === "ENOENT") {
|
|
1776
|
+
return {
|
|
1777
|
+
findings: [
|
|
1778
|
+
{
|
|
1513
1779
|
severity: "error",
|
|
1514
1780
|
check: "config",
|
|
1515
1781
|
message: "Missing .recall/config.json.",
|
|
@@ -1566,21 +1832,24 @@ async function checkConfig(rootDir) {
|
|
|
1566
1832
|
}
|
|
1567
1833
|
|
|
1568
1834
|
// src/core/doctor/checks/content-check.ts
|
|
1569
|
-
import { readFile as
|
|
1570
|
-
import
|
|
1571
|
-
var
|
|
1835
|
+
import { readFile as readFile6, readdir as readdir6 } from "fs/promises";
|
|
1836
|
+
import path8 from "path";
|
|
1837
|
+
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1838
|
+
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1839
|
+
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
1840
|
+
var THREAT_MODEL_PATH = "docs/20-security/THREAT_MODEL.md";
|
|
1572
1841
|
async function checkContent(context) {
|
|
1573
1842
|
if (context.config === void 0) {
|
|
1574
1843
|
return [];
|
|
1575
1844
|
}
|
|
1576
1845
|
const findings = [];
|
|
1577
|
-
const entries = await
|
|
1846
|
+
const entries = await readDirIfExists2(context.rootDir, context.config.featuresDir);
|
|
1578
1847
|
const featureFolders = entries.filter(
|
|
1579
|
-
(entry) => entry.isDirectory() &&
|
|
1848
|
+
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1580
1849
|
);
|
|
1581
1850
|
for (const folder of featureFolders) {
|
|
1582
|
-
const prdPath =
|
|
1583
|
-
const prd = await
|
|
1851
|
+
const prdPath = path8.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1852
|
+
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1584
1853
|
if (prd === void 0) {
|
|
1585
1854
|
continue;
|
|
1586
1855
|
}
|
|
@@ -1601,6 +1870,61 @@ async function checkContent(context) {
|
|
|
1601
1870
|
});
|
|
1602
1871
|
}
|
|
1603
1872
|
}
|
|
1873
|
+
const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
|
|
1874
|
+
const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
|
|
1875
|
+
const adrEntries = await readDirIfExists2(context.rootDir, context.config.adrDir);
|
|
1876
|
+
const acceptedAdrs = adrEntries.filter(
|
|
1877
|
+
(entry) => entry.isFile() && acceptedAdrPattern.test(entry.name)
|
|
1878
|
+
);
|
|
1879
|
+
const hasWork = featureFolders.length > 0 || moduleFolders.length > 0 || acceptedAdrs.length > 0;
|
|
1880
|
+
if (hasWork) {
|
|
1881
|
+
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
1882
|
+
}
|
|
1883
|
+
for (const folder of moduleFolders) {
|
|
1884
|
+
const modulePath = path8.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1885
|
+
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1886
|
+
if (moduleDoc === void 0) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (sectionIsUnfilled(moduleDoc, "Purpose")) {
|
|
1890
|
+
findings.push({
|
|
1891
|
+
severity: "warning",
|
|
1892
|
+
check: "content-module",
|
|
1893
|
+
message: "Module memory purpose is still an unfilled template.",
|
|
1894
|
+
path: modulePath
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
if (sectionIsUnfilled(moduleDoc, "Owns")) {
|
|
1898
|
+
findings.push({
|
|
1899
|
+
severity: "warning",
|
|
1900
|
+
check: "content-module",
|
|
1901
|
+
message: "Module memory owns section is still an unfilled template.",
|
|
1902
|
+
path: modulePath
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return findings;
|
|
1907
|
+
}
|
|
1908
|
+
async function checkSecurityDoc(rootDir) {
|
|
1909
|
+
const findings = [];
|
|
1910
|
+
const security = await readFileIfExists2(rootDir, SECURITY_MODEL_PATH);
|
|
1911
|
+
if (security !== void 0 && sectionIsUnfilled(security, "Authentication And Authorization")) {
|
|
1912
|
+
findings.push({
|
|
1913
|
+
severity: "warning",
|
|
1914
|
+
check: "content-security",
|
|
1915
|
+
message: "Security model authentication and authorization section is still an unfilled template.",
|
|
1916
|
+
path: SECURITY_MODEL_PATH
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
const threat = await readFileIfExists2(rootDir, THREAT_MODEL_PATH);
|
|
1920
|
+
if (threat !== void 0 && sectionIsUnfilled(threat, "Assets")) {
|
|
1921
|
+
findings.push({
|
|
1922
|
+
severity: "warning",
|
|
1923
|
+
check: "content-threat-model",
|
|
1924
|
+
message: "Threat model assets section is still an unfilled template.",
|
|
1925
|
+
path: THREAT_MODEL_PATH
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1604
1928
|
return findings;
|
|
1605
1929
|
}
|
|
1606
1930
|
function sectionIsUnfilled(content, heading) {
|
|
@@ -1615,7 +1939,7 @@ function isUnfilled(value) {
|
|
|
1615
1939
|
if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
|
|
1616
1940
|
return true;
|
|
1617
1941
|
}
|
|
1618
|
-
return normalized.includes("describe why this feature exists");
|
|
1942
|
+
return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns") || normalized.includes("describe how this repository authenticates") || normalized.includes("describe what this repository must protect");
|
|
1619
1943
|
}
|
|
1620
1944
|
function getSection(content, heading) {
|
|
1621
1945
|
const lines = content.split(/\r?\n/u);
|
|
@@ -1633,9 +1957,9 @@ function getSection(content, heading) {
|
|
|
1633
1957
|
}
|
|
1634
1958
|
return body.join("\n").trim();
|
|
1635
1959
|
}
|
|
1636
|
-
async function
|
|
1960
|
+
async function readDirIfExists2(rootDir, relativePath) {
|
|
1637
1961
|
try {
|
|
1638
|
-
return await
|
|
1962
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1639
1963
|
} catch (error) {
|
|
1640
1964
|
const nodeError = error;
|
|
1641
1965
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1644,9 +1968,9 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1644
1968
|
throw error;
|
|
1645
1969
|
}
|
|
1646
1970
|
}
|
|
1647
|
-
async function
|
|
1971
|
+
async function readFileIfExists2(rootDir, relativePath) {
|
|
1648
1972
|
try {
|
|
1649
|
-
return await
|
|
1973
|
+
return await readFile6(path8.join(rootDir, relativePath), "utf8");
|
|
1650
1974
|
} catch (error) {
|
|
1651
1975
|
const nodeError = error;
|
|
1652
1976
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1657,8 +1981,8 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1657
1981
|
}
|
|
1658
1982
|
|
|
1659
1983
|
// src/core/doctor/checks/drift-check.ts
|
|
1660
|
-
import { readFile as
|
|
1661
|
-
import
|
|
1984
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
1985
|
+
import path9 from "path";
|
|
1662
1986
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1663
1987
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1664
1988
|
async function checkDrift(context) {
|
|
@@ -1675,12 +1999,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
1675
1999
|
const known = /* @__PURE__ */ new Map();
|
|
1676
2000
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
1677
2001
|
for (const file of files) {
|
|
1678
|
-
const match = adrFilePattern.exec(
|
|
2002
|
+
const match = adrFilePattern.exec(path9.basename(file));
|
|
1679
2003
|
if (match === null) {
|
|
1680
2004
|
continue;
|
|
1681
2005
|
}
|
|
1682
2006
|
const id = `ADR-${match[1]}`;
|
|
1683
|
-
const content = await
|
|
2007
|
+
const content = await readFile7(path9.join(rootDir, file), "utf8");
|
|
1684
2008
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
1685
2009
|
const existing = known.get(id);
|
|
1686
2010
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -1693,7 +2017,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
1693
2017
|
const findings = [];
|
|
1694
2018
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
1695
2019
|
for (const file of files) {
|
|
1696
|
-
const content = await
|
|
2020
|
+
const content = await readFile7(path9.join(rootDir, file), "utf8");
|
|
1697
2021
|
const referenced = /* @__PURE__ */ new Set();
|
|
1698
2022
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
1699
2023
|
referenced.add(match[0].toUpperCase());
|
|
@@ -1725,10 +2049,10 @@ function stripCode(content) {
|
|
|
1725
2049
|
return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
|
|
1726
2050
|
}
|
|
1727
2051
|
async function readMarkdownFiles(rootDir, relativeDir) {
|
|
1728
|
-
const entries = await
|
|
2052
|
+
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
1729
2053
|
const files = [];
|
|
1730
2054
|
for (const entry of entries) {
|
|
1731
|
-
const childRelative =
|
|
2055
|
+
const childRelative = path9.posix.join(relativeDir, entry.name);
|
|
1732
2056
|
if (entry.isDirectory()) {
|
|
1733
2057
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
1734
2058
|
continue;
|
|
@@ -1759,9 +2083,9 @@ function getSection2(content, heading) {
|
|
|
1759
2083
|
}
|
|
1760
2084
|
return body.join("\n").trim();
|
|
1761
2085
|
}
|
|
1762
|
-
async function
|
|
2086
|
+
async function readDirIfExists3(rootDir, relativePath) {
|
|
1763
2087
|
try {
|
|
1764
|
-
return await
|
|
2088
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1765
2089
|
} catch (error) {
|
|
1766
2090
|
const nodeError = error;
|
|
1767
2091
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1772,9 +2096,39 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1772
2096
|
}
|
|
1773
2097
|
|
|
1774
2098
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
1775
|
-
import { lstat as lstat2, readFile as
|
|
1776
|
-
import
|
|
1777
|
-
|
|
2099
|
+
import { lstat as lstat2, readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2100
|
+
import path10 from "path";
|
|
2101
|
+
|
|
2102
|
+
// src/core/adr/adr-sections.ts
|
|
2103
|
+
var REQUIRED_ADR_SECTIONS = [
|
|
2104
|
+
"## Status",
|
|
2105
|
+
"## Context",
|
|
2106
|
+
"## Decision",
|
|
2107
|
+
"## Alternatives Considered",
|
|
2108
|
+
"## Consequences",
|
|
2109
|
+
"## Related Documents"
|
|
2110
|
+
];
|
|
2111
|
+
var SECTION_PLACEHOLDERS = {
|
|
2112
|
+
"## Related Documents": "- None yet. Link related ADRs, features, or modules as they are accepted."
|
|
2113
|
+
};
|
|
2114
|
+
function ensureRequiredAdrSections(body) {
|
|
2115
|
+
let result = body.replace(/\s+$/u, "");
|
|
2116
|
+
for (const section of REQUIRED_ADR_SECTIONS) {
|
|
2117
|
+
if (!result.includes(section)) {
|
|
2118
|
+
const placeholder = SECTION_PLACEHOLDERS[section] ?? "To be documented.";
|
|
2119
|
+
result += `
|
|
2120
|
+
|
|
2121
|
+
${section}
|
|
2122
|
+
|
|
2123
|
+
${placeholder}`;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return `${result}
|
|
2127
|
+
`;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/core/doctor/checks/memory-integrity-check.ts
|
|
2131
|
+
var featureFolderPattern3 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1778
2132
|
var adrFilePattern2 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1779
2133
|
var requiredFeatureDocs = [
|
|
1780
2134
|
"PRD.md",
|
|
@@ -1788,14 +2142,7 @@ var requiredFeatureDocs = [
|
|
|
1788
2142
|
"COMPLETION_REPORT.md"
|
|
1789
2143
|
];
|
|
1790
2144
|
var requiredModuleDocs = ["MODULE.md", "TASKS.md", "TEST_PLAN.md", "DECISIONS.md"];
|
|
1791
|
-
var requiredAdrSections =
|
|
1792
|
-
"## Status",
|
|
1793
|
-
"## Context",
|
|
1794
|
-
"## Decision",
|
|
1795
|
-
"## Alternatives Considered",
|
|
1796
|
-
"## Consequences",
|
|
1797
|
-
"## Related Documents"
|
|
1798
|
-
];
|
|
2145
|
+
var requiredAdrSections = REQUIRED_ADR_SECTIONS;
|
|
1799
2146
|
async function checkMemoryIntegrity(context) {
|
|
1800
2147
|
if (context.config === void 0) {
|
|
1801
2148
|
return [];
|
|
@@ -1808,13 +2155,13 @@ async function checkMemoryIntegrity(context) {
|
|
|
1808
2155
|
}
|
|
1809
2156
|
async function checkFeatureFolders(rootDir, featuresDir) {
|
|
1810
2157
|
const findings = [];
|
|
1811
|
-
const entries = await
|
|
2158
|
+
const entries = await readDirIfExists4(rootDir, featuresDir);
|
|
1812
2159
|
const featureFolders = entries.filter(
|
|
1813
|
-
(entry) => entry.isDirectory() &&
|
|
2160
|
+
(entry) => entry.isDirectory() && featureFolderPattern3.test(entry.name)
|
|
1814
2161
|
);
|
|
1815
2162
|
for (const featureFolder of featureFolders) {
|
|
1816
2163
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
1817
|
-
const filePath =
|
|
2164
|
+
const filePath = path10.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
1818
2165
|
if (!await isFile(rootDir, filePath)) {
|
|
1819
2166
|
findings.push({
|
|
1820
2167
|
severity: "error",
|
|
@@ -1834,11 +2181,11 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
1834
2181
|
}
|
|
1835
2182
|
async function checkModuleFolders(rootDir, modulesDir) {
|
|
1836
2183
|
const findings = [];
|
|
1837
|
-
const entries = await
|
|
2184
|
+
const entries = await readDirIfExists4(rootDir, modulesDir);
|
|
1838
2185
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
1839
2186
|
for (const moduleFolder of moduleFolders) {
|
|
1840
2187
|
for (const requiredDoc of requiredModuleDocs) {
|
|
1841
|
-
const filePath =
|
|
2188
|
+
const filePath = path10.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
1842
2189
|
if (!await isFile(rootDir, filePath)) {
|
|
1843
2190
|
findings.push({
|
|
1844
2191
|
severity: "error",
|
|
@@ -1858,11 +2205,11 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
1858
2205
|
}
|
|
1859
2206
|
async function checkAdrFiles(rootDir, adrDir) {
|
|
1860
2207
|
const findings = [];
|
|
1861
|
-
const entries = await
|
|
2208
|
+
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
1862
2209
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
1863
2210
|
for (const adrFile of adrFiles) {
|
|
1864
|
-
const filePath =
|
|
1865
|
-
const content = await
|
|
2211
|
+
const filePath = path10.posix.join(adrDir, adrFile.name);
|
|
2212
|
+
const content = await readFile8(path10.join(rootDir, filePath), "utf8");
|
|
1866
2213
|
for (const requiredSection of requiredAdrSections) {
|
|
1867
2214
|
if (!content.includes(requiredSection)) {
|
|
1868
2215
|
findings.push({
|
|
@@ -1881,9 +2228,9 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
1881
2228
|
});
|
|
1882
2229
|
return findings;
|
|
1883
2230
|
}
|
|
1884
|
-
async function
|
|
2231
|
+
async function readDirIfExists4(rootDir, relativePath) {
|
|
1885
2232
|
try {
|
|
1886
|
-
return await
|
|
2233
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
1887
2234
|
} catch (error) {
|
|
1888
2235
|
const nodeError = error;
|
|
1889
2236
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1894,7 +2241,7 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
1894
2241
|
}
|
|
1895
2242
|
async function isFile(rootDir, relativePath) {
|
|
1896
2243
|
try {
|
|
1897
|
-
return (await lstat2(
|
|
2244
|
+
return (await lstat2(path10.join(rootDir, relativePath))).isFile();
|
|
1898
2245
|
} catch (error) {
|
|
1899
2246
|
const nodeError = error;
|
|
1900
2247
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1906,7 +2253,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
1906
2253
|
|
|
1907
2254
|
// src/core/doctor/checks/required-files-check.ts
|
|
1908
2255
|
import { lstat as lstat3 } from "fs/promises";
|
|
1909
|
-
import
|
|
2256
|
+
import path11 from "path";
|
|
1910
2257
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
1911
2258
|
var requiredDocs = [
|
|
1912
2259
|
"00-product/PRD.md",
|
|
@@ -1933,12 +2280,12 @@ async function checkRequiredFiles(context) {
|
|
|
1933
2280
|
}
|
|
1934
2281
|
}
|
|
1935
2282
|
for (const relativeDocPath of requiredDocs) {
|
|
1936
|
-
const filePath =
|
|
2283
|
+
const filePath = path11.posix.join(docsDir, relativeDocPath);
|
|
1937
2284
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
1938
2285
|
findings.push(missingFile(filePath, "required-docs"));
|
|
1939
2286
|
}
|
|
1940
2287
|
}
|
|
1941
|
-
const adrIndexPath =
|
|
2288
|
+
const adrIndexPath = path11.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
1942
2289
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
1943
2290
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
1944
2291
|
}
|
|
@@ -1964,7 +2311,7 @@ async function checkRequiredFiles(context) {
|
|
|
1964
2311
|
}
|
|
1965
2312
|
async function isFile2(rootDir, relativePath) {
|
|
1966
2313
|
try {
|
|
1967
|
-
return (await lstat3(
|
|
2314
|
+
return (await lstat3(path11.join(rootDir, relativePath))).isFile();
|
|
1968
2315
|
} catch (error) {
|
|
1969
2316
|
const nodeError = error;
|
|
1970
2317
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1975,7 +2322,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
1975
2322
|
}
|
|
1976
2323
|
async function isDirectory(rootDir, relativePath) {
|
|
1977
2324
|
try {
|
|
1978
|
-
return (await lstat3(
|
|
2325
|
+
return (await lstat3(path11.join(rootDir, relativePath))).isDirectory();
|
|
1979
2326
|
} catch (error) {
|
|
1980
2327
|
const nodeError = error;
|
|
1981
2328
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1994,9 +2341,9 @@ function missingFile(pathValue, check) {
|
|
|
1994
2341
|
}
|
|
1995
2342
|
|
|
1996
2343
|
// src/core/doctor/checks/standards-check.ts
|
|
1997
|
-
import { lstat as lstat4, readFile as
|
|
1998
|
-
import
|
|
1999
|
-
var
|
|
2344
|
+
import { lstat as lstat4, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2345
|
+
import path12 from "path";
|
|
2346
|
+
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2000
2347
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2001
2348
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
2002
2349
|
async function checkStandards(context) {
|
|
@@ -2010,18 +2357,18 @@ async function checkStandards(context) {
|
|
|
2010
2357
|
}
|
|
2011
2358
|
async function checkFeatureStandards(rootDir, featuresDir) {
|
|
2012
2359
|
const findings = [];
|
|
2013
|
-
const entries = await
|
|
2360
|
+
const entries = await readDirIfExists5(rootDir, featuresDir);
|
|
2014
2361
|
const featureFolders = entries.filter(
|
|
2015
|
-
(entry) => entry.isDirectory() &&
|
|
2362
|
+
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2016
2363
|
);
|
|
2017
2364
|
for (const featureFolder of featureFolders) {
|
|
2018
|
-
const featureDir =
|
|
2019
|
-
const completionReportPath =
|
|
2020
|
-
const reviewPath =
|
|
2021
|
-
const architectureImpactPath =
|
|
2022
|
-
const completionReport = await
|
|
2023
|
-
const review = await
|
|
2024
|
-
const architectureImpact = await
|
|
2365
|
+
const featureDir = path12.posix.join(featuresDir, featureFolder.name);
|
|
2366
|
+
const completionReportPath = path12.posix.join(featureDir, "COMPLETION_REPORT.md");
|
|
2367
|
+
const reviewPath = path12.posix.join(featureDir, "REVIEW.md");
|
|
2368
|
+
const architectureImpactPath = path12.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
|
|
2369
|
+
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2370
|
+
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2371
|
+
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
2025
2372
|
if (completionReport !== void 0) {
|
|
2026
2373
|
const featureIsComplete = sectionContains2(completionReport, "Status", /\bcomplete\b/iu);
|
|
2027
2374
|
if (featureIsComplete) {
|
|
@@ -2065,11 +2412,11 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2065
2412
|
}
|
|
2066
2413
|
async function checkAdrStandards(rootDir, adrDir) {
|
|
2067
2414
|
const findings = [];
|
|
2068
|
-
const entries = await
|
|
2415
|
+
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2069
2416
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2070
2417
|
for (const adrFile of adrFiles) {
|
|
2071
|
-
const adrPath =
|
|
2072
|
-
const content = await
|
|
2418
|
+
const adrPath = path12.posix.join(adrDir, adrFile.name);
|
|
2419
|
+
const content = await readFile9(path12.join(rootDir, adrPath), "utf8");
|
|
2073
2420
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2074
2421
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2075
2422
|
findings.push({
|
|
@@ -2151,9 +2498,9 @@ function isPlaceholder(value) {
|
|
|
2151
2498
|
}
|
|
2152
2499
|
return normalized.includes("implementation is in progress") || normalized.includes("will be completed after implementation");
|
|
2153
2500
|
}
|
|
2154
|
-
async function
|
|
2501
|
+
async function readDirIfExists5(rootDir, relativePath) {
|
|
2155
2502
|
try {
|
|
2156
|
-
return await
|
|
2503
|
+
return await readdir9(path12.join(rootDir, relativePath), { withFileTypes: true });
|
|
2157
2504
|
} catch (error) {
|
|
2158
2505
|
const nodeError = error;
|
|
2159
2506
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2162,12 +2509,12 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2162
2509
|
throw error;
|
|
2163
2510
|
}
|
|
2164
2511
|
}
|
|
2165
|
-
async function
|
|
2512
|
+
async function readFileIfExists3(rootDir, relativePath) {
|
|
2166
2513
|
try {
|
|
2167
2514
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2168
2515
|
return void 0;
|
|
2169
2516
|
}
|
|
2170
|
-
return await
|
|
2517
|
+
return await readFile9(path12.join(rootDir, relativePath), "utf8");
|
|
2171
2518
|
} catch (error) {
|
|
2172
2519
|
const nodeError = error;
|
|
2173
2520
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2178,7 +2525,7 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
2178
2525
|
}
|
|
2179
2526
|
async function isFile3(rootDir, relativePath) {
|
|
2180
2527
|
try {
|
|
2181
|
-
return (await lstat4(
|
|
2528
|
+
return (await lstat4(path12.join(rootDir, relativePath))).isFile();
|
|
2182
2529
|
} catch (error) {
|
|
2183
2530
|
const nodeError = error;
|
|
2184
2531
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2203,6 +2550,7 @@ async function runDoctor(rootDir) {
|
|
|
2203
2550
|
findings.push(...await checkStandards(context));
|
|
2204
2551
|
findings.push(...await checkDrift(context));
|
|
2205
2552
|
findings.push(...await checkContent(context));
|
|
2553
|
+
findings.push(...await checkCodeReferences(context));
|
|
2206
2554
|
}
|
|
2207
2555
|
return createDoctorReport(findings);
|
|
2208
2556
|
}
|
|
@@ -2279,11 +2627,11 @@ function formatDoctorResult(result) {
|
|
|
2279
2627
|
}
|
|
2280
2628
|
|
|
2281
2629
|
// src/commands/init.ts
|
|
2282
|
-
import { existsSync as
|
|
2283
|
-
import
|
|
2630
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2631
|
+
import path15 from "path";
|
|
2284
2632
|
|
|
2285
2633
|
// src/core/generator/generate-init.ts
|
|
2286
|
-
import
|
|
2634
|
+
import path13 from "path";
|
|
2287
2635
|
var neutralTemplates = [
|
|
2288
2636
|
{
|
|
2289
2637
|
path: "AGENTS.md",
|
|
@@ -2308,11 +2656,41 @@ Repository rules override model preferences. If instructions conflict, stop and
|
|
|
2308
2656
|
path: "CLAUDE.md",
|
|
2309
2657
|
content: `# {{repositoryName}} Claude Instructions
|
|
2310
2658
|
|
|
2311
|
-
|
|
2659
|
+
This file is loaded automatically every Claude session. The durable project memory lives in \`docs/\`;
|
|
2660
|
+
do not rely on chat history as source of truth, and repository rules override model preference.
|
|
2661
|
+
|
|
2662
|
+
@AGENTS.md
|
|
2663
|
+
|
|
2664
|
+
Read the docs that \`AGENTS.md\` routes to before changing code or repository memory. A SessionStart
|
|
2665
|
+
hook (\`.claude/hooks/session-start.sh\`) also injects a memory map at the start of each session.
|
|
2666
|
+
`
|
|
2667
|
+
},
|
|
2668
|
+
{
|
|
2669
|
+
// Cursor auto-applies rules under .cursor/rules. alwaysApply makes this the portable equivalent
|
|
2670
|
+
// of the Claude Code SessionStart hook: Cursor injects it into every request so the agent loads
|
|
2671
|
+
// repository memory even though it cannot run the Claude-specific hook.
|
|
2672
|
+
path: ".cursor/rules/recall-memory.mdc",
|
|
2673
|
+
content: `---
|
|
2674
|
+
description: {{repositoryName}} repository memory and rules (Recall OS). Read before non-trivial work.
|
|
2675
|
+
globs:
|
|
2676
|
+
alwaysApply: true
|
|
2677
|
+
---
|
|
2678
|
+
|
|
2679
|
+
# {{repositoryName}} repository memory
|
|
2680
|
+
|
|
2681
|
+
This repository uses Recall OS. Durable memory lives in \`docs/\` and is the source of truth over chat
|
|
2682
|
+
history. Do not treat chat history as truth, and repository rules override model preference.
|
|
2683
|
+
|
|
2684
|
+
Before non-trivial work:
|
|
2312
2685
|
|
|
2313
|
-
|
|
2686
|
+
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2687
|
+
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2688
|
+
- If an instruction conflicts with accepted repository memory, stop and report the conflict.
|
|
2314
2689
|
|
|
2315
|
-
|
|
2690
|
+
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2691
|
+
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
2692
|
+
|
|
2693
|
+
Before claiming work is complete, run \`recall doctor\` and fix reported errors.
|
|
2316
2694
|
`
|
|
2317
2695
|
},
|
|
2318
2696
|
{
|
|
@@ -2396,26 +2774,78 @@ Default behavior:
|
|
|
2396
2774
|
path: "docs/20-security/SECURITY_MODEL.md",
|
|
2397
2775
|
content: `# Security Model
|
|
2398
2776
|
|
|
2399
|
-
##
|
|
2777
|
+
## Status
|
|
2400
2778
|
|
|
2401
|
-
Draft.
|
|
2779
|
+
Draft \u2014 fill the prompted sections below with this repository's real model as it grows. \`recall doctor\`
|
|
2780
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2402
2781
|
|
|
2403
2782
|
## Baseline Rules
|
|
2404
2783
|
|
|
2405
|
-
-
|
|
2406
|
-
-
|
|
2784
|
+
- Never commit secrets or credentials, and never read or copy \`.env\` files into docs.
|
|
2785
|
+
- Validate and authorize untrusted input at every trust boundary.
|
|
2407
2786
|
- Do not add network, telemetry, cloud, MCP runtime, or AI API behavior without explicit review.
|
|
2787
|
+
|
|
2788
|
+
## Authentication And Authorization
|
|
2789
|
+
|
|
2790
|
+
Describe how this repository authenticates users or clients and how it authorizes actions, including
|
|
2791
|
+
where those checks live.
|
|
2792
|
+
|
|
2793
|
+
## Secrets And Configuration
|
|
2794
|
+
|
|
2795
|
+
Describe where secrets live, how they are injected, and how configuration is kept out of version
|
|
2796
|
+
control.
|
|
2797
|
+
|
|
2798
|
+
## Sensitive Data
|
|
2799
|
+
|
|
2800
|
+
Describe the sensitive or personal data this repository handles, and how it is protected at rest and
|
|
2801
|
+
in transit.
|
|
2802
|
+
|
|
2803
|
+
## Dependencies And Supply Chain
|
|
2804
|
+
|
|
2805
|
+
Describe how third-party dependencies are vetted, pinned, and updated.
|
|
2408
2806
|
`
|
|
2409
2807
|
},
|
|
2410
2808
|
{
|
|
2411
2809
|
path: "docs/20-security/THREAT_MODEL.md",
|
|
2412
2810
|
content: `# Threat Model
|
|
2413
2811
|
|
|
2414
|
-
##
|
|
2812
|
+
## Status
|
|
2415
2813
|
|
|
2416
|
-
Draft.
|
|
2814
|
+
Draft \u2014 replace the prompts below with this repository's real analysis as it grows. \`recall doctor\`
|
|
2815
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2816
|
+
|
|
2817
|
+
## Assets
|
|
2818
|
+
|
|
2819
|
+
Describe what this repository must protect: user data, credentials, money, availability, or
|
|
2820
|
+
reputation.
|
|
2821
|
+
|
|
2822
|
+
## Entry Points
|
|
2823
|
+
|
|
2824
|
+
Describe where untrusted input enters: HTTP endpoints, webhooks, file uploads, queues, CLI input, or
|
|
2825
|
+
third-party callbacks.
|
|
2826
|
+
|
|
2827
|
+
## Trust Boundaries
|
|
2417
2828
|
|
|
2418
|
-
|
|
2829
|
+
Describe where trust changes: client to server, service to database, your code to third-party APIs.
|
|
2830
|
+
|
|
2831
|
+
## Threats
|
|
2832
|
+
|
|
2833
|
+
Describe the concrete threats that apply to this repository, by category:
|
|
2834
|
+
|
|
2835
|
+
- Spoofing \u2014 how identities are faked or sessions stolen.
|
|
2836
|
+
- Tampering \u2014 how requests, data, or builds are altered (injection, mass assignment).
|
|
2837
|
+
- Repudiation \u2014 actions that must remain auditable.
|
|
2838
|
+
- Information disclosure \u2014 how sensitive data or secrets could leak.
|
|
2839
|
+
- Denial of service \u2014 how the system can be overwhelmed or abused.
|
|
2840
|
+
- Elevation of privilege \u2014 how a user could gain access they should not have.
|
|
2841
|
+
|
|
2842
|
+
## Mitigations
|
|
2843
|
+
|
|
2844
|
+
Describe the control in place or planned for each threat above.
|
|
2845
|
+
|
|
2846
|
+
## Open Risks
|
|
2847
|
+
|
|
2848
|
+
Describe accepted or unresolved risks and who owns them.
|
|
2419
2849
|
`
|
|
2420
2850
|
},
|
|
2421
2851
|
{
|
|
@@ -2668,14 +3098,38 @@ Agents should not implement meaningful feature work without a feature plan or cl
|
|
|
2668
3098
|
path: "docs/adrs/README.md",
|
|
2669
3099
|
content: `# Architecture Decision Records
|
|
2670
3100
|
|
|
2671
|
-
Accepted
|
|
3101
|
+
Accepted ADRs live in this directory as \`ADR-####-<slug>.md\` with \`## Status\` set to \`Accepted\`.
|
|
3102
|
+
Proposed ADRs live under \`docs/adrs/proposed/\`.
|
|
3103
|
+
|
|
3104
|
+
There is no \`accepted/\` subdirectory: accepted ADRs sit at the top level of \`docs/adrs/\`.
|
|
2672
3105
|
|
|
2673
|
-
Presets and AI agents may propose decisions
|
|
3106
|
+
Presets and AI agents may propose decisions; humans accept them with \`recall adr accept <name>\`,
|
|
3107
|
+
which promotes a proposal into an accepted ADR here.
|
|
3108
|
+
`
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
path: ".github/workflows/recall.yml",
|
|
3112
|
+
content: `name: Recall OS
|
|
3113
|
+
|
|
3114
|
+
on:
|
|
3115
|
+
push:
|
|
3116
|
+
pull_request:
|
|
3117
|
+
|
|
3118
|
+
jobs:
|
|
3119
|
+
doctor:
|
|
3120
|
+
runs-on: ubuntu-latest
|
|
3121
|
+
steps:
|
|
3122
|
+
- uses: actions/checkout@v4
|
|
3123
|
+
- uses: actions/setup-node@v4
|
|
3124
|
+
with:
|
|
3125
|
+
node-version: 20
|
|
3126
|
+
- name: Validate repository memory
|
|
3127
|
+
run: npx --yes recall-os@latest doctor
|
|
2674
3128
|
`
|
|
2675
3129
|
}
|
|
2676
3130
|
];
|
|
2677
3131
|
function generateInitFiles(options) {
|
|
2678
|
-
const repositoryName =
|
|
3132
|
+
const repositoryName = path13.basename(path13.resolve(options.rootDir)) || "repository";
|
|
2679
3133
|
const context = createTemplateContext({ repositoryName });
|
|
2680
3134
|
const files = neutralTemplates.map((template) => ({
|
|
2681
3135
|
path: template.path,
|
|
@@ -2694,24 +3148,25 @@ function generatePresetFiles(preset) {
|
|
|
2694
3148
|
})),
|
|
2695
3149
|
...preset.proposedDecisions.map((decision) => ({
|
|
2696
3150
|
path: decision.destination,
|
|
2697
|
-
|
|
3151
|
+
// Normalize every preset's proposed ADR so it stays Doctor-healthy once accepted.
|
|
3152
|
+
content: ensureRequiredAdrSections(decision.body)
|
|
2698
3153
|
}))
|
|
2699
3154
|
];
|
|
2700
3155
|
}
|
|
2701
3156
|
|
|
2702
3157
|
// src/core/hooks/detect-gates.ts
|
|
2703
|
-
import { existsSync as
|
|
2704
|
-
import { readFile as
|
|
2705
|
-
import
|
|
3158
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3159
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3160
|
+
import path14 from "path";
|
|
2706
3161
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
2707
3162
|
async function detectPreCommitGates(rootDir) {
|
|
2708
|
-
const packageJsonPath =
|
|
2709
|
-
if (!
|
|
3163
|
+
const packageJsonPath = path14.join(rootDir, "package.json");
|
|
3164
|
+
if (!existsSync5(packageJsonPath)) {
|
|
2710
3165
|
return [];
|
|
2711
3166
|
}
|
|
2712
3167
|
let scripts;
|
|
2713
3168
|
try {
|
|
2714
|
-
const raw = await
|
|
3169
|
+
const raw = await readFile10(packageJsonPath, "utf8");
|
|
2715
3170
|
const parsed = JSON.parse(raw);
|
|
2716
3171
|
scripts = parsed.scripts ?? {};
|
|
2717
3172
|
} catch {
|
|
@@ -2720,16 +3175,16 @@ async function detectPreCommitGates(rootDir) {
|
|
|
2720
3175
|
if (typeof scripts !== "object" || scripts === null) {
|
|
2721
3176
|
return [];
|
|
2722
3177
|
}
|
|
2723
|
-
const packageManager =
|
|
3178
|
+
const packageManager = detectPackageManager2(rootDir);
|
|
2724
3179
|
return KNOWN_SCRIPTS.filter((script) => typeof scripts[script] === "string").map(
|
|
2725
3180
|
(script) => `${packageManager} run ${script}`
|
|
2726
3181
|
);
|
|
2727
3182
|
}
|
|
2728
|
-
function
|
|
2729
|
-
if (
|
|
3183
|
+
function detectPackageManager2(rootDir) {
|
|
3184
|
+
if (existsSync5(path14.join(rootDir, "pnpm-lock.yaml"))) {
|
|
2730
3185
|
return "pnpm";
|
|
2731
3186
|
}
|
|
2732
|
-
if (
|
|
3187
|
+
if (existsSync5(path14.join(rootDir, "yarn.lock"))) {
|
|
2733
3188
|
return "yarn";
|
|
2734
3189
|
}
|
|
2735
3190
|
return "npm";
|
|
@@ -2738,6 +3193,39 @@ function detectPackageManager(rootDir) {
|
|
|
2738
3193
|
// src/core/hooks/generate-hook.ts
|
|
2739
3194
|
var PRE_COMMIT_HOOK_PATH = ".recall/hooks/pre-commit";
|
|
2740
3195
|
var HOOKS_PATH_ACTIVATION_COMMAND = "git config core.hooksPath .recall/hooks";
|
|
3196
|
+
var SESSION_START_HOOK_PATH = ".claude/hooks/session-start.sh";
|
|
3197
|
+
var CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
3198
|
+
function renderSessionStartHook() {
|
|
3199
|
+
return `#!/bin/sh
|
|
3200
|
+
# Recall OS Claude Code SessionStart hook.
|
|
3201
|
+
# Generated by \`recall init\`. Injects a repository-memory map into every Claude Code session so a
|
|
3202
|
+
# fresh agent reliably loads durable memory. Wired in .claude/settings.json. Read-only.
|
|
3203
|
+
|
|
3204
|
+
adrs=$(ls docs/adrs/ADR-*.md 2>/dev/null | sed 's|.*/||;s|\\.md$||' | tr '\\n' ' ')
|
|
3205
|
+
modules=$(ls -d docs/30-modules/*/ 2>/dev/null | sed 's|docs/30-modules/||;s|/$||' | tr '\\n' ' ')
|
|
3206
|
+
|
|
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."
|
|
3208
|
+
|
|
3209
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$context"
|
|
3210
|
+
`;
|
|
3211
|
+
}
|
|
3212
|
+
function renderClaudeSettings() {
|
|
3213
|
+
return `${JSON.stringify(
|
|
3214
|
+
{
|
|
3215
|
+
hooks: {
|
|
3216
|
+
SessionStart: [
|
|
3217
|
+
{
|
|
3218
|
+
matcher: "startup",
|
|
3219
|
+
hooks: [{ type: "command", command: `./${SESSION_START_HOOK_PATH}` }]
|
|
3220
|
+
}
|
|
3221
|
+
]
|
|
3222
|
+
}
|
|
3223
|
+
},
|
|
3224
|
+
null,
|
|
3225
|
+
2
|
|
3226
|
+
)}
|
|
3227
|
+
`;
|
|
3228
|
+
}
|
|
2741
3229
|
function renderPreCommitHook(gates) {
|
|
2742
3230
|
const lines = [
|
|
2743
3231
|
"#!/bin/sh",
|
|
@@ -3249,73 +3737,129 @@ Consider MVVM with unidirectional state from ViewModels, awaiting human acceptan
|
|
|
3249
3737
|
]
|
|
3250
3738
|
};
|
|
3251
3739
|
|
|
3252
|
-
// src/presets/
|
|
3253
|
-
var
|
|
3740
|
+
// src/presets/laravel/shared.ts
|
|
3741
|
+
var VARIANTS = {
|
|
3742
|
+
react: {
|
|
3743
|
+
id: "laravel-react",
|
|
3744
|
+
label: "React via Inertia",
|
|
3745
|
+
description: "Opinionated Laravel + Inertia + React opinion pack (proposed decisions only). Matches the official Laravel React starter kit.",
|
|
3746
|
+
frontendLine: "Frontend: Inertia 2 + React 19 + TypeScript + Tailwind, built with Vite (the official React starter kit, with shadcn/ui for components).",
|
|
3747
|
+
deliveryLine: "The app is a server-driven SPA: Laravel controllers return Inertia responses with typed props; there is no separate REST client for first-party screens."
|
|
3748
|
+
},
|
|
3749
|
+
vue: {
|
|
3750
|
+
id: "laravel-vue",
|
|
3751
|
+
label: "Vue via Inertia",
|
|
3752
|
+
description: "Opinionated Laravel + Inertia + Vue opinion pack (proposed decisions only). Matches the official Laravel Vue starter kit.",
|
|
3753
|
+
frontendLine: "Frontend: Inertia 2 + Vue 3 (script setup) + TypeScript + Tailwind, built with Vite (the official Vue starter kit).",
|
|
3754
|
+
deliveryLine: "The app is a server-driven SPA: Laravel controllers return Inertia responses with typed props; there is no separate REST client for first-party screens."
|
|
3755
|
+
},
|
|
3756
|
+
api: {
|
|
3757
|
+
id: "laravel-api",
|
|
3758
|
+
label: "API / SPA backend",
|
|
3759
|
+
description: "Opinionated Laravel API-only opinion pack (proposed decisions only) for a decoupled SPA or mobile client.",
|
|
3760
|
+
frontendLine: "No server-rendered frontend: Laravel is an HTTP JSON API consumed by a separate SPA or mobile app.",
|
|
3761
|
+
deliveryLine: "Controllers return JSON via API Resources; first-party SPAs authenticate with Sanctum cookies, mobile clients with Sanctum tokens."
|
|
3762
|
+
}
|
|
3763
|
+
};
|
|
3764
|
+
function adr(presetId, topic, title, body) {
|
|
3765
|
+
return {
|
|
3766
|
+
id: `${presetId}-${topic}`,
|
|
3767
|
+
title,
|
|
3768
|
+
status: "proposed",
|
|
3769
|
+
destination: `docs/adrs/proposed/ADR-PROPOSED-${presetId}-${topic}.md`,
|
|
3770
|
+
body
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function related(presetId, ...extra) {
|
|
3774
|
+
return [
|
|
3775
|
+
`## Related Documents`,
|
|
3776
|
+
``,
|
|
3777
|
+
`- \`docs/ai/presets/${presetId}-guidance.md\` \u2014 the proposed Laravel stack guidance.`,
|
|
3778
|
+
`- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.`,
|
|
3779
|
+
...extra.map((line) => `- ${line}`),
|
|
3780
|
+
``
|
|
3781
|
+
].join("\n");
|
|
3782
|
+
}
|
|
3783
|
+
function laravelGuidance(variant) {
|
|
3784
|
+
const profile = VARIANTS[variant];
|
|
3785
|
+
return `# Laravel Preset Guidance (${profile.label})
|
|
3254
3786
|
|
|
3255
3787
|
This is proposed guidance, not accepted. Convert any architecture choice into a proposed ADR, then an
|
|
3256
|
-
accepted ADR, before treating it as repository truth.
|
|
3788
|
+
accepted ADR, before treating it as repository truth. Repository rules override model preference.
|
|
3789
|
+
|
|
3790
|
+
## The stack (proposed)
|
|
3791
|
+
|
|
3792
|
+
- Laravel 12 on PHP 8.3+, using the official conventions and directory layout.
|
|
3793
|
+
- ${profile.frontendLine}
|
|
3794
|
+
- Database: PostgreSQL (MySQL is the alternative) through Eloquent and migrations.
|
|
3795
|
+
- Auth: Laravel Sanctum for first-party SPA and mobile clients (Passport only if you need third-party OAuth2).
|
|
3796
|
+
- Background work: queues, with Redis and Laravel Horizon when throughput grows.
|
|
3797
|
+
- Tests: Pest, with database factories and feature tests over real routes.
|
|
3798
|
+
|
|
3799
|
+
${profile.deliveryLine}
|
|
3257
3800
|
|
|
3258
3801
|
## Decision forks this stack forces
|
|
3259
3802
|
|
|
3260
|
-
-
|
|
3261
|
-
-
|
|
3262
|
-
-
|
|
3263
|
-
-
|
|
3264
|
-
-
|
|
3803
|
+
- Frontend delivery: Inertia (server-driven SPA) vs a decoupled API + separate SPA vs Blade + Livewire.
|
|
3804
|
+
- Auth: Sanctum (first-party SPA and mobile) vs Passport (third-party OAuth2) vs a managed identity provider.
|
|
3805
|
+
- Database: PostgreSQL vs MySQL, and where read scaling and queues live (Redis vs database driver).
|
|
3806
|
+
- Authorization: Policies and Gates vs ad-hoc checks; validation via Form Requests vs inline.
|
|
3807
|
+
- Business logic: thin controllers with Action/Service classes vs fat controllers and models.
|
|
3808
|
+
- Testing: Pest vs PHPUnit.
|
|
3265
3809
|
|
|
3266
3810
|
## Recommended structure (proposed)
|
|
3267
3811
|
|
|
3268
|
-
-
|
|
3269
|
-
-
|
|
3270
|
-
-
|
|
3812
|
+
- Keep controllers thin: they validate, authorize, delegate, and return a response \u2014 nothing more.
|
|
3813
|
+
- Put request validation **and** authorization in Form Requests (\`authorize()\` + \`rules()\`).
|
|
3814
|
+
- Put per-model and per-action permission logic in Policies and Gates, not in controllers.
|
|
3815
|
+
- Put business logic in single-purpose Action or Service classes, not in controllers or models.
|
|
3816
|
+
- Shape every outbound payload with API Resources (or typed Inertia props), never raw models.
|
|
3817
|
+
- Declare \`$fillable\` (or \`$guarded\`) explicitly on every Eloquent model to stop mass assignment.
|
|
3818
|
+
|
|
3819
|
+
## Data and performance (proposed)
|
|
3820
|
+
|
|
3821
|
+
- Eager-load relationships (\`with(...)\`) to avoid N+1 queries; enable \`Model::preventLazyLoading()\` in local and CI.
|
|
3822
|
+
- Wrap multi-write operations in database transactions.
|
|
3823
|
+
- Paginate list endpoints; never return unbounded collections.
|
|
3824
|
+
- Move email, exports, third-party calls, and other slow work into queued jobs.
|
|
3825
|
+
- Cache expensive reads deliberately, with explicit invalidation.
|
|
3271
3826
|
|
|
3272
3827
|
## Testing (proposed)
|
|
3273
3828
|
|
|
3274
|
-
-
|
|
3275
|
-
-
|
|
3276
|
-
-
|
|
3829
|
+
- Write Pest feature tests that exercise real routes end to end, using \`RefreshDatabase\`.
|
|
3830
|
+
- Build state with model factories, not hand-rolled fixtures.
|
|
3831
|
+
- Test authorization explicitly: a forbidden action must assert a 403, not just a happy path.
|
|
3832
|
+
- Cover validation failures, not only the success case.
|
|
3277
3833
|
|
|
3278
3834
|
## Security considerations (proposed)
|
|
3279
3835
|
|
|
3280
|
-
-
|
|
3281
|
-
-
|
|
3282
|
-
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
id: `nextjs-${topic}`,
|
|
3287
|
-
title,
|
|
3288
|
-
status: "proposed",
|
|
3289
|
-
destination: `docs/adrs/proposed/ADR-PROPOSED-nextjs-${topic}.md`,
|
|
3290
|
-
body
|
|
3291
|
-
};
|
|
3836
|
+
- Validate every inbound request through Form Requests; persist only validated data.
|
|
3837
|
+
- Authorize every state-changing action through a Policy or Gate.
|
|
3838
|
+
- Keep secrets in \`.env\`; never commit \`.env\` or hardcode credentials.
|
|
3839
|
+
- Apply rate limiting to auth and write endpoints.
|
|
3840
|
+
- Keep mass assignment locked down and never trust client-supplied IDs without an ownership check.
|
|
3841
|
+
${variant === "api" ? "- Scope Sanctum tokens to least privilege; SPA clients use the cookie guard with CSRF protection.\n" : "- Inertia uses Laravel's session and CSRF protection; keep auth and authorization on the server, never the client.\n"}`;
|
|
3292
3842
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
name: "Next.js",
|
|
3296
|
-
description: "Opinionated Next.js opinion pack with proposed decisions only.",
|
|
3297
|
-
templates: [
|
|
3298
|
-
{
|
|
3299
|
-
destination: "docs/ai/presets/nextjs-guidance.md",
|
|
3300
|
-
description: "Next.js guidance that remains proposed until accepted.",
|
|
3301
|
-
content: guidance3
|
|
3302
|
-
}
|
|
3303
|
-
],
|
|
3304
|
-
guidance: [
|
|
3843
|
+
function laravelGuidanceItems() {
|
|
3844
|
+
return [
|
|
3305
3845
|
{
|
|
3306
|
-
title: "Keep
|
|
3307
|
-
body: "
|
|
3846
|
+
title: "Keep stack choices proposed",
|
|
3847
|
+
body: "Framework, frontend delivery, database, auth, and testing choices stay proposed until accepted in repository memory."
|
|
3308
3848
|
},
|
|
3309
3849
|
{
|
|
3310
|
-
title: "
|
|
3311
|
-
body: "
|
|
3850
|
+
title: "Thin controllers, explicit authorization",
|
|
3851
|
+
body: "Validate and authorize in Form Requests and Policies, keep business logic in Action or Service classes, and shape output with Resources \u2014 propose these as decisions before treating them as truth."
|
|
3312
3852
|
}
|
|
3313
|
-
]
|
|
3314
|
-
|
|
3315
|
-
|
|
3853
|
+
];
|
|
3854
|
+
}
|
|
3855
|
+
function laravelProposedDecisions(variant) {
|
|
3856
|
+
const presetId = VARIANTS[variant].id;
|
|
3857
|
+
const base = [
|
|
3858
|
+
adr(
|
|
3859
|
+
presetId,
|
|
3316
3860
|
"framework",
|
|
3317
|
-
"Use
|
|
3318
|
-
`# Proposed ADR: Use
|
|
3861
|
+
"Use Laravel",
|
|
3862
|
+
`# Proposed ADR: Use Laravel
|
|
3319
3863
|
|
|
3320
3864
|
## Status
|
|
3321
3865
|
|
|
@@ -3323,28 +3867,30 @@ Proposed
|
|
|
3323
3867
|
|
|
3324
3868
|
## Context
|
|
3325
3869
|
|
|
3326
|
-
The team needs a
|
|
3870
|
+
The team needs a productive, batteries-included PHP framework for a production web application.
|
|
3327
3871
|
|
|
3328
3872
|
## Decision
|
|
3329
3873
|
|
|
3330
|
-
Consider
|
|
3331
|
-
it.
|
|
3874
|
+
Consider Laravel 12 on PHP 8.3+ as the application framework, following its standard conventions and
|
|
3875
|
+
directory structure. This is not accepted until a human reviews and accepts it.
|
|
3332
3876
|
|
|
3333
3877
|
## Alternatives Considered
|
|
3334
3878
|
|
|
3335
|
-
-
|
|
3336
|
-
-
|
|
3879
|
+
- Symfony for a more component-assembled approach.
|
|
3880
|
+
- A different language or framework entirely.
|
|
3337
3881
|
|
|
3338
3882
|
## Consequences
|
|
3339
3883
|
|
|
3340
|
-
-
|
|
3341
|
-
- Couples the
|
|
3342
|
-
|
|
3884
|
+
- A mature ecosystem (Eloquent, queues, Sanctum, Horizon) and strong conventions.
|
|
3885
|
+
- Couples the application to Laravel's conventions and release cadence.
|
|
3886
|
+
|
|
3887
|
+
${related(presetId)}`
|
|
3343
3888
|
),
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
"
|
|
3347
|
-
|
|
3889
|
+
adr(
|
|
3890
|
+
presetId,
|
|
3891
|
+
"database-eloquent",
|
|
3892
|
+
"Use Eloquent and migrations on PostgreSQL",
|
|
3893
|
+
`# Proposed ADR: Use Eloquent and migrations on PostgreSQL
|
|
3348
3894
|
|
|
3349
3895
|
## Status
|
|
3350
3896
|
|
|
@@ -3352,27 +3898,30 @@ Proposed
|
|
|
3352
3898
|
|
|
3353
3899
|
## Context
|
|
3354
3900
|
|
|
3355
|
-
|
|
3901
|
+
The application needs a relational database and a schema workflow.
|
|
3356
3902
|
|
|
3357
3903
|
## Decision
|
|
3358
3904
|
|
|
3359
|
-
Consider the
|
|
3905
|
+
Consider PostgreSQL (MySQL as the alternative) accessed through Eloquent and versioned migrations,
|
|
3906
|
+
awaiting human acceptance.
|
|
3360
3907
|
|
|
3361
3908
|
## Alternatives Considered
|
|
3362
3909
|
|
|
3363
|
-
-
|
|
3364
|
-
-
|
|
3910
|
+
- MySQL or MariaDB.
|
|
3911
|
+
- The query builder or raw SQL without Eloquent.
|
|
3365
3912
|
|
|
3366
3913
|
## Consequences
|
|
3367
3914
|
|
|
3368
|
-
-
|
|
3369
|
-
- Requires
|
|
3370
|
-
|
|
3915
|
+
- Expressive models, relationships, and reproducible schema migrations.
|
|
3916
|
+
- Requires discipline against N+1 queries and unbounded result sets.
|
|
3917
|
+
|
|
3918
|
+
${related(presetId, "`docs/50-quality/TESTING_STRATEGY.md` \u2014 how database tests use factories and a disposable database.")}`
|
|
3371
3919
|
),
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
"
|
|
3375
|
-
|
|
3920
|
+
adr(
|
|
3921
|
+
presetId,
|
|
3922
|
+
"auth-sanctum",
|
|
3923
|
+
"Use Laravel Sanctum for authentication",
|
|
3924
|
+
`# Proposed ADR: Use Laravel Sanctum for authentication
|
|
3376
3925
|
|
|
3377
3926
|
## Status
|
|
3378
3927
|
|
|
@@ -3380,27 +3929,30 @@ Proposed
|
|
|
3380
3929
|
|
|
3381
3930
|
## Context
|
|
3382
3931
|
|
|
3383
|
-
The
|
|
3932
|
+
The application needs authentication for first-party clients (${variant === "api" ? "a separate SPA and mobile apps" : "an Inertia SPA, and possibly mobile apps"}).
|
|
3384
3933
|
|
|
3385
3934
|
## Decision
|
|
3386
3935
|
|
|
3387
|
-
Consider
|
|
3936
|
+
Consider Laravel Sanctum: the cookie-based guard for first-party SPAs and API tokens for mobile or
|
|
3937
|
+
scripted clients. This is not accepted until a human reviews and accepts it.
|
|
3388
3938
|
|
|
3389
3939
|
## Alternatives Considered
|
|
3390
3940
|
|
|
3391
|
-
-
|
|
3392
|
-
-
|
|
3941
|
+
- Laravel Passport for full OAuth2 (third-party delegated access).
|
|
3942
|
+
- A managed identity provider.
|
|
3393
3943
|
|
|
3394
3944
|
## Consequences
|
|
3395
3945
|
|
|
3396
|
-
-
|
|
3397
|
-
-
|
|
3398
|
-
|
|
3946
|
+
- Lightweight first-party auth without standing up a full OAuth2 server.
|
|
3947
|
+
- If third-party delegated access is ever required, revisit with Passport.
|
|
3948
|
+
|
|
3949
|
+
${related(presetId, "`docs/20-security/SECURITY_MODEL.md` \u2014 record the accepted auth and session model here.")}`
|
|
3399
3950
|
),
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
"
|
|
3403
|
-
|
|
3951
|
+
adr(
|
|
3952
|
+
presetId,
|
|
3953
|
+
"validation-authorization",
|
|
3954
|
+
"Validate with Form Requests and authorize with Policies",
|
|
3955
|
+
`# Proposed ADR: Validate with Form Requests and authorize with Policies
|
|
3404
3956
|
|
|
3405
3957
|
## Status
|
|
3406
3958
|
|
|
@@ -3408,27 +3960,31 @@ Proposed
|
|
|
3408
3960
|
|
|
3409
3961
|
## Context
|
|
3410
3962
|
|
|
3411
|
-
|
|
3963
|
+
Input validation and authorization must be consistent and centralized, not scattered across
|
|
3964
|
+
controllers.
|
|
3412
3965
|
|
|
3413
3966
|
## Decision
|
|
3414
3967
|
|
|
3415
|
-
Consider
|
|
3968
|
+
Consider Form Requests for validation (and request-level authorization) plus Policies and Gates for
|
|
3969
|
+
per-model and per-action permission checks, awaiting human acceptance.
|
|
3416
3970
|
|
|
3417
3971
|
## Alternatives Considered
|
|
3418
3972
|
|
|
3419
|
-
-
|
|
3420
|
-
- A
|
|
3973
|
+
- Inline validation and authorization in controllers.
|
|
3974
|
+
- A third-party permissions package layered on top.
|
|
3421
3975
|
|
|
3422
3976
|
## Consequences
|
|
3423
3977
|
|
|
3424
|
-
-
|
|
3425
|
-
-
|
|
3426
|
-
|
|
3978
|
+
- Controllers stay thin; validation and authorization are testable in isolation.
|
|
3979
|
+
- Every state-changing action must have an explicit authorization path.
|
|
3980
|
+
|
|
3981
|
+
${related(presetId)}`
|
|
3427
3982
|
),
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
"
|
|
3431
|
-
|
|
3983
|
+
adr(
|
|
3984
|
+
presetId,
|
|
3985
|
+
"application-structure",
|
|
3986
|
+
"Keep controllers thin with Action and Service classes",
|
|
3987
|
+
`# Proposed ADR: Keep controllers thin with Action and Service classes
|
|
3432
3988
|
|
|
3433
3989
|
## Status
|
|
3434
3990
|
|
|
@@ -3436,93 +3992,278 @@ Proposed
|
|
|
3436
3992
|
|
|
3437
3993
|
## Context
|
|
3438
3994
|
|
|
3439
|
-
|
|
3995
|
+
Business logic tends to accumulate in controllers and models, which makes it hard to test and reuse.
|
|
3440
3996
|
|
|
3441
3997
|
## Decision
|
|
3442
3998
|
|
|
3443
|
-
Consider
|
|
3999
|
+
Consider thin controllers that delegate to single-purpose Action or Service classes, with outbound
|
|
4000
|
+
payloads shaped by API Resources (or typed Inertia props). This is not accepted until a human accepts
|
|
4001
|
+
it.
|
|
3444
4002
|
|
|
3445
4003
|
## Alternatives Considered
|
|
3446
4004
|
|
|
3447
|
-
-
|
|
3448
|
-
-
|
|
4005
|
+
- Fat controllers.
|
|
4006
|
+
- Fat models holding business logic.
|
|
3449
4007
|
|
|
3450
4008
|
## Consequences
|
|
3451
4009
|
|
|
3452
|
-
-
|
|
3453
|
-
-
|
|
3454
|
-
|
|
4010
|
+
- Reusable, unit-testable business logic and consistent response shapes.
|
|
4011
|
+
- More classes and a convention the team must follow.
|
|
4012
|
+
|
|
4013
|
+
${related(presetId)}`
|
|
4014
|
+
),
|
|
4015
|
+
adr(
|
|
4016
|
+
presetId,
|
|
4017
|
+
"queues-horizon",
|
|
4018
|
+
"Run slow work on queues",
|
|
4019
|
+
`# Proposed ADR: Run slow work on queues
|
|
4020
|
+
|
|
4021
|
+
## Status
|
|
4022
|
+
|
|
4023
|
+
Proposed
|
|
4024
|
+
|
|
4025
|
+
## Context
|
|
4026
|
+
|
|
4027
|
+
Email, exports, and third-party calls slow down requests and can fail independently.
|
|
4028
|
+
|
|
4029
|
+
## Decision
|
|
4030
|
+
|
|
4031
|
+
Consider queued jobs for slow or failure-prone work, using the database driver early and Redis with
|
|
4032
|
+
Laravel Horizon as throughput grows. This is not accepted until a human reviews and accepts it.
|
|
4033
|
+
|
|
4034
|
+
## Alternatives Considered
|
|
4035
|
+
|
|
4036
|
+
- Doing the work synchronously in the request.
|
|
4037
|
+
- An external task queue or serverless functions.
|
|
4038
|
+
|
|
4039
|
+
## Consequences
|
|
4040
|
+
|
|
4041
|
+
- Faster responses and isolated, retryable background work.
|
|
4042
|
+
- Adds a worker process and queue infrastructure to operate and monitor.
|
|
4043
|
+
|
|
4044
|
+
${related(presetId)}`
|
|
4045
|
+
),
|
|
4046
|
+
adr(
|
|
4047
|
+
presetId,
|
|
4048
|
+
"testing-pest",
|
|
4049
|
+
"Use Pest for testing",
|
|
4050
|
+
`# Proposed ADR: Use Pest for testing
|
|
4051
|
+
|
|
4052
|
+
## Status
|
|
4053
|
+
|
|
4054
|
+
Proposed
|
|
4055
|
+
|
|
4056
|
+
## Context
|
|
4057
|
+
|
|
4058
|
+
The application needs a fast, readable testing workflow.
|
|
4059
|
+
|
|
4060
|
+
## Decision
|
|
4061
|
+
|
|
4062
|
+
Consider Pest with model factories and feature tests that exercise real routes against a disposable
|
|
4063
|
+
database, awaiting human acceptance.
|
|
4064
|
+
|
|
4065
|
+
## Alternatives Considered
|
|
4066
|
+
|
|
4067
|
+
- PHPUnit directly.
|
|
4068
|
+
- A thinner test suite focused only on unit tests.
|
|
4069
|
+
|
|
4070
|
+
## Consequences
|
|
4071
|
+
|
|
4072
|
+
- Concise, expressive tests that cover routes, validation, and authorization.
|
|
4073
|
+
- The team standardizes on Pest's syntax and plugins.
|
|
4074
|
+
|
|
4075
|
+
${related(presetId, "`docs/50-quality/TESTING_STRATEGY.md` \u2014 record the accepted testing approach here.")}`
|
|
3455
4076
|
)
|
|
3456
|
-
]
|
|
4077
|
+
];
|
|
4078
|
+
return [...base, frontendDecision(variant)];
|
|
4079
|
+
}
|
|
4080
|
+
function frontendDecision(variant) {
|
|
4081
|
+
const presetId = VARIANTS[variant].id;
|
|
4082
|
+
if (variant === "api") {
|
|
4083
|
+
return adr(
|
|
4084
|
+
presetId,
|
|
4085
|
+
"api-design-rest",
|
|
4086
|
+
"Expose a versioned REST API with API Resources",
|
|
4087
|
+
`# Proposed ADR: Expose a versioned REST API with API Resources
|
|
4088
|
+
|
|
4089
|
+
## Status
|
|
4090
|
+
|
|
4091
|
+
Proposed
|
|
4092
|
+
|
|
4093
|
+
## Context
|
|
4094
|
+
|
|
4095
|
+
A decoupled SPA or mobile client consumes Laravel over HTTP, so the API contract must be stable and
|
|
4096
|
+
explicit.
|
|
4097
|
+
|
|
4098
|
+
## Decision
|
|
4099
|
+
|
|
4100
|
+
Consider a versioned REST API (for example \`/api/v1\`) whose responses are shaped by API Resources,
|
|
4101
|
+
authenticated with Sanctum, and documented (OpenAPI). This is not accepted until a human accepts it.
|
|
4102
|
+
|
|
4103
|
+
## Alternatives Considered
|
|
4104
|
+
|
|
4105
|
+
- GraphQL.
|
|
4106
|
+
- Server-driven Inertia pages instead of a decoupled API.
|
|
4107
|
+
|
|
4108
|
+
## Consequences
|
|
4109
|
+
|
|
4110
|
+
- A stable, documented contract that multiple clients can rely on.
|
|
4111
|
+
- Versioning and serialization become an explicit, maintained concern.
|
|
4112
|
+
|
|
4113
|
+
${related(presetId, "`docs/20-security/SECURITY_MODEL.md` \u2014 record token scopes and the SPA cookie guard here.")}`
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
const framework = variant === "react" ? "React 19" : "Vue 3";
|
|
4117
|
+
const components = variant === "react" ? "shadcn/ui components on Tailwind" : "Tailwind with single-file components (script setup)";
|
|
4118
|
+
return adr(
|
|
4119
|
+
presetId,
|
|
4120
|
+
`frontend-inertia-${variant}`,
|
|
4121
|
+
`Use Inertia with ${framework}`,
|
|
4122
|
+
`# Proposed ADR: Use Inertia with ${framework}
|
|
4123
|
+
|
|
4124
|
+
## Status
|
|
4125
|
+
|
|
4126
|
+
Proposed
|
|
4127
|
+
|
|
4128
|
+
## Context
|
|
4129
|
+
|
|
4130
|
+
The application needs a modern SPA experience without standing up and securing a separate API for
|
|
4131
|
+
first-party screens.
|
|
4132
|
+
|
|
4133
|
+
## Decision
|
|
4134
|
+
|
|
4135
|
+
Consider Inertia 2 with ${framework} and TypeScript, built with Vite, using ${components}. Controllers
|
|
4136
|
+
return Inertia responses with typed props. This is not accepted until a human reviews and accepts it.
|
|
4137
|
+
|
|
4138
|
+
## Alternatives Considered
|
|
4139
|
+
|
|
4140
|
+
- A decoupled REST or GraphQL API with a standalone SPA.
|
|
4141
|
+
- Blade with Livewire.
|
|
4142
|
+
|
|
4143
|
+
## Consequences
|
|
4144
|
+
|
|
4145
|
+
- Server-driven routing and auth with a reactive ${framework} frontend and no duplicate API layer.
|
|
4146
|
+
- Couples the frontend to Inertia's model and the ${framework} ecosystem.
|
|
4147
|
+
|
|
4148
|
+
${related(presetId)}`
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
// src/presets/laravel-api/preset.ts
|
|
4153
|
+
var laravelApiPreset = {
|
|
4154
|
+
id: "laravel-api",
|
|
4155
|
+
name: "Laravel API",
|
|
4156
|
+
description: "Opinionated Laravel API-only opinion pack with proposed decisions only.",
|
|
4157
|
+
templates: [
|
|
4158
|
+
{
|
|
4159
|
+
destination: "docs/ai/presets/laravel-api-guidance.md",
|
|
4160
|
+
description: "Laravel API-only guidance that remains proposed until accepted.",
|
|
4161
|
+
content: laravelGuidance("api")
|
|
4162
|
+
}
|
|
4163
|
+
],
|
|
4164
|
+
guidance: laravelGuidanceItems(),
|
|
4165
|
+
proposedDecisions: laravelProposedDecisions("api")
|
|
3457
4166
|
};
|
|
3458
4167
|
|
|
3459
|
-
// src/presets/
|
|
3460
|
-
var
|
|
4168
|
+
// src/presets/laravel-react/preset.ts
|
|
4169
|
+
var laravelReactPreset = {
|
|
4170
|
+
id: "laravel-react",
|
|
4171
|
+
name: "Laravel + React",
|
|
4172
|
+
description: "Opinionated Laravel + Inertia + React opinion pack with proposed decisions only.",
|
|
4173
|
+
templates: [
|
|
4174
|
+
{
|
|
4175
|
+
destination: "docs/ai/presets/laravel-react-guidance.md",
|
|
4176
|
+
description: "Laravel + React (Inertia) guidance that remains proposed until accepted.",
|
|
4177
|
+
content: laravelGuidance("react")
|
|
4178
|
+
}
|
|
4179
|
+
],
|
|
4180
|
+
guidance: laravelGuidanceItems(),
|
|
4181
|
+
proposedDecisions: laravelProposedDecisions("react")
|
|
4182
|
+
};
|
|
3461
4183
|
|
|
3462
|
-
|
|
3463
|
-
|
|
4184
|
+
// src/presets/laravel-vue/preset.ts
|
|
4185
|
+
var laravelVuePreset = {
|
|
4186
|
+
id: "laravel-vue",
|
|
4187
|
+
name: "Laravel + Vue",
|
|
4188
|
+
description: "Opinionated Laravel + Inertia + Vue opinion pack with proposed decisions only.",
|
|
4189
|
+
templates: [
|
|
4190
|
+
{
|
|
4191
|
+
destination: "docs/ai/presets/laravel-vue-guidance.md",
|
|
4192
|
+
description: "Laravel + Vue (Inertia) guidance that remains proposed until accepted.",
|
|
4193
|
+
content: laravelGuidance("vue")
|
|
4194
|
+
}
|
|
4195
|
+
],
|
|
4196
|
+
guidance: laravelGuidanceItems(),
|
|
4197
|
+
proposedDecisions: laravelProposedDecisions("vue")
|
|
4198
|
+
};
|
|
4199
|
+
|
|
4200
|
+
// src/presets/nextjs/preset.ts
|
|
4201
|
+
var guidance3 = `# Next.js Preset Guidance
|
|
4202
|
+
|
|
4203
|
+
This is proposed guidance, not accepted. Convert any architecture choice into a proposed ADR, then an
|
|
4204
|
+
accepted ADR, before treating it as repository truth.
|
|
3464
4205
|
|
|
3465
4206
|
## Decision forks this stack forces
|
|
3466
4207
|
|
|
3467
|
-
-
|
|
3468
|
-
-
|
|
3469
|
-
-
|
|
3470
|
-
-
|
|
3471
|
-
-
|
|
4208
|
+
- Routing: App Router vs Pages Router.
|
|
4209
|
+
- Rendering: Server Components and server actions vs client-heavy rendering.
|
|
4210
|
+
- Data layer: Drizzle or Prisma with PostgreSQL vs a hosted backend.
|
|
4211
|
+
- Styling: Tailwind CSS vs CSS Modules vs a component library.
|
|
4212
|
+
- Testing: Vitest with Testing Library and Playwright vs other runners.
|
|
3472
4213
|
|
|
3473
4214
|
## Recommended structure (proposed)
|
|
3474
4215
|
|
|
3475
|
-
-
|
|
3476
|
-
-
|
|
3477
|
-
-
|
|
4216
|
+
- The App Router with route groups and colocated server components.
|
|
4217
|
+
- A typed data layer isolated from UI components.
|
|
4218
|
+
- Shared UI primitives and a consistent styling system.
|
|
3478
4219
|
|
|
3479
4220
|
## Testing (proposed)
|
|
3480
4221
|
|
|
3481
|
-
-
|
|
3482
|
-
-
|
|
3483
|
-
-
|
|
4222
|
+
- Unit and component tests with Vitest and Testing Library.
|
|
4223
|
+
- End-to-end tests with Playwright for critical flows.
|
|
4224
|
+
- Type-safe data access tested against a disposable database.
|
|
3484
4225
|
|
|
3485
4226
|
## Security considerations (proposed)
|
|
3486
4227
|
|
|
3487
|
-
-
|
|
3488
|
-
- Validate
|
|
3489
|
-
- Scope authentication and authorization
|
|
4228
|
+
- Keep secrets in server-only environment variables, never in client bundles.
|
|
4229
|
+
- Validate input on the server, including server actions and route handlers.
|
|
4230
|
+
- Scope authentication and authorization on the server, not the client.
|
|
3490
4231
|
`;
|
|
3491
|
-
function
|
|
4232
|
+
function proposedAdr3(topic, title, body) {
|
|
3492
4233
|
return {
|
|
3493
|
-
id: `
|
|
4234
|
+
id: `nextjs-${topic}`,
|
|
3494
4235
|
title,
|
|
3495
4236
|
status: "proposed",
|
|
3496
|
-
destination: `docs/adrs/proposed/ADR-PROPOSED-
|
|
4237
|
+
destination: `docs/adrs/proposed/ADR-PROPOSED-nextjs-${topic}.md`,
|
|
3497
4238
|
body
|
|
3498
4239
|
};
|
|
3499
4240
|
}
|
|
3500
|
-
var
|
|
3501
|
-
id: "
|
|
3502
|
-
name: "
|
|
3503
|
-
description: "Opinionated
|
|
4241
|
+
var nextjsPreset = {
|
|
4242
|
+
id: "nextjs",
|
|
4243
|
+
name: "Next.js",
|
|
4244
|
+
description: "Opinionated Next.js opinion pack with proposed decisions only.",
|
|
3504
4245
|
templates: [
|
|
3505
4246
|
{
|
|
3506
|
-
destination: "docs/ai/presets/
|
|
3507
|
-
description: "
|
|
3508
|
-
content:
|
|
4247
|
+
destination: "docs/ai/presets/nextjs-guidance.md",
|
|
4248
|
+
description: "Next.js guidance that remains proposed until accepted.",
|
|
4249
|
+
content: guidance3
|
|
3509
4250
|
}
|
|
3510
4251
|
],
|
|
3511
4252
|
guidance: [
|
|
3512
4253
|
{
|
|
3513
|
-
title: "Keep framework
|
|
3514
|
-
body: "
|
|
4254
|
+
title: "Keep framework choices proposed",
|
|
4255
|
+
body: "Routing, rendering, data layer, styling, and testing choices must remain proposed until accepted in repository memory."
|
|
3515
4256
|
},
|
|
3516
4257
|
{
|
|
3517
|
-
title: "
|
|
3518
|
-
body: "
|
|
4258
|
+
title: "Keep secrets server-side",
|
|
4259
|
+
body: "Never expose secrets to the client bundle, and record the data and auth approach as proposed decisions before acceptance."
|
|
3519
4260
|
}
|
|
3520
4261
|
],
|
|
3521
4262
|
proposedDecisions: [
|
|
3522
|
-
|
|
4263
|
+
proposedAdr3(
|
|
3523
4264
|
"framework",
|
|
3524
|
-
"Use
|
|
3525
|
-
`# Proposed ADR: Use
|
|
4265
|
+
"Use Next.js",
|
|
4266
|
+
`# Proposed ADR: Use Next.js
|
|
3526
4267
|
|
|
3527
4268
|
## Status
|
|
3528
4269
|
|
|
@@ -3530,27 +4271,28 @@ Proposed
|
|
|
3530
4271
|
|
|
3531
4272
|
## Context
|
|
3532
4273
|
|
|
3533
|
-
The
|
|
4274
|
+
The team needs a React framework for a production web application.
|
|
3534
4275
|
|
|
3535
4276
|
## Decision
|
|
3536
4277
|
|
|
3537
|
-
Consider
|
|
4278
|
+
Consider Next.js as the application framework. This is not accepted until a human reviews and accepts
|
|
4279
|
+
it.
|
|
3538
4280
|
|
|
3539
4281
|
## Alternatives Considered
|
|
3540
4282
|
|
|
3541
|
-
-
|
|
3542
|
-
-
|
|
4283
|
+
- A Vite single-page app with a separate API.
|
|
4284
|
+
- Remix or another framework.
|
|
3543
4285
|
|
|
3544
4286
|
## Consequences
|
|
3545
4287
|
|
|
3546
|
-
-
|
|
3547
|
-
-
|
|
4288
|
+
- Server rendering, routing, and a large ecosystem.
|
|
4289
|
+
- Couples the app to Next.js conventions.
|
|
3548
4290
|
`
|
|
3549
4291
|
),
|
|
3550
|
-
|
|
3551
|
-
"
|
|
3552
|
-
"Use
|
|
3553
|
-
`# Proposed ADR: Use
|
|
4292
|
+
proposedAdr3(
|
|
4293
|
+
"routing-app-router",
|
|
4294
|
+
"Use the App Router",
|
|
4295
|
+
`# Proposed ADR: Use the App Router
|
|
3554
4296
|
|
|
3555
4297
|
## Status
|
|
3556
4298
|
|
|
@@ -3558,27 +4300,27 @@ Proposed
|
|
|
3558
4300
|
|
|
3559
4301
|
## Context
|
|
3560
4302
|
|
|
3561
|
-
|
|
4303
|
+
Next.js offers the App Router and the legacy Pages Router.
|
|
3562
4304
|
|
|
3563
4305
|
## Decision
|
|
3564
4306
|
|
|
3565
|
-
Consider
|
|
4307
|
+
Consider the App Router with Server Components, awaiting human acceptance.
|
|
3566
4308
|
|
|
3567
4309
|
## Alternatives Considered
|
|
3568
4310
|
|
|
3569
|
-
-
|
|
3570
|
-
-
|
|
4311
|
+
- The Pages Router.
|
|
4312
|
+
- A mix during migration.
|
|
3571
4313
|
|
|
3572
4314
|
## Consequences
|
|
3573
4315
|
|
|
3574
|
-
-
|
|
3575
|
-
- Requires
|
|
4316
|
+
- Server Components and nested layouts.
|
|
4317
|
+
- Requires understanding server and client boundaries.
|
|
3576
4318
|
`
|
|
3577
4319
|
),
|
|
3578
|
-
|
|
3579
|
-
"
|
|
3580
|
-
"Use
|
|
3581
|
-
`# Proposed ADR: Use
|
|
4320
|
+
proposedAdr3(
|
|
4321
|
+
"data-layer",
|
|
4322
|
+
"Use a typed data layer with PostgreSQL",
|
|
4323
|
+
`# Proposed ADR: Use a typed data layer with PostgreSQL
|
|
3582
4324
|
|
|
3583
4325
|
## Status
|
|
3584
4326
|
|
|
@@ -3586,29 +4328,235 @@ Proposed
|
|
|
3586
4328
|
|
|
3587
4329
|
## Context
|
|
3588
4330
|
|
|
3589
|
-
The
|
|
4331
|
+
The app needs a database and a typed access layer.
|
|
3590
4332
|
|
|
3591
4333
|
## Decision
|
|
3592
4334
|
|
|
3593
|
-
Consider
|
|
4335
|
+
Consider Drizzle or Prisma with PostgreSQL, awaiting human acceptance.
|
|
3594
4336
|
|
|
3595
4337
|
## Alternatives Considered
|
|
3596
4338
|
|
|
3597
|
-
-
|
|
3598
|
-
-
|
|
4339
|
+
- A hosted backend or BaaS.
|
|
4340
|
+
- Raw SQL.
|
|
3599
4341
|
|
|
3600
4342
|
## Consequences
|
|
3601
4343
|
|
|
3602
|
-
-
|
|
3603
|
-
- Adds
|
|
4344
|
+
- Type-safe queries and migrations.
|
|
4345
|
+
- Adds an ORM and schema workflow.
|
|
3604
4346
|
`
|
|
3605
4347
|
),
|
|
3606
|
-
|
|
3607
|
-
"
|
|
3608
|
-
"Use
|
|
3609
|
-
`# Proposed ADR: Use
|
|
3610
|
-
|
|
3611
|
-
## Status
|
|
4348
|
+
proposedAdr3(
|
|
4349
|
+
"styling-tailwind",
|
|
4350
|
+
"Use Tailwind CSS",
|
|
4351
|
+
`# Proposed ADR: Use Tailwind CSS
|
|
4352
|
+
|
|
4353
|
+
## Status
|
|
4354
|
+
|
|
4355
|
+
Proposed
|
|
4356
|
+
|
|
4357
|
+
## Context
|
|
4358
|
+
|
|
4359
|
+
The app needs a styling approach.
|
|
4360
|
+
|
|
4361
|
+
## Decision
|
|
4362
|
+
|
|
4363
|
+
Consider Tailwind CSS for styling, awaiting human acceptance.
|
|
4364
|
+
|
|
4365
|
+
## Alternatives Considered
|
|
4366
|
+
|
|
4367
|
+
- CSS Modules.
|
|
4368
|
+
- A component library with its own styling.
|
|
4369
|
+
|
|
4370
|
+
## Consequences
|
|
4371
|
+
|
|
4372
|
+
- Fast, consistent utility-based styling.
|
|
4373
|
+
- Markup includes utility classes that teams must standardize.
|
|
4374
|
+
`
|
|
4375
|
+
),
|
|
4376
|
+
proposedAdr3(
|
|
4377
|
+
"testing",
|
|
4378
|
+
"Use Vitest and Playwright",
|
|
4379
|
+
`# Proposed ADR: Use Vitest and Playwright
|
|
4380
|
+
|
|
4381
|
+
## Status
|
|
4382
|
+
|
|
4383
|
+
Proposed
|
|
4384
|
+
|
|
4385
|
+
## Context
|
|
4386
|
+
|
|
4387
|
+
The app needs unit, component, and end-to-end testing.
|
|
4388
|
+
|
|
4389
|
+
## Decision
|
|
4390
|
+
|
|
4391
|
+
Consider Vitest with Testing Library and Playwright, awaiting human acceptance.
|
|
4392
|
+
|
|
4393
|
+
## Alternatives Considered
|
|
4394
|
+
|
|
4395
|
+
- Jest.
|
|
4396
|
+
- Cypress for end-to-end tests.
|
|
4397
|
+
|
|
4398
|
+
## Consequences
|
|
4399
|
+
|
|
4400
|
+
- Fast unit and component tests plus reliable end-to-end coverage.
|
|
4401
|
+
- Teams maintain two test toolchains.
|
|
4402
|
+
`
|
|
4403
|
+
)
|
|
4404
|
+
]
|
|
4405
|
+
};
|
|
4406
|
+
|
|
4407
|
+
// src/presets/python-fastapi/preset.ts
|
|
4408
|
+
var guidance4 = `# Python FastAPI Preset Guidance
|
|
4409
|
+
|
|
4410
|
+
This guidance is proposed, not accepted. Convert any choice you adopt into an accepted ADR in
|
|
4411
|
+
repository memory. Until then, treat everything here as a recommendation awaiting human review.
|
|
4412
|
+
|
|
4413
|
+
## Decision forks this stack forces
|
|
4414
|
+
|
|
4415
|
+
- Web framework: FastAPI vs Flask vs Django REST.
|
|
4416
|
+
- Database and access: PostgreSQL with SQLAlchemy and Alembic vs an async ORM vs raw SQL.
|
|
4417
|
+
- Validation and settings: Pydantic v2 models and settings.
|
|
4418
|
+
- Testing: pytest with httpx vs unittest.
|
|
4419
|
+
- Background work and caching: Redis, task queues, and async workers.
|
|
4420
|
+
|
|
4421
|
+
## Recommended structure (proposed)
|
|
4422
|
+
|
|
4423
|
+
- A layered layout: \`api/\` routers, \`services/\` logic, \`repositories/\` data access, \`models/\` schemas.
|
|
4424
|
+
- Dependency injection through FastAPI dependencies.
|
|
4425
|
+
- Configuration via Pydantic settings loaded from environment variables.
|
|
4426
|
+
|
|
4427
|
+
## Testing (proposed)
|
|
4428
|
+
|
|
4429
|
+
- pytest with the FastAPI test client or httpx \`AsyncClient\`.
|
|
4430
|
+
- A disposable test database and transactional fixtures.
|
|
4431
|
+
- Contract tests for request and response schemas.
|
|
4432
|
+
|
|
4433
|
+
## Security considerations (proposed)
|
|
4434
|
+
|
|
4435
|
+
- Load secrets from environment or a secret manager, never from source.
|
|
4436
|
+
- Validate and constrain all input with Pydantic models.
|
|
4437
|
+
- Scope authentication and authorization at the dependency layer.
|
|
4438
|
+
`;
|
|
4439
|
+
function proposedAdr4(topic, title, body) {
|
|
4440
|
+
return {
|
|
4441
|
+
id: `python-fastapi-${topic}`,
|
|
4442
|
+
title,
|
|
4443
|
+
status: "proposed",
|
|
4444
|
+
destination: `docs/adrs/proposed/ADR-PROPOSED-python-fastapi-${topic}.md`,
|
|
4445
|
+
body
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
var pythonFastapiPreset = {
|
|
4449
|
+
id: "python-fastapi",
|
|
4450
|
+
name: "Python FastAPI",
|
|
4451
|
+
description: "Opinionated Python FastAPI opinion pack with proposed decisions only.",
|
|
4452
|
+
templates: [
|
|
4453
|
+
{
|
|
4454
|
+
destination: "docs/ai/presets/python-fastapi-guidance.md",
|
|
4455
|
+
description: "Python FastAPI guidance that remains proposed until accepted.",
|
|
4456
|
+
content: guidance4
|
|
4457
|
+
}
|
|
4458
|
+
],
|
|
4459
|
+
guidance: [
|
|
4460
|
+
{
|
|
4461
|
+
title: "Keep framework and data choices proposed",
|
|
4462
|
+
body: "FastAPI, the database and ORM, validation, and testing choices must remain proposed until accepted in repository memory."
|
|
4463
|
+
},
|
|
4464
|
+
{
|
|
4465
|
+
title: "Validate all input",
|
|
4466
|
+
body: "Use Pydantic models at the boundary, but record the validation approach as a proposed decision before treating it as accepted."
|
|
4467
|
+
}
|
|
4468
|
+
],
|
|
4469
|
+
proposedDecisions: [
|
|
4470
|
+
proposedAdr4(
|
|
4471
|
+
"framework",
|
|
4472
|
+
"Use FastAPI",
|
|
4473
|
+
`# Proposed ADR: Use FastAPI
|
|
4474
|
+
|
|
4475
|
+
## Status
|
|
4476
|
+
|
|
4477
|
+
Proposed
|
|
4478
|
+
|
|
4479
|
+
## Context
|
|
4480
|
+
|
|
4481
|
+
The service needs a Python web framework for an async API.
|
|
4482
|
+
|
|
4483
|
+
## Decision
|
|
4484
|
+
|
|
4485
|
+
Consider FastAPI as the web framework, awaiting human acceptance.
|
|
4486
|
+
|
|
4487
|
+
## Alternatives Considered
|
|
4488
|
+
|
|
4489
|
+
- Flask.
|
|
4490
|
+
- Django REST Framework.
|
|
4491
|
+
|
|
4492
|
+
## Consequences
|
|
4493
|
+
|
|
4494
|
+
- Async support and automatic OpenAPI docs.
|
|
4495
|
+
- Requires comfort with type hints and dependency injection.
|
|
4496
|
+
`
|
|
4497
|
+
),
|
|
4498
|
+
proposedAdr4(
|
|
4499
|
+
"database-postgres",
|
|
4500
|
+
"Use PostgreSQL with SQLAlchemy and Alembic",
|
|
4501
|
+
`# Proposed ADR: Use PostgreSQL with SQLAlchemy
|
|
4502
|
+
|
|
4503
|
+
## Status
|
|
4504
|
+
|
|
4505
|
+
Proposed
|
|
4506
|
+
|
|
4507
|
+
## Context
|
|
4508
|
+
|
|
4509
|
+
The service needs a relational database and a migration strategy.
|
|
4510
|
+
|
|
4511
|
+
## Decision
|
|
4512
|
+
|
|
4513
|
+
Consider PostgreSQL with SQLAlchemy and Alembic migrations, awaiting human acceptance.
|
|
4514
|
+
|
|
4515
|
+
## Alternatives Considered
|
|
4516
|
+
|
|
4517
|
+
- An async ORM such as Tortoise or SQLModel only.
|
|
4518
|
+
- Raw SQL with a lightweight driver.
|
|
4519
|
+
|
|
4520
|
+
## Consequences
|
|
4521
|
+
|
|
4522
|
+
- Mature tooling and explicit migrations.
|
|
4523
|
+
- Requires session and connection management discipline.
|
|
4524
|
+
`
|
|
4525
|
+
),
|
|
4526
|
+
proposedAdr4(
|
|
4527
|
+
"validation-pydantic",
|
|
4528
|
+
"Use Pydantic for validation and settings",
|
|
4529
|
+
`# Proposed ADR: Use Pydantic
|
|
4530
|
+
|
|
4531
|
+
## Status
|
|
4532
|
+
|
|
4533
|
+
Proposed
|
|
4534
|
+
|
|
4535
|
+
## Context
|
|
4536
|
+
|
|
4537
|
+
The service needs request validation and typed configuration.
|
|
4538
|
+
|
|
4539
|
+
## Decision
|
|
4540
|
+
|
|
4541
|
+
Consider Pydantic v2 models for validation and settings, awaiting human acceptance.
|
|
4542
|
+
|
|
4543
|
+
## Alternatives Considered
|
|
4544
|
+
|
|
4545
|
+
- Marshmallow.
|
|
4546
|
+
- Hand-written validation.
|
|
4547
|
+
|
|
4548
|
+
## Consequences
|
|
4549
|
+
|
|
4550
|
+
- Strong typing at the boundary and clear settings.
|
|
4551
|
+
- Adds Pydantic to the dependency surface.
|
|
4552
|
+
`
|
|
4553
|
+
),
|
|
4554
|
+
proposedAdr4(
|
|
4555
|
+
"testing-pytest",
|
|
4556
|
+
"Use pytest for testing",
|
|
4557
|
+
`# Proposed ADR: Use pytest
|
|
4558
|
+
|
|
4559
|
+
## Status
|
|
3612
4560
|
|
|
3613
4561
|
Proposed
|
|
3614
4562
|
|
|
@@ -3741,8 +4689,8 @@ function parsePreset(value) {
|
|
|
3741
4689
|
if (!result.success) {
|
|
3742
4690
|
throw new PresetValidationError(
|
|
3743
4691
|
result.error.issues.map((issue) => {
|
|
3744
|
-
const
|
|
3745
|
-
return `${
|
|
4692
|
+
const path17 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
4693
|
+
return `${path17}${issue.message}`;
|
|
3746
4694
|
})
|
|
3747
4695
|
);
|
|
3748
4696
|
}
|
|
@@ -3768,6 +4716,9 @@ var builtInPresets = validatePresetRegistry([
|
|
|
3768
4716
|
genericPreset,
|
|
3769
4717
|
iosSwiftPreset,
|
|
3770
4718
|
kotlinAndroidPreset,
|
|
4719
|
+
laravelApiPreset,
|
|
4720
|
+
laravelReactPreset,
|
|
4721
|
+
laravelVuePreset,
|
|
3771
4722
|
nextjsPreset,
|
|
3772
4723
|
pythonFastapiPreset
|
|
3773
4724
|
]);
|
|
@@ -3778,302 +4729,52 @@ function getPreset(id) {
|
|
|
3778
4729
|
return builtInPresets.find((preset) => preset.id === id);
|
|
3779
4730
|
}
|
|
3780
4731
|
|
|
3781
|
-
// src/
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
"Recall OS init write plan contains errors.",
|
|
3817
|
-
plan.entries.filter((entry) => entry.action === "error").map((entry) => `${entry.path}: ${entry.reason}`)
|
|
3818
|
-
);
|
|
3819
|
-
}
|
|
3820
|
-
const writeResult = await executeWritePlan(plan, { dryRun: options.dryRun });
|
|
3821
|
-
return {
|
|
3822
|
-
preset: preset?.id ?? null,
|
|
3823
|
-
dryRun: options.dryRun ?? false,
|
|
3824
|
-
plan,
|
|
3825
|
-
writeResult
|
|
3826
|
-
};
|
|
3827
|
-
}
|
|
3828
|
-
function formatInitResult(result) {
|
|
3829
|
-
const lines = [
|
|
3830
|
-
result.dryRun ? "Recall OS init dry run complete." : "Recall OS init complete.",
|
|
3831
|
-
`Preset: ${result.preset ?? "none"}`
|
|
3832
|
-
];
|
|
3833
|
-
appendWriteSummary(lines, {
|
|
3834
|
-
dryRun: result.dryRun,
|
|
3835
|
-
writeResult: result.writeResult
|
|
3836
|
-
});
|
|
3837
|
-
const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
|
|
3838
|
-
if (hookWritten) {
|
|
3839
|
-
lines.push("");
|
|
3840
|
-
lines.push(
|
|
3841
|
-
result.dryRun ? "Pre-commit hook will be written to .recall/hooks/pre-commit." : "Pre-commit hook written to .recall/hooks/pre-commit."
|
|
3842
|
-
);
|
|
3843
|
-
lines.push(`Enable it once per clone: ${HOOKS_PATH_ACTIVATION_COMMAND}`);
|
|
3844
|
-
}
|
|
3845
|
-
if (!result.dryRun) {
|
|
3846
|
-
appendNextSteps(lines, [
|
|
3847
|
-
"Read CLAUDE.md and AGENTS.md, then the docs/ memory they point to.",
|
|
3848
|
-
"Plan your first feature: `recall feature create <name>`.",
|
|
3849
|
-
"Record a decision: `recall adr create <title>`.",
|
|
3850
|
-
"Check repository memory health anytime: `recall doctor`."
|
|
3851
|
-
]);
|
|
3852
|
-
}
|
|
3853
|
-
return `${lines.join("\n")}
|
|
3854
|
-
`;
|
|
3855
|
-
}
|
|
3856
|
-
function resolvePreset(presetId) {
|
|
3857
|
-
if (presetId === void 0) {
|
|
3858
|
-
return null;
|
|
3859
|
-
}
|
|
3860
|
-
const preset = getPreset(presetId);
|
|
3861
|
-
if (preset === void 0) {
|
|
3862
|
-
throw new InitError("UNKNOWN_PRESET", `Unknown preset "${presetId}".`);
|
|
3863
|
-
}
|
|
3864
|
-
return preset;
|
|
3865
|
-
}
|
|
3866
|
-
function createInitWriteFiles(rootDir, config, preset) {
|
|
3867
|
-
return [
|
|
3868
|
-
{
|
|
3869
|
-
path: CONFIG_PATH,
|
|
3870
|
-
content: `${JSON.stringify(config, null, 2)}
|
|
3871
|
-
`
|
|
3872
|
-
},
|
|
3873
|
-
...generateInitFiles({ rootDir, preset }),
|
|
3874
|
-
{
|
|
3875
|
-
path: PRE_COMMIT_HOOK_PATH,
|
|
3876
|
-
content: renderPreCommitHook(config.preCommitGates),
|
|
3877
|
-
executable: true
|
|
3878
|
-
}
|
|
3879
|
-
];
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
// src/core/mcp/known-servers.ts
|
|
3883
|
-
var KNOWN_SERVERS = {
|
|
3884
|
-
figma: {
|
|
3885
|
-
title: "Figma",
|
|
3886
|
-
purpose: "Design variables, components, and layout metadata for building consistent UI.",
|
|
3887
|
-
dataAccessed: [
|
|
3888
|
-
"Design tokens and variables.",
|
|
3889
|
-
"Component and layout metadata.",
|
|
3890
|
-
"Frames and styles."
|
|
3891
|
-
]
|
|
3892
|
-
},
|
|
3893
|
-
linear: {
|
|
3894
|
-
title: "Linear",
|
|
3895
|
-
purpose: "Tickets, project status, and acceptance criteria.",
|
|
3896
|
-
dataAccessed: [
|
|
3897
|
-
"Issues and tickets.",
|
|
3898
|
-
"Project and cycle status.",
|
|
3899
|
-
"Acceptance criteria in issues."
|
|
3900
|
-
]
|
|
3901
|
-
},
|
|
3902
|
-
jira: {
|
|
3903
|
-
title: "Jira",
|
|
3904
|
-
purpose: "Tickets, sprints, and acceptance criteria for knocking out tasks.",
|
|
3905
|
-
dataAccessed: [
|
|
3906
|
-
"Issues and tickets.",
|
|
3907
|
-
"Sprint and board status.",
|
|
3908
|
-
"Acceptance criteria in issues."
|
|
3909
|
-
]
|
|
3910
|
-
},
|
|
3911
|
-
github: {
|
|
3912
|
-
title: "GitHub",
|
|
3913
|
-
purpose: "Pull requests, issues, and review comments.",
|
|
3914
|
-
dataAccessed: ["Pull requests and diffs.", "Issues.", "Review comments."]
|
|
3915
|
-
},
|
|
3916
|
-
sentry: {
|
|
3917
|
-
title: "Sentry",
|
|
3918
|
-
purpose: "Crash reports and production errors.",
|
|
3919
|
-
dataAccessed: ["Error events and stack traces.", "Release health.", "Issue frequency."]
|
|
3920
|
-
},
|
|
3921
|
-
notion: {
|
|
3922
|
-
title: "Notion",
|
|
3923
|
-
purpose: "Product background and documentation.",
|
|
3924
|
-
dataAccessed: ["Product docs and pages.", "Project background.", "Specifications."]
|
|
3925
|
-
}
|
|
3926
|
-
};
|
|
3927
|
-
function getKnownServer(id) {
|
|
3928
|
-
return KNOWN_SERVERS[id];
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
// src/core/mcp/generate-mcp.ts
|
|
3932
|
-
function mcpDocPath(server) {
|
|
3933
|
-
return `docs/ai/mcp/${server}.md`;
|
|
3934
|
-
}
|
|
3935
|
-
function generateMcpFiles(options) {
|
|
3936
|
-
const known = getKnownServer(options.server);
|
|
3937
|
-
const title = known?.title ?? titleize(options.server);
|
|
3938
|
-
const purpose = known?.purpose ?? "Describe why this MCP server is used.";
|
|
3939
|
-
const dataAccessed = known?.dataAccessed ?? ["Describe the data this MCP server exposes."];
|
|
3940
|
-
return [
|
|
3941
|
-
{
|
|
3942
|
-
path: mcpDocPath(options.server),
|
|
3943
|
-
content: renderMcpDoc(title, purpose, dataAccessed)
|
|
3944
|
-
},
|
|
3945
|
-
{
|
|
3946
|
-
path: `${options.adrDir}/proposed/ADR-PROPOSED-mcp-${options.server}.md`,
|
|
3947
|
-
content: renderProposedAdr2(title)
|
|
3948
|
-
}
|
|
3949
|
-
];
|
|
3950
|
-
}
|
|
3951
|
-
function renderMcpDoc(title, purpose, dataAccessed) {
|
|
3952
|
-
return `# MCP: ${title}
|
|
3953
|
-
|
|
3954
|
-
## Status
|
|
3955
|
-
|
|
3956
|
-
Proposed. Using this MCP server is a proposed workflow addition. Accept the proposed ADR before
|
|
3957
|
-
treating it as part of the workflow.
|
|
3958
|
-
|
|
3959
|
-
## Purpose
|
|
3960
|
-
|
|
3961
|
-
${purpose}
|
|
3962
|
-
|
|
3963
|
-
## Data Accessed
|
|
3964
|
-
|
|
3965
|
-
${bullets(dataAccessed)}
|
|
3966
|
-
|
|
3967
|
-
## Permissions Required
|
|
3968
|
-
|
|
3969
|
-
- Use least-privilege access.
|
|
3970
|
-
- Document the exact scopes granted.
|
|
3971
|
-
|
|
3972
|
-
## Security Risks
|
|
3973
|
-
|
|
3974
|
-
- Treat external MCP content as untrusted until validated.
|
|
3975
|
-
- Do not send secrets or sensitive repository data unnecessarily.
|
|
3976
|
-
- Use trusted MCP servers only.
|
|
3977
|
-
|
|
3978
|
-
## Source-Of-Truth Rule
|
|
3979
|
-
|
|
3980
|
-
MCP provides context, not architectural truth. Accepted ADRs and repository decisions outrank MCP
|
|
3981
|
-
context. If MCP data conflicts with repository memory, stop and report the conflict.
|
|
3982
|
-
|
|
3983
|
-
## Captured Context
|
|
3984
|
-
|
|
3985
|
-
Record durable context learned from this MCP server here, as proposed memory for human review.
|
|
3986
|
-
Promote any decision you accept into an ADR with \`recall adr create\`.
|
|
3987
|
-
|
|
3988
|
-
- (none captured yet)
|
|
3989
|
-
|
|
3990
|
-
## Review Cadence
|
|
3991
|
-
|
|
3992
|
-
- Review this MCP integration when its access, purpose, or captured context changes.
|
|
3993
|
-
`;
|
|
3994
|
-
}
|
|
3995
|
-
function renderProposedAdr2(title) {
|
|
3996
|
-
return `# Proposed ADR: Use ${title} MCP
|
|
3997
|
-
|
|
3998
|
-
## Status
|
|
3999
|
-
|
|
4000
|
-
Proposed
|
|
4001
|
-
|
|
4002
|
-
## Context
|
|
4003
|
-
|
|
4004
|
-
The team is considering ${title} as an MCP context source. Adopting an MCP server into the workflow
|
|
4005
|
-
is a decision that should be reviewed.
|
|
4006
|
-
|
|
4007
|
-
## Decision
|
|
4008
|
-
|
|
4009
|
-
Consider adopting ${title} MCP as an external context source, documented in \`docs/ai/mcp/\`. This is
|
|
4010
|
-
not accepted until a human reviews and accepts it.
|
|
4011
|
-
|
|
4012
|
-
## Alternatives Considered
|
|
4013
|
-
|
|
4014
|
-
- Do not use this MCP server.
|
|
4015
|
-
- Use a different source for the same context.
|
|
4016
|
-
|
|
4017
|
-
## Consequences
|
|
4018
|
-
|
|
4019
|
-
- The team gains durable, reviewable context from ${title}.
|
|
4020
|
-
- MCP context never overrides accepted repository memory.
|
|
4021
|
-
- Captured context remains proposed until promoted to an ADR.
|
|
4022
|
-
`;
|
|
4023
|
-
}
|
|
4024
|
-
function bullets(values) {
|
|
4025
|
-
return values.map((value) => `- ${value}`).join("\n");
|
|
4026
|
-
}
|
|
4027
|
-
function titleize(value) {
|
|
4028
|
-
return value.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
4029
|
-
}
|
|
4030
|
-
|
|
4031
|
-
// src/core/skills/render-skill.ts
|
|
4032
|
-
function renderSkill(skill) {
|
|
4033
|
-
const lines = [
|
|
4034
|
-
"---",
|
|
4035
|
-
`name: ${skill.name}`,
|
|
4036
|
-
// JSON-stringify yields a valid double-quoted YAML scalar, so descriptions with any punctuation
|
|
4037
|
-
// stay valid Agent Skills frontmatter.
|
|
4038
|
-
`description: ${JSON.stringify(skill.description)}`,
|
|
4039
|
-
"---",
|
|
4040
|
-
"",
|
|
4041
|
-
`# Skill: ${skill.title}`,
|
|
4042
|
-
"",
|
|
4043
|
-
"## Purpose",
|
|
4044
|
-
"",
|
|
4045
|
-
...paragraphs(skill.purpose),
|
|
4046
|
-
"",
|
|
4047
|
-
"## Inputs",
|
|
4048
|
-
"",
|
|
4049
|
-
...bullets2(skill.inputs),
|
|
4050
|
-
"",
|
|
4051
|
-
"## Required Reading",
|
|
4052
|
-
"",
|
|
4053
|
-
...bullets2(skill.requiredReading),
|
|
4054
|
-
"",
|
|
4055
|
-
"## Output Files",
|
|
4056
|
-
"",
|
|
4057
|
-
...bullets2(skill.outputFiles),
|
|
4058
|
-
"",
|
|
4059
|
-
"## Process",
|
|
4060
|
-
"",
|
|
4061
|
-
...numbered(skill.process),
|
|
4062
|
-
""
|
|
4063
|
-
];
|
|
4064
|
-
for (const section of skill.extraSections ?? []) {
|
|
4065
|
-
lines.push(`## ${section.heading}`, "", ...bullets2(section.bullets), "");
|
|
4732
|
+
// src/core/skills/render-skill.ts
|
|
4733
|
+
function renderSkill(skill) {
|
|
4734
|
+
const lines = [
|
|
4735
|
+
"---",
|
|
4736
|
+
`name: ${skill.name}`,
|
|
4737
|
+
// JSON-stringify yields a valid double-quoted YAML scalar, so descriptions with any punctuation
|
|
4738
|
+
// stay valid Agent Skills frontmatter.
|
|
4739
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
4740
|
+
"---",
|
|
4741
|
+
"",
|
|
4742
|
+
`# Skill: ${skill.title}`,
|
|
4743
|
+
"",
|
|
4744
|
+
"## Purpose",
|
|
4745
|
+
"",
|
|
4746
|
+
...paragraphs(skill.purpose),
|
|
4747
|
+
"",
|
|
4748
|
+
"## Inputs",
|
|
4749
|
+
"",
|
|
4750
|
+
...bullets(skill.inputs),
|
|
4751
|
+
"",
|
|
4752
|
+
"## Required Reading",
|
|
4753
|
+
"",
|
|
4754
|
+
...bullets(skill.requiredReading),
|
|
4755
|
+
"",
|
|
4756
|
+
"## Output Files",
|
|
4757
|
+
"",
|
|
4758
|
+
...bullets(skill.outputFiles),
|
|
4759
|
+
"",
|
|
4760
|
+
"## Process",
|
|
4761
|
+
"",
|
|
4762
|
+
...numbered(skill.process),
|
|
4763
|
+
""
|
|
4764
|
+
];
|
|
4765
|
+
for (const section of skill.extraSections ?? []) {
|
|
4766
|
+
lines.push(`## ${section.heading}`, "", ...bullets(section.bullets), "");
|
|
4066
4767
|
}
|
|
4067
4768
|
lines.push(
|
|
4068
4769
|
"## Stop Conditions",
|
|
4069
4770
|
"",
|
|
4070
4771
|
"Stop and request human decision if:",
|
|
4071
4772
|
"",
|
|
4072
|
-
...
|
|
4773
|
+
...bullets(skill.stopConditions),
|
|
4073
4774
|
"",
|
|
4074
4775
|
"## Quality Bar",
|
|
4075
4776
|
"",
|
|
4076
|
-
...
|
|
4777
|
+
...bullets(skill.qualityBar)
|
|
4077
4778
|
);
|
|
4078
4779
|
return `${lines.join("\n")}
|
|
4079
4780
|
`;
|
|
@@ -4088,7 +4789,7 @@ function paragraphs(values) {
|
|
|
4088
4789
|
});
|
|
4089
4790
|
return out;
|
|
4090
4791
|
}
|
|
4091
|
-
function
|
|
4792
|
+
function bullets(values) {
|
|
4092
4793
|
return values.map((value) => `- ${value}`);
|
|
4093
4794
|
}
|
|
4094
4795
|
function numbered(values) {
|
|
@@ -4533,134 +5234,432 @@ var SKILL_CATALOG = [
|
|
|
4533
5234
|
"Future agents can tell what the module owns and what it must not own."
|
|
4534
5235
|
]
|
|
4535
5236
|
},
|
|
4536
|
-
{
|
|
4537
|
-
name: "completion-report",
|
|
4538
|
-
title: "Completion Report",
|
|
4539
|
-
description: "Write a completion report with files changed, tests run, results, skipped checks, docs updated, remaining risks, and release readiness notes. Use when recording evidence that a task or feature is complete and ready for review.",
|
|
4540
|
-
purpose: ["Record evidence that a task is complete and safe to review."],
|
|
4541
|
-
inputs: [
|
|
4542
|
-
"Task or feature summary.",
|
|
4543
|
-
"Files changed.",
|
|
4544
|
-
"Commands run.",
|
|
4545
|
-
"Test results.",
|
|
4546
|
-
"Docs updated.",
|
|
4547
|
-
"Known risks."
|
|
4548
|
-
],
|
|
4549
|
-
requiredReading: [
|
|
4550
|
-
"`docs/50-quality/QUALITY_GATES.md`",
|
|
4551
|
-
"`docs/60-engineering/ENGINEERING_STANDARDS.md`",
|
|
4552
|
-
"Relevant feature `TASKS.md`",
|
|
4553
|
-
"Relevant feature `TEST_PLAN.md`",
|
|
4554
|
-
"Relevant feature `REVIEW.md`"
|
|
4555
|
-
],
|
|
4556
|
-
outputFiles: [
|
|
4557
|
-
"Relevant feature `COMPLETION_REPORT.md`",
|
|
4558
|
-
"Task or feature docs that need final status updates."
|
|
4559
|
-
],
|
|
4560
|
-
process: [
|
|
4561
|
-
"Summarize the completed scope.",
|
|
4562
|
-
"List files changed by category.",
|
|
4563
|
-
"List commands run and results.",
|
|
4564
|
-
"List skipped checks and why.",
|
|
4565
|
-
"List docs updated.",
|
|
4566
|
-
"State whether engineering standards were followed.",
|
|
4567
|
-
"List remaining risks and follow-up work.",
|
|
4568
|
-
"State whether the task meets the definition of done."
|
|
4569
|
-
],
|
|
4570
|
-
stopConditions: [
|
|
4571
|
-
"Test results are missing for risky changes.",
|
|
4572
|
-
"Completion claims conflict with evidence.",
|
|
4573
|
-
"Required docs were not updated.",
|
|
4574
|
-
"Engineering standards were violated.",
|
|
4575
|
-
"Remaining risks are release blockers."
|
|
4576
|
-
],
|
|
4577
|
-
qualityBar: [
|
|
4578
|
-
"The report is evidence-based.",
|
|
4579
|
-
"It does not hide skipped checks.",
|
|
4580
|
-
"It separates completed work from remaining risk.",
|
|
4581
|
-
"A reviewer can decide what to do next from the report alone."
|
|
5237
|
+
{
|
|
5238
|
+
name: "completion-report",
|
|
5239
|
+
title: "Completion Report",
|
|
5240
|
+
description: "Write a completion report with files changed, tests run, results, skipped checks, docs updated, remaining risks, and release readiness notes. Use when recording evidence that a task or feature is complete and ready for review.",
|
|
5241
|
+
purpose: ["Record evidence that a task is complete and safe to review."],
|
|
5242
|
+
inputs: [
|
|
5243
|
+
"Task or feature summary.",
|
|
5244
|
+
"Files changed.",
|
|
5245
|
+
"Commands run.",
|
|
5246
|
+
"Test results.",
|
|
5247
|
+
"Docs updated.",
|
|
5248
|
+
"Known risks."
|
|
5249
|
+
],
|
|
5250
|
+
requiredReading: [
|
|
5251
|
+
"`docs/50-quality/QUALITY_GATES.md`",
|
|
5252
|
+
"`docs/60-engineering/ENGINEERING_STANDARDS.md`",
|
|
5253
|
+
"Relevant feature `TASKS.md`",
|
|
5254
|
+
"Relevant feature `TEST_PLAN.md`",
|
|
5255
|
+
"Relevant feature `REVIEW.md`"
|
|
5256
|
+
],
|
|
5257
|
+
outputFiles: [
|
|
5258
|
+
"Relevant feature `COMPLETION_REPORT.md`",
|
|
5259
|
+
"Task or feature docs that need final status updates."
|
|
5260
|
+
],
|
|
5261
|
+
process: [
|
|
5262
|
+
"Summarize the completed scope.",
|
|
5263
|
+
"List files changed by category.",
|
|
5264
|
+
"List commands run and results.",
|
|
5265
|
+
"List skipped checks and why.",
|
|
5266
|
+
"List docs updated.",
|
|
5267
|
+
"State whether engineering standards were followed.",
|
|
5268
|
+
"List remaining risks and follow-up work.",
|
|
5269
|
+
"State whether the task meets the definition of done."
|
|
5270
|
+
],
|
|
5271
|
+
stopConditions: [
|
|
5272
|
+
"Test results are missing for risky changes.",
|
|
5273
|
+
"Completion claims conflict with evidence.",
|
|
5274
|
+
"Required docs were not updated.",
|
|
5275
|
+
"Engineering standards were violated.",
|
|
5276
|
+
"Remaining risks are release blockers."
|
|
5277
|
+
],
|
|
5278
|
+
qualityBar: [
|
|
5279
|
+
"The report is evidence-based.",
|
|
5280
|
+
"It does not hide skipped checks.",
|
|
5281
|
+
"It separates completed work from remaining risk.",
|
|
5282
|
+
"A reviewer can decide what to do next from the report alone."
|
|
5283
|
+
]
|
|
5284
|
+
},
|
|
5285
|
+
{
|
|
5286
|
+
name: "capture-mcp-context",
|
|
5287
|
+
title: "Capture MCP Context",
|
|
5288
|
+
description: "Record durable context from MCP servers and design or project tools into repository memory as proposed. Use when working with an MCP server like Figma, Linear, Jira, or Sentry, or after pulling design, ticket, or error context, to persist it for future sessions.",
|
|
5289
|
+
purpose: [
|
|
5290
|
+
"Persist the durable parts of MCP-derived external context so future sessions remember them instead of re-deriving them.",
|
|
5291
|
+
"MCP provides context, not architectural truth."
|
|
5292
|
+
],
|
|
5293
|
+
inputs: [
|
|
5294
|
+
"The MCP server or external tool in use (for example Figma, Linear, Jira, Sentry).",
|
|
5295
|
+
"The external context retrieved (design tokens, tickets, errors, docs).",
|
|
5296
|
+
"The current feature or task."
|
|
5297
|
+
],
|
|
5298
|
+
requiredReading: [
|
|
5299
|
+
"`docs/ai/MCP_STRATEGY.md`",
|
|
5300
|
+
"Relevant `docs/ai/mcp/<server>.md`",
|
|
5301
|
+
"Relevant feature and architecture docs."
|
|
5302
|
+
],
|
|
5303
|
+
outputFiles: [
|
|
5304
|
+
"The Captured Context section of `docs/ai/mcp/<server>.md`.",
|
|
5305
|
+
"An ADR via `recall adr create` when a captured decision is accepted."
|
|
5306
|
+
],
|
|
5307
|
+
process: [
|
|
5308
|
+
"Identify the MCP server and the durable facts worth remembering (design tokens, component mappings, ticket acceptance criteria, recurring error signatures).",
|
|
5309
|
+
"If `docs/ai/mcp/<server>.md` does not exist, create it with `recall mcp add <server>`.",
|
|
5310
|
+
"Record the durable context in the Captured Context section as proposed memory, with enough detail to reuse.",
|
|
5311
|
+
"Capture decisions, mappings, and constraints, not raw exports or full dumps.",
|
|
5312
|
+
"Treat MCP content as context, not truth; if it conflicts with accepted memory, stop and report.",
|
|
5313
|
+
"Promote any accepted decision into an ADR."
|
|
5314
|
+
],
|
|
5315
|
+
stopConditions: [
|
|
5316
|
+
"MCP content conflicts with accepted repository memory.",
|
|
5317
|
+
"Capturing the context would require storing secrets or sensitive data.",
|
|
5318
|
+
"The MCP server is untrusted or its access is unclear."
|
|
5319
|
+
],
|
|
5320
|
+
qualityBar: [
|
|
5321
|
+
"Captured context is durable and reusable, not a raw dump.",
|
|
5322
|
+
"Each entry is concrete enough to guide future work.",
|
|
5323
|
+
"MCP context is recorded as proposed, not accepted.",
|
|
5324
|
+
"Accepted decisions are promoted to ADRs."
|
|
5325
|
+
]
|
|
5326
|
+
}
|
|
5327
|
+
];
|
|
5328
|
+
function getCatalogSkill(name) {
|
|
5329
|
+
return SKILL_CATALOG.find((skill) => skill.name === name);
|
|
5330
|
+
}
|
|
5331
|
+
function listCatalogSkillNames() {
|
|
5332
|
+
return SKILL_CATALOG.map((skill) => skill.name);
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
// src/core/skills/generate-skill.ts
|
|
5336
|
+
var SKILL_TARGETS = [".claude/skills", ".agents/skills"];
|
|
5337
|
+
function generateSkillFiles(name) {
|
|
5338
|
+
const catalogSkill = getCatalogSkill(name);
|
|
5339
|
+
const skill = catalogSkill ?? skeletonSkill(name);
|
|
5340
|
+
const content = renderSkill(skill);
|
|
5341
|
+
return {
|
|
5342
|
+
files: SKILL_TARGETS.map((target) => ({
|
|
5343
|
+
path: `${target}/${name}/SKILL.md`,
|
|
5344
|
+
content
|
|
5345
|
+
})),
|
|
5346
|
+
fromCatalog: catalogSkill !== void 0
|
|
5347
|
+
};
|
|
5348
|
+
}
|
|
5349
|
+
function skeletonSkill(name) {
|
|
5350
|
+
return {
|
|
5351
|
+
name,
|
|
5352
|
+
title: titleize(name),
|
|
5353
|
+
description: `Describe what the ${name} skill does and when to use it. Use when ... (replace this with concrete trigger keywords).`,
|
|
5354
|
+
purpose: ["Describe the single job this skill performs."],
|
|
5355
|
+
inputs: ["List the inputs this skill needs."],
|
|
5356
|
+
requiredReading: ["List the source-of-truth docs this skill must read."],
|
|
5357
|
+
outputFiles: ["List the files this skill produces or updates."],
|
|
5358
|
+
process: ["Describe the steps, one job, routing to source-of-truth docs."],
|
|
5359
|
+
stopConditions: [
|
|
5360
|
+
"A request conflicts with accepted repository memory or engineering standards.",
|
|
5361
|
+
"The work would add network, telemetry, MCP runtime, AI API, or other accepted non-goals."
|
|
5362
|
+
],
|
|
5363
|
+
qualityBar: ["State how to tell the skill did its job well."]
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
function titleize(name) {
|
|
5367
|
+
return name.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
// src/commands/init.ts
|
|
5371
|
+
var InitError = class extends Error {
|
|
5372
|
+
code;
|
|
5373
|
+
details;
|
|
5374
|
+
constructor(code, message, details = []) {
|
|
5375
|
+
super(message);
|
|
5376
|
+
this.name = "InitError";
|
|
5377
|
+
this.code = code;
|
|
5378
|
+
this.details = details;
|
|
5379
|
+
}
|
|
5380
|
+
};
|
|
5381
|
+
async function initProject(options) {
|
|
5382
|
+
if (options.force === true && options.reinit !== true && existsSync6(path15.join(options.rootDir, CONFIG_PATH))) {
|
|
5383
|
+
throw new InitError(
|
|
5384
|
+
"EXISTING_INSTALLATION",
|
|
5385
|
+
"Refusing to re-initialize an existing Recall OS installation.",
|
|
5386
|
+
[
|
|
5387
|
+
"An existing .recall/config.json was found in this directory.",
|
|
5388
|
+
"Running init --force here would overwrite existing repository memory.",
|
|
5389
|
+
"Pass --reinit together with --force to overwrite an existing installation."
|
|
5390
|
+
]
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
const preset = resolvePreset(options.preset);
|
|
5394
|
+
const detected = await inspectRepo(options.rootDir);
|
|
5395
|
+
const preCommitGates = await detectPreCommitGates(options.rootDir);
|
|
5396
|
+
const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
|
|
5397
|
+
const files = createInitWriteFiles(options.rootDir, config, preset);
|
|
5398
|
+
const plan = createWritePlan({
|
|
5399
|
+
rootDir: options.rootDir,
|
|
5400
|
+
files,
|
|
5401
|
+
force: options.force
|
|
5402
|
+
});
|
|
5403
|
+
if (plan.hasErrors) {
|
|
5404
|
+
throw new InitError(
|
|
5405
|
+
"WRITE_PLAN_ERROR",
|
|
5406
|
+
"Recall OS init write plan contains errors.",
|
|
5407
|
+
plan.entries.filter((entry) => entry.action === "error").map((entry) => `${entry.path}: ${entry.reason}`)
|
|
5408
|
+
);
|
|
5409
|
+
}
|
|
5410
|
+
const writeResult = await executeWritePlan(plan, { dryRun: options.dryRun });
|
|
5411
|
+
return {
|
|
5412
|
+
preset: preset?.id ?? null,
|
|
5413
|
+
dryRun: options.dryRun ?? false,
|
|
5414
|
+
plan,
|
|
5415
|
+
writeResult,
|
|
5416
|
+
detected
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
function formatInitResult(result) {
|
|
5420
|
+
const lines = [
|
|
5421
|
+
result.dryRun ? "Recall OS init dry run complete." : "Recall OS init complete.",
|
|
5422
|
+
`Preset: ${result.preset ?? "none"}`
|
|
5423
|
+
];
|
|
5424
|
+
if (!result.dryRun) {
|
|
5425
|
+
lines.push(
|
|
5426
|
+
`Generated repository memory, ${listCatalogSkillNames().length} agent skills, a pre-commit hook, a CI workflow, a Claude SessionStart hook, and a Cursor rule that load memory automatically.`
|
|
5427
|
+
);
|
|
5428
|
+
}
|
|
5429
|
+
appendWriteSummary(lines, {
|
|
5430
|
+
dryRun: result.dryRun,
|
|
5431
|
+
writeResult: result.writeResult
|
|
5432
|
+
});
|
|
5433
|
+
appendDetectedStack(lines, result.detected);
|
|
5434
|
+
const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
|
|
5435
|
+
if (hookWritten) {
|
|
5436
|
+
lines.push("");
|
|
5437
|
+
lines.push(
|
|
5438
|
+
result.dryRun ? "Pre-commit hook will be written to .recall/hooks/pre-commit." : "Pre-commit hook written to .recall/hooks/pre-commit."
|
|
5439
|
+
);
|
|
5440
|
+
lines.push(`Enable it once per clone: ${HOOKS_PATH_ACTIVATION_COMMAND}`);
|
|
5441
|
+
}
|
|
5442
|
+
if (!result.dryRun) {
|
|
5443
|
+
appendNextSteps(lines, [
|
|
5444
|
+
"Read CLAUDE.md and AGENTS.md, then the docs/ memory they point to.",
|
|
5445
|
+
"AI agent skills are in .claude/skills/ and .agents/skills/ \u2014 restart your AI tool to load them.",
|
|
5446
|
+
"Memory loads automatically per tool: a Claude SessionStart hook (.claude/hooks/session-start.sh), a Cursor rule (.cursor/rules/recall-memory.mdc), and AGENTS.md for Codex.",
|
|
5447
|
+
"CI is wired in .github/workflows/recall.yml; the pre-commit hook is in .recall/hooks/.",
|
|
5448
|
+
"Plan your first feature: `recall feature create <name>`.",
|
|
5449
|
+
"Record a decision: `recall adr create <title>`, then accept it with `recall adr accept`.",
|
|
5450
|
+
"Check repository memory health anytime: `recall doctor`."
|
|
5451
|
+
]);
|
|
5452
|
+
}
|
|
5453
|
+
return `${lines.join("\n")}
|
|
5454
|
+
`;
|
|
5455
|
+
}
|
|
5456
|
+
function appendDetectedStack(lines, detected) {
|
|
5457
|
+
const hasSignal = detected.languages.length > 0 || detected.frameworks.length > 0 || detected.packageManager !== null || detected.testsEvidence !== null;
|
|
5458
|
+
if (!hasSignal) {
|
|
5459
|
+
return;
|
|
5460
|
+
}
|
|
5461
|
+
const stack = summarizeSignals(detected).filter(
|
|
5462
|
+
(line) => !line.startsWith("- README") && !line.startsWith("- Docs")
|
|
5463
|
+
);
|
|
5464
|
+
lines.push("");
|
|
5465
|
+
lines.push("Detected in this repository (proposed \u2014 review, nothing was accepted):");
|
|
5466
|
+
lines.push(...stack);
|
|
5467
|
+
lines.push(
|
|
5468
|
+
"If any signal is wrong, correct the source file noted. Run `recall adopt` to record this as proposed memory."
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
function resolvePreset(presetId) {
|
|
5472
|
+
if (presetId === void 0) {
|
|
5473
|
+
return null;
|
|
5474
|
+
}
|
|
5475
|
+
const preset = getPreset(presetId);
|
|
5476
|
+
if (preset === void 0) {
|
|
5477
|
+
throw new InitError("UNKNOWN_PRESET", `Unknown preset "${presetId}".`);
|
|
5478
|
+
}
|
|
5479
|
+
return preset;
|
|
5480
|
+
}
|
|
5481
|
+
function createInitWriteFiles(rootDir, config, preset) {
|
|
5482
|
+
return [
|
|
5483
|
+
{
|
|
5484
|
+
path: CONFIG_PATH,
|
|
5485
|
+
content: `${JSON.stringify(config, null, 2)}
|
|
5486
|
+
`
|
|
5487
|
+
},
|
|
5488
|
+
...generateInitFiles({ rootDir, preset }),
|
|
5489
|
+
{
|
|
5490
|
+
path: PRE_COMMIT_HOOK_PATH,
|
|
5491
|
+
content: renderPreCommitHook(config.preCommitGates),
|
|
5492
|
+
executable: true
|
|
5493
|
+
},
|
|
5494
|
+
// A Claude Code SessionStart hook that injects a memory map every session, so a fresh agent
|
|
5495
|
+
// reliably loads durable memory, plus the settings that wire it (skipped if settings exist).
|
|
5496
|
+
{
|
|
5497
|
+
path: SESSION_START_HOOK_PATH,
|
|
5498
|
+
content: renderSessionStartHook(),
|
|
5499
|
+
executable: true
|
|
5500
|
+
},
|
|
5501
|
+
{
|
|
5502
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
5503
|
+
content: renderClaudeSettings()
|
|
5504
|
+
},
|
|
5505
|
+
// Generate the agent skill set so a fresh repo has the workflows that guide AI agents,
|
|
5506
|
+
// not just the docs. Written to both the Claude and portable Agent Skills targets.
|
|
5507
|
+
...listCatalogSkillNames().flatMap((name) => generateSkillFiles(name).files)
|
|
5508
|
+
];
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
// src/core/mcp/known-servers.ts
|
|
5512
|
+
var KNOWN_SERVERS = {
|
|
5513
|
+
figma: {
|
|
5514
|
+
title: "Figma",
|
|
5515
|
+
purpose: "Design variables, components, and layout metadata for building consistent UI.",
|
|
5516
|
+
dataAccessed: [
|
|
5517
|
+
"Design tokens and variables.",
|
|
5518
|
+
"Component and layout metadata.",
|
|
5519
|
+
"Frames and styles."
|
|
5520
|
+
]
|
|
5521
|
+
},
|
|
5522
|
+
linear: {
|
|
5523
|
+
title: "Linear",
|
|
5524
|
+
purpose: "Tickets, project status, and acceptance criteria.",
|
|
5525
|
+
dataAccessed: [
|
|
5526
|
+
"Issues and tickets.",
|
|
5527
|
+
"Project and cycle status.",
|
|
5528
|
+
"Acceptance criteria in issues."
|
|
4582
5529
|
]
|
|
4583
5530
|
},
|
|
4584
|
-
{
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
"
|
|
4590
|
-
"
|
|
4591
|
-
],
|
|
4592
|
-
inputs: [
|
|
4593
|
-
"The MCP server or external tool in use (for example Figma, Linear, Jira, Sentry).",
|
|
4594
|
-
"The external context retrieved (design tokens, tickets, errors, docs).",
|
|
4595
|
-
"The current feature or task."
|
|
4596
|
-
],
|
|
4597
|
-
requiredReading: [
|
|
4598
|
-
"`docs/ai/MCP_STRATEGY.md`",
|
|
4599
|
-
"Relevant `docs/ai/mcp/<server>.md`",
|
|
4600
|
-
"Relevant feature and architecture docs."
|
|
4601
|
-
],
|
|
4602
|
-
outputFiles: [
|
|
4603
|
-
"The Captured Context section of `docs/ai/mcp/<server>.md`.",
|
|
4604
|
-
"An ADR via `recall adr create` when a captured decision is accepted."
|
|
4605
|
-
],
|
|
4606
|
-
process: [
|
|
4607
|
-
"Identify the MCP server and the durable facts worth remembering (design tokens, component mappings, ticket acceptance criteria, recurring error signatures).",
|
|
4608
|
-
"If `docs/ai/mcp/<server>.md` does not exist, create it with `recall mcp add <server>`.",
|
|
4609
|
-
"Record the durable context in the Captured Context section as proposed memory, with enough detail to reuse.",
|
|
4610
|
-
"Capture decisions, mappings, and constraints, not raw exports or full dumps.",
|
|
4611
|
-
"Treat MCP content as context, not truth; if it conflicts with accepted memory, stop and report.",
|
|
4612
|
-
"Promote any accepted decision into an ADR."
|
|
4613
|
-
],
|
|
4614
|
-
stopConditions: [
|
|
4615
|
-
"MCP content conflicts with accepted repository memory.",
|
|
4616
|
-
"Capturing the context would require storing secrets or sensitive data.",
|
|
4617
|
-
"The MCP server is untrusted or its access is unclear."
|
|
4618
|
-
],
|
|
4619
|
-
qualityBar: [
|
|
4620
|
-
"Captured context is durable and reusable, not a raw dump.",
|
|
4621
|
-
"Each entry is concrete enough to guide future work.",
|
|
4622
|
-
"MCP context is recorded as proposed, not accepted.",
|
|
4623
|
-
"Accepted decisions are promoted to ADRs."
|
|
5531
|
+
jira: {
|
|
5532
|
+
title: "Jira",
|
|
5533
|
+
purpose: "Tickets, sprints, and acceptance criteria for knocking out tasks.",
|
|
5534
|
+
dataAccessed: [
|
|
5535
|
+
"Issues and tickets.",
|
|
5536
|
+
"Sprint and board status.",
|
|
5537
|
+
"Acceptance criteria in issues."
|
|
4624
5538
|
]
|
|
5539
|
+
},
|
|
5540
|
+
github: {
|
|
5541
|
+
title: "GitHub",
|
|
5542
|
+
purpose: "Pull requests, issues, and review comments.",
|
|
5543
|
+
dataAccessed: ["Pull requests and diffs.", "Issues.", "Review comments."]
|
|
5544
|
+
},
|
|
5545
|
+
sentry: {
|
|
5546
|
+
title: "Sentry",
|
|
5547
|
+
purpose: "Crash reports and production errors.",
|
|
5548
|
+
dataAccessed: ["Error events and stack traces.", "Release health.", "Issue frequency."]
|
|
5549
|
+
},
|
|
5550
|
+
notion: {
|
|
5551
|
+
title: "Notion",
|
|
5552
|
+
purpose: "Product background and documentation.",
|
|
5553
|
+
dataAccessed: ["Product docs and pages.", "Project background.", "Specifications."]
|
|
4625
5554
|
}
|
|
4626
|
-
|
|
4627
|
-
function
|
|
4628
|
-
return
|
|
5555
|
+
};
|
|
5556
|
+
function getKnownServer(id) {
|
|
5557
|
+
return KNOWN_SERVERS[id];
|
|
4629
5558
|
}
|
|
4630
5559
|
|
|
4631
|
-
// src/core/
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
const catalogSkill = getCatalogSkill(name);
|
|
4635
|
-
const skill = catalogSkill ?? skeletonSkill(name);
|
|
4636
|
-
const content = renderSkill(skill);
|
|
4637
|
-
return {
|
|
4638
|
-
files: SKILL_TARGETS.map((target) => ({
|
|
4639
|
-
path: `${target}/${name}/SKILL.md`,
|
|
4640
|
-
content
|
|
4641
|
-
})),
|
|
4642
|
-
fromCatalog: catalogSkill !== void 0
|
|
4643
|
-
};
|
|
5560
|
+
// src/core/mcp/generate-mcp.ts
|
|
5561
|
+
function mcpDocPath(server) {
|
|
5562
|
+
return `docs/ai/mcp/${server}.md`;
|
|
4644
5563
|
}
|
|
4645
|
-
function
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
};
|
|
5564
|
+
function generateMcpFiles(options) {
|
|
5565
|
+
const known = getKnownServer(options.server);
|
|
5566
|
+
const title = known?.title ?? titleize2(options.server);
|
|
5567
|
+
const purpose = known?.purpose ?? "Describe why this MCP server is used.";
|
|
5568
|
+
const dataAccessed = known?.dataAccessed ?? ["Describe the data this MCP server exposes."];
|
|
5569
|
+
return [
|
|
5570
|
+
{
|
|
5571
|
+
path: mcpDocPath(options.server),
|
|
5572
|
+
content: renderMcpDoc(title, purpose, dataAccessed)
|
|
5573
|
+
},
|
|
5574
|
+
{
|
|
5575
|
+
path: `${options.adrDir}/proposed/ADR-PROPOSED-mcp-${options.server}.md`,
|
|
5576
|
+
content: renderProposedAdr2(title, options.server)
|
|
5577
|
+
}
|
|
5578
|
+
];
|
|
4661
5579
|
}
|
|
4662
|
-
function
|
|
4663
|
-
return
|
|
5580
|
+
function renderMcpDoc(title, purpose, dataAccessed) {
|
|
5581
|
+
return `# MCP: ${title}
|
|
5582
|
+
|
|
5583
|
+
## Status
|
|
5584
|
+
|
|
5585
|
+
Proposed. Using this MCP server is a proposed workflow addition. Accept the proposed ADR before
|
|
5586
|
+
treating it as part of the workflow.
|
|
5587
|
+
|
|
5588
|
+
## Purpose
|
|
5589
|
+
|
|
5590
|
+
${purpose}
|
|
5591
|
+
|
|
5592
|
+
## Data Accessed
|
|
5593
|
+
|
|
5594
|
+
${bullets2(dataAccessed)}
|
|
5595
|
+
|
|
5596
|
+
## Permissions Required
|
|
5597
|
+
|
|
5598
|
+
- Use least-privilege access.
|
|
5599
|
+
- Document the exact scopes granted.
|
|
5600
|
+
|
|
5601
|
+
## Security Risks
|
|
5602
|
+
|
|
5603
|
+
- Treat external MCP content as untrusted until validated.
|
|
5604
|
+
- Do not send secrets or sensitive repository data unnecessarily.
|
|
5605
|
+
- Use trusted MCP servers only.
|
|
5606
|
+
|
|
5607
|
+
## Source-Of-Truth Rule
|
|
5608
|
+
|
|
5609
|
+
MCP provides context, not architectural truth. Accepted ADRs and repository decisions outrank MCP
|
|
5610
|
+
context. If MCP data conflicts with repository memory, stop and report the conflict.
|
|
5611
|
+
|
|
5612
|
+
## Captured Context
|
|
5613
|
+
|
|
5614
|
+
Record durable context learned from this MCP server here, as proposed memory for human review.
|
|
5615
|
+
Promote any decision you accept into an ADR with \`recall adr create\`.
|
|
5616
|
+
|
|
5617
|
+
- (none captured yet)
|
|
5618
|
+
|
|
5619
|
+
## Review Cadence
|
|
5620
|
+
|
|
5621
|
+
- Review this MCP integration when its access, purpose, or captured context changes.
|
|
5622
|
+
`;
|
|
5623
|
+
}
|
|
5624
|
+
function renderProposedAdr2(title, server) {
|
|
5625
|
+
return `# Proposed ADR: Use ${title} MCP
|
|
5626
|
+
|
|
5627
|
+
## Status
|
|
5628
|
+
|
|
5629
|
+
Proposed
|
|
5630
|
+
|
|
5631
|
+
## Context
|
|
5632
|
+
|
|
5633
|
+
The team is considering ${title} as an MCP context source. Adopting an MCP server into the workflow
|
|
5634
|
+
is a decision that should be reviewed.
|
|
5635
|
+
|
|
5636
|
+
## Decision
|
|
5637
|
+
|
|
5638
|
+
Consider adopting ${title} MCP as an external context source, documented in \`docs/ai/mcp/\`. This is
|
|
5639
|
+
not accepted until a human reviews and accepts it.
|
|
5640
|
+
|
|
5641
|
+
## Alternatives Considered
|
|
5642
|
+
|
|
5643
|
+
- Do not use this MCP server.
|
|
5644
|
+
- Use a different source for the same context.
|
|
5645
|
+
|
|
5646
|
+
## Consequences
|
|
5647
|
+
|
|
5648
|
+
- The team gains durable, reviewable context from ${title}.
|
|
5649
|
+
- MCP context never overrides accepted repository memory.
|
|
5650
|
+
- Captured context remains proposed until promoted to an ADR.
|
|
5651
|
+
|
|
5652
|
+
## Related Documents
|
|
5653
|
+
|
|
5654
|
+
- \`docs/ai/mcp/${server}.md\` \u2014 captured ${title} MCP context.
|
|
5655
|
+
- \`docs/ai/MCP_STRATEGY.md\` \u2014 how MCP context is captured and ranked.
|
|
5656
|
+
`;
|
|
5657
|
+
}
|
|
5658
|
+
function bullets2(values) {
|
|
5659
|
+
return values.map((value) => `- ${value}`).join("\n");
|
|
5660
|
+
}
|
|
5661
|
+
function titleize2(value) {
|
|
5662
|
+
return value.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
4664
5663
|
}
|
|
4665
5664
|
|
|
4666
5665
|
// src/commands/mcp/add.ts
|
|
@@ -4744,7 +5743,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
4744
5743
|
}
|
|
4745
5744
|
|
|
4746
5745
|
// src/core/generator/generate-module.ts
|
|
4747
|
-
import
|
|
5746
|
+
import path16 from "path";
|
|
4748
5747
|
var moduleTemplates = [
|
|
4749
5748
|
{
|
|
4750
5749
|
fileName: "MODULE.md",
|
|
@@ -4817,14 +5816,14 @@ Record durable module decisions here.
|
|
|
4817
5816
|
];
|
|
4818
5817
|
function generateModuleFiles(options) {
|
|
4819
5818
|
const slug = slugify(options.moduleName);
|
|
4820
|
-
const moduleDir =
|
|
5819
|
+
const moduleDir = path16.posix.join(options.modulesDir, slug);
|
|
4821
5820
|
const title = titleizeModuleName(options.moduleName);
|
|
4822
5821
|
const context = createTemplateContext({
|
|
4823
5822
|
slug,
|
|
4824
5823
|
title
|
|
4825
5824
|
});
|
|
4826
5825
|
return moduleTemplates.map((template) => ({
|
|
4827
|
-
path:
|
|
5826
|
+
path: path16.posix.join(moduleDir, template.fileName),
|
|
4828
5827
|
content: renderTemplate(template.content, context)
|
|
4829
5828
|
}));
|
|
4830
5829
|
}
|