claudeos-core 2.4.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.de.md +9 -9
- package/README.es.md +9 -9
- package/README.fr.md +9 -9
- package/README.hi.md +10 -10
- package/README.ja.md +9 -9
- package/README.ko.md +10 -10
- package/README.md +9 -9
- package/README.ru.md +9 -9
- package/README.vi.md +9 -9
- package/README.zh-CN.md +9 -9
- package/bin/commands/init.js +121 -24
- package/bin/commands/lint.js +2 -0
- package/bin/commands/memory.js +10 -3
- package/content-validator/index.js +82 -13
- package/lib/env-parser.js +50 -12
- package/lib/memory-scaffold.js +35 -16
- package/manifest-generator/index.js +15 -4
- package/package.json +1 -1
- package/pass-prompts/templates/angular/pass3.md +2 -1
- package/pass-prompts/templates/common/claude-md-scaffold.md +1 -1
- package/pass-prompts/templates/common/pass3a-facts.md +11 -9
- package/pass-prompts/templates/common/pass4.md +3 -3
- package/pass-prompts/templates/java-spring/pass3.md +3 -3
- package/pass-prompts/templates/kotlin-spring/pass3.md +2 -2
- package/pass-prompts/templates/node-express/pass3.md +1 -1
- package/pass-prompts/templates/node-fastify/pass3.md +1 -0
- package/pass-prompts/templates/node-nestjs/pass3.md +1 -0
- package/pass-prompts/templates/node-nextjs/pass3.md +1 -1
- package/pass-prompts/templates/node-vite/pass3.md +1 -0
- package/pass-prompts/templates/python-django/pass3.md +1 -1
- package/pass-prompts/templates/python-fastapi/pass3.md +1 -1
- package/pass-prompts/templates/python-flask/pass3.md +1 -0
- package/pass-prompts/templates/vue-nuxt/pass3.md +1 -0
- package/plan-installer/domain-grouper.js +4 -1
- package/plan-installer/index.js +26 -7
- package/plan-installer/pass3-context-builder.js +10 -0
- package/plan-installer/prompt-generator.js +18 -2
- package/plan-installer/scanners/scan-frontend.js +67 -6
- package/plan-installer/scanners/scan-java.js +145 -14
- package/plan-installer/scanners/scan-kotlin.js +68 -3
- package/plan-installer/scanners/scan-node.js +115 -0
- package/plan-installer/scanners/scan-python.js +56 -0
- package/plan-installer/source-paths.js +61 -0
- package/plan-installer/stack-detector.js +262 -24
- package/plan-installer/structure-scanner.js +15 -4
package/bin/commands/init.js
CHANGED
|
@@ -615,6 +615,42 @@ async function runPass3Split(ctx) {
|
|
|
615
615
|
},
|
|
616
616
|
});
|
|
617
617
|
|
|
618
|
+
// v2.5.0 — Deterministic allowlist injection ("LLMs guess, code confirms").
|
|
619
|
+
// The `## Allowed Source Paths` section is the single most important
|
|
620
|
+
// anti-hallucination input for 3b/3c/3d, yet until now it reached
|
|
621
|
+
// pass3a-facts.md only if the LLM faithfully hand-copied up to 500 paths
|
|
622
|
+
// out of pass3-context.json — and the 3a validator only checked file
|
|
623
|
+
// length. Node now writes that section itself from project-analysis.json
|
|
624
|
+
// (replacing any LLM-written version), so the allowlist is byte-exact
|
|
625
|
+
// every run, including resumed runs where 3a was skipped.
|
|
626
|
+
{
|
|
627
|
+
const { injectAllowedPathsSection } = require("../../plan-installer/source-paths");
|
|
628
|
+
let collected = null;
|
|
629
|
+
try {
|
|
630
|
+
const pa = JSON.parse(readFile(path.join(GENERATED_DIR, "project-analysis.json")));
|
|
631
|
+
collected = pa && pa.allowedSourcePaths;
|
|
632
|
+
} catch (_e) { /* tolerated — fallback line is written instead */ }
|
|
633
|
+
const before = readFileSafe(factsFile, "");
|
|
634
|
+
if (before.replace(/^\uFEFF/, "").trim().length === 0) {
|
|
635
|
+
// 3a is marked complete in the marker but the facts file is missing/empty
|
|
636
|
+
// (deleted by hand, or a crash between validate and marker write). Do not
|
|
637
|
+
// fabricate a facts file that contains only the allowlist — 3b would run
|
|
638
|
+
// against an empty fact sheet. Surface it and let the user resume/--force.
|
|
639
|
+
throw new InitError(
|
|
640
|
+
"pass3a-facts.md is missing or empty although Pass 3a is marked complete.\n" +
|
|
641
|
+
" Delete claudeos-core/generated/pass3-complete.json to re-run Pass 3a, or use `init --force`."
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
const after = injectAllowedPathsSection(before, collected);
|
|
645
|
+
if (after !== before) {
|
|
646
|
+
if (!writeFileSafe(factsFile, after)) {
|
|
647
|
+
throw new InitError("Failed to write the Allowed Source Paths section into pass3a-facts.md.");
|
|
648
|
+
}
|
|
649
|
+
const n = collected && Array.isArray(collected.paths) ? collected.paths.length : 0;
|
|
650
|
+
log(` 📎 Allowed Source Paths section written by orchestrator (${n} ${collected && collected.mode === "rollup" ? "dirs" : "paths"})`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
618
654
|
// ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
|
|
619
655
|
//
|
|
620
656
|
// Single batch (domains ≤ 15): keep legacy "3b" marker (backward-compatible).
|
|
@@ -699,8 +735,16 @@ async function runPass3Split(ctx) {
|
|
|
699
735
|
}
|
|
700
736
|
}
|
|
701
737
|
// For every batch, confirm rules/ was generated (at least one staged-rules move must succeed).
|
|
738
|
+
// Count ONLY claudeos-core-managed categories (`NN.` prefix). Since v2.5.0
|
|
739
|
+
// --force/fresh preserve user-authored entries under .claude/rules/, so a
|
|
740
|
+
// plain recursive count could be >0 even when Pass 3 produced nothing.
|
|
702
741
|
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
703
|
-
|
|
742
|
+
let rulesCount = 0;
|
|
743
|
+
try {
|
|
744
|
+
for (const e of fs.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
745
|
+
if (e.isDirectory() && /^\d{2}\./.test(e.name)) rulesCount += countFilesRecursive(path.join(rulesDir, e.name));
|
|
746
|
+
}
|
|
747
|
+
} catch (_e) { rulesCount = 0; }
|
|
704
748
|
if (rulesCount === 0) {
|
|
705
749
|
problems.push(".claude/rules/ has 0 files (staging-override may have been ignored)");
|
|
706
750
|
}
|
|
@@ -855,7 +899,7 @@ function checkPrerequisites() {
|
|
|
855
899
|
);
|
|
856
900
|
if (!hasProjectMarker) {
|
|
857
901
|
log(`\n ⚠️ Warning: ${PROJECT_ROOT} does not look like a project root.`);
|
|
858
|
-
log(" No .git, package.json, build.gradle,
|
|
902
|
+
log(" No .git, package.json, build.gradle(.kts), pom.xml, or pyproject.toml found.");
|
|
859
903
|
log(" Run this command from your project directory.\n");
|
|
860
904
|
}
|
|
861
905
|
|
|
@@ -910,6 +954,28 @@ async function resolveLanguage(parsedArgs) {
|
|
|
910
954
|
return lang;
|
|
911
955
|
}
|
|
912
956
|
|
|
957
|
+
// Remove only the rule categories claudeos-core generates (`NN.` prefixed:
|
|
958
|
+
// 00.core, 10.backend, …, 90.optional). Files/dirs the user authored under
|
|
959
|
+
// .claude/rules/ without that prefix are left untouched. Returns the number
|
|
960
|
+
// of entries removed.
|
|
961
|
+
function wipeManagedRuleCategories(rulesDir) {
|
|
962
|
+
if (!fileExists(rulesDir)) return 0;
|
|
963
|
+
let removed = 0;
|
|
964
|
+
let entries;
|
|
965
|
+
try { entries = fs.readdirSync(rulesDir, { withFileTypes: true }); }
|
|
966
|
+
catch (_e) { return 0; }
|
|
967
|
+
for (const e of entries) {
|
|
968
|
+
// Managed entries are category DIRECTORIES (00.core/, 10.backend/, …).
|
|
969
|
+
// claudeos-core never writes a `NN.`-prefixed FILE at the rules root, so
|
|
970
|
+
// such a file (`.claude/rules/01.team-style.md`) is user-authored — keep it.
|
|
971
|
+
// Guard 2 (zero-rules detection) counts with the same predicate.
|
|
972
|
+
if (!e.isDirectory() || !/^\d{2}\./.test(e.name)) continue;
|
|
973
|
+
fs.rmSync(path.join(rulesDir, e.name), { recursive: true, force: true });
|
|
974
|
+
removed++;
|
|
975
|
+
}
|
|
976
|
+
return removed;
|
|
977
|
+
}
|
|
978
|
+
|
|
913
979
|
// ─── Stage 3: Resume/Fresh selection ──────────────────────────────
|
|
914
980
|
// Returns { wasFreshClean: boolean } — the caller uses this to gate the
|
|
915
981
|
// v1.7.x migration backfill in dispatchPass3.
|
|
@@ -930,14 +996,15 @@ async function applyResumeMode(parsedArgs, lang) {
|
|
|
930
996
|
// (only .json/.md are unlinked above; directories aren't touched).
|
|
931
997
|
const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
932
998
|
if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
933
|
-
// Also wipe .claude/rules/
|
|
934
|
-
//
|
|
935
|
-
// Pass 3 run fails silently (e.g.
|
|
936
|
-
// Step [2] recreates the subdirs
|
|
937
|
-
//
|
|
938
|
-
// ("truly fresh start").
|
|
939
|
-
|
|
940
|
-
|
|
999
|
+
// Also wipe the claudeos-core-managed categories under .claude/rules/
|
|
1000
|
+
// so Guard 2 (zero-rules detection) can't false-negative on stale rules
|
|
1001
|
+
// from a previous run when the fresh Pass 3 run fails silently (e.g.
|
|
1002
|
+
// Claude ignores staging-override). Step [2] recreates the subdirs.
|
|
1003
|
+
// Manual edits to GENERATED rule files are lost — acceptable under
|
|
1004
|
+
// --force ("truly fresh start"). Anything the user placed under
|
|
1005
|
+
// .claude/rules/ that is NOT a claudeos-core category (no `NN.` prefix)
|
|
1006
|
+
// is preserved — it was never ours to delete.
|
|
1007
|
+
wipeManagedRuleCategories(path.join(PROJECT_ROOT, ".claude/rules"));
|
|
941
1008
|
wasFreshClean = true;
|
|
942
1009
|
log(" 🔄 Previous results deleted (--force)\n");
|
|
943
1010
|
return { wasFreshClean };
|
|
@@ -993,11 +1060,9 @@ async function applyResumeMode(parsedArgs, lang) {
|
|
|
993
1060
|
// Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
|
|
994
1061
|
const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
995
1062
|
if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
996
|
-
// Wipe
|
|
997
|
-
// the --force branch
|
|
998
|
-
|
|
999
|
-
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
1000
|
-
if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
|
|
1063
|
+
// Wipe managed rule categories for the same Guard 2 false-negative
|
|
1064
|
+
// reason as the --force branch (user-owned, non-`NN.` entries survive).
|
|
1065
|
+
wipeManagedRuleCategories(path.join(PROJECT_ROOT, ".claude/rules"));
|
|
1001
1066
|
wasFreshClean = true;
|
|
1002
1067
|
} else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
|
|
1003
1068
|
// pass2 exists but no pass1 → pass2 is stale, force re-run
|
|
@@ -1078,7 +1143,7 @@ function ensureDirectories() {
|
|
|
1078
1143
|
}
|
|
1079
1144
|
|
|
1080
1145
|
// ─── Stage 5: Load & validate domain-groups.json ──────────────────
|
|
1081
|
-
function loadDomainGroups() {
|
|
1146
|
+
function loadDomainGroups({ wasFreshClean = false } = {}) {
|
|
1082
1147
|
let domainGroups;
|
|
1083
1148
|
try {
|
|
1084
1149
|
domainGroups = JSON.parse(
|
|
@@ -1089,6 +1154,34 @@ function loadDomainGroups() {
|
|
|
1089
1154
|
}
|
|
1090
1155
|
const totalGroups = domainGroups.totalGroups;
|
|
1091
1156
|
if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
|
|
1157
|
+
if (totalGroups === 0) {
|
|
1158
|
+
let stackLine = "";
|
|
1159
|
+
try {
|
|
1160
|
+
const pa = JSON.parse(readFile(path.join(GENERATED_DIR, "project-analysis.json")));
|
|
1161
|
+
const st = pa.stack || {};
|
|
1162
|
+
stackLine = ` Detected stack: language=${st.language || "none"} framework=${st.framework || "none"} frontend=${st.frontend || "none"}\n`;
|
|
1163
|
+
} catch (_e) { /* best effort */ }
|
|
1164
|
+
throw new InitError(
|
|
1165
|
+
"No domains were found in this project, so there is nothing for Pass 1-3 to analyze.\n" +
|
|
1166
|
+
stackLine +
|
|
1167
|
+
" The scanner recognizes layouts such as:\n" +
|
|
1168
|
+
" Java/Kotlin src/main/java/<pkg>/<domain>/controller/*.java, <pkg>/controller/<domain>/*.java,\n" +
|
|
1169
|
+
" <pkg>/controller/*Controller.java, <pkg>/<domain>/adapter/in/web/*.java,\n" +
|
|
1170
|
+
" <pkg>/<feature>/*Controller.kt, <module>/src/main/java/... (Gradle/Maven multi-module)\n" +
|
|
1171
|
+
" Node src/<domain>/*.ts, src/modules/<domain>/, src/{controllers,routes,services}/<domain>.*\n" +
|
|
1172
|
+
" Python app/<domain>/, app/{routers,routes}/<domain>.py, Django apps with models.py\n" +
|
|
1173
|
+
" Frontend app/<route>/page.tsx (incl. (group)/), pages/<route>/, src/components/<name>/, src/features/<name>/\n" +
|
|
1174
|
+
" If your layout differs, open an issue with your directory tree, or check claudeos-core/generated/project-analysis.json\n" +
|
|
1175
|
+
" to see what was detected. " +
|
|
1176
|
+
(wasFreshClean
|
|
1177
|
+
// --force / "fresh" already wiped the managed rule categories and
|
|
1178
|
+
// generated/*.json|*.md before the scanner ran — say so, do not
|
|
1179
|
+
// claim nothing was touched.
|
|
1180
|
+
? "CLAUDE.md was not modified, but --force / fresh had already removed the previously generated\n" +
|
|
1181
|
+
" .claude/rules/NN.* categories and claudeos-core/generated/ pass files before this check. Restore them from version control if needed."
|
|
1182
|
+
: "Nothing was written to CLAUDE.md or .claude/rules/.")
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1092
1185
|
throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
|
|
1093
1186
|
}
|
|
1094
1187
|
if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
|
|
@@ -1564,7 +1657,8 @@ async function runPass4(opts) {
|
|
|
1564
1657
|
// Note: master plan files are no longer generated (previously this
|
|
1565
1658
|
// included "claudeos-core/plan/50.memory-master.md"). The marker schema
|
|
1566
1659
|
// still accepts an optional planFiles field for backward compatibility.
|
|
1567
|
-
|
|
1660
|
+
// v2.3.0+: CLAUDE.md is never touched by Pass 4 (Pass 3 §8 is authoritative).
|
|
1661
|
+
claudeMdAppended: false,
|
|
1568
1662
|
}, null, 2);
|
|
1569
1663
|
return writeFileSafe(pass4Marker, markerBody);
|
|
1570
1664
|
}
|
|
@@ -1625,7 +1719,7 @@ async function runPass4(opts) {
|
|
|
1625
1719
|
pass4Label = "Pass 4 already present";
|
|
1626
1720
|
} else if (!fileExists(pass4PromptFile)) {
|
|
1627
1721
|
log(" ⚠️ pass4-prompt.md not found — falling back to static scaffold");
|
|
1628
|
-
if (applyStaticFallback()) { log(" ✅ Memory/Rules/
|
|
1722
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Standard scaffolded (static fallback)"); pass4Label = "Pass 4 (static fallback)"; }
|
|
1629
1723
|
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
1630
1724
|
} else {
|
|
1631
1725
|
let prompt4 = injectProjectRoot(readFile(pass4PromptFile));
|
|
@@ -1675,7 +1769,7 @@ async function runPass4(opts) {
|
|
|
1675
1769
|
|
|
1676
1770
|
if (!ok4 || !isValidPass4Marker(pass4Marker)) {
|
|
1677
1771
|
log(" ⚠️ Pass 4 did not produce a valid pass4-memory.json — using static fallback");
|
|
1678
|
-
if (applyStaticFallback()) { log(" ✅ Memory/Rules/
|
|
1772
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Standard scaffolded (static fallback)"); pass4Label = `Pass 4 (static fallback, ${formatElapsed(elapsed4)})`; }
|
|
1679
1773
|
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
1680
1774
|
} else {
|
|
1681
1775
|
// Claude-driven Pass 4 succeeded. Ensure memory + rules + plans + standard + CLAUDE.md append exist
|
|
@@ -1744,8 +1838,11 @@ async function runPass4(opts) {
|
|
|
1744
1838
|
// ─── Stage 11: Run external verification tools ────────────────────
|
|
1745
1839
|
function runVerificationTools() {
|
|
1746
1840
|
const verifyTools = [
|
|
1747
|
-
|
|
1748
|
-
|
|
1841
|
+
// --sync-skills: only `init` is allowed to let manifest-generator patch
|
|
1842
|
+
// CLAUDE.md §6 / MANIFEST.md (post-generation reconciliation). `health`
|
|
1843
|
+
// runs the same tool without the flag and stays read-only.
|
|
1844
|
+
{ name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js"), args: " --sync-skills" },
|
|
1845
|
+
{ name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js"), args: "" },
|
|
1749
1846
|
];
|
|
1750
1847
|
|
|
1751
1848
|
for (const t of verifyTools) {
|
|
@@ -1753,7 +1850,7 @@ function runVerificationTools() {
|
|
|
1753
1850
|
log(` ⏭️ ${t.name} — not found, skipping`);
|
|
1754
1851
|
continue;
|
|
1755
1852
|
}
|
|
1756
|
-
const ok = run(`node "${t.script}"`, { ignoreError: true });
|
|
1853
|
+
const ok = run(`node "${t.script}"${t.args}`, { ignoreError: true });
|
|
1757
1854
|
if (!ok) {
|
|
1758
1855
|
log(` ⚠️ ${t.name} reported issues (non-fatal)`);
|
|
1759
1856
|
}
|
|
@@ -1914,7 +2011,7 @@ async function cmdInit(parsedArgs) {
|
|
|
1914
2011
|
|
|
1915
2012
|
// ─── [4] Pass 1: Deep analysis per domain group ────────────
|
|
1916
2013
|
header("[4] Pass 1 — Deep analysis per domain group...");
|
|
1917
|
-
const { domainGroups, totalGroups } = loadDomainGroups();
|
|
2014
|
+
const { domainGroups, totalGroups } = loadDomainGroups({ wasFreshClean });
|
|
1918
2015
|
const pass1Prompts = loadPass1Prompts();
|
|
1919
2016
|
|
|
1920
2017
|
// Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
|
|
@@ -1976,4 +2073,4 @@ async function cmdInit(parsedArgs) {
|
|
|
1976
2073
|
printCompletionBanner({ lang, totalGroups, totalStart });
|
|
1977
2074
|
}
|
|
1978
2075
|
|
|
1979
|
-
module.exports = { cmdInit, InitError };
|
|
2076
|
+
module.exports = { cmdInit, InitError, wipeManagedRuleCategories };
|
package/bin/commands/lint.js
CHANGED
|
@@ -32,6 +32,8 @@ Checks:
|
|
|
32
32
|
- Section 6 has exactly 3 ### sub-sections
|
|
33
33
|
- Section 8 has exactly 2 ### sub-sections + 2 #### headings
|
|
34
34
|
- Each L4 memory file appears in exactly 1 table row (inside Section 8)
|
|
35
|
+
- Each \`## N.\` heading contains its English canonical token (T1)
|
|
36
|
+
- Each section has non-trivial body content (S2, warning)
|
|
35
37
|
|
|
36
38
|
Exit codes:
|
|
37
39
|
0 — structure valid
|
package/bin/commands/memory.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ClaudeOS-Core — Memory command (L4 memory)
|
|
3
3
|
*
|
|
4
4
|
* Subcommands:
|
|
5
|
-
* memory compact — apply 4-stage compaction to decision-log.md
|
|
5
|
+
* memory compact — apply 4-stage compaction to failure-patterns.md (decision-log.md is append-only)
|
|
6
6
|
* memory score — recompute importance of failure-patterns.md entries
|
|
7
7
|
* memory propose-rules — analyze failure patterns, append suggestions to auto-rule-update.md
|
|
8
8
|
*/
|
|
@@ -247,7 +247,13 @@ function cmdCompact() {
|
|
|
247
247
|
ensureDir(MEMORY_DIR);
|
|
248
248
|
const activeRulePaths = loadActiveRulePaths();
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
// decision-log.md is EXCLUDED from compaction. Its contract (rules/60.memory/
|
|
251
|
+
// 01.decision-log.md, CLAUDE.md §8) is "permanent, append-only". Stage 1
|
|
252
|
+
// summarization dropped the Context/Options/Decision/Consequences body of
|
|
253
|
+
// every entry older than 30 days — i.e. exactly the "why" the file exists
|
|
254
|
+
// to preserve. Only failure-patterns.md carries the frequency/importance
|
|
255
|
+
// metadata the 4-stage policy is designed around.
|
|
256
|
+
const files = ["failure-patterns.md"];
|
|
251
257
|
const summaries = [];
|
|
252
258
|
for (const f of files) {
|
|
253
259
|
const r = compactFile(path.join(MEMORY_DIR, f), activeRulePaths);
|
|
@@ -255,6 +261,7 @@ function cmdCompact() {
|
|
|
255
261
|
if (r.changed) log(` ✅ ${f}: ${r.before} → ${r.after} entries`);
|
|
256
262
|
else log(` ⏭️ ${f}: ${r.reason}`);
|
|
257
263
|
}
|
|
264
|
+
log(" ⏭️ decision-log.md: append-only, never compacted");
|
|
258
265
|
|
|
259
266
|
// Update compaction.md "Last Compaction" section.
|
|
260
267
|
// Replace ONLY the "## Last Compaction" section (up to next `##` heading or EOF).
|
|
@@ -411,7 +418,7 @@ function showHelp() {
|
|
|
411
418
|
Usage: npx claudeos-core memory <subcommand>
|
|
412
419
|
|
|
413
420
|
Subcommands:
|
|
414
|
-
compact Apply 4-stage compaction to decision-log.md
|
|
421
|
+
compact Apply 4-stage compaction to failure-patterns.md (decision-log.md is append-only, never compacted)
|
|
415
422
|
score Recompute importance of failure-patterns.md entries (frequency × recency)
|
|
416
423
|
propose-rules Analyze failure patterns, append rule update suggestions to auto-rule-update.md
|
|
417
424
|
`);
|
|
@@ -423,10 +423,35 @@ async function main() {
|
|
|
423
423
|
// check covers both. No natural-language matching involved.
|
|
424
424
|
console.log(" [10/10] path-claim verification (hallucination + MANIFEST drift)...");
|
|
425
425
|
|
|
426
|
-
// Regex: matches `src/...` paths to
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
426
|
+
// Regex: matches `src/...` paths to source files, not inside inline code
|
|
427
|
+
// already fenced. We still strip fenced blocks first so example blocks
|
|
428
|
+
// inside ```...``` don't produce false positives.
|
|
429
|
+
//
|
|
430
|
+
// v2.5.0 — extension list widened. Pre-v2.5.0 only ts/tsx/js/jsx were
|
|
431
|
+
// checked, so Java/Kotlin/Python projects (and MyBatis mapper XML, the
|
|
432
|
+
// single most-hallucinated path class on Spring projects) got zero
|
|
433
|
+
// path-claim coverage despite "no invented paths" being the headline
|
|
434
|
+
// guarantee. Config extensions (yml/properties/json) are deliberately
|
|
435
|
+
// NOT included: those are cited as illustrative profile names
|
|
436
|
+
// (`application-{profile}.yml`) far more often than as path claims.
|
|
437
|
+
//
|
|
438
|
+
// An optional module prefix (`api/src/…`, `apps/web/src/…`,
|
|
439
|
+
// `servers/query/x/src/…`) is captured as part of the claim. A prefixed
|
|
440
|
+
// claim is checked at that exact location only — citing `core/src/…` for a
|
|
441
|
+
// file that lives under `api/` is a wrong-module claim and must be flagged,
|
|
442
|
+
// which the bare-`src/` module search below would otherwise hide.
|
|
443
|
+
//
|
|
444
|
+
// Two alternatives, each with its own left boundary:
|
|
445
|
+
// (1) `<module>/…/src/…` — the prefix may not start right after a word
|
|
446
|
+
// char, `.`, `/` or `@`, so `@acme/ui/src/Button.tsx` (a package
|
|
447
|
+
// import, not a repo path) is NOT captured as module `acme/ui`;
|
|
448
|
+
// (2) bare `src/…` — may be preceded by `/` (so the `src/Button.tsx`
|
|
449
|
+
// tail of that import is still checked via the module search) but
|
|
450
|
+
// not by a word char, so `libsrc/x.ts` never yields `src/x.ts`.
|
|
451
|
+
// Dependency paths (`node_modules/<pkg>/src/…`) are skipped below.
|
|
452
|
+
// `:` is excluded before a prefix so a dev-server URL (`localhost:5173/src/main.tsx`)
|
|
453
|
+
// does not yield the module `5173`; its `src/main.tsx` tail is still checked.
|
|
454
|
+
const SRC_PATH_RE = /(?:(?<![\w\-./@:])[\w\-][\w\-.]*\/(?:[\w\-][\w\-.]*\/)*src\/|(?<![\w\-.@])src\/)[\w\-./]+\.(?:ts|tsx|js|jsx|mjs|cjs|vue|svelte|java|kt|kts|py|xml|sql)\b/g;
|
|
430
455
|
|
|
431
456
|
// Placeholder paths are scaffold templates / teaching examples, not
|
|
432
457
|
// real path claims. We skip them. Three patterns qualify:
|
|
@@ -541,18 +566,59 @@ async function main() {
|
|
|
541
566
|
// The monorepo fallback only fires for paths starting with `src/`,
|
|
542
567
|
// which is the conventional workspace-relative form. Non-`src/` paths
|
|
543
568
|
// (e.g., `claudeos-core/skills/...`) are checked direct-only.
|
|
569
|
+
// Directories that are never application modules even when they contain a
|
|
570
|
+
// `src/` child: virtualenvs (pip editable installs default to `<venv>/src`),
|
|
571
|
+
// vendored code, docs, tooling, test fixtures.
|
|
572
|
+
//
|
|
573
|
+
// Two tiers: HARD names are never modules at any depth (build output,
|
|
574
|
+
// dependencies, virtualenvs). SOFT names (`docs`, `tools`, `scripts`,
|
|
575
|
+
// `test(s)`, `fixtures`) are skipped only at the project root — inside a
|
|
576
|
+
// workspace container (`apps/docs`, `packages/tools`) they are legitimate
|
|
577
|
+
// packages, and skipping them there produced false STALE_PATH advisories on
|
|
578
|
+
// the default Turborepo layout.
|
|
579
|
+
const MODULE_SCAN_SKIP = new Set(["node_modules", ".git", ".claude", "claudeos-core", "build", "target", "dist", "out", ".gradle", ".idea", "coverage", ".next",
|
|
580
|
+
"venv", ".venv", "env", "virtualenv", "site-packages", "__pycache__", "vendor", "tmp", "temp"]);
|
|
581
|
+
const MODULE_SCAN_SKIP_ROOT_ONLY = new Set(["docs", "doc", "tools", "scripts", "test", "tests", "__tests__", "fixtures"]);
|
|
582
|
+
const WORKSPACE_CONTAINERS = new Set(["apps", "packages", "libs", "services", "servers", "modules"]);
|
|
583
|
+
function listModuleDirs(dir, { root = false, parentName = "" } = {}) {
|
|
584
|
+
let entries;
|
|
585
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
586
|
+
catch (_e) { return []; }
|
|
587
|
+
const inWorkspace = WORKSPACE_CONTAINERS.has(parentName);
|
|
588
|
+
return entries
|
|
589
|
+
.filter(e => e.isDirectory() && !MODULE_SCAN_SKIP.has(e.name) && !e.name.startsWith(".")
|
|
590
|
+
&& !((root || !inWorkspace) && MODULE_SCAN_SKIP_ROOT_ONLY.has(e.name)))
|
|
591
|
+
.map(e => path.join(dir, e.name));
|
|
592
|
+
}
|
|
593
|
+
// Memoized once per run: every directory up to three levels deep that
|
|
594
|
+
// contains a `src/` folder (JS workspaces `apps/x`, Gradle modules `api`,
|
|
595
|
+
// and the nested Kotlin CQRS layout `servers/query/x` the scanner itself
|
|
596
|
+
// supports). Computed lazily on the first `src/...` miss so projects with
|
|
597
|
+
// zero misses pay nothing.
|
|
598
|
+
let moduleSrcRoots = null;
|
|
599
|
+
function getModuleSrcRoots(ROOT) {
|
|
600
|
+
if (moduleSrcRoots) return moduleSrcRoots;
|
|
601
|
+
moduleSrcRoots = [];
|
|
602
|
+
const walk = (dir, depth) => {
|
|
603
|
+
for (const child of listModuleDirs(dir, { root: depth === 1, parentName: path.basename(dir) })) {
|
|
604
|
+
if (fs.existsSync(path.join(child, "src"))) moduleSrcRoots.push(child);
|
|
605
|
+
if (depth < 3) walk(child, depth + 1);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
walk(ROOT, 1);
|
|
609
|
+
return moduleSrcRoots;
|
|
610
|
+
}
|
|
544
611
|
function resolvePathClaim(ROOT, claimed) {
|
|
545
612
|
if (fs.existsSync(path.join(ROOT, claimed))) return true;
|
|
546
613
|
if (!claimed.startsWith("src/")) return false;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
614
|
+
// v2.4.0: JS monorepo workspaces (apps/*, packages/*).
|
|
615
|
+
// v2.5.0: any first- or second-level module directory — Gradle/Maven
|
|
616
|
+
// multi-module (`api/src/main/java/...`, `servers/query/x/src/main/kotlin/...`)
|
|
617
|
+
// cite `src/...` relative to the module, exactly like JS workspaces do.
|
|
618
|
+
// Without this, widening SRC_PATH_RE to .java/.kt/.py would have turned
|
|
619
|
+
// every valid multi-module citation into a STALE_PATH false positive.
|
|
620
|
+
for (const moduleRoot of getModuleSrcRoots(ROOT)) {
|
|
621
|
+
if (fs.existsSync(path.join(moduleRoot, claimed))) return true;
|
|
556
622
|
}
|
|
557
623
|
return false;
|
|
558
624
|
}
|
|
@@ -608,6 +674,9 @@ async function main() {
|
|
|
608
674
|
if (seen.has(claimed)) continue;
|
|
609
675
|
seen.add(claimed);
|
|
610
676
|
if (hasPlaceholder(claimed)) continue;
|
|
677
|
+
// A dependency path is a reference to a library, not a claim about
|
|
678
|
+
// this repository's source tree.
|
|
679
|
+
if (/(^|\/)node_modules\//.test(claimed)) continue;
|
|
611
680
|
pathClaimsChecked++;
|
|
612
681
|
if (!resolvePathClaim(ROOT, claimed)) {
|
|
613
682
|
pathClaimErrors++;
|
package/lib/env-parser.js
CHANGED
|
@@ -247,28 +247,65 @@ function isSensitiveVarName(name) {
|
|
|
247
247
|
return SENSITIVE_VAR_PATTERNS.some(re => re.test(name));
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Mask the userinfo component of a URL-shaped value:
|
|
252
|
+
* postgres://app:s3cret@db.internal:5432/app → postgres://***:***@db.internal:5432/app
|
|
253
|
+
* Scheme, host, port, path and query are preserved so consumers can still
|
|
254
|
+
* identify the DB engine / host. Non-URL values pass through unchanged.
|
|
255
|
+
*/
|
|
256
|
+
function maskUrlCredentials(value) {
|
|
257
|
+
if (typeof value !== "string") return value;
|
|
258
|
+
// Scheme may itself contain `:` (`jdbc:postgresql://`, `jdbc:mysql://`).
|
|
259
|
+
// Userinfo is everything between `://` and the LAST `@` of the authority
|
|
260
|
+
// part, so a password containing `@` (`p@ss`) is masked whole. It may NOT
|
|
261
|
+
// contain `/`, `?` or `#`: an `@` that appears after the first `/` belongs
|
|
262
|
+
// to the path or query (`https://cdn.example.com/npm/@scope/pkg`,
|
|
263
|
+
// `/users/@me`, `?redirect=user@host`) and must never be rewritten —
|
|
264
|
+
// masking it would replace the real host with a path fragment. A raw `/`
|
|
265
|
+
// inside a password is not a valid URL and is deliberately left alone.
|
|
266
|
+
// Credentials carried as connection PARAMETERS rather than userinfo:
|
|
267
|
+
// jdbc:postgresql://db/app?user=app&password=s3cret
|
|
268
|
+
// mongodb://host/db?authSource=admin&password=x
|
|
269
|
+
// sqlserver://host;databaseName=app;user=sa;password=x
|
|
270
|
+
// The parameter NAME is kept, the value becomes `***`.
|
|
271
|
+
const PARAM_RE = /([?&;](?:password|passwd|pwd|pass|secret|token|access[_-]?key|secret[_-]?key|api[_-]?key|sas|signature)=)[^&;\s]*/gi;
|
|
272
|
+
if (/^[a-z][a-z0-9+.:-]*:\/\//i.test(value)) {
|
|
273
|
+
return value
|
|
274
|
+
.replace(/^([a-z][a-z0-9+.:-]*:\/\/)([^/?#\s]*)@([^@/?#\s]+)/i, "$1***:***@$3")
|
|
275
|
+
.replace(PARAM_RE, "$1***");
|
|
276
|
+
}
|
|
277
|
+
// Scheme-less credentials are recognized ONLY in the Go/MySQL DSN shape
|
|
278
|
+
// (`user:pw@tcp(host:3306)/db`, `user:pw@unix(/path)/db`); the password may
|
|
279
|
+
// itself contain `@` (`p@ss`) — everything up to the `@` before `tcp(`/`unix(`
|
|
280
|
+
// is userinfo. A generic `a:b@c` rule would corrupt `mailto:ops@example.com`
|
|
281
|
+
// or `0:30@daily`.
|
|
282
|
+
return value
|
|
283
|
+
.replace(/^([^:@/\s]+):(.*)@(?=(?:tcp|unix)\()/, "***:***@")
|
|
284
|
+
.replace(PARAM_RE, "$1***");
|
|
285
|
+
}
|
|
286
|
+
|
|
250
287
|
/**
|
|
251
288
|
* Redacts sensitive values in an env vars map. Returns a new object;
|
|
252
289
|
* original is not mutated. Preserves keys so "variable exists" signal
|
|
253
290
|
* is kept, but replaces values with a sentinel string.
|
|
254
291
|
*
|
|
255
|
-
*
|
|
256
|
-
* db-identification path
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
292
|
+
* v2.5.0 — the former DATABASE_URL whitelist is gone. Its stated
|
|
293
|
+
* justification ("stack-detector's db-identification path depends on it")
|
|
294
|
+
* was stale: stack-detector scans the raw .env text with includes() and
|
|
295
|
+
* never reads envInfo.vars. Meanwhile the unredacted value — typically
|
|
296
|
+
* `postgres://user:password@host/db` — landed verbatim in
|
|
297
|
+
* project-analysis.json, which Pass 3/4 prompts instruct the LLM to read.
|
|
298
|
+
* Every URL-shaped value (DATABASE_URL, REDIS_URL, MONGO_URI, AMQP_URL, …)
|
|
299
|
+
* now has its userinfo masked while keeping scheme/host/path intact.
|
|
261
300
|
*/
|
|
262
301
|
function redactSensitiveVars(vars) {
|
|
263
302
|
if (!vars || typeof vars !== "object") return vars;
|
|
264
303
|
const out = {};
|
|
265
304
|
for (const [k, v] of Object.entries(vars)) {
|
|
266
|
-
if (k
|
|
267
|
-
out[k] = v; // documented whitelist for stack-detector back-compat
|
|
268
|
-
} else if (isSensitiveVarName(k)) {
|
|
305
|
+
if (isSensitiveVarName(k)) {
|
|
269
306
|
out[k] = "***REDACTED***";
|
|
270
307
|
} else {
|
|
271
|
-
out[k] = v;
|
|
308
|
+
out[k] = maskUrlCredentials(v);
|
|
272
309
|
}
|
|
273
310
|
}
|
|
274
311
|
return out;
|
|
@@ -293,8 +330,8 @@ function readStackEnvInfo(root) {
|
|
|
293
330
|
source: file,
|
|
294
331
|
vars: redactSensitiveVars(vars),
|
|
295
332
|
port: extractPort(vars),
|
|
296
|
-
host: extractHost(vars),
|
|
297
|
-
apiTarget: extractApiTarget(vars),
|
|
333
|
+
host: maskUrlCredentials(extractHost(vars)),
|
|
334
|
+
apiTarget: maskUrlCredentials(extractApiTarget(vars)),
|
|
298
335
|
};
|
|
299
336
|
}
|
|
300
337
|
|
|
@@ -308,6 +345,7 @@ module.exports = {
|
|
|
308
345
|
readStackEnvInfo,
|
|
309
346
|
isSensitiveVarName,
|
|
310
347
|
redactSensitiveVars,
|
|
348
|
+
maskUrlCredentials,
|
|
311
349
|
// Exported for test visibility:
|
|
312
350
|
ENV_FILE_ORDER,
|
|
313
351
|
PORT_VAR_KEYS,
|