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.
Files changed (4) hide show
  1. package/AGENTS.md +12 -825
  2. package/README.md +25 -0
  3. package/index.js +84 -830
  4. 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 full = { spec: readJson(specPath), progress: readJson(progressPath), decisions: readJson(decisionsPath), files_touched: readJson(filesPath), scenarios: readJson(scenariosPath) };
642
- return appendTelemetry({ content: [{ type: "text", text: JSON.stringify(full, null, 2) }] }, inputChars);
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
- rejectedPatterns = JSON.parse(readFileSafe(resolve(".rejected-patterns.json")) || "[]");
2777
- }
2778
- catch { }
2779
- const fileRecords = filesTouched.map(ft => {
2780
- const abs = path.resolve(wsRoot, ft.file_path);
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
- // ── 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
- });
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 AGENTS.md ──────────────────────────────────────────
4708
- const AGENTS_MD_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "AGENTS.md");
4709
- server.resource("secufusion_rules", "secufusion://rules", async (uri) => {
4710
- const content = readFileSafe(AGENTS_MD_PATH);
4711
- if (!content) {
4712
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
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
- contents: [{
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("secufusion_developer", "Starts a conversation with the SecuFusion developer workflow rules loaded.", async () => {
4722
- const content = readFileSafe(AGENTS_MD_PATH);
4723
- if (!content) {
4724
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
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
- // Start transport
4740
- // ─────────────────────────────────────────────
4741
- server.tool("get_secufusion_rules", "Fetches the mandatory AGENTS.md workflow rules for the SecuFusion project. Call this before starting any work.", {}, async () => {
4742
- const content = readFileSafe(AGENTS_MD_PATH);
4743
- if (!content) {
4744
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
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);