@bcelep/capint 0.4.2
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 +28 -0
- package/CHANGELOG.md +58 -0
- package/README.md +94 -0
- package/bin/capint.js +90 -0
- package/design.md +23 -0
- package/docs/architecture-decisions.md +95 -0
- package/docs/execution-intent-contract.md +81 -0
- package/docs/manifest-schema.md +36 -0
- package/docs/release-checklist.md +31 -0
- package/package.json +33 -0
- package/projections/session-start.md +32 -0
- package/registry.json +12 -0
- package/scripts/release-check.mjs +40 -0
- package/scripts/validate-matrix.mjs +83 -0
- package/skill-routing-matrix.json +150 -0
- package/skills/capability-router/SKILL.md +3 -0
- package/skills/context-memory-bridge/SKILL.md +17 -0
- package/skills/localization-hub/SKILL.md +17 -0
- package/skills/refactor/SKILL.md +17 -0
- package/skills/systematic-debugging/SKILL.md +19 -0
- package/src/commands/audit.js +32 -0
- package/src/commands/consult.js +64 -0
- package/src/commands/doctor.js +36 -0
- package/src/commands/ide.js +62 -0
- package/src/commands/init.js +50 -0
- package/src/commands/memory.js +60 -0
- package/src/commands/route.js +30 -0
- package/src/commands/scaffold.js +37 -0
- package/src/commands/status.js +22 -0
- package/src/commands/uninstall.js +46 -0
- package/src/commands/upgrade.js +69 -0
- package/src/lib/audit.js +107 -0
- package/src/lib/capability-router.js +118 -0
- package/src/lib/context-memory-bridge.js +87 -0
- package/src/lib/context-pack.js +39 -0
- package/src/lib/contract.js +71 -0
- package/src/lib/doctor.js +115 -0
- package/src/lib/event-log.js +40 -0
- package/src/lib/execution-policy.js +168 -0
- package/src/lib/ide-sync.js +277 -0
- package/src/lib/intent-parser.js +116 -0
- package/src/lib/orchestration.js +25 -0
- package/src/lib/providers/activation-policy.js +93 -0
- package/src/lib/providers/graph-provider.js +35 -0
- package/src/lib/providers/local-graph-adapter.js +40 -0
- package/src/lib/providers/local-memory-adapter.js +41 -0
- package/src/lib/providers/memory-provider.js +35 -0
- package/src/lib/route-engine.js +191 -0
- package/src/lib/scaffold/file-policy.js +92 -0
- package/src/lib/scaffold/index.js +29 -0
- package/src/lib/scaffold/manifest-schema.js +116 -0
- package/src/lib/scaffold/manifest.js +34 -0
- package/src/lib/scaffold/presets.js +132 -0
- package/src/lib/uninstall.js +126 -0
- package/src/lib/upgrade-matrix.js +120 -0
- package/templates/bundle/skills/capability-router/SKILL.md +12 -0
- package/templates/bundle/skills/context-memory-bridge/SKILL.md +17 -0
- package/templates/bundle/skills/localization-hub/SKILL.md +17 -0
- package/templates/bundle/skills/refactor/SKILL.md +17 -0
- package/templates/bundle/skills/systematic-debugging/SKILL.md +19 -0
- package/templates/bundle/workflows/forge.md +51 -0
- package/templates/minimal/.capint/rules/core.md +18 -0
- package/templates/minimal/AGENT.md +26 -0
- package/templates/minimal/AGENTS.md +9 -0
- package/templates/minimal/design.md +25 -0
- package/templates/minimal/registry.json +7 -0
- package/templates/prismx-compatible/.capint/context.json +8 -0
- package/templates/prismx-compatible/HANDOFF.md +19 -0
- package/workflows/forge.md +51 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const WEIGHT_RANK = { light: 0, medium: 1, heavy: 2 };
|
|
2
|
+
|
|
3
|
+
function normalize(text) {
|
|
4
|
+
return text.toLowerCase().normalize("NFD").replace(/\p{M}/gu, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function splitSubtasks(text) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
const lines = trimmed.split(/\n/).map((l) => l.replace(/^[\s\-*•\d.)]+\s*/, "").trim()).filter(Boolean);
|
|
10
|
+
if (lines.length >= 2) return lines;
|
|
11
|
+
const parts = trimmed.split(/\s*,\s*|\s+ve\s+|\s+and\s+/i).filter((p) => p.trim().length > 3);
|
|
12
|
+
if (parts.length >= 2) return parts;
|
|
13
|
+
return [trimmed];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function matchKeywords(text, keywords) {
|
|
17
|
+
const n = normalize(text);
|
|
18
|
+
return keywords.some((kw) => n.includes(normalize(kw)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseEstimatedFiles(text) {
|
|
22
|
+
const patterns = [
|
|
23
|
+
/(\d+)\s*\+\s*(?:file|files|dosya)/i,
|
|
24
|
+
/(?:file|files|dosya)\s*[:=]?\s*(\d+)/i,
|
|
25
|
+
/(\d+)\s*(?:file|files|dosya)/i
|
|
26
|
+
];
|
|
27
|
+
for (const re of patterns) {
|
|
28
|
+
const m = text.match(re);
|
|
29
|
+
if (m?.[1]) return Number(m[1]);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findBestTaskType(text, matrix) {
|
|
35
|
+
let bestType = null;
|
|
36
|
+
for (const tt of matrix.task_types || []) {
|
|
37
|
+
if (matchKeywords(text, tt.keywords || [])) {
|
|
38
|
+
if (!bestType || WEIGHT_RANK[tt.weight_default || "light"] > WEIGHT_RANK[bestType.weight_default || "light"]) {
|
|
39
|
+
bestType = tt;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return bestType;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function classifySubtask(text, matrix) {
|
|
47
|
+
const c = matrix.classification;
|
|
48
|
+
const reasons = [];
|
|
49
|
+
const estimatedFiles = parseEstimatedFiles(text);
|
|
50
|
+
|
|
51
|
+
if (matchKeywords(text, c.heavy_keywords || [])) {
|
|
52
|
+
reasons.push("heavy keyword");
|
|
53
|
+
return { weight: "heavy", reasons, taskType: findBestTaskType(text, matrix), signals: { estimated_files: estimatedFiles } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (matchKeywords(text, c.security_keywords || [])) {
|
|
57
|
+
reasons.push("security domain");
|
|
58
|
+
const securityFloor = c.security_override_min || "medium";
|
|
59
|
+
return {
|
|
60
|
+
weight: securityFloor,
|
|
61
|
+
reasons,
|
|
62
|
+
taskType: findBestTaskType(text, matrix),
|
|
63
|
+
signals: { estimated_files: estimatedFiles }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (matchKeywords(text, c.medium_keywords || [])) {
|
|
68
|
+
reasons.push("medium keyword");
|
|
69
|
+
return {
|
|
70
|
+
weight: "medium",
|
|
71
|
+
reasons,
|
|
72
|
+
taskType: findBestTaskType(text, matrix),
|
|
73
|
+
signals: { estimated_files: estimatedFiles }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const bestType = findBestTaskType(text, matrix);
|
|
78
|
+
if (bestType) {
|
|
79
|
+
reasons.push(`task_type:${bestType.id}`);
|
|
80
|
+
return {
|
|
81
|
+
weight: bestType.weight_default || "medium",
|
|
82
|
+
reasons,
|
|
83
|
+
taskType: bestType,
|
|
84
|
+
signals: { estimated_files: estimatedFiles }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reasons.push("ambiguous default");
|
|
89
|
+
return { weight: c.ambiguous_default || "medium", reasons, signals: { estimated_files: estimatedFiles } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseIntent(text, matrix) {
|
|
93
|
+
const subtasks = splitSubtasks(text);
|
|
94
|
+
const useMultiple = subtasks.length >= 2;
|
|
95
|
+
const segments = (useMultiple ? subtasks : [text.trim()]).map((sub, index) => ({
|
|
96
|
+
index: index + 1,
|
|
97
|
+
text: sub,
|
|
98
|
+
classification: classifySubtask(sub, matrix)
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
rawText: text,
|
|
103
|
+
useMultiple,
|
|
104
|
+
subtasks,
|
|
105
|
+
overrides: {
|
|
106
|
+
workflow: /\/workflow\b/i.test(text),
|
|
107
|
+
skill: /\/skill\b/i.test(text)
|
|
108
|
+
},
|
|
109
|
+
segments
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
parseIntent,
|
|
115
|
+
matchKeywords
|
|
116
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CONFIRM_TO_MODE = {
|
|
2
|
+
apply_now: "subagent",
|
|
3
|
+
plan_first: "pipeline",
|
|
4
|
+
analyze_only: "explore"
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function resolveOrchestration(taskType, confirmDefault, matrix) {
|
|
8
|
+
const pattern = taskType?.orchestration_pattern || "expert_pool";
|
|
9
|
+
const mode = CONFIRM_TO_MODE[confirmDefault] || "pipeline";
|
|
10
|
+
|
|
11
|
+
const chains = (matrix?.capability_chains || []).filter(
|
|
12
|
+
(c) => c.from_capability === (taskType?.capability || c.from_capability)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
pattern,
|
|
17
|
+
execution_mode: mode,
|
|
18
|
+
chains: chains.map((c) => ({
|
|
19
|
+
then_capability: c.then_capability,
|
|
20
|
+
when: c.when || "after_confirm"
|
|
21
|
+
}))
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { resolveOrchestration, CONFIRM_TO_MODE };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider activation policy — default off; explicit gating only.
|
|
3
|
+
* See docs/architecture-decisions.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const WEIGHT_RANK = { light: 0, medium: 1, heavy: 2 };
|
|
7
|
+
|
|
8
|
+
function readPolicy(matrix) {
|
|
9
|
+
return (
|
|
10
|
+
matrix?.provider_activation_policy || {
|
|
11
|
+
memory_providers: { default: "off", require_doctor_pass: true, allow_on_light: false },
|
|
12
|
+
graph_providers: { default: "off", require_doctor_pass: true, allow_on_light: false }
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function evaluateActivation({ kind, memoryLevel, overallWeight, capability, matrix, doctorPass, featureFlags = {} }) {
|
|
18
|
+
const policy = readPolicy(matrix);
|
|
19
|
+
const block = policy[kind] || { default: "off" };
|
|
20
|
+
const flagKey = kind === "memory_providers" ? "CAPINT_MEMORY" : "CAPINT_GRAPH";
|
|
21
|
+
const flagOn = featureFlags[kind] || process.env[flagKey] === "1";
|
|
22
|
+
|
|
23
|
+
const reasons = [];
|
|
24
|
+
let activated = false;
|
|
25
|
+
|
|
26
|
+
if (block.default === "on") {
|
|
27
|
+
activated = true;
|
|
28
|
+
reasons.push("policy_default_on");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (memoryLevel === "required") {
|
|
32
|
+
activated = true;
|
|
33
|
+
reasons.push("capability_requires_memory");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (flagOn) {
|
|
37
|
+
activated = true;
|
|
38
|
+
reasons.push("feature_flag");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!block.allow_on_light && overallWeight === "light" && memoryLevel !== "required") {
|
|
42
|
+
activated = false;
|
|
43
|
+
reasons.push("blocked_light_weight");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (block.require_doctor_pass && !doctorPass && !flagOn) {
|
|
47
|
+
activated = false;
|
|
48
|
+
reasons.push("doctor_not_pass");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const highRisk = new Set(block.high_risk_capabilities || ["incident-response", "threat-modeling", "memory-retrieval"]);
|
|
52
|
+
if (highRisk.has(capability) && memoryLevel !== "none") {
|
|
53
|
+
activated = true;
|
|
54
|
+
reasons.push("high_risk_capability");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
kind,
|
|
59
|
+
activated,
|
|
60
|
+
default: block.default || "off",
|
|
61
|
+
reasons,
|
|
62
|
+
fallback: activated ? null : "local"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveProviderActivation({ memoryLevel, overallWeight, capability, matrix, doctorPass, featureFlags }) {
|
|
67
|
+
const memory = evaluateActivation({
|
|
68
|
+
kind: "memory_providers",
|
|
69
|
+
memoryLevel,
|
|
70
|
+
overallWeight,
|
|
71
|
+
capability,
|
|
72
|
+
matrix,
|
|
73
|
+
doctorPass,
|
|
74
|
+
featureFlags
|
|
75
|
+
});
|
|
76
|
+
const graph = evaluateActivation({
|
|
77
|
+
kind: "graph_providers",
|
|
78
|
+
memoryLevel: memoryLevel === "required" ? "required" : "optional",
|
|
79
|
+
overallWeight,
|
|
80
|
+
capability,
|
|
81
|
+
matrix,
|
|
82
|
+
doctorPass,
|
|
83
|
+
featureFlags
|
|
84
|
+
});
|
|
85
|
+
return { memory, graph, anyActivated: memory.activated || graph.activated };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
readPolicy,
|
|
90
|
+
evaluateActivation,
|
|
91
|
+
resolveProviderActivation,
|
|
92
|
+
WEIGHT_RANK
|
|
93
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphProvider adapter contract (optional plugin — read-only projection in v0.x).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CONTRACT = {
|
|
6
|
+
name: "string",
|
|
7
|
+
methods: ["health", "query"],
|
|
8
|
+
optionalMethods: ["project"]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function createGraphProviderStub(name = "local") {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
async health() {
|
|
15
|
+
return { ok: true, detail: "local fallback (no graph index)" };
|
|
16
|
+
},
|
|
17
|
+
async query(_query, _scope) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateGraphProvider(provider) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (!provider?.name) errors.push("provider.name required");
|
|
26
|
+
if (typeof provider.health !== "function") errors.push("provider.health required");
|
|
27
|
+
if (typeof provider.query !== "function") errors.push("provider.query required");
|
|
28
|
+
return { valid: errors.length === 0, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
PROVIDER_CONTRACT,
|
|
33
|
+
createGraphProviderStub,
|
|
34
|
+
validateGraphProvider
|
|
35
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PATHS = ["GRAPH_REPORT.md", ".capint/graph/graph.json"];
|
|
5
|
+
|
|
6
|
+
function loadProvidersConfig(rootDir) {
|
|
7
|
+
const p = path.join(rootDir, ".capint", "providers.json");
|
|
8
|
+
if (!fs.existsSync(p)) return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function query(rootDir, topic, options = {}) {
|
|
17
|
+
const cfg = options.config || loadProvidersConfig(rootDir);
|
|
18
|
+
const paths = cfg?.graph?.paths || DEFAULT_PATHS;
|
|
19
|
+
const excerpts = [];
|
|
20
|
+
|
|
21
|
+
for (const rel of paths) {
|
|
22
|
+
const abs = path.join(rootDir, rel);
|
|
23
|
+
if (!fs.existsSync(abs)) continue;
|
|
24
|
+
const raw = fs.readFileSync(abs, "utf-8");
|
|
25
|
+
if (rel.endsWith(".json")) {
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(raw);
|
|
28
|
+
excerpts.push({ source: rel, excerpt: JSON.stringify(data).slice(0, 800) });
|
|
29
|
+
} catch {
|
|
30
|
+
excerpts.push({ source: rel, excerpt: raw.slice(0, 800) });
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
excerpts.push({ source: rel, excerpt: raw.slice(0, 1200) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { driver: "local", topic: topic || null, excerpts, paths_checked: paths };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { query, loadProvidersConfig, DEFAULT_PATHS };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PATHS = ["HANDOFF.md", "design.md", "AGENT.md", ".capint/rules/core.md"];
|
|
5
|
+
|
|
6
|
+
function loadProvidersConfig(rootDir) {
|
|
7
|
+
const p = path.join(rootDir, ".capint", "providers.json");
|
|
8
|
+
if (!fs.existsSync(p)) return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function search(rootDir, query, options = {}) {
|
|
17
|
+
const cfg = options.config || loadProvidersConfig(rootDir);
|
|
18
|
+
const paths = cfg?.memory?.paths || DEFAULT_PATHS;
|
|
19
|
+
const q = (query || "").toLowerCase();
|
|
20
|
+
const hits = [];
|
|
21
|
+
|
|
22
|
+
for (const rel of paths) {
|
|
23
|
+
const abs = path.join(rootDir, rel);
|
|
24
|
+
if (!fs.existsSync(abs)) continue;
|
|
25
|
+
const text = fs.readFileSync(abs, "utf-8");
|
|
26
|
+
const lines = text.split("\n");
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
if (!q || lines[i].toLowerCase().includes(q)) {
|
|
29
|
+
hits.push({
|
|
30
|
+
source: rel,
|
|
31
|
+
line: i + 1,
|
|
32
|
+
excerpt: lines[i].trim().slice(0, 200)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { driver: "local", query, hits: hits.slice(0, 20), paths_checked: paths };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { search, loadProvidersConfig, DEFAULT_PATHS };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryProvider adapter contract (optional plugin — not bundled in core runtime).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CONTRACT = {
|
|
6
|
+
name: "string",
|
|
7
|
+
methods: ["health", "search"],
|
|
8
|
+
optionalMethods: ["ingest"]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function createMemoryProviderStub(name = "local") {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
async health() {
|
|
15
|
+
return { ok: true, detail: "local fallback (HANDOFF, project files)" };
|
|
16
|
+
},
|
|
17
|
+
async search(_query, _scope, _limit = 5) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateMemoryProvider(provider) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (!provider?.name) errors.push("provider.name required");
|
|
26
|
+
if (typeof provider.health !== "function") errors.push("provider.health required");
|
|
27
|
+
if (typeof provider.search !== "function") errors.push("provider.search required");
|
|
28
|
+
return { valid: errors.length === 0, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
PROVIDER_CONTRACT,
|
|
33
|
+
createMemoryProviderStub,
|
|
34
|
+
validateMemoryProvider
|
|
35
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { parseIntent, matchKeywords } = require("./intent-parser");
|
|
4
|
+
const { routeCapability } = require("./capability-router");
|
|
5
|
+
const { buildExecutionPolicy } = require("./execution-policy");
|
|
6
|
+
const { resolveMemoryStrategy } = require("./context-memory-bridge");
|
|
7
|
+
const { appendRouteEvents } = require("./event-log");
|
|
8
|
+
|
|
9
|
+
const WEIGHT_EMOJI = { light: "🟢", medium: "🟡", heavy: "🔴" };
|
|
10
|
+
|
|
11
|
+
function loadMatrix(rootDir) {
|
|
12
|
+
const p = path.join(rootDir, "skill-routing-matrix.json");
|
|
13
|
+
if (!fs.existsSync(p)) return null;
|
|
14
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadRegistry(rootDir) {
|
|
18
|
+
const p = path.join(rootDir, "registry.json");
|
|
19
|
+
if (!fs.existsSync(p)) return { core: [], recommended: [], optional: [], project_added: [] };
|
|
20
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function installedSkills(rootDir, registry) {
|
|
24
|
+
const skillsDir = path.join(rootDir, "skills");
|
|
25
|
+
const names = new Set([
|
|
26
|
+
...(registry.core || []),
|
|
27
|
+
...(registry.recommended || []),
|
|
28
|
+
...(registry.optional || []),
|
|
29
|
+
...(registry.project_added || [])
|
|
30
|
+
]);
|
|
31
|
+
const onDisk = [];
|
|
32
|
+
if (fs.existsSync(skillsDir)) {
|
|
33
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
34
|
+
if (fs.existsSync(path.join(skillsDir, name, "SKILL.md"))) {
|
|
35
|
+
onDisk.push(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...names].filter((n) => onDisk.includes(n));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadFeatureFlags(rootDir) {
|
|
43
|
+
const p = path.join(rootDir, ".capint", "context.json");
|
|
44
|
+
if (!fs.existsSync(p)) return {};
|
|
45
|
+
try {
|
|
46
|
+
const ctx = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
47
|
+
return ctx.feature_flags || {};
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function routeTask(text, rootDir) {
|
|
54
|
+
const matrix = loadMatrix(rootDir);
|
|
55
|
+
if (!matrix) return { error: "skill-routing-matrix.json not found" };
|
|
56
|
+
|
|
57
|
+
const registry = loadRegistry(rootDir);
|
|
58
|
+
const installed = installedSkills(rootDir, registry);
|
|
59
|
+
const parsedIntent = parseIntent(text, matrix);
|
|
60
|
+
const routeResult = routeCapability(parsedIntent, matrix, registry, installed, matchKeywords);
|
|
61
|
+
const memoryStrategy = resolveMemoryStrategy({
|
|
62
|
+
intent: parsedIntent,
|
|
63
|
+
routeResult,
|
|
64
|
+
matrix,
|
|
65
|
+
rootDir,
|
|
66
|
+
featureFlags: loadFeatureFlags(rootDir)
|
|
67
|
+
});
|
|
68
|
+
const executionIntent = buildExecutionPolicy({
|
|
69
|
+
rawText: text,
|
|
70
|
+
routeResult,
|
|
71
|
+
memoryStrategy,
|
|
72
|
+
matrix,
|
|
73
|
+
parsedIntent,
|
|
74
|
+
rootDir
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const routeEvents = [
|
|
78
|
+
...(memoryStrategy.events || []),
|
|
79
|
+
{
|
|
80
|
+
type: "route.provider_selected",
|
|
81
|
+
resolution: executionIntent.resolution,
|
|
82
|
+
capability: executionIntent.capability,
|
|
83
|
+
weight: routeResult.overall
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
const eventLog = appendRouteEvents(rootDir, routeEvents);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
request: text,
|
|
90
|
+
overall_weight: routeResult.overall,
|
|
91
|
+
overall_emoji: WEIGHT_EMOJI[routeResult.overall],
|
|
92
|
+
show_skill_plan: routeResult.showPlan,
|
|
93
|
+
subtasks: routeResult.rows,
|
|
94
|
+
installed_count: installed.length,
|
|
95
|
+
execution_intent: executionIntent,
|
|
96
|
+
events: routeEvents,
|
|
97
|
+
event_log: eventLog
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatTextReport(result, options = {}) {
|
|
102
|
+
if (result.error) return result.error;
|
|
103
|
+
const { verbose = false } = options;
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push("## Execution Intent");
|
|
107
|
+
lines.push("");
|
|
108
|
+
lines.push(`Intent: ${result.execution_intent.intent}`);
|
|
109
|
+
lines.push(`Capability: ${result.execution_intent.capability}`);
|
|
110
|
+
lines.push(`Resolution: ${result.execution_intent.resolution}`);
|
|
111
|
+
if (result.execution_intent.resolution_status) {
|
|
112
|
+
lines.push(`Resolution status: ${result.execution_intent.resolution_status}`);
|
|
113
|
+
if (result.execution_intent.resolution_hint) {
|
|
114
|
+
lines.push(`Hint: ${result.execution_intent.resolution_hint}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
lines.push(`Memory: ${result.execution_intent.memory}`);
|
|
118
|
+
lines.push(`Plan: ${result.execution_intent.plan}`);
|
|
119
|
+
if (result.execution_intent.confirm?.enabled) {
|
|
120
|
+
const optionSummary = (result.execution_intent.confirm_options || []).map((o) => o.id).join("|");
|
|
121
|
+
lines.push(`Confirm: ${result.execution_intent.confirm_question}`);
|
|
122
|
+
lines.push(`Options: ${optionSummary} (default: ${result.execution_intent.confirm_default_option})`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
if (result.execution_intent.fallback) {
|
|
126
|
+
lines.push(`Fallback capability: ${result.execution_intent.fallback.capability}`);
|
|
127
|
+
lines.push(`Override: ${result.execution_intent.fallback.override}`);
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
if (result.execution_intent.verification_required) {
|
|
131
|
+
lines.push(`Verification: required (${result.execution_intent.verification_profile || "standard"})`);
|
|
132
|
+
for (const h of result.execution_intent.verification_hints || []) {
|
|
133
|
+
lines.push(` - ${h}`);
|
|
134
|
+
}
|
|
135
|
+
lines.push("");
|
|
136
|
+
}
|
|
137
|
+
if (result.execution_intent.orchestration) {
|
|
138
|
+
const o = result.execution_intent.orchestration;
|
|
139
|
+
lines.push(`Orchestration: ${o.pattern} (${o.execution_mode})`);
|
|
140
|
+
if (o.chains?.length) {
|
|
141
|
+
lines.push(`Chains: ${o.chains.map((c) => c.then_capability).join(" → ")}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push("");
|
|
144
|
+
}
|
|
145
|
+
if (result.execution_intent.context_pack?.snippets?.length) {
|
|
146
|
+
lines.push(`Context pack: ${result.execution_intent.context_pack.snippets.length} local snippet(s)`);
|
|
147
|
+
lines.push("");
|
|
148
|
+
}
|
|
149
|
+
lines.push(`Weight: ${result.overall_emoji} ${result.overall_weight}`);
|
|
150
|
+
lines.push(`Plan visible: ${result.show_skill_plan ? "yes" : "no"}`);
|
|
151
|
+
if (verbose) {
|
|
152
|
+
const ex = result.execution_intent.route_explain;
|
|
153
|
+
if (ex) {
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("## Route explain (verbose)");
|
|
156
|
+
lines.push(`Matched task type: ${ex.matched_task_type || "—"}`);
|
|
157
|
+
lines.push(`Resolver order: ${(ex.resolver_order || []).join(" → ")}`);
|
|
158
|
+
if (ex.winner) {
|
|
159
|
+
lines.push(`Winner: ${ex.winner.name}/${ex.winner.resource} (confidence ${ex.winner.confidence})`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
lines.push("## Skill Plan (verbose)");
|
|
164
|
+
lines.push("| # | Subtask | Weight | Reason | Skills | Workflow |");
|
|
165
|
+
lines.push("|---|---------|--------|--------|--------|----------|");
|
|
166
|
+
for (const r of result.subtasks) {
|
|
167
|
+
lines.push(
|
|
168
|
+
`| ${r.index} | ${r.subtask} | ${r.emoji} | ${r.reason} | ${r.skills.map((s) => s.name).join(", ")} | ${r.workflow || "—"} |`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push("");
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listCheatsheet(matrix) {
|
|
177
|
+
const lines = ["capability/providers from skill-routing-matrix.json", ""];
|
|
178
|
+
for (const tt of matrix?.task_types || []) {
|
|
179
|
+
const capability = tt.capability || tt.id;
|
|
180
|
+
const providerList = (tt.providers || []).map((p) => `${p.name}/${p.resource}`).join(", ");
|
|
181
|
+
lines.push(` ${tt.id.padEnd(20)} -> ${capability}${providerList ? ` | ${providerList}` : ""}`);
|
|
182
|
+
}
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
routeTask,
|
|
188
|
+
formatTextReport,
|
|
189
|
+
listCheatsheet,
|
|
190
|
+
loadMatrix
|
|
191
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
const MARKER_START = "<!-- capint:managed:start -->";
|
|
6
|
+
const MARKER_END = "<!-- capint:managed:end -->";
|
|
7
|
+
|
|
8
|
+
function fileHash(content) {
|
|
9
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function hasManagedBlock(content) {
|
|
13
|
+
return content.includes(MARKER_START) && content.includes(MARKER_END);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mergeManagedBlock(existing, incoming) {
|
|
17
|
+
if (!hasManagedBlock(existing)) return null;
|
|
18
|
+
const start = existing.indexOf(MARKER_START);
|
|
19
|
+
const end = existing.indexOf(MARKER_END);
|
|
20
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
21
|
+
const before = existing.slice(0, start + MARKER_START.length);
|
|
22
|
+
const after = existing.slice(end);
|
|
23
|
+
const innerStart = incoming.indexOf(MARKER_START);
|
|
24
|
+
const innerEnd = incoming.indexOf(MARKER_END);
|
|
25
|
+
if (innerStart === -1 || innerEnd === -1) return null;
|
|
26
|
+
const inner = incoming.slice(innerStart + MARKER_START.length, innerEnd);
|
|
27
|
+
return `${before}\n${inner}\n${after}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Safe-merge policy:
|
|
32
|
+
* - missing file -> created
|
|
33
|
+
* - existing with managed block -> updated (merge inner)
|
|
34
|
+
* - existing without managed block -> conflict (write *.capint.new.md)
|
|
35
|
+
*/
|
|
36
|
+
function applyFilePolicy({ rootDir, relativePath, content, force = false }) {
|
|
37
|
+
const abs = path.join(rootDir, relativePath);
|
|
38
|
+
const dir = path.dirname(abs);
|
|
39
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(abs)) {
|
|
42
|
+
fs.writeFileSync(abs, content, "utf-8");
|
|
43
|
+
return { path: relativePath, action: "created" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const existing = fs.readFileSync(abs, "utf-8");
|
|
47
|
+
if (force) {
|
|
48
|
+
fs.writeFileSync(abs, content, "utf-8");
|
|
49
|
+
return { path: relativePath, action: "updated", mode: "force" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const merged = mergeManagedBlock(existing, content);
|
|
53
|
+
if (merged && merged !== existing) {
|
|
54
|
+
fs.writeFileSync(abs, merged, "utf-8");
|
|
55
|
+
return { path: relativePath, action: "updated", mode: "safe-merge" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (existing.trim() === content.trim()) {
|
|
59
|
+
return { path: relativePath, action: "skipped", reason: "identical" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sidecar = `${relativePath}.capint.new.md`;
|
|
63
|
+
const sideAbs = path.join(rootDir, sidecar);
|
|
64
|
+
fs.writeFileSync(sideAbs, content, "utf-8");
|
|
65
|
+
return {
|
|
66
|
+
path: relativePath,
|
|
67
|
+
action: "conflict",
|
|
68
|
+
sidecar,
|
|
69
|
+
existing_hash: fileHash(existing),
|
|
70
|
+
incoming_hash: fileHash(content)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildReport(results) {
|
|
75
|
+
const summary = { created: 0, updated: 0, skipped: 0, conflict: 0 };
|
|
76
|
+
for (const r of results) {
|
|
77
|
+
if (r.action === "created") summary.created++;
|
|
78
|
+
else if (r.action === "updated") summary.updated++;
|
|
79
|
+
else if (r.action === "skipped") summary.skipped++;
|
|
80
|
+
else if (r.action === "conflict") summary.conflict++;
|
|
81
|
+
}
|
|
82
|
+
return { summary, results, generated_at: new Date().toISOString() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
MARKER_START,
|
|
87
|
+
MARKER_END,
|
|
88
|
+
applyFilePolicy,
|
|
89
|
+
buildReport,
|
|
90
|
+
mergeManagedBlock,
|
|
91
|
+
hasManagedBlock
|
|
92
|
+
};
|