secufusion-mcp 1.2.2 → 1.2.4
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 +12 -825
- package/README.md +25 -0
- package/index.js +84 -830
- package/package.json +1 -1
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`;
|
|
@@ -2689,801 +2706,16 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2689
2706
|
reason: z.string().describe("Mandatory justification for suppression")
|
|
2690
2707
|
})).optional().default([]).describe("Method-scoped suppressions with mandatory reasons.")
|
|
2691
2708
|
}, async ({ work_item_id, skip_checks = [], suppressions = [] }) => {
|
|
2692
|
-
const inputChars = JSON.stringify({ work_item_id, skip_checks, suppressions }).length;
|
|
2693
|
-
const wsRoot = getWorkspaceRoot();
|
|
2694
|
-
const startTime = Date.now();
|
|
2695
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
2696
|
-
// SETUP — resolve task folder and read all task brain files once
|
|
2697
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
2698
|
-
// ── Resolve task folder ────────────────────────────────────────────────
|
|
2699
|
-
const registry = readRegistry();
|
|
2700
|
-
const taskEntry = registry.find(r => r["work_item_id"] === work_item_id);
|
|
2701
|
-
let taskDir = null;
|
|
2702
|
-
if (taskEntry?.["folder_name"]) {
|
|
2703
|
-
taskDir = path.resolve(wsRoot, `.secufusion/tasks/${taskEntry["folder_name"]}`);
|
|
2704
|
-
}
|
|
2705
|
-
if (!taskDir || !fs.existsSync(taskDir)) {
|
|
2706
|
-
const tasksBase = path.resolve(wsRoot, ".secufusion/tasks");
|
|
2707
|
-
if (fs.existsSync(tasksBase)) {
|
|
2708
|
-
const folders = fs.readdirSync(tasksBase);
|
|
2709
|
-
const match = folders.find(f => f.startsWith(work_item_id));
|
|
2710
|
-
if (match)
|
|
2711
|
-
taskDir = path.join(tasksBase, match);
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
if (!taskDir || !fs.existsSync(taskDir)) {
|
|
2715
|
-
return appendTelemetry({
|
|
2716
|
-
isError: true,
|
|
2717
|
-
content: [{ type: "text", text: `❌ Task folder not found for ${work_item_id}. Call manage_task(initialize) first.` }]
|
|
2718
|
-
}, inputChars);
|
|
2719
|
-
}
|
|
2720
|
-
const taskSpec = (() => { try {
|
|
2721
|
-
return JSON.parse(readFileSafe(path.join(taskDir, "spec.json")) || "{}");
|
|
2722
|
-
}
|
|
2723
|
-
catch {
|
|
2724
|
-
return {};
|
|
2725
|
-
} })();
|
|
2726
|
-
const taskProgress = (() => { try {
|
|
2727
|
-
return JSON.parse(readFileSafe(path.join(taskDir, "progress.json")) || "{}");
|
|
2728
|
-
}
|
|
2729
|
-
catch {
|
|
2730
|
-
return {};
|
|
2731
|
-
} })();
|
|
2732
|
-
const decisions = (() => { try {
|
|
2733
|
-
return JSON.parse(readFileSafe(path.join(taskDir, "decisions.json")) || "[]");
|
|
2734
|
-
}
|
|
2735
|
-
catch {
|
|
2736
|
-
return [];
|
|
2737
|
-
} })();
|
|
2738
|
-
const filesTouched = (() => { try {
|
|
2739
|
-
return JSON.parse(readFileSafe(path.join(taskDir, "files-touched.json")) || "[]");
|
|
2740
|
-
}
|
|
2741
|
-
catch {
|
|
2742
|
-
return [];
|
|
2743
|
-
} })();
|
|
2744
|
-
const scenarios = (() => { try {
|
|
2745
|
-
return JSON.parse(readFileSafe(path.join(taskDir, "scenarios.json")) || "[]");
|
|
2746
|
-
}
|
|
2747
|
-
catch {
|
|
2748
|
-
return [];
|
|
2749
|
-
} })();
|
|
2750
|
-
if (filesTouched.length === 0) {
|
|
2751
|
-
return appendTelemetry({
|
|
2752
|
-
content: [{ type: "text", text: `⚠️ No files logged for ${work_item_id}. Log files with manage_task(log_file_touched) before running checks.` }]
|
|
2753
|
-
}, inputChars);
|
|
2754
|
-
}
|
|
2755
|
-
// ── Load project spec (golden rules + rejected patterns) ───────────────
|
|
2756
|
-
const PROJ_SPEC_FILE = ".secufusion-project-spec.json";
|
|
2757
|
-
const specCandidates = [
|
|
2758
|
-
path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), PROJ_SPEC_FILE),
|
|
2759
|
-
path.resolve(wsRoot, PROJ_SPEC_FILE),
|
|
2760
|
-
];
|
|
2761
|
-
let projectSpec = null;
|
|
2762
|
-
for (const c of specCandidates) {
|
|
2763
|
-
if (fs.existsSync(c)) {
|
|
2764
|
-
try {
|
|
2765
|
-
projectSpec = JSON.parse(readFileSafe(c) || "null");
|
|
2766
|
-
}
|
|
2767
|
-
catch { }
|
|
2768
|
-
break;
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
const goldenRules = projectSpec?.golden_rules || {};
|
|
2772
|
-
const codingStyle = goldenRules.coding_style || {};
|
|
2773
|
-
const authorityRules = goldenRules.authority_rules || [];
|
|
2774
|
-
let rejectedPatterns = [];
|
|
2775
2709
|
try {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
const raw = readFileSafe(abs) || "";
|
|
2782
|
-
const ext = path.extname(ft.file_path).toLowerCase();
|
|
2783
|
-
const base = path.basename(ft.file_path);
|
|
2784
|
-
return {
|
|
2785
|
-
file_path: ft.file_path,
|
|
2786
|
-
change_summary: ft.change_summary,
|
|
2787
|
-
content: raw,
|
|
2788
|
-
lines: raw.split("\n"),
|
|
2789
|
-
ext,
|
|
2790
|
-
isJava: ext === ".java",
|
|
2791
|
-
isTS: ext === ".ts" || ext === ".tsx",
|
|
2792
|
-
isRepo: /Repository/i.test(base) && ext === ".java",
|
|
2793
|
-
isKafka: raw.includes("@KafkaListener"),
|
|
2794
|
-
isMigration: ft.file_path.includes("migration") || ft.file_path.includes("flyway") || ext === ".sql",
|
|
2795
|
-
isController: /Controller/i.test(base) && ext === ".java",
|
|
2796
|
-
isService: /Service/i.test(base) && ext === ".java",
|
|
2797
|
-
isTest: /test|spec/i.test(ft.file_path),
|
|
2798
|
-
exists: fs.existsSync(abs),
|
|
2799
|
-
};
|
|
2800
|
-
});
|
|
2801
|
-
const mechanical = [];
|
|
2802
|
-
const passResults = [];
|
|
2803
|
-
const reviewComments = [];
|
|
2804
|
-
let aiCost = 0;
|
|
2805
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
2806
|
-
// TIER 1 — MECHANICAL CHECKS (P1–P5)
|
|
2807
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
2808
|
-
// ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
|
|
2809
|
-
const p1 = [];
|
|
2810
|
-
const p1Suppressed = [];
|
|
2811
|
-
if (!skip_checks.includes("tenant_isolation")) {
|
|
2812
|
-
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
2813
|
-
const queryAnnotationLines = [];
|
|
2814
|
-
f.lines.forEach((line, idx) => {
|
|
2815
|
-
if (/@Query\s*\(/i.test(line))
|
|
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
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
mechanical.push(...p1);
|
|
2875
|
-
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
|
|
2876
|
-
// Auto-log suppressions to decisions.json
|
|
2877
|
-
if (p1Suppressed.length > 0) {
|
|
2878
|
-
const tasksRoot = resolve(TASKS_DIR);
|
|
2879
|
-
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
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
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
if (modified)
|
|
2897
|
-
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
2898
|
-
}
|
|
2710
|
+
const cp = require('child_process');
|
|
2711
|
+
const scriptPath = require('path').resolve(__dirname, 'scripts/sfn-pr-check.js');
|
|
2712
|
+
const args = JSON.stringify({ work_item_id, skip_checks, suppressions });
|
|
2713
|
+
const output = cp.execFileSync(process.execPath, [scriptPath, args], { encoding: 'utf-8' });
|
|
2714
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2899
2715
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
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
|
-
});
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
mechanical.push(...p2);
|
|
2928
|
-
passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
|
|
2929
|
-
// ── P3: MISSING_INDEX ─────────────────────────────────────────────────
|
|
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);
|
|
2965
|
-
passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
|
|
2966
|
-
// ── P4: KAFKA_SYNC ────────────────────────────────────────────────────
|
|
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);
|
|
2994
|
-
passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
|
|
2995
|
-
// ── P5: EARLY_RETURN ──────────────────────────────────────────────────
|
|
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);
|
|
3053
|
-
passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
|
|
3054
|
-
// ── Tier 1 gate ────────────────────────────────────────────────────────
|
|
3055
|
-
const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
|
|
3056
|
-
const tier1Passed = blockingMechanical.length === 0;
|
|
3057
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3058
|
-
// TIER 2 — AI FILE REVIEWER (Pass 6)
|
|
3059
|
-
// Reads each changed file, applies architectural heuristics.
|
|
3060
|
-
// Only runs if Tier 1 passes.
|
|
3061
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3062
|
-
const AI_COST_PER_100 = 0.0008;
|
|
3063
|
-
if (tier1Passed && !skip_checks.includes("ai_review")) {
|
|
3064
|
-
for (const f of fileRecords) {
|
|
3065
|
-
aiCost += (f.lines.length / 100) * AI_COST_PER_100;
|
|
3066
|
-
// Obvious comments
|
|
3067
|
-
const obviousComments = f.lines.filter(l => /\/\/\s*(get the|set the|loop|return|initialize|this method|this function|create a|update the)/i.test(l));
|
|
3068
|
-
if (obviousComments.length > 0) {
|
|
3069
|
-
reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "CODE_QUALITY",
|
|
3070
|
-
title: `${obviousComments.length} obvious comment(s)`,
|
|
3071
|
-
detail: "Comments explain WHY, not WHAT. Remove or replace with architectural rationale.",
|
|
3072
|
-
action: "Replace // Get the user with // Required because tenant context is not available in JWT claims" });
|
|
3073
|
-
}
|
|
3074
|
-
// Work Item IDs in comments
|
|
3075
|
-
const ticketIdComments = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /(\/\/|\/\*|\*)\s*.*(?:BUG|WI)-\d+/i.test(l));
|
|
3076
|
-
if (ticketIdComments.length > 0) {
|
|
3077
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: ticketIdComments[0].i + 1, severity: "CRITICAL", area: "CODE_QUALITY",
|
|
3078
|
-
title: "Ticket ID in code comment",
|
|
3079
|
-
detail: `Line ${ticketIdComments[0].i + 1} contains a work item ID: \`${ticketIdComments[0].l.trim()}\``,
|
|
3080
|
-
action: "Remove the ticket ID. Explain the durable WHY instead." });
|
|
3081
|
-
}
|
|
3082
|
-
// Hardcoded URLs/IPs
|
|
3083
|
-
const hardcoded = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /https?:\/\/[0-9]{1,3}\.[0-9]{1,3}|localhost:[0-9]{4}|uat\.|staging\.|prod\./i.test(l) && !/\/\//.test(l));
|
|
3084
|
-
if (hardcoded.length > 0) {
|
|
3085
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: hardcoded[0].i + 1, severity: "CRITICAL", area: "SECURITY",
|
|
3086
|
-
title: "Hardcoded URL/IP detected",
|
|
3087
|
-
detail: `Line ${hardcoded[0].i + 1}: \`${hardcoded[0].l.trim()}\``,
|
|
3088
|
-
action: "Move to application.properties / environment variables immediately" });
|
|
3089
|
-
}
|
|
3090
|
-
// Missing @Transactional(readOnly=true) on service GET methods
|
|
3091
|
-
if (f.isService && !f.isTest) {
|
|
3092
|
-
const getters = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /public.*(?:List|Page|Optional|DTO|Response)\s+\w+\s*\(/.test(l));
|
|
3093
|
-
for (const { l, i } of getters) {
|
|
3094
|
-
const prev = f.lines.slice(Math.max(0, i - 3), i).join(" ");
|
|
3095
|
-
if (!/readOnly.*true/.test(prev)) {
|
|
3096
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: i + 1, severity: "WARNING", area: "PERFORMANCE",
|
|
3097
|
-
title: "Missing @Transactional(readOnly = true)",
|
|
3098
|
-
detail: `\`${(l.match(/\w+\s*\(/) || [""])[0].trim()}\` returns data without readOnly — allows dirty reads and skips DB optimizations`,
|
|
3099
|
-
action: "Add @Transactional(readOnly = true) above this method" });
|
|
3100
|
-
break;
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
// Missing @PreAuthorize on controller endpoints
|
|
3105
|
-
if (f.isController && !f.isTest) {
|
|
3106
|
-
const endpoints = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)/.test(l));
|
|
3107
|
-
for (const { l, i } of endpoints) {
|
|
3108
|
-
const prev = f.lines.slice(Math.max(0, i - 5), i).join(" ");
|
|
3109
|
-
if (!/PreAuthorize|permitAll/.test(prev)) {
|
|
3110
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: i + 1, severity: "CRITICAL", area: "SECURITY",
|
|
3111
|
-
title: "Endpoint missing @PreAuthorize",
|
|
3112
|
-
detail: `Endpoint at line ${i + 1} has no @PreAuthorize annotation`,
|
|
3113
|
-
action: "Add @PreAuthorize(\"hasAuthority('domain.action')\") above the mapping" });
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3117
|
-
// Debug statements in production code
|
|
3118
|
-
if (!f.isTest) {
|
|
3119
|
-
const debug = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /console\.log|System\.out\.print|e\.printStackTrace/.test(l));
|
|
3120
|
-
if (debug.length > 0) {
|
|
3121
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: debug[0].i + 1, severity: "WARNING", area: "CODE_QUALITY",
|
|
3122
|
-
title: "Debug statement in production code",
|
|
3123
|
-
detail: `\`${debug[0].l.trim()}\` at line ${debug[0].i + 1}`,
|
|
3124
|
-
action: "Replace with SLF4J log.debug() or remove entirely" });
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
// Exception swallowing
|
|
3128
|
-
if (f.isJava) {
|
|
3129
|
-
f.lines.forEach((line, idx) => {
|
|
3130
|
-
if (/catch\s*\(\s*Exception\s+/.test(line)) {
|
|
3131
|
-
const block = f.lines.slice(idx, idx + 5).join(" ");
|
|
3132
|
-
if (!/log\.|logger\.|throw/.test(block)) {
|
|
3133
|
-
reviewComments.push({ tier: 2, file: f.file_path, line: idx + 1, severity: "WARNING", area: "ERROR_HANDLING",
|
|
3134
|
-
title: "Catch-all exception may be swallowed",
|
|
3135
|
-
detail: `catch(Exception) at line ${idx + 1} has no log or rethrow`,
|
|
3136
|
-
action: "Log with context: log.error(\"[WI-xxx] failed: {}\", e.getMessage(), e) or rethrow as domain exception" });
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
// TypeScript any overuse
|
|
3142
|
-
if (f.isTS && !f.isTest) {
|
|
3143
|
-
const anyLines = f.lines.filter(l => /:\s*any\b/.test(l) && !/\/\//.test(l));
|
|
3144
|
-
if (anyLines.length > 2) {
|
|
3145
|
-
reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "TYPE_SAFETY",
|
|
3146
|
-
title: `${anyLines.length} uses of \`: any\``,
|
|
3147
|
-
detail: "Type safety defeated. `any` propagates silently through the call chain.",
|
|
3148
|
-
action: "Replace with explicit interfaces or `unknown` with a type guard" });
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
// Missing Flyway for @Entity
|
|
3152
|
-
if (f.isJava && /@Entity/.test(f.content) && !f.isTest && !fileRecords.some(r => r.isMigration)) {
|
|
3153
|
-
reviewComments.push({ tier: 2, file: f.file_path, severity: "CRITICAL", area: "DATABASE",
|
|
3154
|
-
title: "@Entity change with no Flyway migration in PR",
|
|
3155
|
-
detail: "Every JPA entity schema change requires a Flyway migration",
|
|
3156
|
-
action: "Create src/main/resources/db/migration/Vxxx__describe_change.sql and log it with manage_task(log_file_touched)" });
|
|
3157
|
-
}
|
|
3158
|
-
// Cross-service call timeout
|
|
3159
|
-
if (f.isJava && /RestTemplate|WebClient|FeignClient/i.test(f.content) && !/connectTimeout|readTimeout|timeout/i.test(f.content)) {
|
|
3160
|
-
reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "RESILIENCE",
|
|
3161
|
-
title: "Cross-service call with no timeout",
|
|
3162
|
-
detail: "RestTemplate/WebClient/FeignClient used without connectTimeout + readTimeout",
|
|
3163
|
-
action: "Configure explicit timeouts. For WebClient: .responseTimeout(Duration.ofSeconds(5))" });
|
|
3164
|
-
}
|
|
3165
|
-
// Praise: proper SLF4J logging
|
|
3166
|
-
if (/log\.(info|debug|warn|error)\(/.test(f.content) && !f.isTest) {
|
|
3167
|
-
reviewComments.push({ tier: 2, file: f.file_path, severity: "PRAISE", area: "CODE_QUALITY",
|
|
3168
|
-
title: "Proper SLF4J logging observed", detail: "" });
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
// ── Tier 2 gate ────────────────────────────────────────────────────────
|
|
3173
|
-
const tier2Blocking = reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL");
|
|
3174
|
-
const tier2Passed = tier1Passed && tier2Blocking.length === 0;
|
|
3175
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3176
|
-
// TIER 3 — CONTEXT REVIEWER
|
|
3177
|
-
// Cross-references task brain + project-spec + file contents.
|
|
3178
|
-
// Only runs if both Tier 1 and Tier 2 pass.
|
|
3179
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3180
|
-
if (tier2Passed && !skip_checks.includes("context_review")) {
|
|
3181
|
-
// ── AC Coverage ────────────────────────────────────────────────────
|
|
3182
|
-
const allACs = [...new Set([
|
|
3183
|
-
...(taskSpec.acceptance_criteria || []),
|
|
3184
|
-
...(taskProgress.completed_acs || []),
|
|
3185
|
-
...(taskProgress.pending_acs || []),
|
|
3186
|
-
])];
|
|
3187
|
-
const changeCorpus = [
|
|
3188
|
-
...fileRecords.map(f => f.file_path + " " + f.change_summary),
|
|
3189
|
-
...decisions.map(d => (d.decision || "") + " " + (d.rationale || "")),
|
|
3190
|
-
].join(" ").toLowerCase();
|
|
3191
|
-
for (const ac of allACs) {
|
|
3192
|
-
if (!ac?.trim())
|
|
3193
|
-
continue;
|
|
3194
|
-
const kws = ac.toLowerCase().split(/\s+/).filter(w => w.length > 3 && !/^(must|shall|should|the|and|for|with|that|this|when|from|into|not|does|will|have|been|each|only|any|all)$/.test(w));
|
|
3195
|
-
const cov = kws.length > 0 ? kws.filter(kw => changeCorpus.includes(kw)).length / kws.length : 1;
|
|
3196
|
-
if (cov < 0.25) {
|
|
3197
|
-
reviewComments.push({ tier: 3, severity: "CRITICAL", area: "AC_COVERAGE",
|
|
3198
|
-
title: "AC may not be implemented",
|
|
3199
|
-
detail: `"${ac.substring(0, 120)}${ac.length > 120 ? "..." : ""}" — no matching file changes or decisions`,
|
|
3200
|
-
action: "Verify this AC is addressed. If so, enrich the change_summary for the relevant file." });
|
|
3201
|
-
}
|
|
3202
|
-
else if (cov < 0.5) {
|
|
3203
|
-
reviewComments.push({ tier: 3, severity: "WARNING", area: "AC_COVERAGE",
|
|
3204
|
-
title: "AC has weak coverage signal",
|
|
3205
|
-
detail: `"${ac.substring(0, 120)}..." — only ${Math.round(cov * 100)}% keyword match in change corpus`,
|
|
3206
|
-
action: "Confirm AC is satisfied; enrich the file change_summary to improve signal." });
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
// Pending ACs
|
|
3210
|
-
if ((taskProgress.pending_acs || []).length > 0) {
|
|
3211
|
-
reviewComments.push({ tier: 3, severity: "CRITICAL", area: "AC_COVERAGE",
|
|
3212
|
-
title: `${taskProgress.pending_acs.length} AC(s) still pending`,
|
|
3213
|
-
detail: taskProgress.pending_acs.map(a => `• ${a}`).join("\n"),
|
|
3214
|
-
action: "Complete all ACs. Call manage_task(update_spec) to move them to completed_acs." });
|
|
3215
|
-
}
|
|
3216
|
-
// ── Decision Drift ─────────────────────────────────────────────────
|
|
3217
|
-
const fileCorpus = fileRecords.map(f => f.file_path + " " + f.change_summary).join(" ").toLowerCase();
|
|
3218
|
-
for (const dec of decisions) {
|
|
3219
|
-
if (!dec.decision || /rollback|strategy|SAFE|RISKY|DANGEROUS/i.test(dec.decision))
|
|
3220
|
-
continue;
|
|
3221
|
-
const kws = dec.decision.toLowerCase().split(/\s+/).filter(w => w.length > 4 && !/^(using|added|decided|chose|going|will|this|that|with|from|into|the|and|for)$/.test(w));
|
|
3222
|
-
const trace = kws.length >= 3 ? kws.filter(kw => fileCorpus.includes(kw)).length / kws.length : 1;
|
|
3223
|
-
if (trace < 0.2) {
|
|
3224
|
-
reviewComments.push({ tier: 3, severity: "WARNING", area: "DECISION_DRIFT",
|
|
3225
|
-
title: `Decision #${dec.id} not traceable in files touched`,
|
|
3226
|
-
detail: `"${dec.decision?.substring(0, 120)}"`,
|
|
3227
|
-
action: "Verify this decision was implemented. Log the file with manage_task(log_file_touched) if missing." });
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
// ── Scope Creep ────────────────────────────────────────────────────
|
|
3231
|
-
const specCorpus = [taskSpec.title || "", taskSpec.description || "", ...(taskSpec.acceptance_criteria || []), ...decisions.map(d => (d.decision || "") + " " + (d.rationale || ""))].join(" ").toLowerCase();
|
|
3232
|
-
for (const f of fileRecords.filter(f => !f.isTest)) {
|
|
3233
|
-
const base = path.basename(f.file_path).replace(/\.[^.]+$/, "").toLowerCase();
|
|
3234
|
-
const words = base.replace(/([A-Z])/g, " $1").toLowerCase().trim().split(/[\s_-]+/).filter(w => w.length > 3);
|
|
3235
|
-
if (words.length > 0 && !words.some(w => specCorpus.includes(w))) {
|
|
3236
|
-
reviewComments.push({ tier: 3, file: f.file_path, severity: "INFO", area: "SCOPE_CREEP",
|
|
3237
|
-
title: "File may be outside task scope",
|
|
3238
|
-
detail: `\`${f.file_path}\` not mentioned in spec, ACs, or decisions`,
|
|
3239
|
-
action: "If intentional, add a decision: manage_task(log_decision) explaining the touch." });
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
// ── Rejected Patterns ──────────────────────────────────────────────
|
|
3243
|
-
for (const f of fileRecords.filter(f => f.exists)) {
|
|
3244
|
-
for (const rp of rejectedPatterns) {
|
|
3245
|
-
if (!rp.pattern)
|
|
3246
|
-
continue;
|
|
3247
|
-
const kws = rp.pattern.toLowerCase().split(/\s+/).filter(w => w.length > 4).slice(0, 4);
|
|
3248
|
-
const hits = kws.filter(kw => f.content.toLowerCase().includes(kw)).length;
|
|
3249
|
-
if (kws.length > 0 && hits / kws.length >= 0.75) {
|
|
3250
|
-
reviewComments.push({ tier: 3, file: f.file_path, severity: "CRITICAL", area: "REJECTED_PATTERNS",
|
|
3251
|
-
title: "Rejected pattern detected",
|
|
3252
|
-
detail: `Pattern: "${rp.pattern}"\nReason: ${rp.reason || "See .rejected-patterns.json"} (category: ${rp.category || "unknown"})`,
|
|
3253
|
-
action: "This pattern was explicitly rejected. Refactor before merging." });
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
// ── Test Coverage ──────────────────────────────────────────────────
|
|
3258
|
-
const testFiles = fileRecords.filter(f => f.isTest);
|
|
3259
|
-
const srcFiles = fileRecords.filter(f => !f.isTest && (f.isJava || f.isTS));
|
|
3260
|
-
if (srcFiles.length > 0 && testFiles.length === 0) {
|
|
3261
|
-
reviewComments.push({ tier: 3, severity: "CRITICAL", area: "TEST_COVERAGE",
|
|
3262
|
-
title: "No test files in PR",
|
|
3263
|
-
detail: `${srcFiles.length} source file(s) changed, zero test files in files-touched`,
|
|
3264
|
-
action: "Add unit and integration tests. Log them with manage_task(log_file_touched)." });
|
|
3265
|
-
}
|
|
3266
|
-
else if (testFiles.length > 0) {
|
|
3267
|
-
reviewComments.push({ tier: 3, severity: "PRAISE", area: "TEST_COVERAGE",
|
|
3268
|
-
title: `${testFiles.length} test file(s) included`,
|
|
3269
|
-
detail: testFiles.map(f => `• ${f.file_path}`).join("\n") });
|
|
3270
|
-
}
|
|
3271
|
-
if (scenarios.filter(s => s.type === "unit").length === 0 && srcFiles.length > 0) {
|
|
3272
|
-
reviewComments.push({ tier: 3, severity: "WARNING", area: "TEST_COVERAGE",
|
|
3273
|
-
title: "No unit test scenarios logged",
|
|
3274
|
-
detail: "manage_task(add_scenario, type='unit') was never called",
|
|
3275
|
-
action: "Call suggest_test_scenarios to auto-generate scenarios from ACs." });
|
|
3276
|
-
}
|
|
3277
|
-
// ── Golden Rules ───────────────────────────────────────────────────
|
|
3278
|
-
// @PreAuthorize on controllers
|
|
3279
|
-
for (const f of fileRecords.filter(f => f.isController && !f.isTest)) {
|
|
3280
|
-
const hasAuth = f.content.includes("@PreAuthorize");
|
|
3281
|
-
const hasEndpoints = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)/.test(f.content);
|
|
3282
|
-
if (hasEndpoints && hasPreAuth(f.content)) {
|
|
3283
|
-
reviewComments.push({ tier: 3, file: f.file_path, severity: "PRAISE", area: "GOLDEN_RULES",
|
|
3284
|
-
title: "@PreAuthorize correctly applied", detail: `\`${f.file_path}\`` });
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
// Entity + migration praise
|
|
3288
|
-
const entityFiles = fileRecords.filter(f => f.isJava && /@Entity/.test(f.content) && !f.isTest);
|
|
3289
|
-
const migFiles = fileRecords.filter(f => f.isMigration);
|
|
3290
|
-
if (entityFiles.length > 0 && migFiles.length > 0) {
|
|
3291
|
-
reviewComments.push({ tier: 3, severity: "PRAISE", area: "GOLDEN_RULES",
|
|
3292
|
-
title: "@Entity change has matching Flyway migration",
|
|
3293
|
-
detail: migFiles.map(f => path.basename(f.file_path)).join(", ") });
|
|
3294
|
-
}
|
|
3295
|
-
// Decision quality praise
|
|
3296
|
-
if (decisions.length >= 3) {
|
|
3297
|
-
reviewComments.push({ tier: 3, severity: "PRAISE", area: "DECISIONS",
|
|
3298
|
-
title: `${decisions.length} architectural decisions logged`,
|
|
3299
|
-
detail: "Good decision hygiene — future agents will benefit from this audit trail." });
|
|
3300
|
-
}
|
|
3301
|
-
if (scenarios.length >= 5) {
|
|
3302
|
-
reviewComments.push({ tier: 3, severity: "PRAISE", area: "TEST_COVERAGE",
|
|
3303
|
-
title: `${scenarios.length} test scenarios logged`,
|
|
3304
|
-
detail: "Comprehensive scenario coverage documented." });
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
// ── Helper (moved inside scope) ────────────────────────────────────────
|
|
3308
|
-
function hasPreAuth(content) { return content.includes("@PreAuthorize"); }
|
|
3309
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3310
|
-
// VERDICT — unified across all three tiers
|
|
3311
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3312
|
-
const allCriticals = [
|
|
3313
|
-
...blockingMechanical,
|
|
3314
|
-
...reviewComments.filter(c => c.severity === "CRITICAL"),
|
|
3315
|
-
];
|
|
3316
|
-
const allWarnings = [
|
|
3317
|
-
...mechanical.filter(f => f.severity === "WARNING"),
|
|
3318
|
-
...reviewComments.filter(c => c.severity === "WARNING"),
|
|
3319
|
-
];
|
|
3320
|
-
const allInfos = reviewComments.filter(c => c.severity === "INFO");
|
|
3321
|
-
const allPraises = reviewComments.filter(c => c.severity === "PRAISE");
|
|
3322
|
-
let verdict;
|
|
3323
|
-
let verdictReason;
|
|
3324
|
-
if (allCriticals.length > 0) {
|
|
3325
|
-
verdict = "❌ CHANGES_REQUESTED";
|
|
3326
|
-
verdictReason = `${allCriticals.length} blocking finding(s) across Tiers 1–3 must be resolved before this PR can merge.`;
|
|
3327
|
-
}
|
|
3328
|
-
else if (allWarnings.length > 3) {
|
|
3329
|
-
verdict = "💬 DISCUSS";
|
|
3330
|
-
verdictReason = `All checks passed. ${allWarnings.length} warnings worth discussing before merge.`;
|
|
3331
|
-
}
|
|
3332
|
-
else if (allWarnings.length > 0) {
|
|
3333
|
-
verdict = "✅ APPROVED";
|
|
3334
|
-
verdictReason = `All checks passed. ${allWarnings.length} minor warning(s) — address at your discretion.`;
|
|
3335
|
-
}
|
|
3336
|
-
else {
|
|
3337
|
-
verdict = "✅ APPROVED";
|
|
3338
|
-
verdictReason = `Full review passed — mechanical checks clean, AI review clean, context review clean.`;
|
|
3339
|
-
}
|
|
3340
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3341
|
-
// OUTPUT
|
|
3342
|
-
// ══════════════════════════════════════════════════════════════════════
|
|
3343
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
3344
|
-
const skippedList = skip_checks.length > 0 ? ` (skipped: ${skip_checks.join(", ")})` : "";
|
|
3345
|
-
let out = `## run_pre_pr_checks_with_reviewer_agent — ${work_item_id}\n\n`;
|
|
3346
|
-
out += `📁 **${fileRecords.length} file(s)** reviewed in **${elapsed}s**${skippedList}\n\n`;
|
|
3347
|
-
// Task context strip
|
|
3348
|
-
out += `> 📋 **${taskSpec.title || "(untitled)"}** | `;
|
|
3349
|
-
out += `ACs: ${(taskSpec.acceptance_criteria || []).length} total, ${(taskProgress.completed_acs || []).length} done, ${(taskProgress.pending_acs || []).length} pending | `;
|
|
3350
|
-
out += `Decisions: ${decisions.length} | Scenarios: ${scenarios.length}\n\n`;
|
|
3351
|
-
// ── Tier 1: Mechanical table ───────────────────────────────────────────
|
|
3352
|
-
out += `### 🔩 Tier 1 — Mechanical Checks\n`;
|
|
3353
|
-
out += `| Pass | Check | Status | Issues |\n|---|---|---|---|\n`;
|
|
3354
|
-
for (let i = 0; i < passResults.length; i++) {
|
|
3355
|
-
const p = passResults[i];
|
|
3356
|
-
const icon = p.skipped ? "⏭️ SKIP" : p.passed ? "✅ PASS" : "❌ FAIL";
|
|
3357
|
-
const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
|
|
3358
|
-
out += `| P${i + 1} | ${p.name} | ${icon} | ${issueText} |\n`;
|
|
3359
|
-
}
|
|
3360
|
-
if (blockingMechanical.length > 0) {
|
|
3361
|
-
out += `\n#### ❌ Blocking Mechanical Findings\n`;
|
|
3362
|
-
for (const f of blockingMechanical) {
|
|
3363
|
-
out += `\n**[${f.check}]** \`${f.file}\`${f.line ? ` line ${f.line}` : ""}\n`;
|
|
3364
|
-
out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
if (p1Suppressed.length > 0) {
|
|
3368
|
-
out += `\n#### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
|
|
3369
|
-
for (const sup of p1Suppressed) {
|
|
3370
|
-
out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
|
|
3371
|
-
out += `> Suppressed — ${sup.reason}\n`;
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
const warnings1 = mechanical.filter(f => f.severity === "WARNING");
|
|
3375
|
-
if (warnings1.length > 0) {
|
|
3376
|
-
out += `\n#### ⚠️ Warnings\n`;
|
|
3377
|
-
for (const f of warnings1) {
|
|
3378
|
-
out += `\n**[${f.check}]** \`${f.file}\`${f.line ? ` line ${f.line}` : ""}\n`;
|
|
3379
|
-
out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
|
|
3380
|
-
}
|
|
3381
|
-
}
|
|
3382
|
-
// ── Tier 2: AI Reviewer ────────────────────────────────────────────────
|
|
3383
|
-
out += `\n### 🤖 Tier 2 — AI File Reviewer\n`;
|
|
3384
|
-
if (!tier1Passed) {
|
|
3385
|
-
out += `> ⏸️ Skipped — resolve Tier 1 blocking failures first.\n`;
|
|
3386
|
-
}
|
|
3387
|
-
else if (skip_checks.includes("ai_review")) {
|
|
3388
|
-
out += `> ⏭️ Skipped by skip_checks.\n`;
|
|
3389
|
-
}
|
|
3390
|
-
else {
|
|
3391
|
-
const t2c = reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL");
|
|
3392
|
-
const t2w = reviewComments.filter(c => c.tier === 2 && c.severity === "WARNING");
|
|
3393
|
-
const t2p = reviewComments.filter(c => c.tier === 2 && c.severity === "PRAISE");
|
|
3394
|
-
if (t2c.length > 0) {
|
|
3395
|
-
out += `\n#### 🔴 Critical (${t2c.length})\n`;
|
|
3396
|
-
for (const c of t2c) {
|
|
3397
|
-
out += `\n**\`${c.file}\`**${c.line ? ` line ${c.line}` : ""}\n> [${c.area}] **${c.title}** — ${c.detail}\n> 🔧 *${c.action}*\n`;
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
if (t2w.length > 0) {
|
|
3401
|
-
out += `\n#### 🟡 Warnings (${t2w.length})\n`;
|
|
3402
|
-
for (const c of t2w) {
|
|
3403
|
-
out += `\n**\`${c.file}\`**${c.line ? ` line ${c.line}` : ""}\n> [${c.area}] ${c.title} — ${c.detail}\n> 💡 *${c.action}*\n`;
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
if (t2p.length > 0) {
|
|
3407
|
-
out += `\n#### ✅ Praise\n`;
|
|
3408
|
-
for (const c of t2p) {
|
|
3409
|
-
out += `- \`${c.file}\`: ${c.title}\n`;
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
if (t2c.length === 0 && t2w.length === 0 && t2p.length === 0) {
|
|
3413
|
-
out += `> ✅ No file-level issues found.\n`;
|
|
3414
|
-
}
|
|
3415
|
-
out += `> 💰 *AI review cost: ~$${aiCost.toFixed(4)}*\n`;
|
|
3416
|
-
}
|
|
3417
|
-
// ── Tier 3: Context Reviewer ───────────────────────────────────────────
|
|
3418
|
-
out += `\n### 🧠 Tier 3 — Context Reviewer\n`;
|
|
3419
|
-
if (!tier2Passed) {
|
|
3420
|
-
out += `> ⏸️ Skipped — resolve Tier 1 and Tier 2 issues first.\n`;
|
|
3421
|
-
}
|
|
3422
|
-
else if (skip_checks.includes("context_review")) {
|
|
3423
|
-
out += `> ⏭️ Skipped by skip_checks.\n`;
|
|
3424
|
-
}
|
|
3425
|
-
else {
|
|
3426
|
-
const t3c = reviewComments.filter(c => c.tier === 3 && c.severity === "CRITICAL");
|
|
3427
|
-
const t3w = reviewComments.filter(c => c.tier === 3 && c.severity === "WARNING");
|
|
3428
|
-
const t3i = reviewComments.filter(c => c.tier === 3 && c.severity === "INFO");
|
|
3429
|
-
const t3p = reviewComments.filter(c => c.tier === 3 && c.severity === "PRAISE");
|
|
3430
|
-
if (t3c.length > 0) {
|
|
3431
|
-
out += `\n#### 🔴 Critical (${t3c.length})\n`;
|
|
3432
|
-
for (const c of t3c) {
|
|
3433
|
-
out += `\n**[${c.area}] ${c.title}**${c.file ? ` — \`${c.file}\`` : ""}\n`;
|
|
3434
|
-
out += `> ${c.detail.replace(/\n/g, "\n> ")}\n`;
|
|
3435
|
-
if (c.action)
|
|
3436
|
-
out += `> 🔧 *${c.action}*\n`;
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
if (t3w.length > 0) {
|
|
3440
|
-
out += `\n#### 🟡 Warnings (${t3w.length})\n`;
|
|
3441
|
-
for (const c of t3w) {
|
|
3442
|
-
out += `\n**[${c.area}] ${c.title}**${c.file ? ` — \`${c.file}\`` : ""}\n`;
|
|
3443
|
-
out += `> ${c.detail.replace(/\n/g, "\n> ")}\n`;
|
|
3444
|
-
if (c.action)
|
|
3445
|
-
out += `> 💡 *${c.action}*\n`;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
if (t3i.length > 0) {
|
|
3449
|
-
out += `\n#### 🟢 Info (${t3i.length})\n`;
|
|
3450
|
-
for (const c of t3i) {
|
|
3451
|
-
out += `- **[${c.area}]** \`${c.file || ""}\` — ${c.detail}\n`;
|
|
3452
|
-
if (c.action)
|
|
3453
|
-
out += ` > 💡 *${c.action}*\n`;
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
if (t3p.length > 0) {
|
|
3457
|
-
out += `\n#### ✅ Praise\n`;
|
|
3458
|
-
for (const c of t3p)
|
|
3459
|
-
out += `- **${c.title}** — ${c.detail.replace(/\n/g, " ")}\n`;
|
|
3460
|
-
}
|
|
3461
|
-
if (t3c.length === 0 && t3w.length === 0 && t3i.length === 0 && t3p.length === 0) {
|
|
3462
|
-
out += `> ✅ Task brain cross-reference clean — all ACs traceable, no rejected patterns, no scope creep.\n`;
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
// ── Summary table ──────────────────────────────────────────────────────
|
|
3466
|
-
out += `\n---\n`;
|
|
3467
|
-
out += `| Tier | Scope | 🔴 Critical | 🟡 Warning | 🟢 Info | ✅ Praise |\n|---|---|---|---|---|---|\n`;
|
|
3468
|
-
out += `| Tier 1 | Mechanical | ${blockingMechanical.length} | ${warnings1.length} | — | — |\n`;
|
|
3469
|
-
out += `| Tier 2 | AI File Review | ${reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL").length} | ${reviewComments.filter(c => c.tier === 2 && c.severity === "WARNING").length} | — | ${reviewComments.filter(c => c.tier === 2 && c.severity === "PRAISE").length} |\n`;
|
|
3470
|
-
out += `| Tier 3 | Context Review | ${reviewComments.filter(c => c.tier === 3 && c.severity === "CRITICAL").length} | ${reviewComments.filter(c => c.tier === 3 && c.severity === "WARNING").length} | ${allInfos.length} | ${reviewComments.filter(c => c.tier === 3 && c.severity === "PRAISE").length} |\n`;
|
|
3471
|
-
out += `| **Total** | | **${allCriticals.length}** | **${allWarnings.length}** | **${allInfos.length}** | **${allPraises.length}** |\n\n`;
|
|
3472
|
-
// ── Verdict ────────────────────────────────────────────────────────────
|
|
3473
|
-
out += `## ${verdict}\n\n${verdictReason}\n`;
|
|
3474
|
-
if (verdict === "✅ APPROVED") {
|
|
3475
|
-
out += `\n**Next steps:**\n`;
|
|
3476
|
-
out += `1. \`manage_task(action: "complete", work_item_id: "${work_item_id}")\`\n`;
|
|
3477
|
-
out += `2. Generate PR and ADO documents manually using AGENTS.md rules\n`;
|
|
3478
|
-
out += `3. Raise the PR\n`;
|
|
3479
|
-
}
|
|
3480
|
-
else if (verdict === "❌ CHANGES_REQUESTED") {
|
|
3481
|
-
out += `\n**Resolve all 🔴 Critical findings above, then re-run \`run_pre_pr_checks_with_reviewer_agent\`.**\n`;
|
|
3482
|
-
}
|
|
3483
|
-
else {
|
|
3484
|
-
out += `\n**Discuss 🟡 warnings with your team, then proceed with \`manage_task(complete)\`.**\n`;
|
|
2716
|
+
catch (e) {
|
|
2717
|
+
return { content: [{ type: 'text', text: e.stdout || e.message }], isError: true };
|
|
3485
2718
|
}
|
|
3486
|
-
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
3487
2719
|
});
|
|
3488
2720
|
// Full-spectrum PR review system.
|
|
3489
2721
|
//
|
|
@@ -4704,48 +3936,70 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
|
|
|
4704
3936
|
}
|
|
4705
3937
|
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
4706
3938
|
});
|
|
4707
|
-
// ─── Resource & Prompt for
|
|
4708
|
-
const
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
3939
|
+
// ─── Resource & Prompt for Personas ──────────────────────────────────────────
|
|
3940
|
+
const AGENTS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "../.agents");
|
|
3941
|
+
const PLANNER_MD_PATH = path.join(AGENTS_DIR, "planner.md");
|
|
3942
|
+
const CODER_MD_PATH = path.join(AGENTS_DIR, "coder.md");
|
|
3943
|
+
const REVIEWER_MD_PATH = path.join(AGENTS_DIR, "reviewer.md");
|
|
3944
|
+
const SKILLS_CATALOG_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "skills-catalog.json");
|
|
3945
|
+
server.prompt("secufusion_planner", "Starts a conversation with the SecuFusion planner workflow rules loaded (Phase 00 - Phase 1).", async () => {
|
|
3946
|
+
const content = readFileSafe(PLANNER_MD_PATH);
|
|
3947
|
+
if (!content)
|
|
3948
|
+
throw new Error(`Rules file not found at ${PLANNER_MD_PATH}`);
|
|
4714
3949
|
return {
|
|
4715
|
-
|
|
4716
|
-
uri: uri.href,
|
|
4717
|
-
text: content
|
|
4718
|
-
}]
|
|
3950
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Planner. Here are your rules:\n\n${content}` } }]
|
|
4719
3951
|
};
|
|
4720
3952
|
});
|
|
4721
|
-
server.prompt("
|
|
4722
|
-
const content = readFileSafe(
|
|
4723
|
-
if (!content)
|
|
4724
|
-
throw new Error(`Rules file not found at ${
|
|
4725
|
-
}
|
|
3953
|
+
server.prompt("secufusion_coder", "Starts a conversation with the SecuFusion coder workflow rules loaded (Phase 2 - Phase 3).", async () => {
|
|
3954
|
+
const content = readFileSafe(CODER_MD_PATH);
|
|
3955
|
+
if (!content)
|
|
3956
|
+
throw new Error(`Rules file not found at ${CODER_MD_PATH}`);
|
|
4726
3957
|
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
|
-
]
|
|
3958
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Coder. Here are your rules:\n\n${content}` } }]
|
|
4736
3959
|
};
|
|
4737
3960
|
});
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
3961
|
+
server.prompt("secufusion_reviewer", "Starts a conversation with the SecuFusion reviewer workflow rules loaded (Phase 4 - Phase 5).", async () => {
|
|
3962
|
+
const content = readFileSafe(REVIEWER_MD_PATH);
|
|
3963
|
+
if (!content)
|
|
3964
|
+
throw new Error(`Rules file not found at ${REVIEWER_MD_PATH}`);
|
|
3965
|
+
return {
|
|
3966
|
+
messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Reviewer. Here are your rules:\n\n${content}` } }]
|
|
3967
|
+
};
|
|
3968
|
+
});
|
|
3969
|
+
// ─── Slash Commands (Aliases for Personas / Workflows) ───────────────────────
|
|
3970
|
+
server.prompt("sfn_spec", "Slash command to start Phase 0.5 (Classification) and Phase 1 (Planning)", async () => {
|
|
3971
|
+
const content = readFileSafe(PLANNER_MD_PATH);
|
|
3972
|
+
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}` } }] };
|
|
3973
|
+
});
|
|
3974
|
+
server.prompt("sfn_code", "Slash command to start Phase 2 (Execution) and Phase 3 (Course Correction)", async () => {
|
|
3975
|
+
const content = readFileSafe(CODER_MD_PATH);
|
|
3976
|
+
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}` } }] };
|
|
3977
|
+
});
|
|
3978
|
+
server.prompt("sfn_review", "Slash command to start Phase 4 (PR Checks) and Phase 5 (Retrospective)", async () => {
|
|
3979
|
+
const content = readFileSafe(REVIEWER_MD_PATH);
|
|
3980
|
+
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}` } }] };
|
|
3981
|
+
});
|
|
3982
|
+
// ─── Tools ─────────────────────────────────────────────
|
|
3983
|
+
server.tool("skill_recommend", "Searches the skill catalog for matching domain skills based on keywords.", {
|
|
3984
|
+
keywords: z.array(z.string()).describe("Keywords to search for (e.g., 'kafka', 'react')")
|
|
3985
|
+
}, async ({ keywords }) => {
|
|
3986
|
+
const content = readFileSafe(SKILLS_CATALOG_PATH);
|
|
3987
|
+
if (!content)
|
|
3988
|
+
return appendTelemetry({ content: [{ type: "text", text: "No skills catalog found." }] }, 0);
|
|
3989
|
+
try {
|
|
3990
|
+
const catalog = JSON.parse(content);
|
|
3991
|
+
const matches = catalog.skills.filter((skill) => keywords.some(kw => skill.tags.includes(kw.toLowerCase())));
|
|
3992
|
+
if (matches.length === 0)
|
|
3993
|
+
return appendTelemetry({ content: [{ type: "text", text: "No matching skills found." }] }, content.length);
|
|
3994
|
+
let out = "Found the following relevant skills:\n";
|
|
3995
|
+
matches.forEach((s) => {
|
|
3996
|
+
out += `- **${s.name}**: ${s.description}\n *Read file:* \`${s.path}\`\n`;
|
|
3997
|
+
});
|
|
3998
|
+
return appendTelemetry({ content: [{ type: "text", text: out }] }, content.length);
|
|
3999
|
+
}
|
|
4000
|
+
catch (e) {
|
|
4001
|
+
return appendTelemetry({ content: [{ type: "text", text: "Error parsing skills catalog." }] }, 0);
|
|
4745
4002
|
}
|
|
4746
|
-
return appendTelemetry({
|
|
4747
|
-
content: [{ type: "text", text: content }]
|
|
4748
|
-
}, 0);
|
|
4749
4003
|
});
|
|
4750
4004
|
const transport = new StdioServerTransport();
|
|
4751
4005
|
await server.connect(transport);
|