@luanpdd/kit-mcp 1.18.0 → 1.20.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 (232) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +648 -648
  3. package/kit/COMANDOS.md +138 -138
  4. package/kit/README.md +52 -52
  5. package/kit/agents/advisor-researcher.md +106 -106
  6. package/kit/agents/assumptions-analyzer.md +107 -107
  7. package/kit/agents/codebase-mapper.md +768 -768
  8. package/kit/agents/debugger.md +772 -772
  9. package/kit/agents/example-reviewer.md +21 -21
  10. package/kit/agents/executor.md +523 -523
  11. package/kit/agents/integration-checker.md +200 -200
  12. package/kit/agents/nyquist-auditor.md +178 -178
  13. package/kit/agents/phase-researcher.md +696 -696
  14. package/kit/agents/plan-checker.md +272 -272
  15. package/kit/agents/planner.md +891 -891
  16. package/kit/agents/project-researcher.md +652 -652
  17. package/kit/agents/research-synthesizer.md +245 -245
  18. package/kit/agents/roadmapper.md +677 -677
  19. package/kit/agents/ui-auditor.md +437 -437
  20. package/kit/agents/ui-checker.md +302 -302
  21. package/kit/agents/ui-researcher.md +355 -355
  22. package/kit/agents/user-profiler.md +175 -175
  23. package/kit/agents/verifier.md +728 -728
  24. package/kit/commands/adicionar-backlog.md +75 -75
  25. package/kit/commands/adicionar-fase.md +42 -42
  26. package/kit/commands/adicionar-tarefa.md +45 -45
  27. package/kit/commands/adicionar-testes.md +41 -41
  28. package/kit/commands/ajuda.md +21 -21
  29. package/kit/commands/atualizar.md +37 -37
  30. package/kit/commands/auditar-marco.md +179 -179
  31. package/kit/commands/auditar-uat.md +23 -23
  32. package/kit/commands/autonomo.md +40 -40
  33. package/kit/commands/branch-pr.md +24 -24
  34. package/kit/commands/burn-rate-status.md +338 -70
  35. package/kit/commands/concluir-marco.md +247 -247
  36. package/kit/commands/configuracoes.md +36 -36
  37. package/kit/commands/definir-perfil.md +10 -10
  38. package/kit/commands/depurar.md +190 -190
  39. package/kit/commands/discutir-fase.md +131 -131
  40. package/kit/commands/entrar-discord.md +17 -17
  41. package/kit/commands/estatisticas.md +18 -18
  42. package/kit/commands/example-greeting.md +33 -33
  43. package/kit/commands/executar-fase.md +58 -58
  44. package/kit/commands/expresso.md +56 -56
  45. package/kit/commands/fase-ui.md +34 -34
  46. package/kit/commands/fazer.md +57 -57
  47. package/kit/commands/fio.md +125 -125
  48. package/kit/commands/fluxos-trabalho.md +64 -64
  49. package/kit/commands/forense.md +176 -176
  50. package/kit/commands/gerenciador.md +38 -38
  51. package/kit/commands/inserir-fase.md +31 -31
  52. package/kit/commands/limpeza.md +17 -17
  53. package/kit/commands/listar-hipoteses-fase.md +45 -45
  54. package/kit/commands/listar-workspaces.md +18 -18
  55. package/kit/commands/mapear-codebase.md +70 -70
  56. package/kit/commands/nota.md +33 -33
  57. package/kit/commands/novo-marco.md +43 -43
  58. package/kit/commands/novo-projeto.md +41 -41
  59. package/kit/commands/novo-workspace.md +43 -43
  60. package/kit/commands/pausar-trabalho.md +37 -37
  61. package/kit/commands/perfil-usuario.md +45 -45
  62. package/kit/commands/pesquisar-fase.md +195 -195
  63. package/kit/commands/planejar-fase.md +67 -67
  64. package/kit/commands/planejar-lacunas.md +33 -33
  65. package/kit/commands/plantar-ideia.md +25 -25
  66. package/kit/commands/progresso.md +24 -24
  67. package/kit/commands/proximo.md +30 -30
  68. package/kit/commands/publicar.md +490 -490
  69. package/kit/commands/rapido.md +35 -35
  70. package/kit/commands/reaplicar-patches.md +124 -124
  71. package/kit/commands/relatorio-sessao.md +19 -19
  72. package/kit/commands/remover-fase.md +31 -31
  73. package/kit/commands/remover-workspace.md +26 -26
  74. package/kit/commands/resumo-marco.md +50 -50
  75. package/kit/commands/retomar-trabalho.md +40 -40
  76. package/kit/commands/revisar-backlog.md +60 -60
  77. package/kit/commands/revisar-ui.md +32 -32
  78. package/kit/commands/revisar.md +37 -37
  79. package/kit/commands/saude.md +21 -21
  80. package/kit/commands/setup-notion.md +93 -93
  81. package/kit/commands/sync-main.md +68 -68
  82. package/kit/commands/validar-fase.md +35 -35
  83. package/kit/commands/verificar-tarefas.md +44 -44
  84. package/kit/commands/verificar-trabalho.md +64 -64
  85. package/kit/file-manifest.json +3 -3
  86. package/kit/framework/bin/lib/commands.cjs +959 -959
  87. package/kit/framework/bin/lib/config.cjs +442 -442
  88. package/kit/framework/bin/lib/core.cjs +1230 -1230
  89. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  90. package/kit/framework/bin/lib/init.cjs +1442 -1442
  91. package/kit/framework/bin/lib/milestone.cjs +252 -252
  92. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  93. package/kit/framework/bin/lib/phase.cjs +888 -888
  94. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  95. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  96. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  97. package/kit/framework/bin/lib/security.cjs +382 -382
  98. package/kit/framework/bin/lib/state.cjs +1031 -1031
  99. package/kit/framework/bin/lib/template.cjs +222 -222
  100. package/kit/framework/bin/lib/uat.cjs +282 -282
  101. package/kit/framework/bin/lib/verify.cjs +888 -888
  102. package/kit/framework/bin/lib/workstream.cjs +491 -491
  103. package/kit/framework/bin/tools.cjs +918 -918
  104. package/kit/framework/commands/workstreams.md +63 -63
  105. package/kit/framework/references/checkpoints.md +778 -778
  106. package/kit/framework/references/continuation-format.md +249 -249
  107. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  108. package/kit/framework/references/git-integration.md +295 -295
  109. package/kit/framework/references/git-planning-commit.md +38 -38
  110. package/kit/framework/references/model-profile-resolution.md +36 -36
  111. package/kit/framework/references/model-profiles.md +139 -139
  112. package/kit/framework/references/phase-argument-parsing.md +61 -61
  113. package/kit/framework/references/planning-config.md +202 -202
  114. package/kit/framework/references/questioning.md +162 -162
  115. package/kit/framework/references/tdd.md +263 -263
  116. package/kit/framework/references/ui-brand.md +160 -160
  117. package/kit/framework/references/user-profiling.md +657 -657
  118. package/kit/framework/references/verification-patterns.md +612 -612
  119. package/kit/framework/references/workstream-flag.md +58 -58
  120. package/kit/framework/templates/DEBUG.md +164 -164
  121. package/kit/framework/templates/UAT.md +265 -265
  122. package/kit/framework/templates/UI-SPEC.md +100 -100
  123. package/kit/framework/templates/VALIDATION.md +76 -76
  124. package/kit/framework/templates/claude-md.md +122 -122
  125. package/kit/framework/templates/codebase/architecture.md +185 -185
  126. package/kit/framework/templates/codebase/concerns.md +205 -205
  127. package/kit/framework/templates/codebase/conventions.md +204 -204
  128. package/kit/framework/templates/codebase/integrations.md +192 -192
  129. package/kit/framework/templates/codebase/stack.md +158 -158
  130. package/kit/framework/templates/codebase/structure.md +199 -199
  131. package/kit/framework/templates/codebase/testing.md +301 -301
  132. package/kit/framework/templates/config.json +44 -44
  133. package/kit/framework/templates/context.md +352 -352
  134. package/kit/framework/templates/continue-here.md +78 -78
  135. package/kit/framework/templates/copilot-instructions.md +7 -7
  136. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  137. package/kit/framework/templates/dev-preferences.md +20 -20
  138. package/kit/framework/templates/discovery.md +146 -146
  139. package/kit/framework/templates/discussion-log.md +63 -63
  140. package/kit/framework/templates/milestone-archive.md +123 -123
  141. package/kit/framework/templates/milestone.md +115 -115
  142. package/kit/framework/templates/phase-prompt.md +610 -610
  143. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  144. package/kit/framework/templates/project.md +186 -186
  145. package/kit/framework/templates/requirements.md +231 -231
  146. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  147. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  148. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  149. package/kit/framework/templates/research-project/STACK.md +120 -120
  150. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  151. package/kit/framework/templates/research.md +419 -419
  152. package/kit/framework/templates/retrospective.md +54 -54
  153. package/kit/framework/templates/roadmap.md +202 -202
  154. package/kit/framework/templates/state.md +176 -176
  155. package/kit/framework/templates/summary-complex.md +59 -59
  156. package/kit/framework/templates/summary-minimal.md +41 -41
  157. package/kit/framework/templates/summary-standard.md +48 -48
  158. package/kit/framework/templates/summary.md +209 -209
  159. package/kit/framework/templates/user-profile.md +146 -146
  160. package/kit/framework/templates/user-setup.md +256 -256
  161. package/kit/framework/templates/verification-report.md +258 -258
  162. package/kit/framework/workflows/add-phase.md +112 -112
  163. package/kit/framework/workflows/add-tests.md +351 -351
  164. package/kit/framework/workflows/add-todo.md +158 -158
  165. package/kit/framework/workflows/audit-milestone.md +340 -340
  166. package/kit/framework/workflows/audit-uat.md +109 -109
  167. package/kit/framework/workflows/autonomous.md +891 -891
  168. package/kit/framework/workflows/check-todos.md +177 -177
  169. package/kit/framework/workflows/cleanup.md +152 -152
  170. package/kit/framework/workflows/complete-milestone.md +696 -696
  171. package/kit/framework/workflows/diagnose-issues.md +231 -231
  172. package/kit/framework/workflows/discovery-phase.md +289 -289
  173. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  174. package/kit/framework/workflows/discuss-phase.md +784 -784
  175. package/kit/framework/workflows/do.md +104 -104
  176. package/kit/framework/workflows/execute-phase.md +838 -838
  177. package/kit/framework/workflows/execute-plan.md +510 -510
  178. package/kit/framework/workflows/fast.md +102 -102
  179. package/kit/framework/workflows/forensics.md +265 -265
  180. package/kit/framework/workflows/health.md +181 -181
  181. package/kit/framework/workflows/help.md +619 -619
  182. package/kit/framework/workflows/insert-phase.md +130 -130
  183. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  184. package/kit/framework/workflows/list-workspaces.md +56 -56
  185. package/kit/framework/workflows/manager.md +362 -362
  186. package/kit/framework/workflows/map-codebase.md +377 -377
  187. package/kit/framework/workflows/milestone-summary.md +223 -223
  188. package/kit/framework/workflows/new-milestone.md +486 -486
  189. package/kit/framework/workflows/new-project.md +1159 -1159
  190. package/kit/framework/workflows/new-workspace.md +237 -237
  191. package/kit/framework/workflows/next.md +97 -97
  192. package/kit/framework/workflows/node-repair.md +92 -92
  193. package/kit/framework/workflows/note.md +156 -156
  194. package/kit/framework/workflows/pause-work.md +176 -176
  195. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  196. package/kit/framework/workflows/plan-phase.md +765 -765
  197. package/kit/framework/workflows/plant-seed.md +169 -169
  198. package/kit/framework/workflows/pr-branch.md +129 -129
  199. package/kit/framework/workflows/profile-user.md +450 -450
  200. package/kit/framework/workflows/progress.md +507 -507
  201. package/kit/framework/workflows/quick.md +757 -757
  202. package/kit/framework/workflows/remove-phase.md +155 -155
  203. package/kit/framework/workflows/remove-workspace.md +90 -90
  204. package/kit/framework/workflows/research-phase.md +82 -82
  205. package/kit/framework/workflows/resume-project.md +326 -326
  206. package/kit/framework/workflows/review.md +228 -228
  207. package/kit/framework/workflows/session-report.md +146 -146
  208. package/kit/framework/workflows/settings.md +283 -283
  209. package/kit/framework/workflows/ship.md +228 -228
  210. package/kit/framework/workflows/stats.md +60 -60
  211. package/kit/framework/workflows/transition.md +671 -671
  212. package/kit/framework/workflows/ui-phase.md +302 -302
  213. package/kit/framework/workflows/ui-review.md +165 -165
  214. package/kit/framework/workflows/update.md +323 -323
  215. package/kit/framework/workflows/validate-phase.md +174 -174
  216. package/kit/framework/workflows/verify-phase.md +252 -252
  217. package/kit/framework/workflows/verify-work.md +637 -637
  218. package/kit/hooks/check-update.js +118 -118
  219. package/kit/hooks/context-monitor.js +163 -163
  220. package/kit/hooks/prompt-guard.js +103 -103
  221. package/kit/hooks/statusline.js +125 -125
  222. package/kit/hooks/workflow-guard.js +101 -101
  223. package/kit/settings.json +45 -45
  224. package/kit/skills/example-skill/SKILL.md +42 -42
  225. package/package.json +63 -59
  226. package/src/core/kit.js +216 -216
  227. package/src/core/metrics.js +135 -10
  228. package/src/core/reflect.js +247 -247
  229. package/src/core/reverse-sync.js +372 -372
  230. package/src/core/sync.js +418 -418
  231. package/src/core/watch.js +121 -121
  232. package/src/mcp-server/index.js +34 -3
package/src/core/kit.js CHANGED
@@ -1,216 +1,216 @@
1
- // Read the canonical kit/ directory and return a structured index.
2
- // Source of truth: kit/agents/*.md, kit/commands/*.md, kit/skills/*/SKILL.md
3
- //
4
- // Frontmatter is parsed loosely (no external dep) — we only need name & description.
5
-
6
- import path from 'node:path';
7
- import fs from 'node:fs/promises';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- // PERF-02: Frontmatter regexes compiled once at module load (was being recompiled
14
- // on every readMdDir / readSkillsDir entry — 60+ times per listKit call).
15
- const FRONTMATTER_SPLIT_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
16
- const FRONTMATTER_RAW_RE = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)/;
17
- const YAML_KEY_RE = /^([A-Za-z0-9_-]+):\s*(.*)$/;
18
-
19
- // Resolution order for the kit root (re-evaluated on each call so env-var
20
- // overrides set after module load — e.g. by the CLI preAction hook — work):
21
- // 1. explicit `kitRoot` opt passed by caller
22
- // 2. KIT_MCP_KIT_ROOT env var (per-session override)
23
- // 3. ./kit relative to this package (the bundled example kit)
24
- export const BUNDLED_KIT_ROOT = path.resolve(__dirname, '../../kit');
25
- export function resolveKitRoot(kitRoot) {
26
- if (kitRoot) return path.resolve(kitRoot);
27
- if (process.env.KIT_MCP_KIT_ROOT) return path.resolve(process.env.KIT_MCP_KIT_ROOT);
28
- return BUNDLED_KIT_ROOT;
29
- }
30
-
31
- // PERF-01: TTL cache for listKit output. Repeated calls within KIT_CACHE_TTL_MS
32
- // return the cached value — sync/reverse-sync/MCP list-* tools used to walk the
33
- // disk on every invocation. Trade-off: callers that edit kit/ inside the same
34
- // process may see stale data for up to 30s. Acceptable for MCP/CLI ergonomics.
35
- const KIT_CACHE_TTL_MS = 30_000;
36
- const kitCache = new Map(); // `${kitRoot}:${mode}` -> { value, ts }
37
-
38
- // PERF-S1: when sync runs in mode=reference (default), the body/content of each
39
- // kit file is never used — only frontmatter (name + description). Reading just
40
- // the first STUB_READ_BYTES is enough for any frontmatter we'd ever produce and
41
- // avoids loading 50 KB+ files (planner.md etc) from disk.
42
- const STUB_READ_BYTES = 4096;
43
-
44
- export function clearKitCache() { kitCache.clear(); }
45
-
46
- export async function listKit(kitRoot, opts = {}) {
47
- kitRoot = resolveKitRoot(kitRoot);
48
- const stubsOnly = opts.stubsOnly === true;
49
- const cacheKey = `${kitRoot}:${stubsOnly ? 'stubs' : 'full'}`;
50
- const cached = kitCache.get(cacheKey);
51
- if (cached && Date.now() - cached.ts < KIT_CACHE_TTL_MS) {
52
- return cached.value;
53
- }
54
- const [agents, commands, skills, skillsExtras] = await Promise.all([
55
- readMdDir(path.join(kitRoot, 'agents'), 'agent', { stubsOnly }),
56
- readMdDir(path.join(kitRoot, 'commands'), 'command', { stubsOnly }),
57
- readSkillsDir(path.join(kitRoot, 'skills'), { stubsOnly }),
58
- readSkillsDir(path.join(kitRoot, 'skills-extras'), { stubsOnly }).catch(() => []),
59
- ]);
60
- const value = { agents, commands, skills, skillsExtras, kitRoot, stubsOnly };
61
- kitCache.set(cacheKey, { value, ts: Date.now() });
62
- return value;
63
- }
64
-
65
- // Read just enough bytes from the head of the file to capture the frontmatter.
66
- // Returns the partial string. fs.open + fd.read avoids the OS pre-fetching the
67
- // rest of the file (which fs.readFile would force).
68
- async function readHead(absPath, n) {
69
- const fd = await fs.open(absPath, 'r');
70
- try {
71
- const buf = Buffer.alloc(n);
72
- const { bytesRead } = await fd.read(buf, 0, n, 0);
73
- return buf.subarray(0, bytesRead).toString('utf8');
74
- } finally {
75
- await fd.close();
76
- }
77
- }
78
-
79
- async function readMdDir(dir, kind, { stubsOnly = false } = {}) {
80
- let entries;
81
- try {
82
- entries = await fs.readdir(dir, { withFileTypes: true });
83
- } catch {
84
- return [];
85
- }
86
- const out = [];
87
- for (const e of entries) {
88
- if (!e.isFile() || !e.name.endsWith('.md')) continue;
89
- const absPath = path.join(dir, e.name);
90
- const raw = stubsOnly
91
- ? await readHead(absPath, STUB_READ_BYTES)
92
- : await fs.readFile(absPath, 'utf8');
93
- const { frontmatter, body } = splitFrontmatter(raw);
94
- const item = {
95
- kind,
96
- name: e.name.replace(/\.md$/, ''),
97
- absPath,
98
- frontmatter,
99
- frontmatterRaw: matchFrontmatterRaw(raw),
100
- description: frontmatter?.description ?? firstNonEmptyLine(body),
101
- };
102
- if (!stubsOnly) {
103
- item.body = body;
104
- item.content = raw;
105
- }
106
- out.push(item);
107
- }
108
- return out.sort((a, b) => a.name.localeCompare(b.name));
109
- }
110
-
111
- async function readSkillsDir(dir, { stubsOnly = false } = {}) {
112
- let entries;
113
- try {
114
- entries = await fs.readdir(dir, { withFileTypes: true });
115
- } catch {
116
- return [];
117
- }
118
- const out = [];
119
- for (const e of entries) {
120
- if (!e.isDirectory()) continue;
121
- const skillPath = path.join(dir, e.name, 'SKILL.md');
122
- let raw;
123
- try {
124
- raw = stubsOnly
125
- ? await readHead(skillPath, STUB_READ_BYTES)
126
- : await fs.readFile(skillPath, 'utf8');
127
- } catch { continue; }
128
- const { frontmatter, body } = splitFrontmatter(raw);
129
- const item = {
130
- kind: 'skill',
131
- name: e.name,
132
- absPath: skillPath,
133
- dirPath: path.join(dir, e.name),
134
- frontmatter,
135
- frontmatterRaw: matchFrontmatterRaw(raw),
136
- description: frontmatter?.description ?? firstNonEmptyLine(body),
137
- };
138
- if (!stubsOnly) {
139
- item.body = body;
140
- item.skillContent = raw;
141
- }
142
- out.push(item);
143
- }
144
- return out.sort((a, b) => a.name.localeCompare(b.name));
145
- }
146
-
147
- // --- minimal YAML-ish frontmatter parser (no deps) ---
148
- // Handles `key: value`, `key: >` multiline, but NOT nested objects/arrays.
149
- // Good enough for our SKILL.md / agent.md headers.
150
-
151
- function splitFrontmatter(raw) {
152
- const m = raw.match(FRONTMATTER_SPLIT_RE);
153
- if (!m) return { frontmatter: null, body: raw };
154
- return { frontmatter: parseLooseYaml(m[1]), body: m[2] };
155
- }
156
-
157
- function matchFrontmatterRaw(raw) {
158
- const m = raw.match(FRONTMATTER_RAW_RE);
159
- return m ? m[1] : '';
160
- }
161
-
162
- function parseLooseYaml(text) {
163
- const out = {};
164
- const lines = text.split(/\r?\n/);
165
- let i = 0;
166
- while (i < lines.length) {
167
- const line = lines[i];
168
- const m = line.match(YAML_KEY_RE);
169
- if (!m) { i++; continue; }
170
- const key = m[1];
171
- let val = m[2];
172
- if (val === '>' || val === '|') {
173
- // Multiline: collect indented lines
174
- const collected = [];
175
- i++;
176
- while (i < lines.length && /^\s+/.test(lines[i])) {
177
- collected.push(lines[i].replace(/^\s+/, ''));
178
- i++;
179
- }
180
- out[key] = collected.join(' ').trim();
181
- continue;
182
- }
183
- out[key] = val.trim().replace(/^["']|["']$/g, '');
184
- i++;
185
- }
186
- return out;
187
- }
188
-
189
- function firstNonEmptyLine(body) {
190
- for (const line of body.split(/\r?\n/)) {
191
- const t = line.trim();
192
- if (!t) continue; // blank
193
- if (t.startsWith('#')) continue; // markdown heading
194
- if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
195
- return t.slice(0, 200);
196
- }
197
- return '';
198
- }
199
-
200
- // --- search helpers ---
201
-
202
- export function searchKit(kit, query) {
203
- const q = query.toLowerCase();
204
- const all = [...kit.agents, ...kit.commands, ...kit.skills, ...kit.skillsExtras];
205
- return all.filter(item =>
206
- item.name.toLowerCase().includes(q) ||
207
- (item.description ?? '').toLowerCase().includes(q)
208
- ).map(({ kind, name, description, absPath }) => ({ kind, name, description, absPath }));
209
- }
210
-
211
- export function findItem(kit, kind, name) {
212
- const buckets = { agent: kit.agents, command: kit.commands, skill: [...kit.skills, ...kit.skillsExtras] };
213
- const b = buckets[kind];
214
- if (!b) throw new Error(`Unknown kind: ${kind}`);
215
- return b.find(x => x.name === name) ?? null;
216
- }
1
+ // Read the canonical kit/ directory and return a structured index.
2
+ // Source of truth: kit/agents/*.md, kit/commands/*.md, kit/skills/*/SKILL.md
3
+ //
4
+ // Frontmatter is parsed loosely (no external dep) — we only need name & description.
5
+
6
+ import path from 'node:path';
7
+ import fs from 'node:fs/promises';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // PERF-02: Frontmatter regexes compiled once at module load (was being recompiled
14
+ // on every readMdDir / readSkillsDir entry — 60+ times per listKit call).
15
+ const FRONTMATTER_SPLIT_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
16
+ const FRONTMATTER_RAW_RE = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)/;
17
+ const YAML_KEY_RE = /^([A-Za-z0-9_-]+):\s*(.*)$/;
18
+
19
+ // Resolution order for the kit root (re-evaluated on each call so env-var
20
+ // overrides set after module load — e.g. by the CLI preAction hook — work):
21
+ // 1. explicit `kitRoot` opt passed by caller
22
+ // 2. KIT_MCP_KIT_ROOT env var (per-session override)
23
+ // 3. ./kit relative to this package (the bundled example kit)
24
+ export const BUNDLED_KIT_ROOT = path.resolve(__dirname, '../../kit');
25
+ export function resolveKitRoot(kitRoot) {
26
+ if (kitRoot) return path.resolve(kitRoot);
27
+ if (process.env.KIT_MCP_KIT_ROOT) return path.resolve(process.env.KIT_MCP_KIT_ROOT);
28
+ return BUNDLED_KIT_ROOT;
29
+ }
30
+
31
+ // PERF-01: TTL cache for listKit output. Repeated calls within KIT_CACHE_TTL_MS
32
+ // return the cached value — sync/reverse-sync/MCP list-* tools used to walk the
33
+ // disk on every invocation. Trade-off: callers that edit kit/ inside the same
34
+ // process may see stale data for up to 30s. Acceptable for MCP/CLI ergonomics.
35
+ const KIT_CACHE_TTL_MS = 30_000;
36
+ const kitCache = new Map(); // `${kitRoot}:${mode}` -> { value, ts }
37
+
38
+ // PERF-S1: when sync runs in mode=reference (default), the body/content of each
39
+ // kit file is never used — only frontmatter (name + description). Reading just
40
+ // the first STUB_READ_BYTES is enough for any frontmatter we'd ever produce and
41
+ // avoids loading 50 KB+ files (planner.md etc) from disk.
42
+ const STUB_READ_BYTES = 4096;
43
+
44
+ export function clearKitCache() { kitCache.clear(); }
45
+
46
+ export async function listKit(kitRoot, opts = {}) {
47
+ kitRoot = resolveKitRoot(kitRoot);
48
+ const stubsOnly = opts.stubsOnly === true;
49
+ const cacheKey = `${kitRoot}:${stubsOnly ? 'stubs' : 'full'}`;
50
+ const cached = kitCache.get(cacheKey);
51
+ if (cached && Date.now() - cached.ts < KIT_CACHE_TTL_MS) {
52
+ return cached.value;
53
+ }
54
+ const [agents, commands, skills, skillsExtras] = await Promise.all([
55
+ readMdDir(path.join(kitRoot, 'agents'), 'agent', { stubsOnly }),
56
+ readMdDir(path.join(kitRoot, 'commands'), 'command', { stubsOnly }),
57
+ readSkillsDir(path.join(kitRoot, 'skills'), { stubsOnly }),
58
+ readSkillsDir(path.join(kitRoot, 'skills-extras'), { stubsOnly }).catch(() => []),
59
+ ]);
60
+ const value = { agents, commands, skills, skillsExtras, kitRoot, stubsOnly };
61
+ kitCache.set(cacheKey, { value, ts: Date.now() });
62
+ return value;
63
+ }
64
+
65
+ // Read just enough bytes from the head of the file to capture the frontmatter.
66
+ // Returns the partial string. fs.open + fd.read avoids the OS pre-fetching the
67
+ // rest of the file (which fs.readFile would force).
68
+ async function readHead(absPath, n) {
69
+ const fd = await fs.open(absPath, 'r');
70
+ try {
71
+ const buf = Buffer.alloc(n);
72
+ const { bytesRead } = await fd.read(buf, 0, n, 0);
73
+ return buf.subarray(0, bytesRead).toString('utf8');
74
+ } finally {
75
+ await fd.close();
76
+ }
77
+ }
78
+
79
+ async function readMdDir(dir, kind, { stubsOnly = false } = {}) {
80
+ let entries;
81
+ try {
82
+ entries = await fs.readdir(dir, { withFileTypes: true });
83
+ } catch {
84
+ return [];
85
+ }
86
+ const out = [];
87
+ for (const e of entries) {
88
+ if (!e.isFile() || !e.name.endsWith('.md')) continue;
89
+ const absPath = path.join(dir, e.name);
90
+ const raw = stubsOnly
91
+ ? await readHead(absPath, STUB_READ_BYTES)
92
+ : await fs.readFile(absPath, 'utf8');
93
+ const { frontmatter, body } = splitFrontmatter(raw);
94
+ const item = {
95
+ kind,
96
+ name: e.name.replace(/\.md$/, ''),
97
+ absPath,
98
+ frontmatter,
99
+ frontmatterRaw: matchFrontmatterRaw(raw),
100
+ description: frontmatter?.description ?? firstNonEmptyLine(body),
101
+ };
102
+ if (!stubsOnly) {
103
+ item.body = body;
104
+ item.content = raw;
105
+ }
106
+ out.push(item);
107
+ }
108
+ return out.sort((a, b) => a.name.localeCompare(b.name));
109
+ }
110
+
111
+ async function readSkillsDir(dir, { stubsOnly = false } = {}) {
112
+ let entries;
113
+ try {
114
+ entries = await fs.readdir(dir, { withFileTypes: true });
115
+ } catch {
116
+ return [];
117
+ }
118
+ const out = [];
119
+ for (const e of entries) {
120
+ if (!e.isDirectory()) continue;
121
+ const skillPath = path.join(dir, e.name, 'SKILL.md');
122
+ let raw;
123
+ try {
124
+ raw = stubsOnly
125
+ ? await readHead(skillPath, STUB_READ_BYTES)
126
+ : await fs.readFile(skillPath, 'utf8');
127
+ } catch { continue; }
128
+ const { frontmatter, body } = splitFrontmatter(raw);
129
+ const item = {
130
+ kind: 'skill',
131
+ name: e.name,
132
+ absPath: skillPath,
133
+ dirPath: path.join(dir, e.name),
134
+ frontmatter,
135
+ frontmatterRaw: matchFrontmatterRaw(raw),
136
+ description: frontmatter?.description ?? firstNonEmptyLine(body),
137
+ };
138
+ if (!stubsOnly) {
139
+ item.body = body;
140
+ item.skillContent = raw;
141
+ }
142
+ out.push(item);
143
+ }
144
+ return out.sort((a, b) => a.name.localeCompare(b.name));
145
+ }
146
+
147
+ // --- minimal YAML-ish frontmatter parser (no deps) ---
148
+ // Handles `key: value`, `key: >` multiline, but NOT nested objects/arrays.
149
+ // Good enough for our SKILL.md / agent.md headers.
150
+
151
+ function splitFrontmatter(raw) {
152
+ const m = raw.match(FRONTMATTER_SPLIT_RE);
153
+ if (!m) return { frontmatter: null, body: raw };
154
+ return { frontmatter: parseLooseYaml(m[1]), body: m[2] };
155
+ }
156
+
157
+ function matchFrontmatterRaw(raw) {
158
+ const m = raw.match(FRONTMATTER_RAW_RE);
159
+ return m ? m[1] : '';
160
+ }
161
+
162
+ function parseLooseYaml(text) {
163
+ const out = {};
164
+ const lines = text.split(/\r?\n/);
165
+ let i = 0;
166
+ while (i < lines.length) {
167
+ const line = lines[i];
168
+ const m = line.match(YAML_KEY_RE);
169
+ if (!m) { i++; continue; }
170
+ const key = m[1];
171
+ let val = m[2];
172
+ if (val === '>' || val === '|') {
173
+ // Multiline: collect indented lines
174
+ const collected = [];
175
+ i++;
176
+ while (i < lines.length && /^\s+/.test(lines[i])) {
177
+ collected.push(lines[i].replace(/^\s+/, ''));
178
+ i++;
179
+ }
180
+ out[key] = collected.join(' ').trim();
181
+ continue;
182
+ }
183
+ out[key] = val.trim().replace(/^["']|["']$/g, '');
184
+ i++;
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function firstNonEmptyLine(body) {
190
+ for (const line of body.split(/\r?\n/)) {
191
+ const t = line.trim();
192
+ if (!t) continue; // blank
193
+ if (t.startsWith('#')) continue; // markdown heading
194
+ if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
195
+ return t.slice(0, 200);
196
+ }
197
+ return '';
198
+ }
199
+
200
+ // --- search helpers ---
201
+
202
+ export function searchKit(kit, query) {
203
+ const q = query.toLowerCase();
204
+ const all = [...kit.agents, ...kit.commands, ...kit.skills, ...kit.skillsExtras];
205
+ return all.filter(item =>
206
+ item.name.toLowerCase().includes(q) ||
207
+ (item.description ?? '').toLowerCase().includes(q)
208
+ ).map(({ kind, name, description, absPath }) => ({ kind, name, description, absPath }));
209
+ }
210
+
211
+ export function findItem(kit, kind, name) {
212
+ const buckets = { agent: kit.agents, command: kit.commands, skill: [...kit.skills, ...kit.skillsExtras] };
213
+ const b = buckets[kind];
214
+ if (!b) throw new Error(`Unknown kind: ${kind}`);
215
+ return b.find(x => x.name === name) ?? null;
216
+ }
@@ -1,4 +1,5 @@
1
1
  // OBS-18-01 / OBS-18-02 — in-memory golden signals for kit-mcp server.
2
+ // OBS-19-01 / OBS-19-02 / OBS-19-03 — disk-persistent rolling snapshots.
2
3
  //
3
4
  // Phase 94: Eat Your Own Dog Food. The skill `four-golden-signals` says any
4
5
  // user-facing service worth its salt instruments Latency + Traffic + Errors
@@ -7,31 +8,48 @@
7
8
  // operator wants when something feels off.
8
9
  //
9
10
  // Scope decisions (see .planning/phases/94-golden-signals-mcp-server/94-CONTEXT.md):
10
- // - Zero dependencies. Map + array stdlib only — preserves the 6-deps budget
11
- // that Phase 92.01 fought to maintain and that Phase 93.01 enforces in CI.
12
- // - In-memory only. No file persistence, no socket export, no OTel SDK.
13
- // kit-mcp is a developer tool launched on demand by an IDE; cross-process
14
- // telemetry pipelines are explicit non-goals (see <deferred> block in
15
- // 94-CONTEXT.md). A future phase can layer OTel on top of this API.
11
+ // - Zero new dependencies. Phase 99 adds fs/promises + path from stdlib only —
12
+ // the 6-deps budget Phase 92.01 fought to maintain and Phase 93.01 enforces
13
+ // in CI is preserved.
14
+ // - In-memory primary, on-demand persistence. The Map+array core stays
15
+ // in-memory; persistSnapshot writes a JSON file under .planning/metrics/
16
+ // snapshots/ when called. No background timer, no implicit writes the
17
+ // /burn-rate-status command and metrics-snapshot tool are the writers.
16
18
  // - Bounded memory. Histograms cap at HISTOGRAM_CAP=1000 samples per tool
17
- // with FIFO drop. At cap, p50/p95/p99 over the latest 1000 samples is
18
- // more useful than an unbounded array that could grow for the lifetime
19
- // of a long-lived MCP session.
19
+ // with FIFO drop.
20
+ // - Bounded disk. cleanupOldSnapshots prunes files > 30 days old on every
21
+ // persistSnapshot call (rolling window, no separate retention job).
20
22
  // - Snapshot is read-only. Returns a fresh plain-object copy so callers
21
23
  // can JSON.stringify it without exposing internal Map references.
24
+ // - Persisted shape includes `ts` (epoch ms) inside the JSON. We do NOT
25
+ // parse the filename for windowing — filesystem-safe ISO encoding
26
+ // (`replace(/[:.]/g, '-')`) is one-way (cannot reliably round-trip back
27
+ // through Date.parse) and mtime is unreliable across copy/touch. The
28
+ // in-file ts is authoritative.
22
29
  //
23
- // API surface (4 exports):
30
+ // API surface (5 exports + 2 async):
24
31
  // incrementInvocation(tool, status) — counter++ keyed `${tool}:${status}`
25
32
  // recordLatency(tool, ms) — push to histogram, FIFO at cap
26
33
  // snapshot() — { counters, latency } plain object
27
34
  // reset() — clear both maps; called on boot if
28
35
  // KIT_MCP_METRICS_RESET=1
36
+ // persistSnapshot(rootDir) — write {ts, counters, latency} to
37
+ // .planning/metrics/snapshots/<ts>.json
38
+ // + cleanup files > 30d
39
+ // loadSnapshots(rootDir, windowMs) — read all snapshots whose in-file ts
40
+ // is within windowMs (default 30d),
41
+ // sorted ascending by ts
29
42
  //
30
43
  // Boot-time reset honors the env var by calling reset() at module load when
31
44
  // the flag is set. This keeps the signal "fresh" for a probe in tests or for
32
45
  // an operator who spawned the server with the flag for a clean comparison.
33
46
 
47
+ import fs from 'node:fs/promises';
48
+ import path from 'node:path';
49
+
34
50
  const HISTOGRAM_CAP = 1000;
51
+ const DEFAULT_RETENTION_MS = 30 * 86400 * 1000; // 30 days rolling.
52
+ const SNAPSHOT_DIR_REL = path.join('.planning', 'metrics', 'snapshots');
35
53
 
36
54
  const counters = new Map(); // key: `${tool}:${status}` → count (number)
37
55
  const histograms = new Map(); // key: tool → number[] (length ≤ HISTOGRAM_CAP)
@@ -130,6 +148,112 @@ export function reset() {
130
148
  histograms.clear();
131
149
  }
132
150
 
151
+ /**
152
+ * OBS-19-01 — Persist the current snapshot to disk under
153
+ * `<rootDir>/.planning/metrics/snapshots/<timestamp>.json`. Runs the rolling
154
+ * cleanup of files older than `retentionMs` (default 30d) on every call so
155
+ * callers don't need a separate retention job.
156
+ *
157
+ * The on-disk shape is `{ ts: <epoch_ms>, counters, latency }`. The `ts` field
158
+ * inside the JSON — NOT the filename — is the authoritative timestamp for
159
+ * loadSnapshots windowing. The filename uses an ISO encoding with `:` and `.`
160
+ * replaced by `-` for filesystem safety; that encoding is one-way (cannot
161
+ * round-trip back through Date.parse), so we never parse it for ordering.
162
+ *
163
+ * @param {string} [rootDir=process.cwd()] Project root. Snapshots land under
164
+ * `<rootDir>/.planning/metrics/snapshots/`.
165
+ * @param {object} [opts]
166
+ * @param {number} [opts.retentionMs] Override the rolling-window age in ms.
167
+ * Defaults to 30 days. Tests use shorter windows to drive the cleanup path.
168
+ * @returns {Promise<{file: string, snap: {ts: number, counters: object, latency: object}}>}
169
+ */
170
+ export async function persistSnapshot(rootDir = process.cwd(), opts = {}) {
171
+ const retentionMs = Number.isFinite(opts.retentionMs) ? opts.retentionMs : DEFAULT_RETENTION_MS;
172
+ const dir = path.join(rootDir, SNAPSHOT_DIR_REL);
173
+ await fs.mkdir(dir, { recursive: true });
174
+ const ts = Date.now();
175
+ const snap = { ts, ...snapshot() };
176
+ // Filesystem-safe ISO encoding — Windows forbids `:` in paths and `.` is
177
+ // ambiguous with extension separators on shells with brace expansion.
178
+ const isoSafe = new Date(ts).toISOString().replace(/[:.]/g, '-');
179
+ const file = path.join(dir, `${isoSafe}.json`);
180
+ await fs.writeFile(file, JSON.stringify(snap, null, 2));
181
+ await cleanupOldSnapshots(dir, retentionMs);
182
+ return { file, snap };
183
+ }
184
+
185
+ /**
186
+ * OBS-19-02 — Load all snapshots from disk whose in-file `ts` is within the
187
+ * sliding window. Returns the array sorted ascending by `ts` so consumers
188
+ * (`/burn-rate-status`) can compute first-vs-last deltas without re-sorting.
189
+ *
190
+ * Defensive against malformed JSON: a corrupt file is skipped silently rather
191
+ * than aborting the whole load. The 30d window is rolling from "now" — pass a
192
+ * smaller value to drive recent-only views (e.g. `60 * 60 * 1000` for last
193
+ * hour) when computing burn rate over a baseline window.
194
+ *
195
+ * @param {string} [rootDir=process.cwd()] Project root.
196
+ * @param {number} [windowMs] Sliding window in ms. Defaults to 30 days.
197
+ * @returns {Promise<Array<{ts: number, counters: object, latency: object}>>}
198
+ * Empty array if the snapshots directory does not exist.
199
+ */
200
+ export async function loadSnapshots(rootDir = process.cwd(), windowMs = DEFAULT_RETENTION_MS) {
201
+ const dir = path.join(rootDir, SNAPSHOT_DIR_REL);
202
+ const cutoff = Date.now() - windowMs;
203
+ let files;
204
+ try {
205
+ files = await fs.readdir(dir);
206
+ } catch {
207
+ return []; // Dir absent on first run — not an error.
208
+ }
209
+ const results = [];
210
+ for (const f of files) {
211
+ if (!f.endsWith('.json')) continue;
212
+ try {
213
+ const raw = await fs.readFile(path.join(dir, f), 'utf-8');
214
+ const parsed = JSON.parse(raw);
215
+ if (Number.isFinite(parsed?.ts) && parsed.ts >= cutoff) {
216
+ results.push(parsed);
217
+ }
218
+ } catch {
219
+ // Corrupt file — skip silently rather than break the whole burn-rate
220
+ // calculation. A future phase can surface counts via a doctor probe.
221
+ }
222
+ }
223
+ return results.sort((a, b) => a.ts - b.ts);
224
+ }
225
+
226
+ /**
227
+ * OBS-19-03 — Internal helper: delete snapshot files older than `maxAgeMs`.
228
+ * Called from persistSnapshot on every write so retention is implicit.
229
+ * Uses fs.stat().mtimeMs as the age proxy; we accept the small drift versus
230
+ * the in-file `ts` because cleanup is best-effort eviction, not authoritative
231
+ * windowing (loadSnapshots reads the in-file ts).
232
+ *
233
+ * @param {string} dir Absolute path to the snapshots directory.
234
+ * @param {number} maxAgeMs Files with mtime older than this are unlinked.
235
+ * @returns {Promise<void>}
236
+ */
237
+ async function cleanupOldSnapshots(dir, maxAgeMs) {
238
+ const cutoff = Date.now() - maxAgeMs;
239
+ let files;
240
+ try {
241
+ files = await fs.readdir(dir);
242
+ } catch {
243
+ return;
244
+ }
245
+ for (const f of files) {
246
+ if (!f.endsWith('.json')) continue;
247
+ const fp = path.join(dir, f);
248
+ try {
249
+ const stat = await fs.stat(fp);
250
+ if (stat.mtimeMs < cutoff) await fs.unlink(fp);
251
+ } catch {
252
+ // Unlink can race with concurrent cleanup; ignore ENOENT and friends.
253
+ }
254
+ }
255
+ }
256
+
133
257
  // Boot-time reset honors KIT_MCP_METRICS_RESET=1. We call reset() instead of
134
258
  // merely skipping init because the maps are already empty at module load —
135
259
  // the call is a no-op today but documents the contract for any future module
@@ -141,3 +265,4 @@ if (process.env.KIT_MCP_METRICS_RESET === '1') {
141
265
  // Exported for tests only — keeps the API surface explicit while letting unit
142
266
  // tests assert on the FIFO behavior at the boundary.
143
267
  export const __TEST_HISTOGRAM_CAP = HISTOGRAM_CAP;
268
+ export const __TEST_SNAPSHOT_DIR_REL = SNAPSHOT_DIR_REL;