@sayansr26/agent-os 0.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/.claude-plugin/marketplace.json +27 -0
- package/CHANGELOG.md +203 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/bin/agent-os.mjs +6 -0
- package/package.json +49 -0
- package/plugins/agent-os/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-os/agents/architect.md +77 -0
- package/plugins/agent-os/agents/builder.md +90 -0
- package/plugins/agent-os/agents/documenter.md +87 -0
- package/plugins/agent-os/agents/feature-cartographer.md +142 -0
- package/plugins/agent-os/agents/orchestrator.md +96 -0
- package/plugins/agent-os/agents/reviewer.md +82 -0
- package/plugins/agent-os/agents/tester.md +83 -0
- package/plugins/agent-os/hooks/hooks.json +17 -0
- package/plugins/agent-os/hooks/session-resume.mjs +136 -0
- package/plugins/agent-os/skills/init/SKILL.md +134 -0
- package/plugins/agent-os/skills/init/references/changing-a-feature.md +104 -0
- package/plugins/agent-os/skills/init/references/establishing.md +128 -0
- package/plugins/agent-os/skills/init/references/git-permissions.md +118 -0
- package/plugins/agent-os/skills/init/references/migrating.md +43 -0
- package/plugins/agent-os/skills/init/references/writing-rules.md +50 -0
- package/plugins/agent-os/skills/init/scripts/audit.mjs +328 -0
- package/plugins/agent-os/skills/map/SKILL.md +67 -0
- package/plugins/agent-os/skills/memory/SKILL.md +82 -0
- package/plugins/agent-os/skills/memory/scripts/memory.mjs +97 -0
- package/src/cli.mjs +157 -0
- package/src/detect.mjs +54 -0
- package/src/selftest.mjs +71 -0
- package/src/source.mjs +102 -0
- package/src/targets.mjs +186 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-os audit — one call, whole picture.
|
|
4
|
+
*
|
|
5
|
+
* The audit is deterministic: line counts, frontmatter presence, file
|
|
6
|
+
* existence, JSON keys. Having a model discover that with a dozen Read and
|
|
7
|
+
* Grep round trips costs tokens on every one. This emits the lot in a single
|
|
8
|
+
* tool result.
|
|
9
|
+
*
|
|
10
|
+
* Read-only. Never writes, never mutates. Exits 0 even on failures so a
|
|
11
|
+
* partial audit still reaches the caller.
|
|
12
|
+
*
|
|
13
|
+
* Usage: node audit.mjs [projectDir] (defaults to cwd)
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import { join, basename } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
|
|
19
|
+
const ROOT = process.argv[2] || process.cwd();
|
|
20
|
+
const HOME = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
21
|
+
const out = [];
|
|
22
|
+
const findings = [];
|
|
23
|
+
const say = (s = "") => out.push(s);
|
|
24
|
+
const flag = (sev, msg) => findings.push(`${sev} ${msg}`);
|
|
25
|
+
|
|
26
|
+
const read = (p) => { try { return readFileSync(p, "utf8"); } catch { return null; } };
|
|
27
|
+
const lines = (s) => (s ? s.split("\n").length : 0);
|
|
28
|
+
const ls = (p) => { try { return readdirSync(p); } catch { return []; } };
|
|
29
|
+
const size = (p) => { try { return statSync(p).size; } catch { return 0; } };
|
|
30
|
+
const dirBytes = (p) => ls(p).reduce((n, f) => {
|
|
31
|
+
const fp = join(p, f);
|
|
32
|
+
try { return n + (statSync(fp).isDirectory() ? dirBytes(fp) : statSync(fp).size); } catch { return n; }
|
|
33
|
+
}, 0);
|
|
34
|
+
const frontmatter = (t) => (t && t.startsWith("---\n")) ? t.slice(4).split("\n---")[0] : "";
|
|
35
|
+
|
|
36
|
+
say(`AGENT-OS AUDIT ${ROOT}`);
|
|
37
|
+
say(` ${new Date().toISOString().slice(0, 10)}`);
|
|
38
|
+
say();
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------- always-loaded
|
|
41
|
+
let residentBytes = 0;
|
|
42
|
+
say("ALWAYS-LOADED (cost on every turn)");
|
|
43
|
+
for (const f of ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"]) {
|
|
44
|
+
const t = read(join(ROOT, f));
|
|
45
|
+
if (!t) continue;
|
|
46
|
+
const n = lines(t), b = t.length;
|
|
47
|
+
residentBytes += b;
|
|
48
|
+
const over = n > 200;
|
|
49
|
+
say(` ${f.padEnd(30)} ${String(n).padStart(4)} lines ${String(b).padStart(6)} B ${over ? "OVER BUDGET (>200)" : "ok"}`);
|
|
50
|
+
if (over) flag("WARN", `${f} is ${n} lines; budget is 200. Move path-scoped content to .claude/rules/.`);
|
|
51
|
+
}
|
|
52
|
+
if (!residentBytes) flag("WARN", "No CLAUDE.md at all — this project has no always-loaded instructions.");
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------- rules
|
|
55
|
+
const rulesDir = join(ROOT, ".claude/rules");
|
|
56
|
+
const ruleFiles = ls(rulesDir).filter((f) => f.endsWith(".md"));
|
|
57
|
+
say();
|
|
58
|
+
say(`RULES .claude/rules/ — ${ruleFiles.length} file(s)`);
|
|
59
|
+
if (!ruleFiles.length && existsSync(rulesDir) === false) say(" (no rules directory)");
|
|
60
|
+
for (const f of ruleFiles) {
|
|
61
|
+
const t = read(join(rulesDir, f)) || "";
|
|
62
|
+
const fm = frontmatter(t);
|
|
63
|
+
const scoped = /^paths:/m.test(fm);
|
|
64
|
+
const globs = (fm.match(/-\s+["']/g) || []).length;
|
|
65
|
+
say(` ${scoped ? "ok " : "WARN"} ${f.padEnd(26)} ${String(lines(t)).padStart(4)} lines ${scoped ? `paths: ${globs}` : "NO paths: — loads every session"}`);
|
|
66
|
+
if (!scoped) { residentBytes += t.length; flag("WARN", `.claude/rules/${f} has no paths: frontmatter, so it loads every session like CLAUDE.md.`); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------- hooks
|
|
70
|
+
const readJson = (p) => { const t = read(p); if (!t) return null; try { return JSON.parse(t); } catch { return "INVALID"; } };
|
|
71
|
+
const projSettingsPath = join(ROOT, ".claude/settings.json");
|
|
72
|
+
const projSettings = readJson(projSettingsPath);
|
|
73
|
+
say();
|
|
74
|
+
say("HOOKS .claude/settings.json");
|
|
75
|
+
if (projSettings === null) say(" (no project settings.json)");
|
|
76
|
+
else if (projSettings === "INVALID") flag("FAIL", ".claude/settings.json is not valid JSON.");
|
|
77
|
+
else {
|
|
78
|
+
const hooks = projSettings.hooks || {};
|
|
79
|
+
if (!Object.keys(hooks).length) say(" (none)");
|
|
80
|
+
for (const [evt, entries] of Object.entries(hooks)) {
|
|
81
|
+
for (const e of entries) for (const h of e.hooks || []) {
|
|
82
|
+
const cmd = h.command || "";
|
|
83
|
+
const m = cmd.match(/\$\{CLAUDE_PROJECT_DIR\}\/([^"']+)/);
|
|
84
|
+
if (!m) { say(` ok ${evt.padEnd(14)} ${cmd.slice(0, 60)}`); continue; }
|
|
85
|
+
const target = m[1], exists = existsSync(join(ROOT, target));
|
|
86
|
+
say(` ${exists ? "ok " : "FAIL"} ${evt.padEnd(14)} ${target}${exists ? "" : " <- TARGET MISSING"}`);
|
|
87
|
+
if (!exists) flag("FAIL", `${evt} hook points at ${target}, which does not exist. It fires at a missing path every session.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------- legacy stores
|
|
93
|
+
say();
|
|
94
|
+
say("LEGACY STORES");
|
|
95
|
+
const legacy = [
|
|
96
|
+
["memory-bank/", join(ROOT, "memory-bank")],
|
|
97
|
+
[".serena/memories/", join(ROOT, ".serena/memories")],
|
|
98
|
+
[".cursorrules", join(ROOT, ".cursorrules")],
|
|
99
|
+
[".cursor/rules/", join(ROOT, ".cursor/rules")],
|
|
100
|
+
[".windsurfrules", join(ROOT, ".windsurfrules")],
|
|
101
|
+
[".clinerules", join(ROOT, ".clinerules")],
|
|
102
|
+
];
|
|
103
|
+
// A store this tool generated is not legacy — telling someone to fold their
|
|
104
|
+
// own compiled output back into CLAUDE.md and delete it would destroy the
|
|
105
|
+
// thing `sync` just wrote. Generated files carry the banner; check for it.
|
|
106
|
+
const generated = (p) => {
|
|
107
|
+
const files = statSync(p).isDirectory() ? ls(p).map((f) => join(p, f)) : [p];
|
|
108
|
+
const readable = files.filter((f) => statSync(f).isFile());
|
|
109
|
+
return readable.length > 0 &&
|
|
110
|
+
readable.every((f) => (read(f) || "").includes("agent-os: generated from .agent-os/"));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let anyLegacy = false, anyListed = false;
|
|
114
|
+
for (const [label, p] of legacy) {
|
|
115
|
+
if (!existsSync(p)) continue;
|
|
116
|
+
const isDir = statSync(p).isDirectory();
|
|
117
|
+
const n = isDir ? ls(p).length : 1, b = isDir ? dirBytes(p) : size(p);
|
|
118
|
+
if (generated(p)) {
|
|
119
|
+
say(` ${label.padEnd(22)} ${n} file(s) ${b} B — generated by agent-os, not legacy`);
|
|
120
|
+
anyListed = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
anyLegacy = true;
|
|
124
|
+
anyListed = true;
|
|
125
|
+
say(` FOUND ${label.padEnd(22)} ${n} file(s) ${b} B`);
|
|
126
|
+
flag("INFO", `${label} exists — preserve its content into CLAUDE.md or .claude/rules/ before deleting anything.`);
|
|
127
|
+
}
|
|
128
|
+
const mcp = readJson(join(ROOT, ".mcp.json"));
|
|
129
|
+
if (mcp && mcp !== "INVALID") {
|
|
130
|
+
const servers = Object.keys(mcp.mcpServers || {});
|
|
131
|
+
const graphy = servers.filter((s) => /graphiti|memory|knowledge|mem0|zep|serena/i.test(s));
|
|
132
|
+
say(` .mcp.json servers: ${servers.join(", ") || "(none)"}`);
|
|
133
|
+
if (graphy.length) flag("INFO", `.mcp.json has memory-ish server(s): ${graphy.join(", ")}. Check they are still wanted.`);
|
|
134
|
+
}
|
|
135
|
+
if (!anyListed) say(" none");
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------- auto memory + agent memory
|
|
138
|
+
say();
|
|
139
|
+
say("MEMORY");
|
|
140
|
+
for (const [label, dir] of [["agent memory", join(ROOT, ".claude/agent-memory")], ["agent memory (local)", join(ROOT, ".claude/agent-memory-local")]]) {
|
|
141
|
+
if (!existsSync(dir)) continue;
|
|
142
|
+
for (const agent of ls(dir)) {
|
|
143
|
+
const adir = join(dir, agent);
|
|
144
|
+
const files = ls(adir).filter((f) => f.endsWith(".md"));
|
|
145
|
+
const idx = read(join(adir, "MEMORY.md"));
|
|
146
|
+
const idxLines = idx ? idx.split("\n").filter((l) => l.trim()).length : 0;
|
|
147
|
+
const topics = files.filter((f) => f !== "MEMORY.md");
|
|
148
|
+
say(` ${agent.padEnd(34)} index ${String(idxLines).padStart(3)} line(s) ${topics.length} topic file(s)`);
|
|
149
|
+
if (idxLines < topics.length) flag("WARN", `${agent}: ${topics.length} topic files but only ${idxLines} indexed in MEMORY.md — the unindexed ones are invisible next session.`);
|
|
150
|
+
const slugs = topics.map((f) => basename(f, ".md").replace(/[-_]/g, ""));
|
|
151
|
+
const dupes = slugs.filter((s, i) => slugs.indexOf(s) !== i);
|
|
152
|
+
if (dupes.length) flag("WARN", `${agent}: near-duplicate topic filenames (hyphen/underscore variants). Merge them.`);
|
|
153
|
+
if (idx && idx.split("\n").length > 200) flag("WARN", `${agent}: MEMORY.md over 200 lines — everything past that is dropped at startup.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!existsSync(join(ROOT, ".claude/agent-memory")) && !existsSync(join(ROOT, ".claude/agent-memory-local")))
|
|
157
|
+
say(" no agent memory yet (agents have not run in this project)");
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------- machine layer
|
|
160
|
+
say();
|
|
161
|
+
say("MACHINE " + HOME);
|
|
162
|
+
const g = (p) => join(HOME, p);
|
|
163
|
+
const gClaude = read(g("CLAUDE.md"));
|
|
164
|
+
say(` CLAUDE.md${" ".repeat(24)}${gClaude ? `present ${gClaude.length} B` : "ABSENT"}`);
|
|
165
|
+
if (!gClaude && residentBytes) {
|
|
166
|
+
const pc = read(join(ROOT, "CLAUDE.md")) || "";
|
|
167
|
+
if (/~\/\.claude\/CLAUDE\.md/.test(pc)) flag("FAIL", "Project CLAUDE.md refers to ~/.claude/CLAUDE.md, which does not exist — a dangling reference.");
|
|
168
|
+
}
|
|
169
|
+
for (const d of ["agents", "skills"]) {
|
|
170
|
+
const names = ls(g(d)).map((f) => basename(f, ".md"));
|
|
171
|
+
say(` ${(d + "/").padEnd(33)}${names.length ? names.join(", ") : "absent"}`);
|
|
172
|
+
const plugin = ["orchestrator", "architect", "builder", "reviewer", "tester", "documenter", "feature-cartographer", "init"];
|
|
173
|
+
const clash = names.filter((n) => plugin.includes(n));
|
|
174
|
+
if (clash.length) flag("FAIL", `~/.claude/${d}/ contains ${clash.join(", ")} — user scope OVERRIDES the plugin's copy, so plugin updates stop reaching you.`);
|
|
175
|
+
}
|
|
176
|
+
const projAgents = ls(join(ROOT, ".claude/agents")).map((f) => basename(f, ".md"));
|
|
177
|
+
const pluginNames = ["orchestrator", "architect", "builder", "reviewer", "tester", "documenter", "feature-cartographer"];
|
|
178
|
+
const projClash = projAgents.filter((n) => pluginNames.includes(n));
|
|
179
|
+
if (projClash.length) flag("FAIL", `.claude/agents/ contains ${projClash.join(", ")} — shadows the plugin agent of the same name.`);
|
|
180
|
+
|
|
181
|
+
const gs = readJson(g("settings.json"));
|
|
182
|
+
if (gs === "INVALID") flag("FAIL", "~/.claude/settings.json is not valid JSON.");
|
|
183
|
+
else if (gs) {
|
|
184
|
+
const perms = gs.permissions || {};
|
|
185
|
+
const deny = perms.deny || [];
|
|
186
|
+
const mode = perms.defaultMode || "(unset)";
|
|
187
|
+
say(` permissions.defaultMode${" ".repeat(10)}${mode}`);
|
|
188
|
+
say(` permissions.deny${" ".repeat(17)}${deny.length} rule(s)`);
|
|
189
|
+
if (/auto|accept/i.test(mode) && !deny.length)
|
|
190
|
+
flag("WARN", `defaultMode is "${mode}" with an empty deny list. A CLAUDE.md rule is context, not enforcement — see references/git-permissions.md.`);
|
|
191
|
+
const gHooks = JSON.stringify((gs.hooks || {}).SessionStart || []);
|
|
192
|
+
if (gHooks.includes("session-resume"))
|
|
193
|
+
flag("FAIL", "~/.claude/settings.json also registers session-resume — the plugin registers it too, so the resume block prints twice. Remove the user-scope copy.");
|
|
194
|
+
say(` SessionStart in user scope${" ".repeat(7)}${(gs.hooks || {}).SessionStart ? "yes" : "no"}`);
|
|
195
|
+
} else say(" settings.json absent");
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------- stack + scale
|
|
198
|
+
say();
|
|
199
|
+
say("PROJECT");
|
|
200
|
+
const pkg = readJson(join(ROOT, "package.json"));
|
|
201
|
+
const stacks = [];
|
|
202
|
+
const dep = (n) => {
|
|
203
|
+
if (!pkg || pkg === "INVALID") return false;
|
|
204
|
+
return !!((pkg.dependencies || {})[n] || (pkg.devDependencies || {})[n]);
|
|
205
|
+
};
|
|
206
|
+
if (pkg && pkg !== "INVALID") {
|
|
207
|
+
if (dep("next")) stacks.push("Next.js");
|
|
208
|
+
else if (dep("react")) stacks.push("React");
|
|
209
|
+
if (dep("vue")) stacks.push("Vue");
|
|
210
|
+
if (dep("@nestjs/core")) stacks.push("NestJS");
|
|
211
|
+
else if (dep("express")) stacks.push("Express");
|
|
212
|
+
if (dep("typescript")) stacks.push("TypeScript");
|
|
213
|
+
if (dep("prisma") || dep("@prisma/client")) stacks.push("Prisma");
|
|
214
|
+
}
|
|
215
|
+
for (const [f, label] of [["pyproject.toml", "Python"], ["requirements.txt", "Python"],
|
|
216
|
+
["go.mod", "Go"], ["Cargo.toml", "Rust"], ["pom.xml", "Java/Maven"],
|
|
217
|
+
["Gemfile", "Ruby"], ["composer.json", "PHP"]])
|
|
218
|
+
if (existsSync(join(ROOT, f)) && !stacks.includes(label)) stacks.push(label);
|
|
219
|
+
|
|
220
|
+
// source scale — cheap walk, skips the usual noise
|
|
221
|
+
const SKIP = new Set(["node_modules", ".git", "dist", "build", "vendor", ".next", "target", "__pycache__", ".venv", "coverage"]);
|
|
222
|
+
const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|vue|svelte|kt|swift|cs)$/;
|
|
223
|
+
let srcFiles = 0, srcBytes = 0, deepest = 0;
|
|
224
|
+
(function walk(d, depth) {
|
|
225
|
+
if (depth > 8 || srcFiles > 20000) return;
|
|
226
|
+
for (const e of ls(d)) {
|
|
227
|
+
if (SKIP.has(e) || e.startsWith(".")) continue;
|
|
228
|
+
const fp = join(d, e);
|
|
229
|
+
let st; try { st = statSync(fp); } catch { continue; }
|
|
230
|
+
if (st.isDirectory()) { deepest = Math.max(deepest, depth + 1); walk(fp, depth + 1); }
|
|
231
|
+
else if (CODE.test(e)) { srcFiles++; srcBytes += st.size; }
|
|
232
|
+
}
|
|
233
|
+
})(ROOT, 0);
|
|
234
|
+
say(` stack${" ".repeat(28)}${stacks.join(", ") || "not detected"}`);
|
|
235
|
+
say(` source files${" ".repeat(21)}${srcFiles} (~${Math.round(srcBytes / 1024)} KB)`);
|
|
236
|
+
|
|
237
|
+
// LSP — the official plugins answer "find the definition" without reading files
|
|
238
|
+
const LSP = { TypeScript: "typescript-lsp", Python: "pyright-lsp", Go: "gopls-lsp", Rust: "rust-analyzer-lsp", "Java/Maven": "jdtls-lsp", Ruby: "ruby-lsp", PHP: "php-lsp" };
|
|
239
|
+
const wantLsp = stacks.map((s) => LSP[s]).filter(Boolean);
|
|
240
|
+
if (wantLsp.length && srcFiles > 50)
|
|
241
|
+
flag("INFO", `Install code intelligence: /plugin install ${wantLsp[0]}@claude-plugins-official — lets Claude jump to a definition instead of scanning files.`);
|
|
242
|
+
|
|
243
|
+
// generated/vendored code that is checked in costs reads
|
|
244
|
+
const genDirs = ["dist", "build", "vendor", "generated", "src/generated", ".next"].filter((d) => existsSync(join(ROOT, d)));
|
|
245
|
+
const gi = read(join(ROOT, ".gitignore")) || "";
|
|
246
|
+
const unignored = genDirs.filter((d) => !gi.split("\n").some((l) => l.trim().replace(/\/$/, "") === d));
|
|
247
|
+
if (unignored.length)
|
|
248
|
+
flag("INFO", `Checked-in generated/vendored dirs (${unignored.join(", ")}) — add Read deny rules so Claude never opens them.`);
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------- what to add
|
|
251
|
+
// Signals for each extension mechanism Claude Code offers. Only suggest one
|
|
252
|
+
// when the repo shows evidence it would help — an unused mechanism is cost.
|
|
253
|
+
say();
|
|
254
|
+
say("WHAT THIS PROJECT COULD ADD");
|
|
255
|
+
const rec = [];
|
|
256
|
+
|
|
257
|
+
// per-directory CLAUDE.md for monorepos
|
|
258
|
+
const pkgDirs = ["packages", "apps", "services", "libs"].filter((d) => existsSync(join(ROOT, d)));
|
|
259
|
+
if (pkgDirs.length) {
|
|
260
|
+
const subs = pkgDirs.flatMap((d) => ls(join(ROOT, d)).filter((s) => { try { return statSync(join(ROOT, d, s)).isDirectory(); } catch { return false; } }));
|
|
261
|
+
if (subs.length >= 3) rec.push(["CLAUDE.md (nested)", `${subs.length} packages under ${pkgDirs.join("/")} — a per-package CLAUDE.md loads only when Claude reads there`]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// hooks: a linter exists but nothing runs it
|
|
265
|
+
const lintCfg = ["eslint.config.js", ".eslintrc", ".eslintrc.json", "biome.json", "ruff.toml", ".golangci.yml"].find((f) => existsSync(join(ROOT, f)));
|
|
266
|
+
const hookEvents = projSettings && projSettings !== "INVALID" ? Object.keys(projSettings.hooks || {}) : [];
|
|
267
|
+
if (lintCfg && !hookEvents.includes("PostToolUse"))
|
|
268
|
+
rec.push(["hook: PostToolUse", `${lintCfg} exists but nothing lints after an edit — a PostToolUse hook feeds errors straight back`]);
|
|
269
|
+
if (ruleFiles.length && !hookEvents.includes("PreToolUse"))
|
|
270
|
+
rec.push(["hook: PreToolUse", "rules are written but nothing enforces them — a PreToolUse guard blocks the violation instead of describing it"]);
|
|
271
|
+
|
|
272
|
+
// MCP: dependencies that postdate model training
|
|
273
|
+
if (pkg && pkg !== "INVALID") {
|
|
274
|
+
const fast = ["react", "next", "tailwindcss", "react-router-dom", "vue", "svelte", "@angular/core"].filter(dep);
|
|
275
|
+
const servers = mcp && mcp !== "INVALID" ? Object.keys(mcp.mcpServers || {}) : [];
|
|
276
|
+
if (fast.length && !servers.includes("context7"))
|
|
277
|
+
rec.push(["MCP: context7", `${fast.slice(0, 3).join(", ")} move faster than model training — context7 serves current API docs`]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// skills: repeated procedures worth capturing
|
|
281
|
+
const skillDirs = ls(join(ROOT, ".claude/skills"));
|
|
282
|
+
if (!skillDirs.length && srcFiles > 100)
|
|
283
|
+
rec.push(["skills", "no project skills — a multi-step procedure you repeat (scaffolding a feature, a release) belongs in one, loaded on invoke not every turn"]);
|
|
284
|
+
|
|
285
|
+
// agents: only when there is a real repeated specialised review
|
|
286
|
+
const projAgentFiles = ls(join(ROOT, ".claude/agents"));
|
|
287
|
+
if (!projAgentFiles.length && ruleFiles.length >= 4)
|
|
288
|
+
rec.push(["agents (project)", `${ruleFiles.length} rule files — if one area needs auditing after every change, a project subagent enforces it`]);
|
|
289
|
+
|
|
290
|
+
// settings: worktree/read hygiene
|
|
291
|
+
if (srcFiles > 500 && !(projSettings && projSettings !== "INVALID" && projSettings.permissions?.deny?.length))
|
|
292
|
+
rec.push(["settings: Read deny", "large tree with no Read deny rules — block generated and vendored paths"]);
|
|
293
|
+
|
|
294
|
+
if (!rec.length) say(" nothing obvious — the mechanisms in use look proportionate");
|
|
295
|
+
for (const [what, why] of rec) say(` ${what.padEnd(24)} ${why}`);
|
|
296
|
+
if (rec.length) flag("INFO", `${rec.length} extension(s) this project could use — see the list above. Each one costs context, so add only what earns it.`);
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------- mode
|
|
299
|
+
const hasLayer = residentBytes > 0 || ruleFiles.length > 0;
|
|
300
|
+
const mapped = existsSync(join(ROOT, ".claude/agent-memory/agent-os-feature-cartographer"));
|
|
301
|
+
let MODE;
|
|
302
|
+
if (srcFiles < 5 && !hasLayer) MODE = "TOO-EARLY";
|
|
303
|
+
else if (!hasLayer) MODE = "ESTABLISH";
|
|
304
|
+
else if (anyLegacy || findings.some((f) => f.startsWith("WARN") || f.startsWith("FAIL"))) MODE = "MIGRATE";
|
|
305
|
+
else if (!mapped) MODE = "MAP";
|
|
306
|
+
else MODE = "MAINTAIN";
|
|
307
|
+
say();
|
|
308
|
+
say(`MODE ${MODE}`);
|
|
309
|
+
say({
|
|
310
|
+
"TOO-EARLY": " Barely any source yet. Do not build a context layer over nothing —\n write code first, then run Claude Code's own /init, then come back.",
|
|
311
|
+
ESTABLISH: " Real code, no context layer. Build one FROM THE CODE: read\n references/establishing.md. Do not invent conventions.",
|
|
312
|
+
MIGRATE: " A layer exists but has problems. Fix the findings below;\n read references/migrating.md for anything legacy.",
|
|
313
|
+
MAP: " Layer is healthy but the codebase has never been mapped. Build the\n architecture map: read references/establishing.md, 'Map the architecture'.",
|
|
314
|
+
MAINTAIN: " Layer is healthy and the codebase is mapped. Nothing to set up.",
|
|
315
|
+
}[MODE]);
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------- verdict
|
|
318
|
+
say();
|
|
319
|
+
say(`STARTUP COST ~${residentBytes} B ~= ${Math.round(residentBytes / 4)} tokens (always-loaded files only)`);
|
|
320
|
+
say();
|
|
321
|
+
if (!findings.length) say("VERDICT no findings — setup is clean.");
|
|
322
|
+
else {
|
|
323
|
+
say(`VERDICT ${findings.length} finding(s), worst first:`);
|
|
324
|
+
const order = { FAIL: 0, WARN: 1, INFO: 2 };
|
|
325
|
+
findings.sort((a, b) => order[a.split(" ")[0]] - order[b.split(" ")[0]]);
|
|
326
|
+
for (const f of findings) say(" " + f);
|
|
327
|
+
}
|
|
328
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: map
|
|
3
|
+
description: Build or refresh the architecture map so changes run off known structure instead of rediscovering the codebase. Use for "map this codebase", "build the architecture map", "how is this project structured", "map the login flow", "refresh the map", or before changing a feature you have not touched before.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Map the codebase
|
|
7
|
+
|
|
8
|
+
A map written once is an answer given free for the rest of the project's life.
|
|
9
|
+
This is what makes a request like *"change the login flow from email to OTP"*
|
|
10
|
+
start from known structure instead of a cold grep.
|
|
11
|
+
|
|
12
|
+
You do not do the exploring. `feature-cartographer` does, in its own context
|
|
13
|
+
window, and returns a summary. That is the whole point — the file reads never
|
|
14
|
+
enter this conversation.
|
|
15
|
+
|
|
16
|
+
## `$ARGUMENTS`
|
|
17
|
+
|
|
18
|
+
| Argument | Do |
|
|
19
|
+
|---|---|
|
|
20
|
+
| *(none)* or `architecture` | Build or refresh `_architecture.md`, the system-level map |
|
|
21
|
+
| `<feature or area>` | Map that feature — "map the login flow", "map checkout" |
|
|
22
|
+
| `refresh` | Re-map whatever `/agent-os:memory stale` flagged as drifted |
|
|
23
|
+
|
|
24
|
+
## The architecture map
|
|
25
|
+
|
|
26
|
+
Dispatch `feature-cartographer` with a system-level brief, not a feature
|
|
27
|
+
question:
|
|
28
|
+
|
|
29
|
+
> Map this codebase at the system level. Produce `_architecture.md`: stack and
|
|
30
|
+
> versions, the layers and what each owns, where a request enters and how it
|
|
31
|
+
> reaches data, state management, the network edge, the auth and permission
|
|
32
|
+
> model, build and run commands, and the three or four files a newcomer must read
|
|
33
|
+
> first. Name real files. State what you could not determine.
|
|
34
|
+
|
|
35
|
+
Everything in it must be observed in this repository. Versions come from a
|
|
36
|
+
manifest, not from what the model knows about the framework.
|
|
37
|
+
|
|
38
|
+
Build this once per project. Refresh it when the architecture actually changes —
|
|
39
|
+
a new service, a swapped data layer, a routing migration — not on a schedule.
|
|
40
|
+
|
|
41
|
+
## Mapping one feature
|
|
42
|
+
|
|
43
|
+
Dispatch the cartographer with the feature name. It reads `_architecture.md`
|
|
44
|
+
first, so it explores a fraction of what it would cold, and returns entry point,
|
|
45
|
+
the files that matter, state, network edge, what gates it, and the blast radius.
|
|
46
|
+
|
|
47
|
+
Then it writes the map to disk and indexes it, so the next question about that
|
|
48
|
+
area is near free.
|
|
49
|
+
|
|
50
|
+
## After the map exists
|
|
51
|
+
|
|
52
|
+
Point the user at the change workflow — the map only pays off if changes actually
|
|
53
|
+
use it: `references/changing-a-feature.md` in the `init` skill. In short:
|
|
54
|
+
cartographer answers *how is it built*, the path-scoped rules load themselves,
|
|
55
|
+
`builder` works from the map and the nearest precedent rather than the one-line
|
|
56
|
+
request, `reviewer` checks against written rules, and the cartographer updates
|
|
57
|
+
the map in the same turn.
|
|
58
|
+
|
|
59
|
+
That last step is the one people skip. A map that was right last week and is
|
|
60
|
+
wrong today is worse than no map, because it gets trusted.
|
|
61
|
+
|
|
62
|
+
## Verify before trusting it
|
|
63
|
+
|
|
64
|
+
Ask the cartographer something about the codebase you already know the answer to,
|
|
65
|
+
and check it. Do this the first time a map is built. A confidently wrong map is
|
|
66
|
+
the failure mode worth catching early, and this is the cheapest moment to catch
|
|
67
|
+
it.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory
|
|
3
|
+
description: Inspect, repair or edit what this project remembers — agent memory, Claude Code auto memory, and path-scoped rules. Use for "show my memories", "what does Claude remember about this project", "clean up agent memory", "forget X", "my agent memory is a mess", "which maps are stale", or when memory looks duplicated, orphaned or wrong.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project memory
|
|
7
|
+
|
|
8
|
+
Every durable store this project has, in one place: per-agent memory, Claude
|
|
9
|
+
Code's own auto memory, and `.claude/rules/`.
|
|
10
|
+
|
|
11
|
+
## Run the inspection first
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/memory/scripts/memory.mjs"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Add `--stale` to also compare each map's `mapped:` date against the last commit
|
|
18
|
+
that touched the file it describes — that is how you find maps the code has
|
|
19
|
+
moved past. Pass a path as the first argument for a project other than the
|
|
20
|
+
working directory. Read-only.
|
|
21
|
+
|
|
22
|
+
It prints every store, every topic file with line counts and map dates, and a
|
|
23
|
+
health list: topic files missing from their index, hyphen/underscore duplicates
|
|
24
|
+
of the same subject, oversized indexes, and unscoped rules.
|
|
25
|
+
|
|
26
|
+
Show the output before changing anything.
|
|
27
|
+
|
|
28
|
+
## `$ARGUMENTS`
|
|
29
|
+
|
|
30
|
+
| Argument | Do |
|
|
31
|
+
|---|---|
|
|
32
|
+
| *(none)* or `list` | Run the inspection, report, stop |
|
|
33
|
+
| `show <name>` | Read that topic file or index and summarise it |
|
|
34
|
+
| `clean` | Fix everything in the health list — see below |
|
|
35
|
+
| `forget <subject>` | Delete that topic file **and** its index line, and anything derived solely from it |
|
|
36
|
+
| `stale` | Run with `--stale` and report only what has drifted |
|
|
37
|
+
|
|
38
|
+
## `clean` — what to actually do
|
|
39
|
+
|
|
40
|
+
Work through the health list in this order. Show the user each change before
|
|
41
|
+
making it; this is their accumulated knowledge, not scratch.
|
|
42
|
+
|
|
43
|
+
1. **Merge duplicates.** Two files for one subject (`defect-patterns.md` and
|
|
44
|
+
`defect_patterns.md`) hold different content written in different sessions —
|
|
45
|
+
the agent thought it was updating one file. **Read both fully and merge**,
|
|
46
|
+
keeping every distinct fact. Do not pick one and delete the other. Keep the
|
|
47
|
+
kebab-case name.
|
|
48
|
+
2. **Index the orphans.** A topic file missing from `MEMORY.md` is invisible: the
|
|
49
|
+
agent will not find it next session, will write the same knowledge again under
|
|
50
|
+
a new name, and the copies will diverge. Add one line per file.
|
|
51
|
+
3. **Trim an oversized index.** Only the first 200 lines of `MEMORY.md` load at
|
|
52
|
+
startup. Everything past that is silently dropped. One line per topic file,
|
|
53
|
+
detail in the topic files.
|
|
54
|
+
4. **Scope unscoped rules.** A `.claude/rules/` file without `paths:` frontmatter
|
|
55
|
+
loads every session, exactly like `CLAUDE.md`. Either give it `paths:` or move
|
|
56
|
+
its content into `CLAUDE.md` where it belongs.
|
|
57
|
+
5. **Refresh stale maps.** For anything `--stale` flagged, dispatch
|
|
58
|
+
`feature-cartographer` to re-map that area. Do not edit a map by hand from
|
|
59
|
+
memory — that is how a map becomes confidently wrong.
|
|
60
|
+
|
|
61
|
+
## Where a fact belongs
|
|
62
|
+
|
|
63
|
+
If the user is adding something rather than repairing, route it:
|
|
64
|
+
|
|
65
|
+
| Kind of fact | Store |
|
|
66
|
+
|---|---|
|
|
67
|
+
| True in every session, every file | project `CLAUDE.md`, ≤200 lines |
|
|
68
|
+
| Durable but scoped to some files | `.claude/rules/<topic>.md` with `paths:` |
|
|
69
|
+
| How a feature is built | agent memory — let the cartographer write it, do not hand-author |
|
|
70
|
+
| A correction the user gave you, a preference, a decision | Claude Code auto memory — it writes this itself |
|
|
71
|
+
| Work in flight | the project's task vault, not memory |
|
|
72
|
+
| Derivable by reading the code | **nowhere** |
|
|
73
|
+
|
|
74
|
+
Never hand-write into an agent's memory directory. That directory is the agent's
|
|
75
|
+
working knowledge and it maintains its own index; writing into it from outside
|
|
76
|
+
produces exactly the orphan-and-duplicate mess this skill exists to clean up. If
|
|
77
|
+
the cartographer's map is wrong, re-dispatch the cartographer.
|
|
78
|
+
|
|
79
|
+
## What this will not do
|
|
80
|
+
|
|
81
|
+
It will not delete a store because it looks untidy. Duplicates get merged, not
|
|
82
|
+
dropped. `forget` removes only what the user named.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Inspect every memory store this project has, and check each one's health.
|
|
4
|
+
* Read-only. The deterministic half of `/agent-os:memory`.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node memory.mjs [projectDir] [--stale]
|
|
7
|
+
* --stale also compare each map's `mapped:` date against git's last commit
|
|
8
|
+
* touching the file it describes, so you can see what has drifted.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { join, basename } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const ROOT = process.argv.find((a, i) => i > 1 && !a.startsWith("--")) || process.cwd();
|
|
16
|
+
const STALE = process.argv.includes("--stale");
|
|
17
|
+
const HOME = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
18
|
+
const read = (p) => { try { return readFileSync(p, "utf8"); } catch { return null; } };
|
|
19
|
+
const ls = (p) => { try { return readdirSync(p); } catch { return []; } };
|
|
20
|
+
const git = (c) => { try { return execSync(c, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim(); } catch { return ""; } };
|
|
21
|
+
|
|
22
|
+
const out = [], issues = [];
|
|
23
|
+
const say = (s = "") => out.push(s);
|
|
24
|
+
|
|
25
|
+
say(`MEMORY STORES ${ROOT}\n`);
|
|
26
|
+
|
|
27
|
+
// ---- agent memory ----
|
|
28
|
+
for (const [scope, dir] of [["project", join(ROOT, ".claude/agent-memory")], ["local", join(ROOT, ".claude/agent-memory-local")]]) {
|
|
29
|
+
if (!existsSync(dir)) continue;
|
|
30
|
+
for (const agent of ls(dir).sort()) {
|
|
31
|
+
const adir = join(dir, agent);
|
|
32
|
+
const files = ls(adir).filter((f) => f.endsWith(".md"));
|
|
33
|
+
const topics = files.filter((f) => f !== "MEMORY.md");
|
|
34
|
+
const idxRaw = read(join(adir, "MEMORY.md"));
|
|
35
|
+
const idx = idxRaw ? idxRaw.split("\n").filter((l) => l.trim().startsWith("-")) : [];
|
|
36
|
+
|
|
37
|
+
say(`${agent} (${scope})`);
|
|
38
|
+
say(` index: ${idx.length} entr${idx.length === 1 ? "y" : "ies"} topics: ${topics.length} file(s)`);
|
|
39
|
+
|
|
40
|
+
// which topic files are not referenced anywhere in the index
|
|
41
|
+
const orphans = topics.filter((f) => !idxRaw || !idxRaw.includes(basename(f, ".md")));
|
|
42
|
+
// hyphen/underscore collisions — the same subject written twice
|
|
43
|
+
const norm = (f) => basename(f, ".md").replace(/[-_]/g, "").toLowerCase();
|
|
44
|
+
const seen = {};
|
|
45
|
+
const dupes = [];
|
|
46
|
+
for (const f of topics) { const k = norm(f); if (seen[k]) dupes.push([seen[k], f]); else seen[k] = f; }
|
|
47
|
+
|
|
48
|
+
for (const f of topics.sort()) {
|
|
49
|
+
const t = read(join(adir, f)) || "";
|
|
50
|
+
const mapped = (t.match(/^mapped:\s*(\S+)/m) || [])[1];
|
|
51
|
+
const entry = (t.match(/^entry:\s*(\S+)/m) || [])[1];
|
|
52
|
+
let note = "";
|
|
53
|
+
if (orphans.includes(f)) note += " NOT IN INDEX";
|
|
54
|
+
if (STALE && mapped && entry) {
|
|
55
|
+
const last = git(`git log -1 --format=%cs -- ${JSON.stringify(entry)}`);
|
|
56
|
+
if (last && last > mapped) note += ` STALE (mapped ${mapped}, code changed ${last})`;
|
|
57
|
+
}
|
|
58
|
+
say(` ${f.padEnd(34)} ${String(t.split("\n").length).padStart(4)} lines${mapped ? ` mapped ${mapped}` : ""}${note}`);
|
|
59
|
+
}
|
|
60
|
+
if (orphans.length) issues.push(`${agent}: ${orphans.length} topic file(s) not in MEMORY.md — invisible next session: ${orphans.join(", ")}`);
|
|
61
|
+
for (const [a, b] of dupes) issues.push(`${agent}: "${a}" and "${b}" are the same subject — merge them`);
|
|
62
|
+
if (idxRaw && idxRaw.split("\n").length > 200) issues.push(`${agent}: MEMORY.md over 200 lines — everything past that is dropped at startup`);
|
|
63
|
+
say();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!out.some((l) => l.includes("index:"))) say("no agent memory yet — agents have not run in this project\n");
|
|
67
|
+
|
|
68
|
+
// ---- Claude Code auto memory ----
|
|
69
|
+
const repo = git("git rev-parse --show-toplevel") || ROOT;
|
|
70
|
+
const slug = repo.replace(/\//g, "-");
|
|
71
|
+
const autoDir = join(HOME, "projects", slug, "memory");
|
|
72
|
+
say("auto memory (Claude Code's own)");
|
|
73
|
+
if (!existsSync(autoDir)) say(` none yet at ${autoDir}`);
|
|
74
|
+
else {
|
|
75
|
+
const idx = read(join(autoDir, "MEMORY.md"));
|
|
76
|
+
const topics = ls(autoDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
|
|
77
|
+
say(` ${autoDir}`);
|
|
78
|
+
say(` MEMORY.md: ${idx ? idx.split("\n").length + " lines" : "absent"} topics: ${topics.length}`);
|
|
79
|
+
for (const f of topics.sort()) say(` ${f}`);
|
|
80
|
+
if (idx && idx.split("\n").length > 200) issues.push("auto memory MEMORY.md over 200 lines — content past that is dropped at startup");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- rules, the other durable store ----
|
|
84
|
+
const rules = ls(join(ROOT, ".claude/rules")).filter((f) => f.endsWith(".md"));
|
|
85
|
+
say();
|
|
86
|
+
say(`project rules .claude/rules/ — ${rules.length} file(s)`);
|
|
87
|
+
for (const f of rules.sort()) {
|
|
88
|
+
const t = read(join(ROOT, ".claude/rules", f)) || "";
|
|
89
|
+
const scoped = /^paths:/m.test(t.split("---")[1] || "");
|
|
90
|
+
say(` ${scoped ? "scoped " : "UNSCOPED"} ${f}`);
|
|
91
|
+
if (!scoped) issues.push(`.claude/rules/${f} has no paths: — it loads every session`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
say();
|
|
95
|
+
if (!issues.length) say("HEALTH no issues.");
|
|
96
|
+
else { say(`HEALTH ${issues.length} issue(s):`); for (const i of issues) say(" - " + i); }
|
|
97
|
+
process.stdout.write(out.join("\n") + "\n");
|