rebar-mcp 2.0.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.
Files changed (248) hide show
  1. package/.claude/agents/template-writer.md +43 -0
  2. package/.claude/agents/test-runner.md +47 -0
  3. package/.claude/mcp.json +9 -0
  4. package/.claude/settings.json +29 -0
  5. package/.claude/skills/ /SKILL.md +21 -0
  6. package/.claude/skills/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/SKILL.md +21 -0
  7. package/.claude/skills/bmmibwetxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  8. package/.claude/skills/bmmibwjgvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  9. package/.claude/skills/bmmibwsesxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  10. package/.claude/skills/bmmibwxufxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  11. package/.claude/skills/bmmibx3r9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  12. package/.claude/skills/bmmji0lrkxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  13. package/.claude/skills/bmmjiniphxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  14. package/.claude/skills/bmmjio86zxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  15. package/.claude/skills/bmmjiolfbxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  16. package/.claude/skills/bmmjit1lvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  17. package/.claude/skills/bmmjita1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
  18. package/.claude/skills/bnd-mmibweu3/SKILL.md +21 -0
  19. package/.claude/skills/bnd-mmibwjh4/SKILL.md +21 -0
  20. package/.claude/skills/bnd-mmibwsey/SKILL.md +21 -0
  21. package/.claude/skills/bnd-mmibwxup/SKILL.md +21 -0
  22. package/.claude/skills/bnd-mmibx3rg/SKILL.md +21 -0
  23. package/.claude/skills/bnd-mmji0lrp/SKILL.md +21 -0
  24. package/.claude/skills/bnd-mmjinipm/SKILL.md +21 -0
  25. package/.claude/skills/bnd-mmjio875/SKILL.md +21 -0
  26. package/.claude/skills/bnd-mmjiolfg/SKILL.md +21 -0
  27. package/.claude/skills/bnd-mmjit1m3/SKILL.md +21 -0
  28. package/.claude/skills/bnd-mmjita1x/SKILL.md +21 -0
  29. package/.claude/skills/coercion-test/SKILL.md +50 -0
  30. package/.claude/skills/large-skill/SKILL.md +21 -0
  31. package/.claude/skills/long-desc-skill/SKILL.md +21 -0
  32. package/.claude/skills/mcp-dev/SKILL.md +61 -0
  33. package/.claude/skills/nl-mmibweus/SKILL.md +25 -0
  34. package/.claude/skills/nl-mmibwjhf/SKILL.md +25 -0
  35. package/.claude/skills/nl-mmibwsf7/SKILL.md +25 -0
  36. package/.claude/skills/nl-mmibwxvq/SKILL.md +25 -0
  37. package/.claude/skills/nl-mmibx3rt/SKILL.md +25 -0
  38. package/.claude/skills/nl-mmji0lrz/SKILL.md +25 -0
  39. package/.claude/skills/nl-mmjinipx/SKILL.md +25 -0
  40. package/.claude/skills/nl-mmjio87f/SKILL.md +25 -0
  41. package/.claude/skills/nl-mmjiolfs/SKILL.md +25 -0
  42. package/.claude/skills/nl-mmjit1mc/SKILL.md +25 -0
  43. package/.claude/skills/nl-mmjita26/SKILL.md +25 -0
  44. package/.claude/skills/rapid-1/SKILL.md +21 -0
  45. package/.claude/skills/rapid-2/SKILL.md +21 -0
  46. package/.claude/skills/rapid-3/SKILL.md +21 -0
  47. package/.claude/skills/rapid-4/SKILL.md +21 -0
  48. package/.claude/skills/rapid-5/SKILL.md +21 -0
  49. package/.claude/skills/test/", /"malicious/": /"true/SKILL.md" +69 -0
  50. package/.claude/skills/test-emoji-/360/237/230/200-skill/SKILL.md +69 -0
  51. package/.claude/skills/test-skill/SKILL.md +69 -0
  52. package/.claude/skills/test; rm -rf /; skill/SKILL.md +69 -0
  53. package/.claude/skills/test<script>alert(1)</script>skill/SKILL.md +69 -0
  54. package/.claudeignore +5 -0
  55. package/.mcp.json +3 -0
  56. package/CHANGELOG.md +29 -0
  57. package/CLAUDE.md +76 -0
  58. package/LICENSE +21 -0
  59. package/README.md +149 -0
  60. package/ROADMAP.md +526 -0
  61. package/ccboot-PRD-v1.0.docx.md +732 -0
  62. package/ccboot-v1.2.0-enforcement-spec.md +1272 -0
  63. package/dist/cli.d.ts +3 -0
  64. package/dist/cli.d.ts.map +1 -0
  65. package/dist/cli.js +674 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/constants.d.ts +25 -0
  68. package/dist/constants.d.ts.map +1 -0
  69. package/dist/constants.js +118 -0
  70. package/dist/constants.js.map +1 -0
  71. package/dist/index.d.ts +3 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +47 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/schemas/common.d.ts +62 -0
  76. package/dist/schemas/common.d.ts.map +1 -0
  77. package/dist/schemas/common.js +15 -0
  78. package/dist/schemas/common.js.map +1 -0
  79. package/dist/schemas/scaffolding.d.ts +277 -0
  80. package/dist/schemas/scaffolding.d.ts.map +1 -0
  81. package/dist/schemas/scaffolding.js +133 -0
  82. package/dist/schemas/scaffolding.js.map +1 -0
  83. package/dist/services/claudemd-generator.d.ts +16 -0
  84. package/dist/services/claudemd-generator.d.ts.map +1 -0
  85. package/dist/services/claudemd-generator.js +426 -0
  86. package/dist/services/claudemd-generator.js.map +1 -0
  87. package/dist/services/codex-generator.d.ts +6 -0
  88. package/dist/services/codex-generator.d.ts.map +1 -0
  89. package/dist/services/codex-generator.js +35 -0
  90. package/dist/services/codex-generator.js.map +1 -0
  91. package/dist/services/cursor-generator.d.ts +15 -0
  92. package/dist/services/cursor-generator.d.ts.map +1 -0
  93. package/dist/services/cursor-generator.js +134 -0
  94. package/dist/services/cursor-generator.js.map +1 -0
  95. package/dist/services/file-ops.d.ts +48 -0
  96. package/dist/services/file-ops.d.ts.map +1 -0
  97. package/dist/services/file-ops.js +153 -0
  98. package/dist/services/file-ops.js.map +1 -0
  99. package/dist/services/output-formatter.d.ts +57 -0
  100. package/dist/services/output-formatter.d.ts.map +1 -0
  101. package/dist/services/output-formatter.js +88 -0
  102. package/dist/services/output-formatter.js.map +1 -0
  103. package/dist/services/platform-detect.d.ts +14 -0
  104. package/dist/services/platform-detect.d.ts.map +1 -0
  105. package/dist/services/platform-detect.js +63 -0
  106. package/dist/services/platform-detect.js.map +1 -0
  107. package/dist/services/project-analyzer.d.ts +71 -0
  108. package/dist/services/project-analyzer.d.ts.map +1 -0
  109. package/dist/services/project-analyzer.js +595 -0
  110. package/dist/services/project-analyzer.js.map +1 -0
  111. package/dist/services/rules-engine.d.ts +41 -0
  112. package/dist/services/rules-engine.d.ts.map +1 -0
  113. package/dist/services/rules-engine.js +304 -0
  114. package/dist/services/rules-engine.js.map +1 -0
  115. package/dist/services/strictness.d.ts +37 -0
  116. package/dist/services/strictness.d.ts.map +1 -0
  117. package/dist/services/strictness.js +182 -0
  118. package/dist/services/strictness.js.map +1 -0
  119. package/dist/services/template-engine.d.ts +16 -0
  120. package/dist/services/template-engine.d.ts.map +1 -0
  121. package/dist/services/template-engine.js +85 -0
  122. package/dist/services/template-engine.js.map +1 -0
  123. package/dist/services/validation.d.ts +41 -0
  124. package/dist/services/validation.d.ts.map +1 -0
  125. package/dist/services/validation.js +104 -0
  126. package/dist/services/validation.js.map +1 -0
  127. package/dist/services/windsurf-generator.d.ts +15 -0
  128. package/dist/services/windsurf-generator.d.ts.map +1 -0
  129. package/dist/services/windsurf-generator.js +127 -0
  130. package/dist/services/windsurf-generator.js.map +1 -0
  131. package/dist/tests/enforcement.test.d.ts +2 -0
  132. package/dist/tests/enforcement.test.d.ts.map +1 -0
  133. package/dist/tests/enforcement.test.js +541 -0
  134. package/dist/tests/enforcement.test.js.map +1 -0
  135. package/dist/tests/enterprise.test.d.ts +2 -0
  136. package/dist/tests/enterprise.test.d.ts.map +1 -0
  137. package/dist/tests/enterprise.test.js +353 -0
  138. package/dist/tests/enterprise.test.js.map +1 -0
  139. package/dist/tests/fuzzing.test.d.ts +2 -0
  140. package/dist/tests/fuzzing.test.d.ts.map +1 -0
  141. package/dist/tests/fuzzing.test.js +596 -0
  142. package/dist/tests/fuzzing.test.js.map +1 -0
  143. package/dist/tests/knowledge.test.d.ts +2 -0
  144. package/dist/tests/knowledge.test.d.ts.map +1 -0
  145. package/dist/tests/knowledge.test.js +292 -0
  146. package/dist/tests/knowledge.test.js.map +1 -0
  147. package/dist/tests/management.test.d.ts +2 -0
  148. package/dist/tests/management.test.d.ts.map +1 -0
  149. package/dist/tests/management.test.js +338 -0
  150. package/dist/tests/management.test.js.map +1 -0
  151. package/dist/tests/scaffolding.test.d.ts +2 -0
  152. package/dist/tests/scaffolding.test.d.ts.map +1 -0
  153. package/dist/tests/scaffolding.test.js +419 -0
  154. package/dist/tests/scaffolding.test.js.map +1 -0
  155. package/dist/tests/test-utils.d.ts +76 -0
  156. package/dist/tests/test-utils.d.ts.map +1 -0
  157. package/dist/tests/test-utils.js +171 -0
  158. package/dist/tests/test-utils.js.map +1 -0
  159. package/dist/tests/tool-harness.d.ts +18 -0
  160. package/dist/tests/tool-harness.d.ts.map +1 -0
  161. package/dist/tests/tool-harness.js +51 -0
  162. package/dist/tests/tool-harness.js.map +1 -0
  163. package/dist/tools/enterprise.d.ts +8 -0
  164. package/dist/tools/enterprise.d.ts.map +1 -0
  165. package/dist/tools/enterprise.js +571 -0
  166. package/dist/tools/enterprise.js.map +1 -0
  167. package/dist/tools/knowledge.d.ts +7 -0
  168. package/dist/tools/knowledge.d.ts.map +1 -0
  169. package/dist/tools/knowledge.js +120 -0
  170. package/dist/tools/knowledge.js.map +1 -0
  171. package/dist/tools/management.d.ts +10 -0
  172. package/dist/tools/management.d.ts.map +1 -0
  173. package/dist/tools/management.js +1541 -0
  174. package/dist/tools/management.js.map +1 -0
  175. package/dist/tools/scaffolding.d.ts +8 -0
  176. package/dist/tools/scaffolding.d.ts.map +1 -0
  177. package/dist/tools/scaffolding.js +736 -0
  178. package/dist/tools/scaffolding.js.map +1 -0
  179. package/dist/types.d.ts +54 -0
  180. package/dist/types.d.ts.map +1 -0
  181. package/dist/types.js +5 -0
  182. package/dist/types.js.map +1 -0
  183. package/landing/app/layout.tsx +30 -0
  184. package/landing/app/page.tsx +944 -0
  185. package/landing/next-env.d.ts +6 -0
  186. package/landing/next.config.js +6 -0
  187. package/landing/package-lock.json +896 -0
  188. package/landing/package.json +20 -0
  189. package/landing/tsconfig.json +40 -0
  190. package/package.json +49 -0
  191. package/rebar-v2.0.0-platform-spec.md +1567 -0
  192. package/server.json +20 -0
  193. package/src/cli.ts +735 -0
  194. package/src/constants.ts +131 -0
  195. package/src/index.ts +54 -0
  196. package/src/schemas/common.ts +22 -0
  197. package/src/schemas/scaffolding.ts +161 -0
  198. package/src/services/claudemd-generator.ts +481 -0
  199. package/src/services/codex-generator.ts +44 -0
  200. package/src/services/cursor-generator.ts +153 -0
  201. package/src/services/file-ops.ts +172 -0
  202. package/src/services/platform-detect.ts +80 -0
  203. package/src/services/project-analyzer.ts +690 -0
  204. package/src/services/rules-engine.ts +353 -0
  205. package/src/services/strictness.ts +202 -0
  206. package/src/services/template-engine.ts +119 -0
  207. package/src/services/validation.ts +138 -0
  208. package/src/services/windsurf-generator.ts +145 -0
  209. package/src/tests/enforcement.test.ts +794 -0
  210. package/src/tests/enterprise.test.ts +483 -0
  211. package/src/tests/fuzzing.test.ts +690 -0
  212. package/src/tests/knowledge.test.ts +371 -0
  213. package/src/tests/management.test.ts +451 -0
  214. package/src/tests/scaffolding.test.ts +575 -0
  215. package/src/tests/test-utils.ts +206 -0
  216. package/src/tests/tool-harness.ts +70 -0
  217. package/src/tools/enterprise.ts +666 -0
  218. package/src/tools/knowledge.ts +162 -0
  219. package/src/tools/management.ts +1706 -0
  220. package/src/tools/scaffolding.ts +909 -0
  221. package/src/types.ts +93 -0
  222. package/supabase/.temp/cli-latest +1 -0
  223. package/supabase/.temp/gotrue-version +1 -0
  224. package/supabase/.temp/pooler-url +1 -0
  225. package/supabase/.temp/postgres-version +1 -0
  226. package/supabase/.temp/project-ref +1 -0
  227. package/supabase/.temp/rest-version +1 -0
  228. package/supabase/.temp/storage-migration +1 -0
  229. package/supabase/.temp/storage-version +1 -0
  230. package/templates/agents/explore.md +41 -0
  231. package/templates/agents/plan.md +73 -0
  232. package/templates/agents/security-auditor.md +77 -0
  233. package/templates/agents/test-runner.md +60 -0
  234. package/templates/claudemd/fastapi.md +49 -0
  235. package/templates/claudemd/monorepo.md +48 -0
  236. package/templates/claudemd/nextjs.md +52 -0
  237. package/templates/claudemd/react-spa.md +50 -0
  238. package/templates/claudemd/springboot.md +50 -0
  239. package/templates/hooks/danger-blocker.json +11 -0
  240. package/templates/hooks/format-on-write.json +17 -0
  241. package/templates/hooks/lint-on-write.json +16 -0
  242. package/templates/hooks/secret-detector.json +11 -0
  243. package/templates/skills/code-review.md +68 -0
  244. package/templates/skills/documentation.md +62 -0
  245. package/templates/skills/performance-audit.md +80 -0
  246. package/templates/skills/security-scan.md +66 -0
  247. package/templates/skills/test-writer.md +56 -0
  248. package/tsconfig.json +19 -0
@@ -0,0 +1,1541 @@
1
+ import * as path from "node:path";
2
+ import { ListArtifactsInputSchema, ValidateConfigInputSchema, AuditContextInputSchema, DoctorInputSchema, MetricsInputSchema, BadgeInputSchema, CustomRulesInputSchema, } from "../schemas/scaffolding.js";
3
+ import { loadCustomRules, evaluateAllRules, generateDefaultRulesTemplate, } from "../services/rules-engine.js";
4
+ import { detectCurrentProfile } from "../services/strictness.js";
5
+ import { resolveProjectPath, sanitizePath, atomicWrite, readFileSafe, listFiles, listFilesRecursive, fileExists, } from "../services/file-ops.js";
6
+ import { validateSkillFrontmatter, validateJSON, validateClaudeMD, estimateTokens, } from "../services/validation.js";
7
+ import { CONTEXT_WINDOW_TOKENS, SKILL_BUDGET_PERCENT, CLAUDEMD_MAX_LINES } from "../constants.js";
8
+ function makeResult(text, isError = false) {
9
+ return {
10
+ content: [{ type: "text", text }],
11
+ ...(isError ? { isError: true } : {}),
12
+ };
13
+ }
14
+ export function registerManagementTools(server) {
15
+ // ─── rebar_list_artifacts ────────────────────────────────────────
16
+ server.registerTool("rebar_list_artifacts", {
17
+ title: "List Artifacts",
18
+ description: "[Rebar] Lists all Claude Code configuration artifacts in the project with type, location, " +
19
+ "validation status, and issue details. Supports filtering by artifact type.\n\n" +
20
+ "Artifact types discovered:\n" +
21
+ "• claudemd: CLAUDE.md files (root and subdirectory)\n" +
22
+ "• skill: Skills in .claude/skills/\n" +
23
+ "• agent: Subagents in .claude/agents/\n" +
24
+ "• hook: Hooks in .claude/settings.json\n" +
25
+ "• knowledge: Docs in .claude/docs/\n" +
26
+ "• mcp-config: .mcp.json configuration\n" +
27
+ "• settings: .claude/settings.json\n\n" +
28
+ "Examples:\n" +
29
+ " rebar_list_artifacts({ project_path: '.' })\n" +
30
+ " rebar_list_artifacts({ project_path: '.', type_filter: 'skill' })\n" +
31
+ " rebar_list_artifacts({ project_path: '.', type_filter: 'hook' })\n\n" +
32
+ "Returns: Formatted list with validation status per artifact.\n" +
33
+ "Error: If project_path doesn't exist.",
34
+ inputSchema: ListArtifactsInputSchema,
35
+ annotations: {
36
+ readOnlyHint: true,
37
+ destructiveHint: false,
38
+ idempotentHint: true,
39
+ openWorldHint: false,
40
+ },
41
+ }, async (args) => {
42
+ const { project_path, type_filter } = args;
43
+ const absPath = resolveProjectPath(project_path);
44
+ const artifacts = [];
45
+ try {
46
+ // ── CLAUDE.md ───────────────────────────────────────────
47
+ if (type_filter === "all" || type_filter === "claudemd") {
48
+ // Check root CLAUDE.md
49
+ const claudemdPath = sanitizePath(absPath, "CLAUDE.md");
50
+ if (await fileExists(claudemdPath)) {
51
+ const content = await readFileSafe(claudemdPath);
52
+ if (content) {
53
+ const validation = validateClaudeMD(content, claudemdPath);
54
+ artifacts.push({
55
+ type: "claudemd",
56
+ name: "CLAUDE.md (root)",
57
+ path: claudemdPath,
58
+ valid: validation.valid,
59
+ issues: [...validation.errors, ...validation.warnings],
60
+ });
61
+ }
62
+ }
63
+ // Check for subdirectory CLAUDE.md files
64
+ for (const subdir of ["apps", "packages", "services", "libs"]) {
65
+ const subdirPath = path.join(absPath, subdir);
66
+ if (await fileExists(subdirPath)) {
67
+ const subClaudeMds = await listFilesRecursive(subdirPath, "CLAUDE.md");
68
+ // listFilesRecursive matches by extension, but CLAUDE.md has .md extension
69
+ // Filter to only actual CLAUDE.md files
70
+ for (const md of subClaudeMds) {
71
+ if (path.basename(md) === "CLAUDE.md") {
72
+ const content = await readFileSafe(md);
73
+ const relPath = path.relative(absPath, md);
74
+ if (content) {
75
+ const validation = validateClaudeMD(content, md);
76
+ artifacts.push({
77
+ type: "claudemd",
78
+ name: `CLAUDE.md (${relPath})`,
79
+ path: md,
80
+ valid: validation.valid,
81
+ issues: [...validation.errors, ...validation.warnings],
82
+ });
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ // ── Skills ──────────────────────────────────────────────
90
+ if (type_filter === "all" || type_filter === "skill") {
91
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
92
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
93
+ for (const sf of skillFiles) {
94
+ const content = await readFileSafe(sf);
95
+ if (content) {
96
+ const validation = validateSkillFrontmatter(content);
97
+ const skillName = path.basename(path.dirname(sf));
98
+ const tokens = estimateTokens(content);
99
+ artifacts.push({
100
+ type: "skill",
101
+ name: `${skillName} (~${tokens} tokens)`,
102
+ path: sf,
103
+ valid: validation.valid,
104
+ issues: validation.errors,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ // ── Agents ──────────────────────────────────────────────
110
+ if (type_filter === "all" || type_filter === "agent") {
111
+ const agentsDir = sanitizePath(absPath, ".claude/agents");
112
+ const agentFiles = await listFiles(agentsDir, ".md");
113
+ for (const af of agentFiles) {
114
+ const content = await readFileSafe(af);
115
+ const tokens = content ? estimateTokens(content) : 0;
116
+ artifacts.push({
117
+ type: "agent",
118
+ name: `${path.basename(af, ".md")} (~${tokens} tokens)`,
119
+ path: af,
120
+ valid: true,
121
+ issues: [],
122
+ });
123
+ }
124
+ }
125
+ // ── Hooks ───────────────────────────────────────────────
126
+ if (type_filter === "all" || type_filter === "hook") {
127
+ const settingsPath = sanitizePath(absPath, ".claude/settings.json");
128
+ const settingsContent = await readFileSafe(settingsPath);
129
+ if (settingsContent) {
130
+ const validation = validateJSON(settingsContent, "settings.json");
131
+ if (validation.valid && validation.parsed && typeof validation.parsed === "object") {
132
+ const settings = validation.parsed;
133
+ if (settings.hooks && typeof settings.hooks === "object") {
134
+ const hooks = settings.hooks;
135
+ for (const [event, hookList] of Object.entries(hooks)) {
136
+ if (Array.isArray(hookList)) {
137
+ for (const hook of hookList) {
138
+ const h = hook;
139
+ artifacts.push({
140
+ type: "hook",
141
+ name: `[${event}] ${String(h.description || h.command || "unnamed")}`,
142
+ path: settingsPath,
143
+ valid: true,
144
+ issues: [],
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ // ── Knowledge ───────────────────────────────────────────
154
+ if (type_filter === "all" || type_filter === "knowledge") {
155
+ const docsDir = sanitizePath(absPath, ".claude/docs");
156
+ const docFiles = await listFilesRecursive(docsDir, ".md");
157
+ for (const df of docFiles) {
158
+ const relPath = path.relative(absPath, df);
159
+ const tokens = (await readFileSafe(df))?.length ?? 0;
160
+ artifacts.push({
161
+ type: "knowledge",
162
+ name: `${path.basename(df, ".md")} (~${estimateTokens(String(tokens))} tokens)`,
163
+ path: df,
164
+ valid: true,
165
+ issues: [],
166
+ });
167
+ }
168
+ }
169
+ // ── MCP Config ──────────────────────────────────────────
170
+ if (type_filter === "all" || type_filter === "mcp-config") {
171
+ const mcpPath = sanitizePath(absPath, ".mcp.json");
172
+ if (await fileExists(mcpPath)) {
173
+ const content = await readFileSafe(mcpPath);
174
+ if (content) {
175
+ const validation = validateJSON(content, ".mcp.json");
176
+ let serverCount = 0;
177
+ if (validation.valid && validation.parsed && typeof validation.parsed === "object") {
178
+ const cfg = validation.parsed;
179
+ if (cfg.mcpServers && typeof cfg.mcpServers === "object") {
180
+ serverCount = Object.keys(cfg.mcpServers).length;
181
+ }
182
+ }
183
+ artifacts.push({
184
+ type: "mcp-config",
185
+ name: `.mcp.json (${serverCount} server${serverCount !== 1 ? "s" : ""})`,
186
+ path: mcpPath,
187
+ valid: validation.valid,
188
+ issues: validation.errors,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ // ── Settings ────────────────────────────────────────────
194
+ if (type_filter === "all" || type_filter === "settings") {
195
+ const settingsPath = sanitizePath(absPath, ".claude/settings.json");
196
+ if (await fileExists(settingsPath)) {
197
+ const content = await readFileSafe(settingsPath);
198
+ if (content) {
199
+ const validation = validateJSON(content, "settings.json");
200
+ artifacts.push({
201
+ type: "settings",
202
+ name: ".claude/settings.json",
203
+ path: settingsPath,
204
+ valid: validation.valid,
205
+ issues: validation.errors,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ if (artifacts.length === 0) {
211
+ return makeResult(`No Claude Code artifacts found in ${absPath}\n\n` +
212
+ `This project hasn't been configured for Claude Code yet.\n` +
213
+ `Run rebar_init_project to set up a complete configuration.\n\n` +
214
+ `Or create individual artifacts:\n` +
215
+ `• rebar_generate_claudemd — Create a CLAUDE.md\n` +
216
+ `• rebar_create_skill — Add a skill\n` +
217
+ `• rebar_create_hook — Add a lifecycle hook`);
218
+ }
219
+ // Group by type for better readability
220
+ const byType = new Map();
221
+ for (const a of artifacts) {
222
+ const list = byType.get(a.type) || [];
223
+ list.push(a);
224
+ byType.set(a.type, list);
225
+ }
226
+ const validCount = artifacts.filter((a) => a.valid).length;
227
+ const invalidCount = artifacts.length - validCount;
228
+ let output = `Found ${artifacts.length} artifact${artifacts.length !== 1 ? "s" : ""} in ${absPath}`;
229
+ if (invalidCount > 0) {
230
+ output += ` (${invalidCount} with issues)`;
231
+ }
232
+ output += "\n\n";
233
+ for (const [type, items] of byType) {
234
+ output += `── ${type.toUpperCase()} (${items.length}) ──\n`;
235
+ for (const item of items) {
236
+ const status = item.valid ? "OK" : "!!";
237
+ output += ` [${status}] ${item.name}\n`;
238
+ output += ` ${item.path}\n`;
239
+ if (item.issues.length > 0) {
240
+ for (const issue of item.issues) {
241
+ output += ` ⚠ ${issue}\n`;
242
+ }
243
+ }
244
+ }
245
+ output += "\n";
246
+ }
247
+ return makeResult(output.trimEnd());
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ return makeResult(`Failed to list artifacts: ${message}`, true);
252
+ }
253
+ });
254
+ // ─── rebar_validate_config ───────────────────────────────────────
255
+ server.registerTool("rebar_validate_config", {
256
+ title: "Validate Configuration",
257
+ description: "[Rebar] Deep validation of all Claude Code configuration files. Catches real problems " +
258
+ "before they cause confusing behavior in Claude Code sessions.\n\n" +
259
+ "Checks performed:\n" +
260
+ "• CLAUDE.md: Exists, not empty, starts with heading, under 500 lines\n" +
261
+ "• Skills: Valid YAML frontmatter, required 'name' and 'description' fields, " +
262
+ "name under 64 chars, description under 200 chars\n" +
263
+ "• Settings: Valid JSON, proper hook structure\n" +
264
+ "• .mcp.json: Valid JSON structure\n" +
265
+ "• Agents: Files exist and are readable\n\n" +
266
+ "auto_fix mode can repair:\n" +
267
+ "• Trailing commas in JSON\n" +
268
+ "• Missing frontmatter fields (adds placeholders)\n\n" +
269
+ "Examples:\n" +
270
+ " rebar_validate_config({ project_path: '.', fix_mode: 'report' })\n" +
271
+ " rebar_validate_config({ project_path: '.', fix_mode: 'auto_fix' })\n\n" +
272
+ "Returns: Validation report with errors, warnings, and applied fixes.",
273
+ inputSchema: ValidateConfigInputSchema,
274
+ annotations: {
275
+ readOnlyHint: false, // can write in auto_fix mode
276
+ destructiveHint: false,
277
+ idempotentHint: true,
278
+ openWorldHint: false,
279
+ },
280
+ }, async (args) => {
281
+ const { project_path, fix_mode } = args;
282
+ const absPath = resolveProjectPath(project_path);
283
+ const errors = [];
284
+ const warnings = [];
285
+ const fixes = [];
286
+ try {
287
+ // ── CLAUDE.md validation ────────────────────────────────
288
+ const claudemdPath = sanitizePath(absPath, "CLAUDE.md");
289
+ const claudemd = await readFileSafe(claudemdPath);
290
+ if (claudemd) {
291
+ const result = validateClaudeMD(claudemd, claudemdPath);
292
+ errors.push(...result.errors);
293
+ warnings.push(...result.warnings);
294
+ // Additional deep checks
295
+ const lines = claudemd.split("\n");
296
+ if (lines.length > CLAUDEMD_MAX_LINES) {
297
+ warnings.push(`CLAUDE.md has ${lines.length} lines (recommended max: ${CLAUDEMD_MAX_LINES}). ` +
298
+ `Consider splitting into subdirectory CLAUDE.md files.`);
299
+ }
300
+ // Check for common issues
301
+ if (claudemd.includes("TODO") || claudemd.includes("FIXME")) {
302
+ warnings.push("CLAUDE.md contains TODO/FIXME markers — consider resolving them");
303
+ }
304
+ if (!claudemd.includes("## Build") && !claudemd.includes("## Test")) {
305
+ warnings.push("CLAUDE.md is missing Build & Test section — Claude won't know how to build your project");
306
+ }
307
+ }
308
+ else {
309
+ errors.push("No CLAUDE.md found — Claude Code works significantly better with one");
310
+ }
311
+ // ── Skill validation ────────────────────────────────────
312
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
313
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
314
+ for (const sf of skillFiles) {
315
+ const content = await readFileSafe(sf);
316
+ if (!content)
317
+ continue;
318
+ const result = validateSkillFrontmatter(content);
319
+ if (!result.valid) {
320
+ for (const err of result.errors) {
321
+ errors.push(`${path.relative(absPath, sf)}: ${err}`);
322
+ }
323
+ // Auto-fix: add missing frontmatter
324
+ if (fix_mode === "auto_fix") {
325
+ const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
326
+ if (!fm) {
327
+ // No frontmatter at all — add it
328
+ const skillName = path.basename(path.dirname(sf));
329
+ const fixed = `---\nname: ${skillName}\ndescription: TODO - add description\ninvocation: user\ncontext: inline\n---\n\n${content}`;
330
+ await atomicWrite(sf, fixed);
331
+ fixes.push(`Added missing frontmatter to ${path.relative(absPath, sf)}`);
332
+ }
333
+ }
334
+ }
335
+ // Check token budget
336
+ const tokens = estimateTokens(content);
337
+ if (tokens > 1000) {
338
+ warnings.push(`${path.relative(absPath, sf)}: Skill is large (~${tokens} tokens). ` +
339
+ `Skills should be concise to minimize context window usage.`);
340
+ }
341
+ }
342
+ // ── Settings validation ─────────────────────────────────
343
+ const settingsPath = sanitizePath(absPath, ".claude/settings.json");
344
+ const settingsContent = await readFileSafe(settingsPath);
345
+ if (settingsContent) {
346
+ const result = validateJSON(settingsContent, ".claude/settings.json");
347
+ if (!result.valid) {
348
+ errors.push(...result.errors);
349
+ if (fix_mode === "auto_fix") {
350
+ try {
351
+ const cleaned = settingsContent
352
+ .replace(/,\s*}/g, "}")
353
+ .replace(/,\s*]/g, "]");
354
+ JSON.parse(cleaned);
355
+ await atomicWrite(settingsPath, cleaned);
356
+ fixes.push("Fixed JSON syntax in .claude/settings.json");
357
+ }
358
+ catch {
359
+ // Can't auto-fix this
360
+ }
361
+ }
362
+ }
363
+ else if (result.parsed) {
364
+ // Validate hook structure
365
+ const settings = result.parsed;
366
+ if (settings.hooks && typeof settings.hooks === "object") {
367
+ const hooks = settings.hooks;
368
+ for (const [event, hookList] of Object.entries(hooks)) {
369
+ if (!Array.isArray(hookList)) {
370
+ errors.push(`Hook event "${event}" should be an array, got ${typeof hookList}`);
371
+ }
372
+ else {
373
+ for (let i = 0; i < hookList.length; i++) {
374
+ const h = hookList[i];
375
+ if (!h.command) {
376
+ errors.push(`Hook ${event}[${i}] missing required "command" field`);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ // ── .mcp.json validation ────────────────────────────────
385
+ const mcpPath = sanitizePath(absPath, ".mcp.json");
386
+ const mcpContent = await readFileSafe(mcpPath);
387
+ if (mcpContent) {
388
+ const result = validateJSON(mcpContent, ".mcp.json");
389
+ if (!result.valid) {
390
+ errors.push(...result.errors);
391
+ }
392
+ else if (result.parsed) {
393
+ const cfg = result.parsed;
394
+ if (!cfg.mcpServers) {
395
+ warnings.push(".mcp.json missing 'mcpServers' key — file may not be recognized");
396
+ }
397
+ }
398
+ }
399
+ // ── Results ─────────────────────────────────────────────
400
+ const totalIssues = errors.length + warnings.length;
401
+ const status = errors.length === 0 ? "PASSED" : "FAILED";
402
+ let output = `Validation: ${status}\n`;
403
+ output += `Checked: CLAUDE.md, ${skillFiles.length} skill(s), settings.json, .mcp.json\n\n`;
404
+ if (errors.length > 0) {
405
+ output += `ERRORS (${errors.length}):\n`;
406
+ for (const e of errors) {
407
+ output += ` ✗ ${e}\n`;
408
+ }
409
+ output += "\n";
410
+ }
411
+ if (warnings.length > 0) {
412
+ output += `WARNINGS (${warnings.length}):\n`;
413
+ for (const w of warnings) {
414
+ output += ` ⚠ ${w}\n`;
415
+ }
416
+ output += "\n";
417
+ }
418
+ if (fixes.length > 0) {
419
+ output += `FIXES APPLIED (${fixes.length}):\n`;
420
+ for (const f of fixes) {
421
+ output += ` ✓ ${f}\n`;
422
+ }
423
+ output += "\n";
424
+ }
425
+ if (totalIssues === 0) {
426
+ output += "All configuration files are valid and well-structured.";
427
+ }
428
+ else if (errors.length > 0 && fix_mode === "report") {
429
+ output += `Run with fix_mode: "auto_fix" to automatically repair fixable issues.`;
430
+ }
431
+ return makeResult(output);
432
+ }
433
+ catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ return makeResult(`Validation failed: ${message}`, true);
436
+ }
437
+ });
438
+ // ─── rebar_audit_context ─────────────────────────────────────────
439
+ server.registerTool("rebar_audit_context", {
440
+ title: "Audit Context Budget",
441
+ description: "[Rebar] Analyzes how much of Claude's context window your configuration artifacts consume. " +
442
+ "Every token spent on CLAUDE.md, skills, agents, and knowledge docs is a token NOT " +
443
+ "available for your actual code and conversation.\n\n" +
444
+ "Budget guidelines:\n" +
445
+ "• Skills should use < 2% of context window (~4,000 tokens)\n" +
446
+ "• Total config should use < 5% (~10,000 tokens)\n" +
447
+ "• Individual skills over 500 tokens should be trimmed\n\n" +
448
+ "Examples:\n" +
449
+ " rebar_audit_context({ project_path: '.' })\n\n" +
450
+ "Returns: Token breakdown by category, budget percentage, specific warnings.\n" +
451
+ "Read-only: Does not modify any files.",
452
+ inputSchema: AuditContextInputSchema,
453
+ annotations: {
454
+ readOnlyHint: true,
455
+ destructiveHint: false,
456
+ idempotentHint: true,
457
+ openWorldHint: false,
458
+ },
459
+ }, async (args) => {
460
+ const { project_path } = args;
461
+ const absPath = resolveProjectPath(project_path);
462
+ try {
463
+ let claudemdTokens = 0;
464
+ let skillTokens = 0;
465
+ let agentTokens = 0;
466
+ let knowledgeTokens = 0;
467
+ const warnings = [];
468
+ const details = [];
469
+ // ── CLAUDE.md ───────────────────────────────────────────
470
+ const claudemd = await readFileSafe(sanitizePath(absPath, "CLAUDE.md"));
471
+ if (claudemd) {
472
+ claudemdTokens = estimateTokens(claudemd);
473
+ const lines = claudemd.split("\n").length;
474
+ details.push(` CLAUDE.md: ~${claudemdTokens} tokens (${lines} lines)`);
475
+ if (claudemdTokens > 3000) {
476
+ warnings.push(`CLAUDE.md is ${claudemdTokens} tokens — consider trimming. ` +
477
+ `Focus on rules Claude needs to follow, not general documentation.`);
478
+ }
479
+ }
480
+ // ── Skills ──────────────────────────────────────────────
481
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
482
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
483
+ const skillDetails = [];
484
+ for (const sf of skillFiles) {
485
+ const content = await readFileSafe(sf);
486
+ if (content) {
487
+ const tokens = estimateTokens(content);
488
+ skillTokens += tokens;
489
+ const skillName = path.basename(path.dirname(sf));
490
+ skillDetails.push({ name: skillName, tokens });
491
+ if (tokens > 500) {
492
+ warnings.push(`Skill "${skillName}" is ${tokens} tokens. ` +
493
+ `Keep skills under 500 tokens for best performance.`);
494
+ }
495
+ }
496
+ }
497
+ if (skillDetails.length > 0) {
498
+ details.push(` Skills (${skillDetails.length}):`);
499
+ for (const s of skillDetails.sort((a, b) => b.tokens - a.tokens)) {
500
+ const bar = "█".repeat(Math.min(Math.ceil(s.tokens / 100), 20));
501
+ details.push(` ${s.name}: ~${s.tokens} tokens ${bar}`);
502
+ }
503
+ }
504
+ // ── Agents ──────────────────────────────────────────────
505
+ const agentsDir = sanitizePath(absPath, ".claude/agents");
506
+ const agentFiles = await listFiles(agentsDir, ".md");
507
+ const agentDetails = [];
508
+ for (const af of agentFiles) {
509
+ const content = await readFileSafe(af);
510
+ if (content) {
511
+ const tokens = estimateTokens(content);
512
+ agentTokens += tokens;
513
+ agentDetails.push({ name: path.basename(af, ".md"), tokens });
514
+ }
515
+ }
516
+ if (agentDetails.length > 0) {
517
+ details.push(` Agents (${agentDetails.length}):`);
518
+ for (const a of agentDetails.sort((a, b) => b.tokens - a.tokens)) {
519
+ details.push(` ${a.name}: ~${a.tokens} tokens`);
520
+ }
521
+ }
522
+ // ── Knowledge ───────────────────────────────────────────
523
+ const docsDir = sanitizePath(absPath, ".claude/docs");
524
+ const docFiles = await listFilesRecursive(docsDir, ".md");
525
+ for (const df of docFiles) {
526
+ const content = await readFileSafe(df);
527
+ if (content) {
528
+ knowledgeTokens += estimateTokens(content);
529
+ }
530
+ }
531
+ if (docFiles.length > 0) {
532
+ details.push(` Knowledge docs (${docFiles.length}): ~${knowledgeTokens} tokens`);
533
+ }
534
+ // ── Budget calculation ──────────────────────────────────
535
+ const totalTokens = claudemdTokens + skillTokens + agentTokens + knowledgeTokens;
536
+ const budgetPercent = (totalTokens / CONTEXT_WINDOW_TOKENS) * 100;
537
+ const skillBudgetPercent = (skillTokens / CONTEXT_WINDOW_TOKENS) * 100;
538
+ if (skillBudgetPercent > SKILL_BUDGET_PERCENT) {
539
+ warnings.push(`Skills consume ${skillBudgetPercent.toFixed(1)}% of context ` +
540
+ `(max recommended: ${SKILL_BUDGET_PERCENT}%). Trim skill content or reduce skill count.`);
541
+ }
542
+ if (budgetPercent > 5) {
543
+ warnings.push(`Total config uses ${budgetPercent.toFixed(1)}% of context. ` +
544
+ `This reduces space for code and conversation. Target < 5%.`);
545
+ }
546
+ // ── Output ──────────────────────────────────────────────
547
+ const budgetBar = "█".repeat(Math.min(Math.ceil(budgetPercent), 50)) +
548
+ "░".repeat(Math.max(50 - Math.ceil(budgetPercent), 0));
549
+ let output = `Context Budget Audit\n\n` +
550
+ `${budgetBar} ${budgetPercent.toFixed(1)}%\n\n` +
551
+ `── Breakdown ──\n` +
552
+ details.join("\n") + "\n" +
553
+ `\n── Totals ──\n` +
554
+ ` CLAUDE.md: ${String(claudemdTokens).padStart(6)} tokens\n` +
555
+ ` Skills: ${String(skillTokens).padStart(6)} tokens\n` +
556
+ ` Agents: ${String(agentTokens).padStart(6)} tokens\n` +
557
+ ` Knowledge: ${String(knowledgeTokens).padStart(6)} tokens\n` +
558
+ ` ─────────────────────\n` +
559
+ ` Total: ${String(totalTokens).padStart(6)} tokens\n` +
560
+ ` Budget: ${budgetPercent.toFixed(1)}% of ${(CONTEXT_WINDOW_TOKENS / 1000).toFixed(0)}K window\n`;
561
+ if (warnings.length > 0) {
562
+ output += `\n── Recommendations ──\n`;
563
+ for (const w of warnings) {
564
+ output += ` ⚠ ${w}\n`;
565
+ }
566
+ }
567
+ else {
568
+ output += `\n✓ Context budget is healthy — plenty of room for code and conversation.`;
569
+ }
570
+ return makeResult(output);
571
+ }
572
+ catch (err) {
573
+ const message = err instanceof Error ? err.message : String(err);
574
+ return makeResult(`Context audit failed: ${message}`, true);
575
+ }
576
+ });
577
+ // ─── rebar_doctor ───────────────────────────────────────────────
578
+ server.registerTool("rebar_doctor", {
579
+ title: "Rebar Doctor — Diagnose Setup Health",
580
+ description: "[Rebar] Comprehensive health check for your Rebar enforcement setup. " +
581
+ "Performs 10 diagnostic checks and returns PASS/WARN/FAIL status for each.\n\n" +
582
+ "Checks performed:\n" +
583
+ "1. CLAUDE.md exists with mandatory sections\n" +
584
+ "2. Enforcement hooks installed\n" +
585
+ "3. Skills installed and valid\n" +
586
+ "4. Agents installed\n" +
587
+ "5. .claudeignore exists\n" +
588
+ "6. Test command configured\n" +
589
+ "7. Strictness profile consistency\n" +
590
+ "8. Context budget within limits\n" +
591
+ "9. Cross-platform config consistency\n" +
592
+ "10. No configuration conflicts\n\n" +
593
+ "Examples:\n" +
594
+ " rebar_doctor({ project_path: '.' })\n" +
595
+ " rebar_doctor({ project_path: '.', output_format: 'json' })\n\n" +
596
+ "Returns: Health report with overall status (Healthy/Needs Attention/Broken).",
597
+ inputSchema: DoctorInputSchema,
598
+ annotations: {
599
+ readOnlyHint: true,
600
+ destructiveHint: false,
601
+ idempotentHint: true,
602
+ openWorldHint: false,
603
+ },
604
+ }, async (args) => {
605
+ const { project_path, output_format } = args;
606
+ const absPath = resolveProjectPath(project_path);
607
+ const checks = [];
608
+ try {
609
+ // ── Check 1: CLAUDE.md exists with mandatory sections ────
610
+ const claudemdPath = sanitizePath(absPath, "CLAUDE.md");
611
+ const claudemd = await readFileSafe(claudemdPath);
612
+ if (!claudemd) {
613
+ checks.push({
614
+ name: "CLAUDE.md",
615
+ status: "FAIL",
616
+ message: "No CLAUDE.md found",
617
+ details: ["Run rebar_generate_claudemd to create one"],
618
+ });
619
+ }
620
+ else {
621
+ const result = validateClaudeMD(claudemd, claudemdPath);
622
+ const hasBuildSection = /##\s*(Build|Test)/i.test(claudemd);
623
+ const hasArchSection = /##\s*(Architecture|Tech)/i.test(claudemd);
624
+ if (result.errors.length > 0) {
625
+ checks.push({
626
+ name: "CLAUDE.md",
627
+ status: "FAIL",
628
+ message: "CLAUDE.md has validation errors",
629
+ details: result.errors,
630
+ });
631
+ }
632
+ else if (!hasBuildSection || !hasArchSection) {
633
+ const missing = [];
634
+ if (!hasBuildSection)
635
+ missing.push("Build & Test section");
636
+ if (!hasArchSection)
637
+ missing.push("Architecture section");
638
+ checks.push({
639
+ name: "CLAUDE.md",
640
+ status: "WARN",
641
+ message: "CLAUDE.md missing recommended sections",
642
+ details: missing,
643
+ });
644
+ }
645
+ else {
646
+ checks.push({
647
+ name: "CLAUDE.md",
648
+ status: "PASS",
649
+ message: "CLAUDE.md present with required sections",
650
+ });
651
+ }
652
+ }
653
+ // ── Check 2: Enforcement hooks installed ─────────────────
654
+ const settingsPath = sanitizePath(absPath, ".claude/settings.json");
655
+ const settingsContent = await readFileSafe(settingsPath);
656
+ if (!settingsContent) {
657
+ checks.push({
658
+ name: "Enforcement Hooks",
659
+ status: "FAIL",
660
+ message: "No .claude/settings.json found",
661
+ details: ["Run rebar_init_project or rebar_set_strictness to configure hooks"],
662
+ });
663
+ }
664
+ else {
665
+ const parsed = validateJSON(settingsContent, "settings.json");
666
+ if (!parsed.valid) {
667
+ checks.push({
668
+ name: "Enforcement Hooks",
669
+ status: "FAIL",
670
+ message: "settings.json is invalid JSON",
671
+ details: parsed.errors,
672
+ });
673
+ }
674
+ else {
675
+ const settings = parsed.parsed;
676
+ const hooks = settings.hooks;
677
+ if (!hooks || Object.keys(hooks).length === 0) {
678
+ checks.push({
679
+ name: "Enforcement Hooks",
680
+ status: "WARN",
681
+ message: "No hooks configured",
682
+ details: ["Run rebar_set_strictness to add enforcement hooks"],
683
+ });
684
+ }
685
+ else {
686
+ const hookCount = Object.values(hooks).reduce((sum, arr) => sum + arr.length, 0);
687
+ const hasPreToolCall = Array.isArray(hooks.PreToolCall) && hooks.PreToolCall.length > 0;
688
+ const hasPostToolCall = Array.isArray(hooks.PostToolCall) && hooks.PostToolCall.length > 0;
689
+ if (!hasPreToolCall && !hasPostToolCall) {
690
+ checks.push({
691
+ name: "Enforcement Hooks",
692
+ status: "WARN",
693
+ message: `${hookCount} hook(s) found but no tool-level enforcement`,
694
+ details: ["Consider adding PreToolCall/PostToolCall hooks"],
695
+ });
696
+ }
697
+ else {
698
+ checks.push({
699
+ name: "Enforcement Hooks",
700
+ status: "PASS",
701
+ message: `${hookCount} enforcement hook(s) installed`,
702
+ });
703
+ }
704
+ }
705
+ }
706
+ }
707
+ // ── Check 3: Skills installed ────────────────────────────
708
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
709
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
710
+ if (skillFiles.length === 0) {
711
+ checks.push({
712
+ name: "Skills",
713
+ status: "WARN",
714
+ message: "No skills installed",
715
+ details: ["Skills provide reusable prompts. Run rebar_create_skill to add one."],
716
+ });
717
+ }
718
+ else {
719
+ const invalidSkills = [];
720
+ for (const sf of skillFiles) {
721
+ const content = await readFileSafe(sf);
722
+ if (content) {
723
+ const validation = validateSkillFrontmatter(content);
724
+ if (!validation.valid) {
725
+ invalidSkills.push(path.basename(path.dirname(sf)));
726
+ }
727
+ }
728
+ }
729
+ if (invalidSkills.length > 0) {
730
+ checks.push({
731
+ name: "Skills",
732
+ status: "WARN",
733
+ message: `${skillFiles.length} skill(s) found, ${invalidSkills.length} with invalid frontmatter`,
734
+ details: invalidSkills.map(s => `Invalid: ${s}`),
735
+ });
736
+ }
737
+ else {
738
+ checks.push({
739
+ name: "Skills",
740
+ status: "PASS",
741
+ message: `${skillFiles.length} skill(s) installed and valid`,
742
+ });
743
+ }
744
+ }
745
+ // ── Check 4: Agents installed ────────────────────────────
746
+ const agentsDir = sanitizePath(absPath, ".claude/agents");
747
+ const agentFiles = await listFiles(agentsDir, ".md");
748
+ if (agentFiles.length === 0) {
749
+ checks.push({
750
+ name: "Agents",
751
+ status: "WARN",
752
+ message: "No custom agents installed",
753
+ details: ["Custom agents extend Claude's capabilities. Optional but recommended."],
754
+ });
755
+ }
756
+ else {
757
+ checks.push({
758
+ name: "Agents",
759
+ status: "PASS",
760
+ message: `${agentFiles.length} agent(s) installed`,
761
+ });
762
+ }
763
+ // ── Check 5: .claudeignore exists ────────────────────────
764
+ const claudeignorePath = sanitizePath(absPath, ".claudeignore");
765
+ if (await fileExists(claudeignorePath)) {
766
+ checks.push({
767
+ name: ".claudeignore",
768
+ status: "PASS",
769
+ message: ".claudeignore present",
770
+ });
771
+ }
772
+ else {
773
+ checks.push({
774
+ name: ".claudeignore",
775
+ status: "WARN",
776
+ message: "No .claudeignore file",
777
+ details: ["Add .claudeignore to exclude large or sensitive files from context"],
778
+ });
779
+ }
780
+ // ── Check 6: Test command configured ─────────────────────
781
+ let testCommandFound = false;
782
+ if (claudemd) {
783
+ // Look for test command in CLAUDE.md
784
+ testCommandFound = /npm (run )?test|yarn test|pnpm test|pytest|cargo test|go test|mvn test|gradle test/i.test(claudemd);
785
+ }
786
+ // Also check package.json for test script
787
+ const packageJsonPath = sanitizePath(absPath, "package.json");
788
+ const packageJson = await readFileSafe(packageJsonPath);
789
+ if (packageJson) {
790
+ try {
791
+ const pkg = JSON.parse(packageJson);
792
+ if (pkg.scripts?.test && !pkg.scripts.test.includes("no test specified")) {
793
+ testCommandFound = true;
794
+ }
795
+ }
796
+ catch {
797
+ // Invalid JSON, ignore
798
+ }
799
+ }
800
+ if (testCommandFound) {
801
+ checks.push({
802
+ name: "Test Command",
803
+ status: "PASS",
804
+ message: "Test command detected",
805
+ });
806
+ }
807
+ else {
808
+ checks.push({
809
+ name: "Test Command",
810
+ status: "WARN",
811
+ message: "No test command found",
812
+ details: ["Add test instructions to CLAUDE.md or configure npm test"],
813
+ });
814
+ }
815
+ // ── Check 7: Strictness profile consistency ──────────────
816
+ if (settingsContent) {
817
+ const parsed = validateJSON(settingsContent, "settings.json");
818
+ if (parsed.valid && parsed.parsed) {
819
+ const settings = parsed.parsed;
820
+ const hooks = settings.hooks;
821
+ if (hooks) {
822
+ const profile = detectCurrentProfile(hooks);
823
+ if (profile) {
824
+ checks.push({
825
+ name: "Strictness Profile",
826
+ status: "PASS",
827
+ message: `Detected profile: ${profile}`,
828
+ });
829
+ }
830
+ else {
831
+ checks.push({
832
+ name: "Strictness Profile",
833
+ status: "WARN",
834
+ message: "Custom or mixed hook configuration",
835
+ details: ["Hooks don't match a standard strictness profile"],
836
+ });
837
+ }
838
+ }
839
+ else {
840
+ checks.push({
841
+ name: "Strictness Profile",
842
+ status: "WARN",
843
+ message: "No strictness profile configured",
844
+ details: ["Run rebar_set_strictness to set a profile"],
845
+ });
846
+ }
847
+ }
848
+ }
849
+ else {
850
+ checks.push({
851
+ name: "Strictness Profile",
852
+ status: "FAIL",
853
+ message: "Cannot determine profile - settings.json missing",
854
+ });
855
+ }
856
+ // ── Check 8: Context budget ──────────────────────────────
857
+ let totalTokens = 0;
858
+ if (claudemd)
859
+ totalTokens += estimateTokens(claudemd);
860
+ for (const sf of skillFiles) {
861
+ const content = await readFileSafe(sf);
862
+ if (content)
863
+ totalTokens += estimateTokens(content);
864
+ }
865
+ for (const af of agentFiles) {
866
+ const content = await readFileSafe(af);
867
+ if (content)
868
+ totalTokens += estimateTokens(content);
869
+ }
870
+ const budgetPercent = (totalTokens / CONTEXT_WINDOW_TOKENS) * 100;
871
+ if (budgetPercent > 10) {
872
+ checks.push({
873
+ name: "Context Budget",
874
+ status: "FAIL",
875
+ message: `Config uses ${budgetPercent.toFixed(1)}% of context (>10%)`,
876
+ details: ["Reduce CLAUDE.md size or trim skills"],
877
+ });
878
+ }
879
+ else if (budgetPercent > 5) {
880
+ checks.push({
881
+ name: "Context Budget",
882
+ status: "WARN",
883
+ message: `Config uses ${budgetPercent.toFixed(1)}% of context`,
884
+ details: ["Consider trimming for better performance"],
885
+ });
886
+ }
887
+ else {
888
+ checks.push({
889
+ name: "Context Budget",
890
+ status: "PASS",
891
+ message: `Config uses ${budgetPercent.toFixed(1)}% of context`,
892
+ });
893
+ }
894
+ // ── Check 9: Cross-platform config consistency ───────────
895
+ const cursorRulesDir = sanitizePath(absPath, ".cursor/rules");
896
+ const windsurfRulesDir = sanitizePath(absPath, ".windsurf/rules");
897
+ const agentsMdPath = sanitizePath(absPath, "AGENTS.md");
898
+ const hasCursor = await fileExists(cursorRulesDir);
899
+ const hasWindsurf = await fileExists(windsurfRulesDir);
900
+ const hasCodex = await fileExists(agentsMdPath);
901
+ const hasClaude = !!claudemd;
902
+ const platforms = [];
903
+ if (hasClaude)
904
+ platforms.push("Claude Code");
905
+ if (hasCursor)
906
+ platforms.push("Cursor");
907
+ if (hasWindsurf)
908
+ platforms.push("Windsurf");
909
+ if (hasCodex)
910
+ platforms.push("Codex");
911
+ if (platforms.length === 0) {
912
+ checks.push({
913
+ name: "Platform Config",
914
+ status: "FAIL",
915
+ message: "No AI platform configuration found",
916
+ });
917
+ }
918
+ else if (platforms.length === 1) {
919
+ checks.push({
920
+ name: "Platform Config",
921
+ status: "PASS",
922
+ message: `Configured for: ${platforms[0]}`,
923
+ });
924
+ }
925
+ else {
926
+ checks.push({
927
+ name: "Platform Config",
928
+ status: "PASS",
929
+ message: `Multi-platform: ${platforms.join(", ")}`,
930
+ });
931
+ }
932
+ // ── Check 10: No configuration conflicts ─────────────────
933
+ const conflicts = [];
934
+ // Check for conflicting hook behaviors
935
+ if (settingsContent) {
936
+ const parsed = validateJSON(settingsContent, "settings.json");
937
+ if (parsed.valid && parsed.parsed) {
938
+ const settings = parsed.parsed;
939
+ const hooks = settings.hooks;
940
+ if (hooks?.PreToolCall && Array.isArray(hooks.PreToolCall)) {
941
+ const blockingHooks = hooks.PreToolCall.filter((h) => {
942
+ const hook = h;
943
+ return hook.type === "block" || hook.exitBehavior === "block";
944
+ });
945
+ if (blockingHooks.length > 3) {
946
+ conflicts.push("Many blocking hooks may slow down Claude significantly");
947
+ }
948
+ }
949
+ }
950
+ }
951
+ // Check for duplicate skills
952
+ const skillNames = new Set();
953
+ for (const sf of skillFiles) {
954
+ const name = path.basename(path.dirname(sf));
955
+ if (skillNames.has(name)) {
956
+ conflicts.push(`Duplicate skill name: ${name}`);
957
+ }
958
+ skillNames.add(name);
959
+ }
960
+ if (conflicts.length > 0) {
961
+ checks.push({
962
+ name: "Config Conflicts",
963
+ status: "WARN",
964
+ message: `${conflicts.length} potential conflict(s)`,
965
+ details: conflicts,
966
+ });
967
+ }
968
+ else {
969
+ checks.push({
970
+ name: "Config Conflicts",
971
+ status: "PASS",
972
+ message: "No configuration conflicts detected",
973
+ });
974
+ }
975
+ // ── Calculate overall health ─────────────────────────────
976
+ const passCount = checks.filter(c => c.status === "PASS").length;
977
+ const warnCount = checks.filter(c => c.status === "WARN").length;
978
+ const failCount = checks.filter(c => c.status === "FAIL").length;
979
+ let overallStatus;
980
+ let overallEmoji;
981
+ if (failCount === 0 && warnCount === 0) {
982
+ overallStatus = "Healthy";
983
+ overallEmoji = "✓";
984
+ }
985
+ else if (failCount === 0) {
986
+ overallStatus = "Needs Attention";
987
+ overallEmoji = "⚠";
988
+ }
989
+ else {
990
+ overallStatus = "Broken";
991
+ overallEmoji = "✗";
992
+ }
993
+ // ── Format output ────────────────────────────────────────
994
+ if (output_format === "json") {
995
+ return makeResult(JSON.stringify({
996
+ overall: overallStatus,
997
+ summary: { pass: passCount, warn: warnCount, fail: failCount },
998
+ checks: checks.map(c => ({
999
+ name: c.name,
1000
+ status: c.status,
1001
+ message: c.message,
1002
+ details: c.details,
1003
+ })),
1004
+ }, null, 2));
1005
+ }
1006
+ // Markdown format
1007
+ let output = `# Rebar Doctor Report\n\n`;
1008
+ output += `**Overall: ${overallEmoji} ${overallStatus}**\n`;
1009
+ output += `${passCount} passed, ${warnCount} warnings, ${failCount} failed\n\n`;
1010
+ output += `## Health Checks\n\n`;
1011
+ for (const check of checks) {
1012
+ const icon = check.status === "PASS" ? "✓" : check.status === "WARN" ? "⚠" : "✗";
1013
+ output += `### ${icon} ${check.name}: ${check.status}\n`;
1014
+ output += `${check.message}\n`;
1015
+ if (check.details && check.details.length > 0) {
1016
+ for (const d of check.details) {
1017
+ output += ` - ${d}\n`;
1018
+ }
1019
+ }
1020
+ output += "\n";
1021
+ }
1022
+ if (failCount > 0) {
1023
+ output += `## Next Steps\n\n`;
1024
+ output += `Fix the failing checks above to get your project into a healthy state.\n`;
1025
+ output += `Run \`rebar_init_project\` for a guided setup, or fix issues individually.\n`;
1026
+ }
1027
+ else if (warnCount > 0) {
1028
+ output += `## Recommendations\n\n`;
1029
+ output += `Your setup is functional but could be improved. Review the warnings above.\n`;
1030
+ }
1031
+ return makeResult(output.trimEnd());
1032
+ }
1033
+ catch (err) {
1034
+ const message = err instanceof Error ? err.message : String(err);
1035
+ return makeResult(`Doctor check failed: ${message}`, true);
1036
+ }
1037
+ });
1038
+ // ─── rebar_metrics ──────────────────────────────────────────────
1039
+ server.registerTool("rebar_metrics", {
1040
+ title: "Rebar Metrics — Quality Score Dashboard",
1041
+ description: "[Rebar] View or record quality score metrics over time. Tracks trends and " +
1042
+ "helps identify when code quality is improving or declining.\n\n" +
1043
+ "Actions:\n" +
1044
+ "• view: Display historical quality scores and trends\n" +
1045
+ "• record: Calculate and store current quality score\n\n" +
1046
+ "Metrics stored in .rebar/metrics.json include:\n" +
1047
+ "• Overall score (0-100)\n" +
1048
+ "• Category breakdown (CLAUDE.md, hooks, skills, context, practices)\n" +
1049
+ "• Timestamp for trend analysis\n\n" +
1050
+ "Examples:\n" +
1051
+ " rebar_metrics({ project_path: '.', action: 'view' })\n" +
1052
+ " rebar_metrics({ project_path: '.', action: 'record' })\n\n" +
1053
+ "Returns: Quality score history with trend indicators.",
1054
+ inputSchema: MetricsInputSchema,
1055
+ annotations: {
1056
+ readOnlyHint: false,
1057
+ destructiveHint: false,
1058
+ idempotentHint: true,
1059
+ openWorldHint: false,
1060
+ },
1061
+ }, async (args) => {
1062
+ const { project_path, action, output_format } = args;
1063
+ const absPath = resolveProjectPath(project_path);
1064
+ const metricsPath = sanitizePath(absPath, ".rebar/metrics.json");
1065
+ try {
1066
+ // Load existing metrics
1067
+ let metrics = [];
1068
+ const existing = await readFileSafe(metricsPath);
1069
+ if (existing) {
1070
+ try {
1071
+ metrics = JSON.parse(existing);
1072
+ }
1073
+ catch {
1074
+ metrics = [];
1075
+ }
1076
+ }
1077
+ if (action === "record") {
1078
+ // Calculate current score
1079
+ const claudemd = await readFileSafe(sanitizePath(absPath, "CLAUDE.md"));
1080
+ const settings = await readFileSafe(sanitizePath(absPath, ".claude/settings.json"));
1081
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
1082
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
1083
+ // CLAUDE.md score (0-25)
1084
+ let claudemdScore = 0;
1085
+ if (claudemd) {
1086
+ claudemdScore += 10;
1087
+ const result = validateClaudeMD(claudemd, "CLAUDE.md");
1088
+ if (result.valid)
1089
+ claudemdScore += 5;
1090
+ if (/##\s*(Build|Test)/i.test(claudemd))
1091
+ claudemdScore += 5;
1092
+ if (/##\s*(Architecture)/i.test(claudemd))
1093
+ claudemdScore += 5;
1094
+ }
1095
+ // Enforcement score (0-25)
1096
+ let enforcementScore = 0;
1097
+ if (settings) {
1098
+ const parsed = validateJSON(settings, "settings.json");
1099
+ if (parsed.valid && parsed.parsed) {
1100
+ const s = parsed.parsed;
1101
+ const hooks = s.hooks;
1102
+ if (hooks && Object.keys(hooks).length > 0) {
1103
+ enforcementScore += 10;
1104
+ const profile = detectCurrentProfile(hooks);
1105
+ if (profile === "standard")
1106
+ enforcementScore += 5;
1107
+ if (profile === "strict")
1108
+ enforcementScore += 10;
1109
+ if (profile === "paranoid")
1110
+ enforcementScore += 15;
1111
+ }
1112
+ }
1113
+ }
1114
+ // Skills score (0-20)
1115
+ let skillsScore = 0;
1116
+ if (skillFiles.length > 0) {
1117
+ skillsScore += 10;
1118
+ let validSkills = 0;
1119
+ for (const sf of skillFiles) {
1120
+ const content = await readFileSafe(sf);
1121
+ if (content && validateSkillFrontmatter(content).valid) {
1122
+ validSkills++;
1123
+ }
1124
+ }
1125
+ if (validSkills === skillFiles.length)
1126
+ skillsScore += 10;
1127
+ }
1128
+ else {
1129
+ skillsScore = 10; // Optional, no penalty
1130
+ }
1131
+ // Context score (0-15)
1132
+ let contextScore = 0;
1133
+ let totalTokens = 0;
1134
+ if (claudemd)
1135
+ totalTokens += estimateTokens(claudemd);
1136
+ for (const sf of skillFiles) {
1137
+ const content = await readFileSafe(sf);
1138
+ if (content)
1139
+ totalTokens += estimateTokens(content);
1140
+ }
1141
+ const budgetPercent = (totalTokens / CONTEXT_WINDOW_TOKENS) * 100;
1142
+ if (budgetPercent <= 2)
1143
+ contextScore = 15;
1144
+ else if (budgetPercent <= 5)
1145
+ contextScore = 10;
1146
+ else if (budgetPercent <= 10)
1147
+ contextScore = 5;
1148
+ // Practices score (0-15)
1149
+ let practicesScore = 0;
1150
+ if (await fileExists(sanitizePath(absPath, ".claudeignore")))
1151
+ practicesScore += 5;
1152
+ const agentsDir = sanitizePath(absPath, ".claude/agents");
1153
+ const agentFiles = await listFiles(agentsDir, ".md");
1154
+ if (agentFiles.length > 0)
1155
+ practicesScore += 5;
1156
+ if (await fileExists(sanitizePath(absPath, ".claude/docs")))
1157
+ practicesScore += 5;
1158
+ const totalScore = claudemdScore + enforcementScore + skillsScore + contextScore + practicesScore;
1159
+ // Add new entry
1160
+ const entry = {
1161
+ timestamp: new Date().toISOString(),
1162
+ score: totalScore,
1163
+ categories: {
1164
+ claudemd: claudemdScore,
1165
+ enforcement: enforcementScore,
1166
+ skills: skillsScore,
1167
+ context: contextScore,
1168
+ practices: practicesScore,
1169
+ },
1170
+ };
1171
+ metrics.push(entry);
1172
+ // Keep only last 100 entries
1173
+ if (metrics.length > 100) {
1174
+ metrics = metrics.slice(-100);
1175
+ }
1176
+ // Ensure directory exists and save
1177
+ await atomicWrite(metricsPath, JSON.stringify(metrics, null, 2));
1178
+ // Calculate trend
1179
+ let trend = "→";
1180
+ if (metrics.length >= 2) {
1181
+ const prev = metrics[metrics.length - 2].score;
1182
+ if (totalScore > prev)
1183
+ trend = "↑";
1184
+ else if (totalScore < prev)
1185
+ trend = "↓";
1186
+ }
1187
+ if (output_format === "json") {
1188
+ return makeResult(JSON.stringify({
1189
+ action: "recorded",
1190
+ score: totalScore,
1191
+ trend,
1192
+ categories: entry.categories,
1193
+ historyCount: metrics.length,
1194
+ }, null, 2));
1195
+ }
1196
+ return makeResult(`✓ Quality score recorded: ${totalScore}/100 ${trend}\n\n` +
1197
+ `Categories:\n` +
1198
+ ` CLAUDE.md: ${claudemdScore}/25\n` +
1199
+ ` Enforcement: ${enforcementScore}/25\n` +
1200
+ ` Skills: ${skillsScore}/20\n` +
1201
+ ` Context: ${contextScore}/15\n` +
1202
+ ` Practices: ${practicesScore}/15\n\n` +
1203
+ `History: ${metrics.length} entries stored`);
1204
+ }
1205
+ // action === "view"
1206
+ if (metrics.length === 0) {
1207
+ return makeResult("No metrics history found.\n\n" +
1208
+ "Run `rebar_metrics` with `action: 'record'` to capture the current score.\n" +
1209
+ "Or use `rebar audit` from the CLI to generate and store metrics.");
1210
+ }
1211
+ // Calculate trend
1212
+ const latest = metrics[metrics.length - 1];
1213
+ let trend = "stable";
1214
+ let trendEmoji = "→";
1215
+ if (metrics.length >= 3) {
1216
+ const recent = metrics.slice(-3).map(m => m.score);
1217
+ const avg = recent.reduce((a, b) => a + b, 0) / recent.length;
1218
+ if (latest.score > avg + 2) {
1219
+ trend = "improving";
1220
+ trendEmoji = "↑";
1221
+ }
1222
+ else if (latest.score < avg - 2) {
1223
+ trend = "declining";
1224
+ trendEmoji = "↓";
1225
+ }
1226
+ }
1227
+ if (output_format === "json") {
1228
+ return makeResult(JSON.stringify({
1229
+ latest: latest.score,
1230
+ trend,
1231
+ historyCount: metrics.length,
1232
+ history: metrics.slice(-10),
1233
+ }, null, 2));
1234
+ }
1235
+ // Markdown format
1236
+ let output = `# Rebar Quality Metrics\n\n`;
1237
+ output += `**Current Score: ${latest.score}/100 ${trendEmoji} (${trend})**\n\n`;
1238
+ output += `## History (last 10)\n\n`;
1239
+ output += `| Date | Score | Trend |\n`;
1240
+ output += `|------|-------|-------|\n`;
1241
+ const recentMetrics = metrics.slice(-10);
1242
+ for (let i = 0; i < recentMetrics.length; i++) {
1243
+ const m = recentMetrics[i];
1244
+ const date = new Date(m.timestamp).toLocaleDateString();
1245
+ let itemTrend = "→";
1246
+ if (i > 0) {
1247
+ const prev = recentMetrics[i - 1].score;
1248
+ if (m.score > prev)
1249
+ itemTrend = "↑";
1250
+ else if (m.score < prev)
1251
+ itemTrend = "↓";
1252
+ }
1253
+ output += `| ${date} | ${m.score}/100 | ${itemTrend} |\n`;
1254
+ }
1255
+ output += `\n## Latest Category Breakdown\n\n`;
1256
+ const cat = latest.categories;
1257
+ output += `- CLAUDE.md: ${cat.claudemd}/25\n`;
1258
+ output += `- Enforcement: ${cat.enforcement}/25\n`;
1259
+ output += `- Skills: ${cat.skills}/20\n`;
1260
+ output += `- Context: ${cat.context}/15\n`;
1261
+ output += `- Practices: ${cat.practices}/15\n`;
1262
+ return makeResult(output.trimEnd());
1263
+ }
1264
+ catch (err) {
1265
+ const message = err instanceof Error ? err.message : String(err);
1266
+ return makeResult(`Metrics operation failed: ${message}`, true);
1267
+ }
1268
+ });
1269
+ // ─── rebar_badge ────────────────────────────────────────────────
1270
+ server.registerTool("rebar_badge", {
1271
+ title: "Rebar Badge — Generate Quality Score Badge",
1272
+ description: "[Rebar] Generates an SVG badge showing the project's quality score. " +
1273
+ "Perfect for README files to showcase code quality.\n\n" +
1274
+ "Badge colors:\n" +
1275
+ "• Green (80-100): Excellent quality\n" +
1276
+ "• Yellow-green (60-79): Good quality\n" +
1277
+ "• Yellow (40-59): Fair quality\n" +
1278
+ "• Red (0-39): Needs work\n\n" +
1279
+ "The badge is saved to .rebar/badge.svg and can be embedded in README.md.\n\n" +
1280
+ "Examples:\n" +
1281
+ " rebar_badge({ project_path: '.' })\n" +
1282
+ " rebar_badge({ project_path: '.', style: 'flat-square' })\n\n" +
1283
+ "Returns: SVG badge content and embedding instructions.",
1284
+ inputSchema: BadgeInputSchema,
1285
+ annotations: {
1286
+ readOnlyHint: false,
1287
+ destructiveHint: false,
1288
+ idempotentHint: true,
1289
+ openWorldHint: false,
1290
+ },
1291
+ }, async (args) => {
1292
+ const { project_path, style, output_format } = args;
1293
+ const absPath = resolveProjectPath(project_path);
1294
+ try {
1295
+ // Calculate current score (quick version)
1296
+ let score = 0;
1297
+ const claudemd = await readFileSafe(sanitizePath(absPath, "CLAUDE.md"));
1298
+ const settings = await readFileSafe(sanitizePath(absPath, ".claude/settings.json"));
1299
+ // CLAUDE.md (25 points)
1300
+ if (claudemd) {
1301
+ score += 10;
1302
+ const result = validateClaudeMD(claudemd, "CLAUDE.md");
1303
+ if (result.valid)
1304
+ score += 5;
1305
+ if (/##\s*(Build|Test)/i.test(claudemd))
1306
+ score += 5;
1307
+ if (/##\s*(Architecture)/i.test(claudemd))
1308
+ score += 5;
1309
+ }
1310
+ // Enforcement (25 points)
1311
+ if (settings) {
1312
+ const parsed = validateJSON(settings, "settings.json");
1313
+ if (parsed.valid && parsed.parsed) {
1314
+ const s = parsed.parsed;
1315
+ const hooks = s.hooks;
1316
+ if (hooks && Object.keys(hooks).length > 0) {
1317
+ score += 10;
1318
+ const profile = detectCurrentProfile(hooks);
1319
+ if (profile === "standard")
1320
+ score += 5;
1321
+ if (profile === "strict")
1322
+ score += 10;
1323
+ if (profile === "paranoid")
1324
+ score += 15;
1325
+ }
1326
+ }
1327
+ }
1328
+ // Skills (20 points - simplified)
1329
+ const skillsDir = sanitizePath(absPath, ".claude/skills");
1330
+ const skillFiles = await listFilesRecursive(skillsDir, ".md");
1331
+ if (skillFiles.length > 0)
1332
+ score += 15;
1333
+ else
1334
+ score += 10; // Optional
1335
+ // Context budget (15 points - simplified)
1336
+ let totalTokens = 0;
1337
+ if (claudemd)
1338
+ totalTokens += estimateTokens(claudemd);
1339
+ const budgetPercent = (totalTokens / CONTEXT_WINDOW_TOKENS) * 100;
1340
+ if (budgetPercent <= 5)
1341
+ score += 15;
1342
+ else if (budgetPercent <= 10)
1343
+ score += 10;
1344
+ else
1345
+ score += 5;
1346
+ // Best practices (15 points)
1347
+ if (await fileExists(sanitizePath(absPath, ".claudeignore")))
1348
+ score += 5;
1349
+ if (await fileExists(sanitizePath(absPath, ".claude/agents")))
1350
+ score += 5;
1351
+ if (await fileExists(sanitizePath(absPath, ".claude/docs")))
1352
+ score += 5;
1353
+ // Determine color and label
1354
+ let color;
1355
+ let label;
1356
+ if (score >= 80) {
1357
+ color = "#4c1"; // Green
1358
+ label = "excellent";
1359
+ }
1360
+ else if (score >= 60) {
1361
+ color = "#97ca00"; // Yellow-green
1362
+ label = "good";
1363
+ }
1364
+ else if (score >= 40) {
1365
+ color = "#dfb317"; // Yellow
1366
+ label = "fair";
1367
+ }
1368
+ else {
1369
+ color = "#e05d44"; // Red
1370
+ label = "needs work";
1371
+ }
1372
+ // Generate SVG based on style
1373
+ let svg;
1374
+ const radius = style === "flat-square" ? "0" : style === "plastic" ? "4" : "3";
1375
+ svg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
1376
+ <linearGradient id="b" x2="0" y2="100%">
1377
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
1378
+ <stop offset="1" stop-opacity=".1"/>
1379
+ </linearGradient>
1380
+ <clipPath id="a">
1381
+ <rect width="120" height="20" rx="${radius}" fill="#fff"/>
1382
+ </clipPath>
1383
+ <g clip-path="url(#a)">
1384
+ <path fill="#555" d="M0 0h55v20H0z"/>
1385
+ <path fill="${color}" d="M55 0h65v20H55z"/>
1386
+ <path fill="url(#b)" d="M0 0h120v20H0z"/>
1387
+ </g>
1388
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
1389
+ <text x="27.5" y="15" fill="#010101" fill-opacity=".3">rebar</text>
1390
+ <text x="27.5" y="14">rebar</text>
1391
+ <text x="87.5" y="15" fill="#010101" fill-opacity=".3">${score}/100</text>
1392
+ <text x="87.5" y="14">${score}/100</text>
1393
+ </g>
1394
+ </svg>`;
1395
+ if (output_format === "json") {
1396
+ return makeResult(JSON.stringify({
1397
+ score,
1398
+ label,
1399
+ color,
1400
+ style,
1401
+ svg,
1402
+ embedMarkdown: "![Rebar Score](.rebar/badge.svg)",
1403
+ embedHtml: `<img src=".rebar/badge.svg" alt="Rebar Score: ${score}/100">`,
1404
+ }, null, 2));
1405
+ }
1406
+ // Save badge to file
1407
+ const badgePath = sanitizePath(absPath, ".rebar/badge.svg");
1408
+ await atomicWrite(badgePath, svg);
1409
+ return makeResult(`✓ Badge generated: .rebar/badge.svg\n\n` +
1410
+ `Score: ${score}/100 (${label})\n\n` +
1411
+ `## Embed in README.md\n\n` +
1412
+ "```markdown\n" +
1413
+ "![Rebar Score](.rebar/badge.svg)\n" +
1414
+ "```\n\n" +
1415
+ "## Or use HTML\n\n" +
1416
+ "```html\n" +
1417
+ `<img src=".rebar/badge.svg" alt="Rebar Score: ${score}/100">\n` +
1418
+ "```");
1419
+ }
1420
+ catch (err) {
1421
+ const message = err instanceof Error ? err.message : String(err);
1422
+ return makeResult(`Badge generation failed: ${message}`, true);
1423
+ }
1424
+ });
1425
+ // ─── rebar_custom_rules ─────────────────────────────────────────
1426
+ server.registerTool("rebar_custom_rules", {
1427
+ title: "Rebar Custom Rules — Manage Quality Rules",
1428
+ description: "[Rebar] Manage custom quality rules defined in .rebar/rules.yaml. " +
1429
+ "Custom rules extend the built-in checks with project-specific requirements.\n\n" +
1430
+ "Actions:\n" +
1431
+ "• list: Show configured custom rules\n" +
1432
+ "• init: Create a default rules.yaml template\n" +
1433
+ "• evaluate: Run all custom rules and report results\n\n" +
1434
+ "Rule types supported:\n" +
1435
+ "• file-exists: Check if a file exists\n" +
1436
+ "• file-contains: Check if file contains a pattern\n" +
1437
+ "• file-not-contains: Check file doesn't contain a pattern\n" +
1438
+ "• glob-count: Count files matching a pattern (min/max)\n\n" +
1439
+ "Examples:\n" +
1440
+ " rebar_custom_rules({ project_path: '.', action: 'init' })\n" +
1441
+ " rebar_custom_rules({ project_path: '.', action: 'evaluate' })\n\n" +
1442
+ "Returns: Rule list, template, or evaluation results.",
1443
+ inputSchema: CustomRulesInputSchema,
1444
+ annotations: {
1445
+ readOnlyHint: false,
1446
+ destructiveHint: false,
1447
+ idempotentHint: true,
1448
+ openWorldHint: false,
1449
+ },
1450
+ }, async (args) => {
1451
+ const { project_path, action, output_format } = args;
1452
+ const absPath = resolveProjectPath(project_path);
1453
+ try {
1454
+ if (action === "init") {
1455
+ // Create default rules template
1456
+ const rulesPath = sanitizePath(absPath, ".rebar/rules.yaml");
1457
+ const template = generateDefaultRulesTemplate();
1458
+ await atomicWrite(rulesPath, template);
1459
+ return makeResult(`✓ Created .rebar/rules.yaml\n\n` +
1460
+ `Default rules included:\n` +
1461
+ `• readme-exists: Check README.md exists\n` +
1462
+ `• license-exists: Check LICENSE exists\n` +
1463
+ `• no-hardcoded-localhost: Prevent localhost URLs\n` +
1464
+ `• test-files-exist: Ensure tests exist\n\n` +
1465
+ `Edit .rebar/rules.yaml to customize rules for your project.\n` +
1466
+ `Run with action: 'evaluate' to check rules.`);
1467
+ }
1468
+ // Load existing rules
1469
+ const config = await loadCustomRules(absPath);
1470
+ if (action === "list") {
1471
+ if (!config || !config.rules || config.rules.length === 0) {
1472
+ return makeResult("No custom rules configured.\n\n" +
1473
+ "Run with `action: 'init'` to create a default rules.yaml template.\n" +
1474
+ "Or create .rebar/rules.yaml manually.");
1475
+ }
1476
+ if (output_format === "json") {
1477
+ return makeResult(JSON.stringify(config, null, 2));
1478
+ }
1479
+ let output = `# Custom Rules\n\n`;
1480
+ output += `Found ${config.rules.length} rule(s) in .rebar/rules.yaml\n\n`;
1481
+ for (const rule of config.rules) {
1482
+ const status = rule.enabled === false ? " (disabled)" : "";
1483
+ const icon = rule.severity === "error" ? "❌" : rule.severity === "warning" ? "⚠️" : "ℹ️";
1484
+ output += `## ${icon} ${rule.name}${status}\n`;
1485
+ output += `ID: \`${rule.id}\`\n`;
1486
+ output += `Type: ${rule.type}\n`;
1487
+ if (rule.description)
1488
+ output += `${rule.description}\n`;
1489
+ if (rule.target)
1490
+ output += `Target: \`${rule.target}\`\n`;
1491
+ if (rule.pattern)
1492
+ output += `Pattern: \`${rule.pattern}\`\n`;
1493
+ output += "\n";
1494
+ }
1495
+ return makeResult(output.trimEnd());
1496
+ }
1497
+ // action === "evaluate"
1498
+ if (!config || !config.rules || config.rules.length === 0) {
1499
+ return makeResult("No custom rules to evaluate.\n\n" +
1500
+ "Run with `action: 'init'` to create a default rules.yaml template.");
1501
+ }
1502
+ const results = await evaluateAllRules(absPath);
1503
+ const passed = results.filter(r => r.passed).length;
1504
+ const failed = results.filter(r => !r.passed).length;
1505
+ const errors = results.filter(r => !r.passed && r.severity === "error").length;
1506
+ const warnings = results.filter(r => !r.passed && r.severity === "warning").length;
1507
+ if (output_format === "json") {
1508
+ return makeResult(JSON.stringify({
1509
+ total: results.length,
1510
+ passed,
1511
+ failed,
1512
+ errors,
1513
+ warnings,
1514
+ results,
1515
+ }, null, 2));
1516
+ }
1517
+ let output = `# Custom Rules Evaluation\n\n`;
1518
+ output += `**Results: ${passed}/${results.length} passed**\n`;
1519
+ if (errors > 0)
1520
+ output += `❌ ${errors} error(s)\n`;
1521
+ if (warnings > 0)
1522
+ output += `⚠️ ${warnings} warning(s)\n`;
1523
+ output += "\n";
1524
+ for (const result of results) {
1525
+ const icon = result.passed ? "✓" : result.severity === "error" ? "✗" : "⚠";
1526
+ output += `${icon} **${result.name}**\n`;
1527
+ output += ` ${result.message}\n\n`;
1528
+ }
1529
+ if (errors > 0) {
1530
+ output += `---\n`;
1531
+ output += `⚠️ ${errors} error-level rule(s) failed. Fix these to improve quality.`;
1532
+ }
1533
+ return makeResult(output.trimEnd());
1534
+ }
1535
+ catch (err) {
1536
+ const message = err instanceof Error ? err.message : String(err);
1537
+ return makeResult(`Custom rules operation failed: ${message}`, true);
1538
+ }
1539
+ });
1540
+ }
1541
+ //# sourceMappingURL=management.js.map