secufusion-mcp 1.2.1 → 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 +115 -286
- 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
|
@@ -524,24 +524,32 @@ function generateNewTaskFolder(workItemId, title, tasksRoot) {
|
|
|
524
524
|
let numericId = workItemId.replace(/\D/g, ''); // Extract just the digits
|
|
525
525
|
if (!numericId)
|
|
526
526
|
numericId = workItemId; // fallback
|
|
527
|
-
let taskTypeStr = "
|
|
527
|
+
let taskTypeStr = "Task";
|
|
528
528
|
// Try to load the classification to get the true task_type
|
|
529
529
|
const classifPath = path.join(tasksRoot, '..', 'classifications', `${workItemId}.json`);
|
|
530
530
|
const classif = readJson(classifPath);
|
|
531
531
|
if (classif && typeof classif.task_type === 'string') {
|
|
532
532
|
const rawType = classif.task_type.toLowerCase();
|
|
533
533
|
if (rawType === 'user_story')
|
|
534
|
-
taskTypeStr = '
|
|
534
|
+
taskTypeStr = 'US';
|
|
535
|
+
else if (rawType === 'bug')
|
|
536
|
+
taskTypeStr = 'Bug';
|
|
535
537
|
else
|
|
536
|
-
taskTypeStr = rawType;
|
|
538
|
+
taskTypeStr = rawType.charAt(0).toUpperCase() + rawType.slice(1);
|
|
537
539
|
}
|
|
538
540
|
else {
|
|
539
541
|
// Fallback: guess from the workItemId string itself if passed as "BUG-1173"
|
|
540
542
|
const prefixMatch = workItemId.match(/^([a-zA-Z]+)-/);
|
|
541
543
|
if (prefixMatch) {
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
taskTypeStr = '
|
|
544
|
+
const lowerPrefix = prefixMatch[1].toLowerCase();
|
|
545
|
+
if (lowerPrefix === 'wi')
|
|
546
|
+
taskTypeStr = 'Task';
|
|
547
|
+
else if (lowerPrefix === 'bug')
|
|
548
|
+
taskTypeStr = 'Bug';
|
|
549
|
+
else if (lowerPrefix === 'us')
|
|
550
|
+
taskTypeStr = 'US';
|
|
551
|
+
else
|
|
552
|
+
taskTypeStr = lowerPrefix.charAt(0).toUpperCase() + lowerPrefix.slice(1);
|
|
545
553
|
}
|
|
546
554
|
}
|
|
547
555
|
return path.join(tasksRoot, `${taskTypeStr}-${numericId}-${slug}`);
|
|
@@ -630,8 +638,19 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
630
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);
|
|
631
639
|
}
|
|
632
640
|
if (action === "read") {
|
|
633
|
-
const
|
|
634
|
-
|
|
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);
|
|
635
654
|
}
|
|
636
655
|
if (action === "read_summary") {
|
|
637
656
|
const spec = readJson(specPath) || {};
|
|
@@ -2128,7 +2147,13 @@ server.tool("prime_session", "Hyper-efficient session startup — combines Phase
|
|
|
2128
2147
|
if (pending.length > 0) {
|
|
2129
2148
|
out += `**Pending ACs:**\n${pending.map(ac => ` - [ ] ${ac}`).join("\n")}\n`;
|
|
2130
2149
|
}
|
|
2131
|
-
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`;
|
|
2132
2157
|
}
|
|
2133
2158
|
else {
|
|
2134
2159
|
out += `⚠️ No active task found for ${work_item_id}. Call manage_task(initialize) to create it.\n\n`;
|
|
@@ -2790,262 +2815,44 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2790
2815
|
exists: fs.existsSync(abs),
|
|
2791
2816
|
};
|
|
2792
2817
|
});
|
|
2793
|
-
|
|
2818
|
+
let mechanical = [];
|
|
2794
2819
|
const passResults = [];
|
|
2795
2820
|
const reviewComments = [];
|
|
2796
2821
|
let aiCost = 0;
|
|
2797
2822
|
// ══════════════════════════════════════════════════════════════════════
|
|
2798
|
-
// TIER 1 — MECHANICAL CHECKS (P1–P5)
|
|
2823
|
+
// TIER 1 — MECHANICAL CHECKS (P1–P5) Via Subprocess
|
|
2799
2824
|
// ══════════════════════════════════════════════════════════════════════
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
queryAnnotationLines.push(idx);
|
|
2809
|
-
});
|
|
2810
|
-
f.lines.forEach((line, idx) => {
|
|
2811
|
-
const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
|
|
2812
|
-
if (dm && !dm[1].toLowerCase().includes("tenant")) {
|
|
2813
|
-
let suppressionReason = "";
|
|
2814
|
-
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === dm[0] || s.method.endsWith(`.${dm[0]}`)));
|
|
2815
|
-
if (explicitSup)
|
|
2816
|
-
suppressionReason = explicitSup.reason;
|
|
2817
|
-
else {
|
|
2818
|
-
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
2819
|
-
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2820
|
-
if (match)
|
|
2821
|
-
suppressionReason = match[1];
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
if (suppressionReason) {
|
|
2825
|
-
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
|
|
2826
|
-
}
|
|
2827
|
-
else {
|
|
2828
|
-
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2829
|
-
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
2830
|
-
suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
});
|
|
2834
|
-
queryAnnotationLines.forEach(ai => {
|
|
2835
|
-
const block = f.lines.slice(ai, ai + 5).join(" ");
|
|
2836
|
-
if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
|
|
2837
|
-
let suppressionReason = "";
|
|
2838
|
-
let queryMethodName = "";
|
|
2839
|
-
const methodDeclMatch = block.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
|
|
2840
|
-
if (methodDeclMatch)
|
|
2841
|
-
queryMethodName = methodDeclMatch[1];
|
|
2842
|
-
if (queryMethodName) {
|
|
2843
|
-
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
|
|
2844
|
-
if (explicitSup)
|
|
2845
|
-
suppressionReason = explicitSup.reason;
|
|
2846
|
-
}
|
|
2847
|
-
if (!suppressionReason) {
|
|
2848
|
-
for (let i = Math.max(0, ai - 5); i < ai; i++) {
|
|
2849
|
-
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2850
|
-
if (match)
|
|
2851
|
-
suppressionReason = match[1];
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
if (suppressionReason) {
|
|
2855
|
-
p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
|
|
2856
|
-
}
|
|
2857
|
-
else {
|
|
2858
|
-
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
|
|
2859
|
-
message: `@Query at line ${ai + 1} has no tenantId filter`,
|
|
2860
|
-
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
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;
|
|
2865
2833
|
}
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
if (dir) {
|
|
2873
|
-
const decisionsPath = path.join(dir, "decisions.json");
|
|
2874
|
-
const decisions = readJson(decisionsPath) || [];
|
|
2875
|
-
let modified = false;
|
|
2876
|
-
for (const sup of p1Suppressed) {
|
|
2877
|
-
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
2878
|
-
if (!decisions.some(d => d.decision === desc)) {
|
|
2879
|
-
decisions.push({
|
|
2880
|
-
id: decisions.length + 1,
|
|
2881
|
-
decision: desc,
|
|
2882
|
-
rationale: sup.reason,
|
|
2883
|
-
logged_at: new Date().toISOString()
|
|
2884
|
-
});
|
|
2885
|
-
modified = true;
|
|
2886
|
-
}
|
|
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 || [];
|
|
2887
2840
|
}
|
|
2888
|
-
|
|
2889
|
-
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
// ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
|
|
2893
|
-
const p2 = [];
|
|
2894
|
-
if (!skip_checks.includes("n_plus_one")) {
|
|
2895
|
-
for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
|
|
2896
|
-
let depth = 0, inLoop = false, loopDepth = -1;
|
|
2897
|
-
f.lines.forEach((line, idx) => {
|
|
2898
|
-
const o = (line.match(/\{/g) || []).length;
|
|
2899
|
-
const c = (line.match(/\}/g) || []).length;
|
|
2900
|
-
if (/\b(for|forEach|stream\(\)|\.map\(|\.forEach\(|while\s*\()/i.test(line) && !inLoop) {
|
|
2901
|
-
inLoop = true;
|
|
2902
|
-
loopDepth = depth;
|
|
2903
|
-
}
|
|
2904
|
-
if (inLoop) {
|
|
2905
|
-
const rm = /\.(findById|findByIdAndTenantId|findAll|findAllBy|save|saveAll|delete|deleteById|existsById|count)\s*\(/.exec(line);
|
|
2906
|
-
if (rm)
|
|
2907
|
-
p2.push({ check: "N_PLUS_ONE", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2908
|
-
message: `Repository call \`${rm[0]}\` inside a loop`,
|
|
2909
|
-
suggestion: "Extract IDs first, then use findAllById() / saveAll() outside the loop" });
|
|
2910
|
-
}
|
|
2911
|
-
depth += o - c;
|
|
2912
|
-
if (inLoop && depth <= loopDepth) {
|
|
2913
|
-
inLoop = false;
|
|
2914
|
-
loopDepth = -1;
|
|
2915
|
-
}
|
|
2916
|
-
});
|
|
2841
|
+
catch (e2) { }
|
|
2917
2842
|
}
|
|
2843
|
+
tier1Passed = false;
|
|
2918
2844
|
}
|
|
2919
|
-
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");
|
|
2920
2848
|
passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
|
|
2921
|
-
|
|
2922
|
-
const p3 = [];
|
|
2923
|
-
if (!skip_checks.includes("missing_index")) {
|
|
2924
|
-
const queryCols = [];
|
|
2925
|
-
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
2926
|
-
f.lines.forEach((line, idx) => {
|
|
2927
|
-
const dm = line.match(/(?:find|delete|count|exists)\w+By([\w]+)/);
|
|
2928
|
-
if (dm) {
|
|
2929
|
-
for (const p of dm[1].replace(/And|Or/g, ",").split(",")) {
|
|
2930
|
-
const col = p.charAt(0).toLowerCase() + p.slice(1);
|
|
2931
|
-
if (!["tenantId", "id"].includes(col))
|
|
2932
|
-
queryCols.push({ col, file: f.file_path, line: idx + 1 });
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
const wm = line.match(/WHERE\s+\w+\.(\w+)\s*=/gi);
|
|
2936
|
-
if (wm)
|
|
2937
|
-
for (const w of wm) {
|
|
2938
|
-
const cm = w.match(/\.(\w+)\s*=/i);
|
|
2939
|
-
if (cm && !["tenantId", "id", "tenant_id"].includes(cm[1]))
|
|
2940
|
-
queryCols.push({ col: cm[1], file: f.file_path, line: idx + 1 });
|
|
2941
|
-
}
|
|
2942
|
-
});
|
|
2943
|
-
}
|
|
2944
|
-
const migrationContent = fileRecords.filter(f => f.isMigration).map(f => f.content.toLowerCase()).join("\n");
|
|
2945
|
-
if (fileRecords.some(f => f.isMigration)) {
|
|
2946
|
-
for (const { col, file, line } of queryCols) {
|
|
2947
|
-
const snake = col.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
2948
|
-
if (!new RegExp(`create.*index.*(${snake}|${col.toLowerCase()})`, "i").test(migrationContent)) {
|
|
2949
|
-
p3.push({ check: "MISSING_INDEX", severity: "WARNING", file, line,
|
|
2950
|
-
message: `Query on column \`${col}\` has no CREATE INDEX in migration`,
|
|
2951
|
-
suggestion: `Add: CREATE INDEX idx_<table>_${snake} ON <table>(${snake});` });
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
mechanical.push(...p3);
|
|
2849
|
+
const p3 = mechanical.filter(m => m.check === "MISSING_INDEX");
|
|
2957
2850
|
passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
|
|
2958
|
-
|
|
2959
|
-
const p4 = [];
|
|
2960
|
-
if (!skip_checks.includes("kafka_sync")) {
|
|
2961
|
-
for (const f of fileRecords.filter(f => f.isKafka)) {
|
|
2962
|
-
let inListener = false, lDepth = 0;
|
|
2963
|
-
f.lines.forEach((line, idx) => {
|
|
2964
|
-
if (/@KafkaListener/.test(line)) {
|
|
2965
|
-
inListener = true;
|
|
2966
|
-
lDepth = 0;
|
|
2967
|
-
}
|
|
2968
|
-
if (inListener) {
|
|
2969
|
-
lDepth += (line.match(/\{/g) || []).length;
|
|
2970
|
-
lDepth -= (line.match(/\}/g) || []).length;
|
|
2971
|
-
const sm = /\.(save|saveAll|findById|findAll|delete|deleteById|restTemplate|webClient|getForObject|postForObject|exchange)\s*\(/.exec(line);
|
|
2972
|
-
if (sm) {
|
|
2973
|
-
const isAsync = /@Async|CompletableFuture|executorService|threadPool/i.test(f.lines.slice(Math.max(0, idx - 3), idx + 1).join(" "));
|
|
2974
|
-
if (!isAsync)
|
|
2975
|
-
p4.push({ check: "KAFKA_SYNC", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2976
|
-
message: `Sync call \`${sm[0]}\` inside @KafkaListener — blocks consumer thread`,
|
|
2977
|
-
suggestion: "Offload to @Async method or a dedicated ExecutorService" });
|
|
2978
|
-
}
|
|
2979
|
-
if (lDepth <= 0 && lDepth !== 0)
|
|
2980
|
-
inListener = false;
|
|
2981
|
-
}
|
|
2982
|
-
});
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
mechanical.push(...p4);
|
|
2851
|
+
const p4 = mechanical.filter(m => m.check === "KAFKA_SYNC");
|
|
2986
2852
|
passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
|
|
2987
|
-
|
|
2988
|
-
// Algorithm: collect all return lines per method via flush-on-close.
|
|
2989
|
-
// Every return except the last in a method body is an early return.
|
|
2990
|
-
const p5 = [];
|
|
2991
|
-
if (!skip_checks.includes("early_return")) {
|
|
2992
|
-
for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
|
|
2993
|
-
let depth = 0, methodDepth = -1, methodStart = -1, inMethod = false, mName = "";
|
|
2994
|
-
let mReturns = [], mBody = [];
|
|
2995
|
-
const flush = () => {
|
|
2996
|
-
if (mReturns.length > 1) {
|
|
2997
|
-
for (const ri of mReturns.slice(0, -1)) {
|
|
2998
|
-
p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
|
|
2999
|
-
message: `Early return in \`${mName}()\` — violates single-exit rule`,
|
|
3000
|
-
suggestion: "Declare result at top, use nested if blocks, single return at method end" });
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
else if (mReturns.length === 1) {
|
|
3004
|
-
const ri = mReturns[0];
|
|
3005
|
-
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("//"));
|
|
3006
|
-
if (after.length > 0 && mBody.length > 0 && after[0].ai <= mBody[mBody.length - 1]) {
|
|
3007
|
-
p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
|
|
3008
|
-
message: `Early return in \`${mName}()\` — violates single-exit rule`,
|
|
3009
|
-
suggestion: "Declare result at top, use nested if blocks, single return at method end" });
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
mReturns = [];
|
|
3013
|
-
mBody = [];
|
|
3014
|
-
};
|
|
3015
|
-
f.lines.forEach((line, idx) => {
|
|
3016
|
-
const trimmed = line.trim();
|
|
3017
|
-
const o = (line.match(/\{/g) || []).length;
|
|
3018
|
-
const c = (line.match(/\}/g) || []).length;
|
|
3019
|
-
const jm = line.match(/(?:public|private|protected|static)\s+\S+\s+(\w+)\s*\(/);
|
|
3020
|
-
const tm = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|async\s+(\w+)\s*\()/);
|
|
3021
|
-
if ((f.isJava ? jm : tm) && !inMethod) {
|
|
3022
|
-
inMethod = true;
|
|
3023
|
-
methodDepth = depth;
|
|
3024
|
-
methodStart = idx;
|
|
3025
|
-
mReturns = [];
|
|
3026
|
-
mBody = [];
|
|
3027
|
-
mName = jm?.[1] || tm?.[1] || tm?.[2] || tm?.[3] || "unknown";
|
|
3028
|
-
}
|
|
3029
|
-
depth += o - c;
|
|
3030
|
-
if (inMethod && idx > methodStart) {
|
|
3031
|
-
if (trimmed.length > 0)
|
|
3032
|
-
mBody.push(idx);
|
|
3033
|
-
if (/\breturn\b/.test(line) && !/^\s*\/\//.test(line))
|
|
3034
|
-
mReturns.push(idx);
|
|
3035
|
-
if (depth <= methodDepth) {
|
|
3036
|
-
flush();
|
|
3037
|
-
inMethod = false;
|
|
3038
|
-
methodDepth = -1;
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
});
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
mechanical.push(...p5);
|
|
2853
|
+
const p5 = mechanical.filter(m => m.check === "EARLY_RETURN");
|
|
3045
2854
|
passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
|
|
3046
|
-
// ── Tier 1 gate ────────────────────────────────────────────────────────
|
|
3047
2855
|
const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
|
|
3048
|
-
const tier1Passed = blockingMechanical.length === 0;
|
|
3049
2856
|
// ══════════════════════════════════════════════════════════════════════
|
|
3050
2857
|
// TIER 2 — AI FILE REVIEWER (Pass 6)
|
|
3051
2858
|
// Reads each changed file, applies architectural heuristics.
|
|
@@ -4696,48 +4503,70 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
|
|
|
4696
4503
|
}
|
|
4697
4504
|
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
4698
4505
|
});
|
|
4699
|
-
// ─── Resource & Prompt for
|
|
4700
|
-
const
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
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}`);
|
|
4706
4516
|
return {
|
|
4707
|
-
|
|
4708
|
-
uri: uri.href,
|
|
4709
|
-
text: content
|
|
4710
|
-
}]
|
|
4517
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Planner. Here are your rules:\n\n${content}` } }]
|
|
4711
4518
|
};
|
|
4712
4519
|
});
|
|
4713
|
-
server.prompt("
|
|
4714
|
-
const content = readFileSafe(
|
|
4715
|
-
if (!content)
|
|
4716
|
-
throw new Error(`Rules file not found at ${
|
|
4717
|
-
}
|
|
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}`);
|
|
4718
4524
|
return {
|
|
4719
|
-
messages: [
|
|
4720
|
-
{
|
|
4721
|
-
role: "user",
|
|
4722
|
-
content: {
|
|
4723
|
-
type: "text",
|
|
4724
|
-
text: `You are now the SecuFusion Senior Architect. Here are your rules:\n\n${content}`
|
|
4725
|
-
}
|
|
4726
|
-
}
|
|
4727
|
-
]
|
|
4525
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Coder. Here are your rules:\n\n${content}` } }]
|
|
4728
4526
|
};
|
|
4729
4527
|
});
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
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);
|
|
4737
4569
|
}
|
|
4738
|
-
return appendTelemetry({
|
|
4739
|
-
content: [{ type: "text", text: content }]
|
|
4740
|
-
}, 0);
|
|
4741
4570
|
});
|
|
4742
4571
|
const transport = new StdioServerTransport();
|
|
4743
4572
|
await server.connect(transport);
|