secufusion-mcp 1.2.2 → 1.2.3
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/AGENTS.md +9 -10
- package/README.md +13 -0
- package/index.js +101 -280
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ You are an elite Senior Developer and Architect working on the SecuFusion worksp
|
|
|
9
9
|
|
|
10
10
|
**DYNAMIC ENFORCEMENT**: You must dynamically adhere to these rules at all times. Whether you are starting a fresh task, resuming an interrupted session, or answering a mid-task prompt, you must strictly respect this sequence and never skip ahead.
|
|
11
11
|
|
|
12
|
-
1. **Rule 0 (Pre-requisite to all rules) — Load Project DNA First**: Before ANY other action, call `
|
|
12
|
+
1. **Rule 0 (Pre-requisite to all rules) — Load Project DNA First**: Before ANY other action, call `prime_session(work_item_id: <id>)`. This is Phase 00. Without the project DNA loaded, you are not allowed to reason, classify, plan, or code. Period.
|
|
13
13
|
2. **Rule 1 - ReAct (Reason, Observe, Act) First**: After DNA is loaded, you MUST deeply reason about the problem statement. Apply the ReAct framework: analyze the problem, observe context, and formulate a high-level solution hypothesis.
|
|
14
14
|
3. **Rule 2 - Classify Second**: Only after you have reasoned through the problem statement, you MUST call the `classify_task` tool. This will formally categorize the task and lock in architectural boundaries.
|
|
15
15
|
4. **Rule 3 - STRICT YIELD (Stop and Wait)**: Immediately after classifying the task, you MUST YIELD YOUR TURN. **DO NOT CHAIN TOOL CALLS.** Output the classification, ask the user for the "green signal," and STOP.
|
|
@@ -29,15 +29,11 @@ You are an elite Senior Developer and Architect working on the SecuFusion worksp
|
|
|
29
29
|
|
|
30
30
|
```
|
|
31
31
|
STEP 1 — ALWAYS, unconditionally:
|
|
32
|
-
→ call
|
|
33
|
-
This loads
|
|
34
|
-
table ownership,
|
|
32
|
+
→ call prime_session(work_item_id: <active_id>)
|
|
33
|
+
This loads ONLY the relevant microservices, ports, repos, Kafka topics,
|
|
34
|
+
table ownership, golden rules, and task progress into your context using Thin Indexes.
|
|
35
35
|
|
|
36
|
-
STEP 2 —
|
|
37
|
-
→ call manage_project_spec(action: "get_golden_rules")
|
|
38
|
-
This loads all non-negotiable architectural guardrails.
|
|
39
|
-
|
|
40
|
-
STEP 3 — if you are working on a specific service:
|
|
36
|
+
STEP 2 — if you need more details about a specific service not returned by prime_session:
|
|
41
37
|
→ call manage_project_spec(action: "get_service", service_name: <that service>)
|
|
42
38
|
```
|
|
43
39
|
|
|
@@ -72,7 +68,7 @@ There are NO circumstances under which Phase 00 can be skipped, abbreviated, or
|
|
|
72
68
|
❌ Write any code
|
|
73
69
|
❌ Proceed to Phase 0.5 or any other phase
|
|
74
70
|
|
|
75
|
-
The ONLY tool calls permitted before Phase 00 completes are
|
|
71
|
+
The ONLY tool calls permitted before Phase 00 completes are `prime_session` and `manage_project_spec`.
|
|
76
72
|
|
|
77
73
|
---
|
|
78
74
|
|
|
@@ -351,6 +347,9 @@ STEP 3: if implementing anything similar to a past feature:
|
|
|
351
347
|
call get_pattern_from_task(work_item_id: <matching id>)
|
|
352
348
|
— extract reusable patterns
|
|
353
349
|
|
|
350
|
+
STEP 3.5: call skill_recommend(query: <task description>)
|
|
351
|
+
— ALWAYS. Retrieve domain-specific skills and patterns from the skill catalog.
|
|
352
|
+
|
|
354
353
|
STEP 4: call manage_task(action: "initialize",
|
|
355
354
|
work_item_id: ...,
|
|
356
355
|
title: ...,
|
package/README.md
CHANGED
|
@@ -25,6 +25,19 @@
|
|
|
25
25
|
| `run_pre_pr_checks_with_reviewer_agent` | **Phase 4** — PR Handoff | **NEW** — Unified 3-tier PR gate. Runs mechanical checks, AI file reviews, and context-aware task evaluation in a single pass. |
|
|
26
26
|
| `get_secufusion_rules` | **Setup** | Returns the `AGENTS.md` rules for AI clients that don't natively support MCP Resources |
|
|
27
27
|
| `classify_task` | **Phase 0.5** — Task Classification | **NEW** — Deep multi-pass analysis engine. Classifies any task as `BACKEND_ONLY`, `FRONTEND_ONLY`, `FULL_STACK`, or `EXTENSION_ONLY` based on root cause (where the fix lives), not surface symptoms. Must be the first tool called on any task. |
|
|
28
|
+
| `prime_session` | **Phase 0** — Session Start | **NEW** — Hyper-efficient session startup. Combines Phase 00 (spec) and Phase 0 (task) into one call using Thin Indexes to optimize context tokens. |
|
|
29
|
+
| `skill_recommend` | **Phase 1** — Planning | **NEW** — Dynamically recommends and retrieves domain-specific coding skills from `skills-catalog.json`. |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 What's New: MLMCPS Architecture Alignment
|
|
34
|
+
|
|
35
|
+
The SecuFusion MCP has been refactored to align with the advanced MLMCPS framework principles, bringing massive efficiency and UX improvements:
|
|
36
|
+
|
|
37
|
+
1. **Persona-Driven Slash Commands**: The monolithic agent prompt has been split. You can now use `/sfn_spec`, `/sfn_code`, and `/sfn_review` in your AI chat to instantly summon the Planner, Coder, or Reviewer persona, ensuring razor-sharp focus per phase.
|
|
38
|
+
2. **Subprocess PR Checks**: Tier 1 mechanical checks (`TENANT_ISOLATION`, `N_PLUS_ONE`, etc.) are now extracted into a standalone CLI script (`scripts/sfn-pr-check.js`), allowing them to be run by the AI *or* natively within your CI/CD pipelines.
|
|
39
|
+
3. **Thin Index Token Discipline**: Context bloat is gone. Tools like `manage_task(read)` and `prime_session` now return lightweight "Thin Indexes"—compact Markdown summaries with absolute file paths—so the AI only reads the full JSON via `view_file` when truly necessary.
|
|
40
|
+
4. **Dynamic Skill Registry**: A new `skill_recommend` tool allows the AI to dynamically discover domain-specific architectural skills without bloating the base prompt.
|
|
28
41
|
|
|
29
42
|
---
|
|
30
43
|
|
package/index.js
CHANGED
|
@@ -638,8 +638,19 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
638
638
|
return appendTelemetry({ isError: true, content: [{ type: "text", text: `ERROR: Task WI-${work_item_id} not found. Call action=initialize first.\n\nExpected folder: ${dir}` }] }, inputChars);
|
|
639
639
|
}
|
|
640
640
|
if (action === "read") {
|
|
641
|
-
const
|
|
642
|
-
|
|
641
|
+
const specObj = readJson(specPath) || {};
|
|
642
|
+
const decisions = readJson(decisionsPath) || [];
|
|
643
|
+
const files = readJson(filesPath) || [];
|
|
644
|
+
const scenarios = readJson(scenariosPath) || [];
|
|
645
|
+
let out = `## Task ${work_item_id} (Thin Index)\n\n`;
|
|
646
|
+
out += `**Title:** ${specObj.title || 'unknown'}\n\n`;
|
|
647
|
+
out += `To optimize context, full task JSON is not returned. Use \`view_file\` on the following absolute paths if you need details:\n`;
|
|
648
|
+
out += `- **Spec**: ${specPath}\n`;
|
|
649
|
+
out += `- **Progress**: ${progressPath}\n`;
|
|
650
|
+
out += `- **Decisions** (${decisions.length}): ${decisionsPath}\n`;
|
|
651
|
+
out += `- **Files Touched** (${files.length}): ${filesPath}\n`;
|
|
652
|
+
out += `- **Scenarios** (${scenarios.length}): ${scenariosPath}\n`;
|
|
653
|
+
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
643
654
|
}
|
|
644
655
|
if (action === "read_summary") {
|
|
645
656
|
const spec = readJson(specPath) || {};
|
|
@@ -2136,7 +2147,13 @@ server.tool("prime_session", "Hyper-efficient session startup — combines Phase
|
|
|
2136
2147
|
if (pending.length > 0) {
|
|
2137
2148
|
out += `**Pending ACs:**\n${pending.map(ac => ` - [ ] ${ac}`).join("\n")}\n`;
|
|
2138
2149
|
}
|
|
2139
|
-
out += "\n";
|
|
2150
|
+
out += "\n### Artifact Pointers\n";
|
|
2151
|
+
out += `Use \`view_file\` on these absolute paths to read full task JSON data:\n`;
|
|
2152
|
+
out += `- **Spec**: ${path.join(taskDir, "spec.json")}\n`;
|
|
2153
|
+
out += `- **Progress**: ${path.join(taskDir, "progress.json")}\n`;
|
|
2154
|
+
out += `- **Decisions**: ${path.join(taskDir, "decisions.json")}\n`;
|
|
2155
|
+
out += `- **Files Touched**: ${path.join(taskDir, "files-touched.json")}\n`;
|
|
2156
|
+
out += `- **Scenarios**: ${path.join(taskDir, "scenarios.json")}\n\n`;
|
|
2140
2157
|
}
|
|
2141
2158
|
else {
|
|
2142
2159
|
out += `⚠️ No active task found for ${work_item_id}. Call manage_task(initialize) to create it.\n\n`;
|
|
@@ -2798,262 +2815,44 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2798
2815
|
exists: fs.existsSync(abs),
|
|
2799
2816
|
};
|
|
2800
2817
|
});
|
|
2801
|
-
|
|
2818
|
+
let mechanical = [];
|
|
2802
2819
|
const passResults = [];
|
|
2803
2820
|
const reviewComments = [];
|
|
2804
2821
|
let aiCost = 0;
|
|
2805
2822
|
// ══════════════════════════════════════════════════════════════════════
|
|
2806
|
-
// TIER 1 — MECHANICAL CHECKS (P1–P5)
|
|
2823
|
+
// TIER 1 — MECHANICAL CHECKS (P1–P5) Via Subprocess
|
|
2807
2824
|
// ══════════════════════════════════════════════════════════════════════
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
queryAnnotationLines.push(idx);
|
|
2817
|
-
});
|
|
2818
|
-
f.lines.forEach((line, idx) => {
|
|
2819
|
-
const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
|
|
2820
|
-
if (dm && !dm[1].toLowerCase().includes("tenant")) {
|
|
2821
|
-
let suppressionReason = "";
|
|
2822
|
-
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === dm[0] || s.method.endsWith(`.${dm[0]}`)));
|
|
2823
|
-
if (explicitSup)
|
|
2824
|
-
suppressionReason = explicitSup.reason;
|
|
2825
|
-
else {
|
|
2826
|
-
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
2827
|
-
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2828
|
-
if (match)
|
|
2829
|
-
suppressionReason = match[1];
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
if (suppressionReason) {
|
|
2833
|
-
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
|
|
2834
|
-
}
|
|
2835
|
-
else {
|
|
2836
|
-
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2837
|
-
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
2838
|
-
suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
});
|
|
2842
|
-
queryAnnotationLines.forEach(ai => {
|
|
2843
|
-
const block = f.lines.slice(ai, ai + 5).join(" ");
|
|
2844
|
-
if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
|
|
2845
|
-
let suppressionReason = "";
|
|
2846
|
-
let queryMethodName = "";
|
|
2847
|
-
const methodDeclMatch = block.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
|
|
2848
|
-
if (methodDeclMatch)
|
|
2849
|
-
queryMethodName = methodDeclMatch[1];
|
|
2850
|
-
if (queryMethodName) {
|
|
2851
|
-
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
|
|
2852
|
-
if (explicitSup)
|
|
2853
|
-
suppressionReason = explicitSup.reason;
|
|
2854
|
-
}
|
|
2855
|
-
if (!suppressionReason) {
|
|
2856
|
-
for (let i = Math.max(0, ai - 5); i < ai; i++) {
|
|
2857
|
-
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2858
|
-
if (match)
|
|
2859
|
-
suppressionReason = match[1];
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
if (suppressionReason) {
|
|
2863
|
-
p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
|
|
2864
|
-
}
|
|
2865
|
-
else {
|
|
2866
|
-
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
|
|
2867
|
-
message: `@Query at line ${ai + 1} has no tenantId filter`,
|
|
2868
|
-
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
});
|
|
2872
|
-
}
|
|
2825
|
+
let tier1Passed = false;
|
|
2826
|
+
try {
|
|
2827
|
+
const scriptPath = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "scripts/sfn-pr-check.js");
|
|
2828
|
+
const execResult = require('child_process').execFileSync('node', [scriptPath, '--task', work_item_id, '--workspace', wsRoot], { encoding: 'utf-8' });
|
|
2829
|
+
const jsonStr = execResult.trim().split('\n').pop() || "{}";
|
|
2830
|
+
const parsed = JSON.parse(jsonStr);
|
|
2831
|
+
mechanical = parsed.mechanical || [];
|
|
2832
|
+
tier1Passed = parsed.pass === true;
|
|
2873
2833
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
if (dir) {
|
|
2881
|
-
const decisionsPath = path.join(dir, "decisions.json");
|
|
2882
|
-
const decisions = readJson(decisionsPath) || [];
|
|
2883
|
-
let modified = false;
|
|
2884
|
-
for (const sup of p1Suppressed) {
|
|
2885
|
-
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
2886
|
-
if (!decisions.some(d => d.decision === desc)) {
|
|
2887
|
-
decisions.push({
|
|
2888
|
-
id: decisions.length + 1,
|
|
2889
|
-
decision: desc,
|
|
2890
|
-
rationale: sup.reason,
|
|
2891
|
-
logged_at: new Date().toISOString()
|
|
2892
|
-
});
|
|
2893
|
-
modified = true;
|
|
2894
|
-
}
|
|
2834
|
+
catch (e) {
|
|
2835
|
+
if (e.stdout) {
|
|
2836
|
+
const jsonStr = e.stdout.trim().split('\n').pop() || "{}";
|
|
2837
|
+
try {
|
|
2838
|
+
const parsed = JSON.parse(jsonStr);
|
|
2839
|
+
mechanical = parsed.mechanical || [];
|
|
2895
2840
|
}
|
|
2896
|
-
|
|
2897
|
-
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
// ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
|
|
2901
|
-
const p2 = [];
|
|
2902
|
-
if (!skip_checks.includes("n_plus_one")) {
|
|
2903
|
-
for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
|
|
2904
|
-
let depth = 0, inLoop = false, loopDepth = -1;
|
|
2905
|
-
f.lines.forEach((line, idx) => {
|
|
2906
|
-
const o = (line.match(/\{/g) || []).length;
|
|
2907
|
-
const c = (line.match(/\}/g) || []).length;
|
|
2908
|
-
if (/\b(for|forEach|stream\(\)|\.map\(|\.forEach\(|while\s*\()/i.test(line) && !inLoop) {
|
|
2909
|
-
inLoop = true;
|
|
2910
|
-
loopDepth = depth;
|
|
2911
|
-
}
|
|
2912
|
-
if (inLoop) {
|
|
2913
|
-
const rm = /\.(findById|findByIdAndTenantId|findAll|findAllBy|save|saveAll|delete|deleteById|existsById|count)\s*\(/.exec(line);
|
|
2914
|
-
if (rm)
|
|
2915
|
-
p2.push({ check: "N_PLUS_ONE", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2916
|
-
message: `Repository call \`${rm[0]}\` inside a loop`,
|
|
2917
|
-
suggestion: "Extract IDs first, then use findAllById() / saveAll() outside the loop" });
|
|
2918
|
-
}
|
|
2919
|
-
depth += o - c;
|
|
2920
|
-
if (inLoop && depth <= loopDepth) {
|
|
2921
|
-
inLoop = false;
|
|
2922
|
-
loopDepth = -1;
|
|
2923
|
-
}
|
|
2924
|
-
});
|
|
2841
|
+
catch (e2) { }
|
|
2925
2842
|
}
|
|
2843
|
+
tier1Passed = false;
|
|
2926
2844
|
}
|
|
2927
|
-
mechanical.
|
|
2845
|
+
const p1 = mechanical.filter(m => m.check === "TENANT_ISOLATION");
|
|
2846
|
+
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
|
|
2847
|
+
const p2 = mechanical.filter(m => m.check === "N_PLUS_ONE");
|
|
2928
2848
|
passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
|
|
2929
|
-
|
|
2930
|
-
const p3 = [];
|
|
2931
|
-
if (!skip_checks.includes("missing_index")) {
|
|
2932
|
-
const queryCols = [];
|
|
2933
|
-
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
2934
|
-
f.lines.forEach((line, idx) => {
|
|
2935
|
-
const dm = line.match(/(?:find|delete|count|exists)\w+By([\w]+)/);
|
|
2936
|
-
if (dm) {
|
|
2937
|
-
for (const p of dm[1].replace(/And|Or/g, ",").split(",")) {
|
|
2938
|
-
const col = p.charAt(0).toLowerCase() + p.slice(1);
|
|
2939
|
-
if (!["tenantId", "id"].includes(col))
|
|
2940
|
-
queryCols.push({ col, file: f.file_path, line: idx + 1 });
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
const wm = line.match(/WHERE\s+\w+\.(\w+)\s*=/gi);
|
|
2944
|
-
if (wm)
|
|
2945
|
-
for (const w of wm) {
|
|
2946
|
-
const cm = w.match(/\.(\w+)\s*=/i);
|
|
2947
|
-
if (cm && !["tenantId", "id", "tenant_id"].includes(cm[1]))
|
|
2948
|
-
queryCols.push({ col: cm[1], file: f.file_path, line: idx + 1 });
|
|
2949
|
-
}
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
const migrationContent = fileRecords.filter(f => f.isMigration).map(f => f.content.toLowerCase()).join("\n");
|
|
2953
|
-
if (fileRecords.some(f => f.isMigration)) {
|
|
2954
|
-
for (const { col, file, line } of queryCols) {
|
|
2955
|
-
const snake = col.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
2956
|
-
if (!new RegExp(`create.*index.*(${snake}|${col.toLowerCase()})`, "i").test(migrationContent)) {
|
|
2957
|
-
p3.push({ check: "MISSING_INDEX", severity: "WARNING", file, line,
|
|
2958
|
-
message: `Query on column \`${col}\` has no CREATE INDEX in migration`,
|
|
2959
|
-
suggestion: `Add: CREATE INDEX idx_<table>_${snake} ON <table>(${snake});` });
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
mechanical.push(...p3);
|
|
2849
|
+
const p3 = mechanical.filter(m => m.check === "MISSING_INDEX");
|
|
2965
2850
|
passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
|
|
2966
|
-
|
|
2967
|
-
const p4 = [];
|
|
2968
|
-
if (!skip_checks.includes("kafka_sync")) {
|
|
2969
|
-
for (const f of fileRecords.filter(f => f.isKafka)) {
|
|
2970
|
-
let inListener = false, lDepth = 0;
|
|
2971
|
-
f.lines.forEach((line, idx) => {
|
|
2972
|
-
if (/@KafkaListener/.test(line)) {
|
|
2973
|
-
inListener = true;
|
|
2974
|
-
lDepth = 0;
|
|
2975
|
-
}
|
|
2976
|
-
if (inListener) {
|
|
2977
|
-
lDepth += (line.match(/\{/g) || []).length;
|
|
2978
|
-
lDepth -= (line.match(/\}/g) || []).length;
|
|
2979
|
-
const sm = /\.(save|saveAll|findById|findAll|delete|deleteById|restTemplate|webClient|getForObject|postForObject|exchange)\s*\(/.exec(line);
|
|
2980
|
-
if (sm) {
|
|
2981
|
-
const isAsync = /@Async|CompletableFuture|executorService|threadPool/i.test(f.lines.slice(Math.max(0, idx - 3), idx + 1).join(" "));
|
|
2982
|
-
if (!isAsync)
|
|
2983
|
-
p4.push({ check: "KAFKA_SYNC", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2984
|
-
message: `Sync call \`${sm[0]}\` inside @KafkaListener — blocks consumer thread`,
|
|
2985
|
-
suggestion: "Offload to @Async method or a dedicated ExecutorService" });
|
|
2986
|
-
}
|
|
2987
|
-
if (lDepth <= 0 && lDepth !== 0)
|
|
2988
|
-
inListener = false;
|
|
2989
|
-
}
|
|
2990
|
-
});
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
mechanical.push(...p4);
|
|
2851
|
+
const p4 = mechanical.filter(m => m.check === "KAFKA_SYNC");
|
|
2994
2852
|
passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
|
|
2995
|
-
|
|
2996
|
-
// Algorithm: collect all return lines per method via flush-on-close.
|
|
2997
|
-
// Every return except the last in a method body is an early return.
|
|
2998
|
-
const p5 = [];
|
|
2999
|
-
if (!skip_checks.includes("early_return")) {
|
|
3000
|
-
for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
|
|
3001
|
-
let depth = 0, methodDepth = -1, methodStart = -1, inMethod = false, mName = "";
|
|
3002
|
-
let mReturns = [], mBody = [];
|
|
3003
|
-
const flush = () => {
|
|
3004
|
-
if (mReturns.length > 1) {
|
|
3005
|
-
for (const ri of mReturns.slice(0, -1)) {
|
|
3006
|
-
p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
|
|
3007
|
-
message: `Early return in \`${mName}()\` — violates single-exit rule`,
|
|
3008
|
-
suggestion: "Declare result at top, use nested if blocks, single return at method end" });
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
else if (mReturns.length === 1) {
|
|
3012
|
-
const ri = mReturns[0];
|
|
3013
|
-
const after = f.lines.slice(ri + 1).map((l, o) => ({ l: l.trim(), ai: ri + 1 + o })).filter(({ l }) => l.length > 0 && l !== "}" && l !== "};" && !l.startsWith("//"));
|
|
3014
|
-
if (after.length > 0 && mBody.length > 0 && after[0].ai <= mBody[mBody.length - 1]) {
|
|
3015
|
-
p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
|
|
3016
|
-
message: `Early return in \`${mName}()\` — violates single-exit rule`,
|
|
3017
|
-
suggestion: "Declare result at top, use nested if blocks, single return at method end" });
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
mReturns = [];
|
|
3021
|
-
mBody = [];
|
|
3022
|
-
};
|
|
3023
|
-
f.lines.forEach((line, idx) => {
|
|
3024
|
-
const trimmed = line.trim();
|
|
3025
|
-
const o = (line.match(/\{/g) || []).length;
|
|
3026
|
-
const c = (line.match(/\}/g) || []).length;
|
|
3027
|
-
const jm = line.match(/(?:public|private|protected|static)\s+\S+\s+(\w+)\s*\(/);
|
|
3028
|
-
const tm = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|async\s+(\w+)\s*\()/);
|
|
3029
|
-
if ((f.isJava ? jm : tm) && !inMethod) {
|
|
3030
|
-
inMethod = true;
|
|
3031
|
-
methodDepth = depth;
|
|
3032
|
-
methodStart = idx;
|
|
3033
|
-
mReturns = [];
|
|
3034
|
-
mBody = [];
|
|
3035
|
-
mName = jm?.[1] || tm?.[1] || tm?.[2] || tm?.[3] || "unknown";
|
|
3036
|
-
}
|
|
3037
|
-
depth += o - c;
|
|
3038
|
-
if (inMethod && idx > methodStart) {
|
|
3039
|
-
if (trimmed.length > 0)
|
|
3040
|
-
mBody.push(idx);
|
|
3041
|
-
if (/\breturn\b/.test(line) && !/^\s*\/\//.test(line))
|
|
3042
|
-
mReturns.push(idx);
|
|
3043
|
-
if (depth <= methodDepth) {
|
|
3044
|
-
flush();
|
|
3045
|
-
inMethod = false;
|
|
3046
|
-
methodDepth = -1;
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
});
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
mechanical.push(...p5);
|
|
2853
|
+
const p5 = mechanical.filter(m => m.check === "EARLY_RETURN");
|
|
3053
2854
|
passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
|
|
3054
|
-
// ── Tier 1 gate ────────────────────────────────────────────────────────
|
|
3055
2855
|
const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
|
|
3056
|
-
const tier1Passed = blockingMechanical.length === 0;
|
|
3057
2856
|
// ══════════════════════════════════════════════════════════════════════
|
|
3058
2857
|
// TIER 2 — AI FILE REVIEWER (Pass 6)
|
|
3059
2858
|
// Reads each changed file, applies architectural heuristics.
|
|
@@ -4704,48 +4503,70 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
|
|
|
4704
4503
|
}
|
|
4705
4504
|
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
4706
4505
|
});
|
|
4707
|
-
// ─── Resource & Prompt for
|
|
4708
|
-
const
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4506
|
+
// ─── Resource & Prompt for Personas ──────────────────────────────────────────
|
|
4507
|
+
const AGENTS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "../.agents");
|
|
4508
|
+
const PLANNER_MD_PATH = path.join(AGENTS_DIR, "planner.md");
|
|
4509
|
+
const CODER_MD_PATH = path.join(AGENTS_DIR, "coder.md");
|
|
4510
|
+
const REVIEWER_MD_PATH = path.join(AGENTS_DIR, "reviewer.md");
|
|
4511
|
+
const SKILLS_CATALOG_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "skills-catalog.json");
|
|
4512
|
+
server.prompt("secufusion_planner", "Starts a conversation with the SecuFusion planner workflow rules loaded (Phase 00 - Phase 1).", async () => {
|
|
4513
|
+
const content = readFileSafe(PLANNER_MD_PATH);
|
|
4514
|
+
if (!content)
|
|
4515
|
+
throw new Error(`Rules file not found at ${PLANNER_MD_PATH}`);
|
|
4714
4516
|
return {
|
|
4715
|
-
|
|
4716
|
-
uri: uri.href,
|
|
4717
|
-
text: content
|
|
4718
|
-
}]
|
|
4517
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Planner. Here are your rules:\n\n${content}` } }]
|
|
4719
4518
|
};
|
|
4720
4519
|
});
|
|
4721
|
-
server.prompt("
|
|
4722
|
-
const content = readFileSafe(
|
|
4723
|
-
if (!content)
|
|
4724
|
-
throw new Error(`Rules file not found at ${
|
|
4725
|
-
}
|
|
4520
|
+
server.prompt("secufusion_coder", "Starts a conversation with the SecuFusion coder workflow rules loaded (Phase 2 - Phase 3).", async () => {
|
|
4521
|
+
const content = readFileSafe(CODER_MD_PATH);
|
|
4522
|
+
if (!content)
|
|
4523
|
+
throw new Error(`Rules file not found at ${CODER_MD_PATH}`);
|
|
4726
4524
|
return {
|
|
4727
|
-
messages: [
|
|
4728
|
-
{
|
|
4729
|
-
role: "user",
|
|
4730
|
-
content: {
|
|
4731
|
-
type: "text",
|
|
4732
|
-
text: `You are now the SecuFusion Senior Architect. Here are your rules:\n\n${content}`
|
|
4733
|
-
}
|
|
4734
|
-
}
|
|
4735
|
-
]
|
|
4525
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Coder. Here are your rules:\n\n${content}` } }]
|
|
4736
4526
|
};
|
|
4737
4527
|
});
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4528
|
+
server.prompt("secufusion_reviewer", "Starts a conversation with the SecuFusion reviewer workflow rules loaded (Phase 4 - Phase 5).", async () => {
|
|
4529
|
+
const content = readFileSafe(REVIEWER_MD_PATH);
|
|
4530
|
+
if (!content)
|
|
4531
|
+
throw new Error(`Rules file not found at ${REVIEWER_MD_PATH}`);
|
|
4532
|
+
return {
|
|
4533
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Reviewer. Here are your rules:\n\n${content}` } }]
|
|
4534
|
+
};
|
|
4535
|
+
});
|
|
4536
|
+
// ─── Slash Commands (Aliases for Personas / Workflows) ───────────────────────
|
|
4537
|
+
server.prompt("sfn_spec", "Slash command to start Phase 0.5 (Classification) and Phase 1 (Planning)", async () => {
|
|
4538
|
+
const content = readFileSafe(PLANNER_MD_PATH);
|
|
4539
|
+
return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_spec]\nYou are in Spec/Planning mode. Execute Phase 00, Phase 0.5, and Phase 1 rules:\n\n${content}` } }] };
|
|
4540
|
+
});
|
|
4541
|
+
server.prompt("sfn_code", "Slash command to start Phase 2 (Execution) and Phase 3 (Course Correction)", async () => {
|
|
4542
|
+
const content = readFileSafe(CODER_MD_PATH);
|
|
4543
|
+
return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_code]\nYou are in Coder mode. Execute Phase 2 and Phase 3 rules:\n\n${content}` } }] };
|
|
4544
|
+
});
|
|
4545
|
+
server.prompt("sfn_review", "Slash command to start Phase 4 (PR Checks) and Phase 5 (Retrospective)", async () => {
|
|
4546
|
+
const content = readFileSafe(REVIEWER_MD_PATH);
|
|
4547
|
+
return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_review]\nYou are in Review mode. Execute Phase 4 and Phase 5 rules:\n\n${content}` } }] };
|
|
4548
|
+
});
|
|
4549
|
+
// ─── Tools ─────────────────────────────────────────────
|
|
4550
|
+
server.tool("skill_recommend", "Searches the skill catalog for matching domain skills based on keywords.", {
|
|
4551
|
+
keywords: z.array(z.string()).describe("Keywords to search for (e.g., 'kafka', 'react')")
|
|
4552
|
+
}, async ({ keywords }) => {
|
|
4553
|
+
const content = readFileSafe(SKILLS_CATALOG_PATH);
|
|
4554
|
+
if (!content)
|
|
4555
|
+
return appendTelemetry({ content: [{ type: "text", text: "No skills catalog found." }] }, 0);
|
|
4556
|
+
try {
|
|
4557
|
+
const catalog = JSON.parse(content);
|
|
4558
|
+
const matches = catalog.skills.filter((skill) => keywords.some(kw => skill.tags.includes(kw.toLowerCase())));
|
|
4559
|
+
if (matches.length === 0)
|
|
4560
|
+
return appendTelemetry({ content: [{ type: "text", text: "No matching skills found." }] }, content.length);
|
|
4561
|
+
let out = "Found the following relevant skills:\n";
|
|
4562
|
+
matches.forEach((s) => {
|
|
4563
|
+
out += `- **${s.name}**: ${s.description}\n *Read file:* \`${s.path}\`\n`;
|
|
4564
|
+
});
|
|
4565
|
+
return appendTelemetry({ content: [{ type: "text", text: out }] }, content.length);
|
|
4566
|
+
}
|
|
4567
|
+
catch (e) {
|
|
4568
|
+
return appendTelemetry({ content: [{ type: "text", text: "Error parsing skills catalog." }] }, 0);
|
|
4745
4569
|
}
|
|
4746
|
-
return appendTelemetry({
|
|
4747
|
-
content: [{ type: "text", text: content }]
|
|
4748
|
-
}, 0);
|
|
4749
4570
|
});
|
|
4750
4571
|
const transport = new StdioServerTransport();
|
|
4751
4572
|
await server.connect(transport);
|