@bcelep/capint 0.6.5 → 0.7.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 (55) hide show
  1. package/AGENT.md +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/bin/capint.js +11 -2
  4. package/docs/PRD-capint.md +94 -0
  5. package/docs/PRD-v0.5-agent-capability-activation.md +10 -1
  6. package/docs/architecture-decisions.md +2 -0
  7. package/docs/capint-rehber.md +439 -0
  8. package/docs/capint-stack.md +118 -0
  9. package/docs/community.md +19 -0
  10. package/docs/conventions/daily-use.md +15 -1
  11. package/docs/eval/beginner-test-protocol.md +39 -0
  12. package/docs/eval/survey-template.md +16 -0
  13. package/docs/generated/README.md +6 -0
  14. package/docs/generated/execution-intent.v1.md +211 -0
  15. package/docs/generated/explanation.v1.md +127 -0
  16. package/docs/generated/explanation.v2.md +142 -0
  17. package/docs/generated/ui-api.v1.md +178 -0
  18. package/docs/maintainer-dogfood.md +3 -2
  19. package/docs/recipes/memory-graph-pairing.md +200 -0
  20. package/docs/skill-audit-runbook.md +32 -0
  21. package/locales/en.json +27 -0
  22. package/locales/tr.json +27 -0
  23. package/package.json +4 -2
  24. package/projections/session-start.md +14 -2
  25. package/schemas/execution-intent.v1.json +69 -0
  26. package/schemas/explanation.v2.json +51 -0
  27. package/schemas/ui-api.v1.json +85 -0
  28. package/scripts/generate-schema-docs.mjs +104 -0
  29. package/scripts/release-check.mjs +10 -0
  30. package/skills/prismx-skill-gateway/SKILL.md +10 -3
  31. package/src/commands/doctor.js +9 -0
  32. package/src/commands/feedback.js +29 -0
  33. package/src/commands/init.js +12 -0
  34. package/src/commands/providers.js +54 -0
  35. package/src/commands/skill.js +27 -0
  36. package/src/commands/stack.js +97 -0
  37. package/src/lib/audit.js +9 -1
  38. package/src/lib/contract.js +9 -0
  39. package/src/lib/doctor.js +69 -3
  40. package/src/lib/execution-policy.js +3 -1
  41. package/src/lib/explanation-plain.js +72 -0
  42. package/src/lib/explanation.js +26 -2
  43. package/src/lib/feedback.js +65 -0
  44. package/src/lib/i18n.js +43 -0
  45. package/src/lib/providers/doctor.js +205 -0
  46. package/src/lib/scaffold/index.js +2 -1
  47. package/src/lib/scaffold/presets.js +20 -0
  48. package/src/lib/skill-audit.js +141 -0
  49. package/src/lib/stack/index.js +259 -0
  50. package/src/lib/ui-server.js +92 -5
  51. package/templates/minimal/DONE.md +10 -0
  52. package/templates/minimal/GUNLUK.md +15 -1
  53. package/templates/minimal/docs/conventions/daily-use.md +3 -1
  54. package/ui/app.js +100 -10
  55. package/ui/index.html +22 -1
@@ -0,0 +1,72 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { resolveLocale, t } = require("./i18n");
4
+
5
+ const TASK_LABELS = {
6
+ tr: {
7
+ write_prd: "PRD / ürün gereksinim belgesi yazımı",
8
+ spec_authoring: "teknik spec / dokümantasyon",
9
+ debug: "hata ayıklama",
10
+ refactor: "kod sadeleştirme / refactor",
11
+ localization: "çeviri / dil dosyaları",
12
+ default: "görev sınıflandırması"
13
+ },
14
+ en: {
15
+ write_prd: "PRD / product requirements writing",
16
+ spec_authoring: "technical spec / documentation",
17
+ debug: "debugging",
18
+ refactor: "refactoring / simplification",
19
+ localization: "translation / locale files",
20
+ default: "task classification"
21
+ }
22
+ };
23
+
24
+ function taskLabel(taskTypeId, locale) {
25
+ const loc = locale === "en" ? "en" : "tr";
26
+ const map = TASK_LABELS[loc];
27
+ if (!taskTypeId) return map.default;
28
+ if (taskTypeId.includes("prd") || taskTypeId === "write_prd") return map.write_prd;
29
+ if (taskTypeId.includes("spec") || taskTypeId.includes("author")) return map.spec_authoring;
30
+ if (taskTypeId.includes("debug")) return map.debug;
31
+ if (taskTypeId.includes("refactor")) return map.refactor;
32
+ if (taskTypeId.includes("i18n") || taskTypeId.includes("local")) return map.localization;
33
+ return map.default;
34
+ }
35
+
36
+ function buildTooltip({ taskType, winner, locale }) {
37
+ const loc = resolveLocale(locale);
38
+ if (taskType?.id?.includes("prd") || winner?.resource === "prd") {
39
+ return t("term.prd", loc);
40
+ }
41
+ if (winner?.type === "skill") {
42
+ return t("term.installed", loc);
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function buildReasonPlain({ taskType, matched, winner, resolutionStatus, locale }) {
48
+ const loc = resolveLocale(locale);
49
+ const label = taskLabel(taskType?.id, loc);
50
+ const cap = winner?.resource || winner?.type || "capability-router";
51
+
52
+ if (resolutionStatus === "not_installed" && winner?.resource) {
53
+ if (loc === "en") {
54
+ return `The route picked ${cap}, but that skill is not available on disk or is disabled.`;
55
+ }
56
+ return `Seçilen yetenek (${cap}) diskte yok veya devre dışı.`;
57
+ }
58
+
59
+ const kw = matched.length ? matched.join(", ") : null;
60
+ if (loc === "en") {
61
+ if (kw) {
62
+ return `This looks like ${label} (keywords: ${kw}). Selected: ${cap}.`;
63
+ }
64
+ return `This looks like ${label}. Selected: ${cap}.`;
65
+ }
66
+ if (kw) {
67
+ return `Görevin ${label} gibi görünüyor (anahtar kelimeler: ${kw}). Seçilen: ${cap}.`;
68
+ }
69
+ return `Görevin ${label} gibi görünüyor. Seçilen: ${cap}.`;
70
+ }
71
+
72
+ module.exports = { buildReasonPlain, buildTooltip, taskLabel };
@@ -1,6 +1,8 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { detectWorkspaceBoundary } = require("./workspace-boundary");
4
+ const { resolveLocale } = require("./i18n");
5
+ const { buildReasonPlain, buildTooltip } = require("./explanation-plain");
4
6
 
5
7
  const GATEWAY_SKILL = "prismx-skill-gateway";
6
8
 
@@ -82,7 +84,17 @@ function extractFilesIntended(text) {
82
84
  return found.slice(0, 8);
83
85
  }
84
86
 
85
- function buildExplanation({ text, routeResult, matrix, rootDir, matchKeywords, resolutionStatus, confirmDefaultOption }) {
87
+ function buildExplanation({
88
+ text,
89
+ routeResult,
90
+ matrix,
91
+ rootDir,
92
+ matchKeywords,
93
+ resolutionStatus,
94
+ confirmDefaultOption,
95
+ locale
96
+ }) {
97
+ const resolvedLocale = resolveLocale(locale);
86
98
  const taskType = routeResult.taskTypeResolved;
87
99
  const winner = routeResult.winner
88
100
  ? { type: routeResult.winner.type, resource: routeResult.winner.resource }
@@ -99,6 +111,15 @@ function buildExplanation({ text, routeResult, matrix, rootDir, matchKeywords, r
99
111
  winner,
100
112
  files_to_read: filesToRead({ taskType, winner, rootDir }),
101
113
  reason_summary: buildReasonSummary({ taskType, matched, winner, resolutionStatus }),
114
+ reason_plain: buildReasonPlain({
115
+ taskType,
116
+ matched,
117
+ winner,
118
+ resolutionStatus,
119
+ locale: resolvedLocale
120
+ }),
121
+ tooltip: buildTooltip({ taskType, winner, locale: resolvedLocale }),
122
+ locale: resolvedLocale,
102
123
  alternatives_considered: buildAlternatives(taskType, winner),
103
124
  resolver_order: matrix?.resolver_order || []
104
125
  };
@@ -123,7 +144,10 @@ function formatExplanationCard(explanation) {
123
144
  lines.push("");
124
145
  lines.push("## Explanation");
125
146
  lines.push("");
126
- lines.push(`Why: ${explanation.reason_summary}`);
147
+ if (explanation.reason_plain) {
148
+ lines.push(`Why: ${explanation.reason_plain}`);
149
+ }
150
+ lines.push(`Technical: ${explanation.reason_summary}`);
127
151
  if (explanation.task_type_id) {
128
152
  lines.push(`Task type: ${explanation.task_type_id}`);
129
153
  }
@@ -0,0 +1,65 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const FEEDBACK_DIR = ".capint/feedback";
5
+
6
+ function feedbackDir(rootDir) {
7
+ return path.join(rootDir, FEEDBACK_DIR);
8
+ }
9
+
10
+ function appendFeedback(rootDir, { rating, comment = "", source = "ui" } = {}) {
11
+ if (rating != null) {
12
+ const n = Number(rating);
13
+ if (!Number.isInteger(n) || n < 1 || n > 5) {
14
+ return { error: "rating must be integer 1-5" };
15
+ }
16
+ }
17
+ const dir = feedbackDir(rootDir);
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ const entry = {
20
+ ts: new Date().toISOString(),
21
+ rating: rating != null ? Number(rating) : null,
22
+ comment: String(comment || "").slice(0, 2000),
23
+ source: String(source || "ui")
24
+ };
25
+ const file = path.join(dir, "entries.jsonl");
26
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, "utf-8");
27
+ return { ok: true, entry };
28
+ }
29
+
30
+ function readEntries(rootDir) {
31
+ const file = path.join(feedbackDir(rootDir), "entries.jsonl");
32
+ if (!fs.existsSync(file)) return [];
33
+ return fs
34
+ .readFileSync(file, "utf-8")
35
+ .split("\n")
36
+ .filter(Boolean)
37
+ .map((line) => {
38
+ try {
39
+ return JSON.parse(line);
40
+ } catch {
41
+ return null;
42
+ }
43
+ })
44
+ .filter(Boolean);
45
+ }
46
+
47
+ function summarizeFeedback(rootDir) {
48
+ const entries = readEntries(rootDir);
49
+ const rated = entries.filter((e) => typeof e.rating === "number");
50
+ const avg =
51
+ rated.length > 0 ? rated.reduce((s, e) => s + e.rating, 0) / rated.length : null;
52
+ const bySource = {};
53
+ for (const e of entries) {
54
+ bySource[e.source] = (bySource[e.source] || 0) + 1;
55
+ }
56
+ return {
57
+ count: entries.length,
58
+ rated_count: rated.length,
59
+ average_rating: avg != null ? Math.round(avg * 100) / 100 : null,
60
+ by_source: bySource,
61
+ recent: entries.slice(-5).reverse()
62
+ };
63
+ }
64
+
65
+ module.exports = { FEEDBACK_DIR, appendFeedback, summarizeFeedback, readEntries };
@@ -0,0 +1,43 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const LOCALES = new Set(["tr", "en"]);
5
+ let cache = {};
6
+
7
+ function pkgLocalesDir() {
8
+ return path.join(__dirname, "..", "..", "locales");
9
+ }
10
+
11
+ function loadLocale(locale) {
12
+ const loc = LOCALES.has(locale) ? locale : "en";
13
+ if (cache[loc]) return cache[loc];
14
+ const p = path.join(pkgLocalesDir(), `${loc}.json`);
15
+ if (!fs.existsSync(p)) return {};
16
+ cache[loc] = JSON.parse(fs.readFileSync(p, "utf-8"));
17
+ return cache[loc];
18
+ }
19
+
20
+ function resolveLocale(preferred) {
21
+ if (preferred && LOCALES.has(preferred)) return preferred;
22
+ const env = process.env.CAPINT_LOCALE;
23
+ if (env && LOCALES.has(env.trim().toLowerCase())) return env.trim().toLowerCase();
24
+ return "tr";
25
+ }
26
+
27
+ function parseAcceptLanguage(header) {
28
+ if (!header) return null;
29
+ const first = header.split(",")[0]?.trim().slice(0, 2).toLowerCase();
30
+ return LOCALES.has(first) ? first : null;
31
+ }
32
+
33
+ function t(key, locale = "tr", vars = {}) {
34
+ const dict = loadLocale(locale);
35
+ const fallback = loadLocale("en");
36
+ let text = dict[key] ?? fallback[key] ?? key;
37
+ for (const [k, v] of Object.entries(vars)) {
38
+ text = text.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
39
+ }
40
+ return text;
41
+ }
42
+
43
+ module.exports = { LOCALES, resolveLocale, parseAcceptLanguage, t, loadLocale };
@@ -0,0 +1,205 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { execSync } = require("child_process");
4
+ const { loadProvidersConfig, DEFAULT_PATHS, search } = require("./local-memory-adapter");
5
+ const { loadProvidersConfig: loadGraphCfg, DEFAULT_PATHS: GRAPH_PATHS, query } = require("./local-graph-adapter");
6
+ const { localContextEnabled, doctorPassQuick } = require("../context-memory-bridge");
7
+ const { resolveProviderActivation } = require("./activation-policy");
8
+ const { loadMatrix } = require("../route-engine");
9
+
10
+ const AGENTMEMORY_HEALTH_PATH = "/agentmemory/health";
11
+ const GRAPHIFY_CANDIDATES = [
12
+ "GRAPH_REPORT.md",
13
+ "graphify-out/GRAPH_REPORT.md",
14
+ ".capint/graph/graph.json",
15
+ "graphify-out/graph.json",
16
+ "graph.json"
17
+ ];
18
+
19
+ function fileExists(rootDir, rel) {
20
+ return fs.existsSync(path.join(rootDir, rel));
21
+ }
22
+
23
+ function commandExists(name) {
24
+ try {
25
+ if (process.platform === "win32") {
26
+ execSync(`where ${name}`, { stdio: "ignore" });
27
+ } else {
28
+ execSync(`command -v ${name}`, { stdio: "ignore" });
29
+ }
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ async function probeHttpHealth(baseUrl, healthPath, timeoutMs = 2500) {
37
+ const url = `${String(baseUrl).replace(/\/$/, "")}${healthPath}`;
38
+ try {
39
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
40
+ let body = null;
41
+ try {
42
+ body = await res.json();
43
+ } catch {
44
+ body = null;
45
+ }
46
+ return { ok: res.ok, status: res.status, url, body };
47
+ } catch (e) {
48
+ return { ok: false, url, error: e.message };
49
+ }
50
+ }
51
+
52
+ function tierLocal(rootDir) {
53
+ const memCfg = loadProvidersConfig(rootDir);
54
+ const memPaths = memCfg?.memory?.paths || DEFAULT_PATHS;
55
+ const pathStatus = memPaths.map((p) => ({ path: p, exists: fileExists(rootDir, p) }));
56
+ const hits = search(rootDir, "CapInt", { config: memCfg }).hits.length;
57
+ const usable = pathStatus.some((p) => p.exists);
58
+
59
+ return {
60
+ id: "tier0_local",
61
+ label: "Local memory files (HANDOFF, AGENT, …)",
62
+ status: usable ? "ok" : "missing",
63
+ paths: pathStatus,
64
+ sample_hits: hits,
65
+ enable: "Always available via context-memory-bridge skill"
66
+ };
67
+ }
68
+
69
+ function tierGraphify(rootDir) {
70
+ const graphCfg = loadGraphCfg(rootDir);
71
+ const configured = graphCfg?.graph?.paths || GRAPH_PATHS;
72
+ const found = GRAPHIFY_CANDIDATES.filter((p) => fileExists(rootDir, p));
73
+ const configuredStatus = configured.map((p) => ({ path: p, exists: fileExists(rootDir, p) }));
74
+ const excerpts = query(rootDir, null, { config: graphCfg }).excerpts.length;
75
+ const cli = commandExists("graphify");
76
+
77
+ let status = "missing";
78
+ if (excerpts > 0) status = "ok";
79
+ else if (found.length) status = "partial";
80
+
81
+ return {
82
+ id: "tier1_graphify",
83
+ label: "Graph artifacts (graphify outputs)",
84
+ status,
85
+ artifacts_found: found,
86
+ configured_paths: configuredStatus,
87
+ sample_excerpts: excerpts,
88
+ cli_on_path: cli,
89
+ recipe: "Run graphify . then copy GRAPH_REPORT.md + graph.json — see docs/recipes/memory-graph-pairing.md"
90
+ };
91
+ }
92
+
93
+ async function tierAgentmemory() {
94
+ const baseUrl = process.env.AGENTMEMORY_URL || "http://localhost:3111";
95
+ const health = await probeHttpHealth(baseUrl, AGENTMEMORY_HEALTH_PATH);
96
+ const cli = commandExists("agentmemory");
97
+
98
+ let status = "offline";
99
+ if (health.ok) status = "ok";
100
+ else if (cli) status = "cli_only";
101
+
102
+ return {
103
+ id: "tier2_agentmemory",
104
+ label: "agentmemory MCP server",
105
+ status,
106
+ url: baseUrl,
107
+ health,
108
+ cli_on_path: cli,
109
+ wiring: "agentmemory connect cursor — see docs/recipes/memory-graph-pairing.md"
110
+ };
111
+ }
112
+
113
+ function tierMempalace() {
114
+ const cli = commandExists("mempalace");
115
+ return {
116
+ id: "tier3_mempalace",
117
+ label: "MemPalace (optional Python alt)",
118
+ status: cli ? "cli_only" : "not_installed",
119
+ cli_on_path: cli,
120
+ note: "Use MCP separately; CapInt adapter backlog 0.8.0. Official: github.com/MemPalace/mempalace only."
121
+ };
122
+ }
123
+
124
+ function envFlags() {
125
+ return {
126
+ CAPINT_LOCAL_CONTEXT: process.env.CAPINT_LOCAL_CONTEXT || null,
127
+ CAPINT_MEMORY: process.env.CAPINT_MEMORY || null,
128
+ CAPINT_GRAPH: process.env.CAPINT_GRAPH || null,
129
+ AGENTMEMORY_URL: process.env.AGENTMEMORY_URL || null,
130
+ local_context_enabled: localContextEnabled({})
131
+ };
132
+ }
133
+
134
+ function sampleActivation(rootDir) {
135
+ const matrix = loadMatrix(rootDir);
136
+ if (!matrix) return null;
137
+ return resolveProviderActivation({
138
+ memoryLevel: "required",
139
+ overallWeight: "medium",
140
+ capability: "memory-retrieval",
141
+ matrix,
142
+ doctorPass: doctorPassQuick(rootDir),
143
+ featureFlags: {}
144
+ });
145
+ }
146
+
147
+ function buildRecommendations(report) {
148
+ const rec = [];
149
+ if (report.tiers.tier0_local.status !== "ok") {
150
+ rec.push("Run capint init or add HANDOFF.md / AGENT.md for local memory.");
151
+ }
152
+ if (report.tiers.tier1_graphify.status === "missing") {
153
+ rec.push("Optional: run graphify . and copy GRAPH_REPORT.md + graph.json (Tier 1).");
154
+ }
155
+ if (report.tiers.tier2_agentmemory.status === "offline") {
156
+ rec.push("Optional: npm i -g @agentmemory/agentmemory && agentmemory (Tier 2 sidecar).");
157
+ }
158
+ if (!report.env.local_context_enabled) {
159
+ rec.push("Set CAPINT_LOCAL_CONTEXT=1 to attach local_adapters on route.");
160
+ }
161
+ if (report.sample_activation?.memory?.activated && report.tiers.tier2_agentmemory.status !== "ok") {
162
+ rec.push("Memory provider would activate on memory-retrieval routes but agentmemory is not healthy (0.7.x uses stubs until 0.8 adapter).");
163
+ }
164
+ return rec;
165
+ }
166
+
167
+ async function runProvidersDoctor(rootDir) {
168
+ const tiers = {
169
+ tier0_local: tierLocal(rootDir),
170
+ tier1_graphify: tierGraphify(rootDir),
171
+ tier2_agentmemory: await tierAgentmemory(),
172
+ tier3_mempalace: tierMempalace()
173
+ };
174
+
175
+ const providersConfig = fs.existsSync(path.join(rootDir, ".capint", "providers.json"));
176
+
177
+ const report = {
178
+ mode: "read_only",
179
+ recipe: "docs/capint-stack.md",
180
+ env: envFlags(),
181
+ providers_json: providersConfig,
182
+ tiers,
183
+ sample_activation: sampleActivation(rootDir),
184
+ recommendations: []
185
+ };
186
+
187
+ report.recommendations = buildRecommendations(report);
188
+ report.summary = {
189
+ local_ok: tiers.tier0_local.status === "ok",
190
+ graph_ready: tiers.tier1_graphify.status === "ok",
191
+ agentmemory_ok: tiers.tier2_agentmemory.status === "ok",
192
+ sidecar_optional: true
193
+ };
194
+
195
+ return report;
196
+ }
197
+
198
+ module.exports = {
199
+ runProvidersDoctor,
200
+ probeHttpHealth,
201
+ tierLocal,
202
+ tierGraphify,
203
+ tierAgentmemory,
204
+ tierMempalace
205
+ };
@@ -1,5 +1,5 @@
1
1
  const { applyFilePolicy, buildReport } = require("./file-policy");
2
- const { getPresetFiles, PRESET_MANIFEST, copyBundle, normalizeBundleMode } = require("./presets");
2
+ const { getPresetFiles, PRESET_MANIFEST, copyBundle, copyPackageSchemas, normalizeBundleMode } = require("./presets");
3
3
  const { writeScaffoldManifest } = require("./manifest");
4
4
 
5
5
  function runScaffold({
@@ -33,6 +33,7 @@ function runScaffold({
33
33
  }
34
34
 
35
35
  results.push(...copyBundle({ rootDir, force, bundle: bundleMode }));
36
+ results.push(...copyPackageSchemas({ rootDir, force }));
36
37
 
37
38
  const report = buildReport(results);
38
39
  report.preset = preset;
@@ -221,6 +221,23 @@ function copyBundleTree({ rootDir, srcDir, prefix, force = false }) {
221
221
  return results;
222
222
  }
223
223
 
224
+ function copyPackageSchemas({ rootDir, force = false }) {
225
+ const schemaDir = path.join(pkgRoot(), "schemas");
226
+ const results = [];
227
+ if (!fs.existsSync(schemaDir)) return results;
228
+ for (const file of fs.readdirSync(schemaDir).filter((f) => f.endsWith(".json"))) {
229
+ results.push(
230
+ applyFilePolicy({
231
+ rootDir,
232
+ relativePath: `.capint/schemas/${file}`,
233
+ content: fs.readFileSync(path.join(schemaDir, file), "utf-8"),
234
+ force
235
+ })
236
+ );
237
+ }
238
+ return results;
239
+ }
240
+
224
241
  /** Copy skills/ + workflows/ into the target project (`full` default, `minimal` for CI). */
225
242
  function copyBundle({ rootDir, force = false, bundle = "full" }) {
226
243
  const root = pkgRoot();
@@ -262,6 +279,7 @@ const PRESET_MANIFEST = {
262
279
  description: "AGENT.md, design.md, rules, matrix, registry, skill bundle (full or --bundle minimal)",
263
280
  files: [
264
281
  "GUNLUK.md",
282
+ "DONE.md",
265
283
  "AGENT.md",
266
284
  "AGENTS.md",
267
285
  "design.md",
@@ -269,6 +287,7 @@ const PRESET_MANIFEST = {
269
287
  "docs/conventions/daily-use.md",
270
288
  "docs/conventions/task-to-capability-cheatsheet.md",
271
289
  ".capint/rules/core.md",
290
+ ".capint/schemas/",
272
291
  ".gitignore",
273
292
  "skill-routing-matrix.json",
274
293
  "registry.json",
@@ -290,6 +309,7 @@ module.exports = {
290
309
  interpolate,
291
310
  deriveRegistryFromMatrix,
292
311
  copyBundle,
312
+ copyPackageSchemas,
293
313
  listPackageSkillNames,
294
314
  listProjectSkillNames,
295
315
  resolveBundleSkillNames,
@@ -0,0 +1,141 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { loadMatrix } = require("./route-engine");
4
+
5
+ const STALE_BACKUP_DAYS = 90;
6
+ const TIERS = ["core", "recommended", "optional", "project_added"];
7
+
8
+ function loadRegistry(rootDir) {
9
+ const p = path.join(rootDir, "registry.json");
10
+ if (!fs.existsSync(p)) return null;
11
+ try {
12
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
13
+ } catch (e) {
14
+ return { parseError: e.message };
15
+ }
16
+ }
17
+
18
+ function skillsOnDisk(rootDir) {
19
+ const skillsDir = path.join(rootDir, "skills");
20
+ if (!fs.existsSync(skillsDir)) return [];
21
+ return fs
22
+ .readdirSync(skillsDir)
23
+ .filter((n) => fs.existsSync(path.join(skillsDir, n, "SKILL.md")));
24
+ }
25
+
26
+ function registrySkillSet(registry) {
27
+ const set = new Set();
28
+ if (!registry || registry.parseError) return set;
29
+ for (const tier of TIERS) {
30
+ for (const name of registry[tier] || []) set.add(name);
31
+ }
32
+ return set;
33
+ }
34
+
35
+ function matrixReferencedSkills(matrix) {
36
+ const set = new Set();
37
+ for (const tt of matrix?.task_types || []) {
38
+ for (const s of tt.skills || []) set.add(s);
39
+ }
40
+ for (const meta of Object.values(matrix?.rules_domain_map || {})) {
41
+ for (const s of meta.skills || []) set.add(s);
42
+ }
43
+ return set;
44
+ }
45
+
46
+ function listStaleBackupGroups(rootDir, maxAgeDays = STALE_BACKUP_DAYS) {
47
+ const dir = path.join(rootDir, ".capint", "backups");
48
+ if (!fs.existsSync(dir)) return [];
49
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
50
+ const stamps = new Map();
51
+ for (const file of fs.readdirSync(dir).filter((f) => f.endsWith(".json"))) {
52
+ const stat = fs.statSync(path.join(dir, file));
53
+ const m = file.match(/^(skill-routing-matrix|registry)\.(.+)\.json$/);
54
+ if (!m) continue;
55
+ const stamp = m[2];
56
+ const prev = stamps.get(stamp);
57
+ if (!prev || stat.mtimeMs > prev.mtime) {
58
+ stamps.set(stamp, { stamp, mtime: stat.mtimeMs });
59
+ }
60
+ }
61
+ return [...stamps.values()].filter((g) => g.mtime < cutoff);
62
+ }
63
+
64
+ function runSkillAudit(rootDir, options = {}) {
65
+ const findings = [];
66
+ const recommendations = [];
67
+ const matrix = loadMatrix(rootDir);
68
+ const registry = loadRegistry(rootDir);
69
+ const disk = skillsOnDisk(rootDir);
70
+ const diskSet = new Set(disk);
71
+ const regSet = registrySkillSet(registry);
72
+ const matrixRefs = matrixReferencedSkills(matrix);
73
+
74
+ for (const name of disk) {
75
+ if (!regSet.has(name)) {
76
+ findings.push({
77
+ level: "warning",
78
+ code: "orphan_on_disk",
79
+ skill: name,
80
+ message: `Skill on disk but not in registry: ${name}`
81
+ });
82
+ recommendations.push(`capint skill pin ${name} # or remove skills/${name}/`);
83
+ }
84
+ }
85
+
86
+ if (registry && !registry.parseError) {
87
+ for (const name of regSet) {
88
+ if (!diskSet.has(name)) {
89
+ const level = (registry.core || []).includes(name) ? "issue" : "warning";
90
+ findings.push({
91
+ level,
92
+ code: "registry_ghost",
93
+ skill: name,
94
+ message: `Registry entry missing on disk: ${name}`
95
+ });
96
+ recommendations.push(`capint init --bundle full # or remove from registry`);
97
+ }
98
+ }
99
+
100
+ for (const name of registry.optional || []) {
101
+ if (diskSet.has(name) && !matrixRefs.has(name)) {
102
+ findings.push({
103
+ level: "info",
104
+ code: "matrix_unreferenced",
105
+ skill: name,
106
+ message: `Optional skill not referenced by matrix: ${name}`
107
+ });
108
+ }
109
+ }
110
+ }
111
+
112
+ const stale = listStaleBackupGroups(rootDir, options.staleDays ?? STALE_BACKUP_DAYS);
113
+ for (const g of stale) {
114
+ findings.push({
115
+ level: "info",
116
+ code: "stale_backup",
117
+ stamp: g.stamp,
118
+ message: `Backup older than ${STALE_BACKUP_DAYS} days: ${g.stamp}`
119
+ });
120
+ recommendations.push("Review .capint/backups/ and prune old snapshots if no longer needed");
121
+ }
122
+
123
+ const issueCount = findings.filter((f) => f.level === "issue").length;
124
+ const warningCount = findings.filter((f) => f.level === "warning").length;
125
+
126
+ return {
127
+ pass: issueCount === 0,
128
+ exit_code: issueCount === 0 ? 0 : 1,
129
+ summary: {
130
+ issues: issueCount,
131
+ warnings: warningCount,
132
+ info: findings.filter((f) => f.level === "info").length,
133
+ skills_on_disk: disk.length,
134
+ registry_count: regSet.size
135
+ },
136
+ findings,
137
+ recommendations: [...new Set(recommendations)]
138
+ };
139
+ }
140
+
141
+ module.exports = { runSkillAudit, STALE_BACKUP_DAYS, skillsOnDisk, loadRegistry };