@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.
- package/AGENT.md +1 -1
- package/CHANGELOG.md +23 -0
- package/bin/capint.js +11 -2
- package/docs/PRD-capint.md +94 -0
- package/docs/PRD-v0.5-agent-capability-activation.md +10 -1
- package/docs/architecture-decisions.md +2 -0
- package/docs/capint-rehber.md +439 -0
- package/docs/capint-stack.md +118 -0
- package/docs/community.md +19 -0
- package/docs/conventions/daily-use.md +15 -1
- package/docs/eval/beginner-test-protocol.md +39 -0
- package/docs/eval/survey-template.md +16 -0
- package/docs/generated/README.md +6 -0
- package/docs/generated/execution-intent.v1.md +211 -0
- package/docs/generated/explanation.v1.md +127 -0
- package/docs/generated/explanation.v2.md +142 -0
- package/docs/generated/ui-api.v1.md +178 -0
- package/docs/maintainer-dogfood.md +3 -2
- package/docs/recipes/memory-graph-pairing.md +200 -0
- package/docs/skill-audit-runbook.md +32 -0
- package/locales/en.json +27 -0
- package/locales/tr.json +27 -0
- package/package.json +4 -2
- package/projections/session-start.md +14 -2
- package/schemas/execution-intent.v1.json +69 -0
- package/schemas/explanation.v2.json +51 -0
- package/schemas/ui-api.v1.json +85 -0
- package/scripts/generate-schema-docs.mjs +104 -0
- package/scripts/release-check.mjs +10 -0
- package/skills/prismx-skill-gateway/SKILL.md +10 -3
- package/src/commands/doctor.js +9 -0
- package/src/commands/feedback.js +29 -0
- package/src/commands/init.js +12 -0
- package/src/commands/providers.js +54 -0
- package/src/commands/skill.js +27 -0
- package/src/commands/stack.js +97 -0
- package/src/lib/audit.js +9 -1
- package/src/lib/contract.js +9 -0
- package/src/lib/doctor.js +69 -3
- package/src/lib/execution-policy.js +3 -1
- package/src/lib/explanation-plain.js +72 -0
- package/src/lib/explanation.js +26 -2
- package/src/lib/feedback.js +65 -0
- package/src/lib/i18n.js +43 -0
- package/src/lib/providers/doctor.js +205 -0
- package/src/lib/scaffold/index.js +2 -1
- package/src/lib/scaffold/presets.js +20 -0
- package/src/lib/skill-audit.js +141 -0
- package/src/lib/stack/index.js +259 -0
- package/src/lib/ui-server.js +92 -5
- package/templates/minimal/DONE.md +10 -0
- package/templates/minimal/GUNLUK.md +15 -1
- package/templates/minimal/docs/conventions/daily-use.md +3 -1
- package/ui/app.js +100 -10
- 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 };
|
package/src/lib/explanation.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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 };
|
package/src/lib/i18n.js
ADDED
|
@@ -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 };
|