secufusion-mcp 1.0.37 → 1.0.38
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 +2 -18
- package/index.js +0 -396
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -436,25 +436,13 @@ STEP 3: do NOT repeat the rejected pattern — ever
|
|
|
436
436
|
## Phase 4 — PR Handoff
|
|
437
437
|
(TRIGGER: developer says "prepare PR", "run checks", or "ready to merge")
|
|
438
438
|
|
|
439
|
-
### MANDATORY sequence
|
|
439
|
+
### MANDATORY sequence
|
|
440
440
|
|
|
441
441
|
```
|
|
442
442
|
STEP 1: call manage_task(action: "complete", work_item_id: <id>)
|
|
443
443
|
— marks status complete, auto-generates pr-summary.md
|
|
444
444
|
|
|
445
|
-
STEP 2: call
|
|
446
|
-
— runs: spec checkbox check, AST linting, tenant isolation scan,
|
|
447
|
-
hardcoded URL scan, Flyway migration coverage
|
|
448
|
-
|
|
449
|
-
STEP 3: if run_pre_pr_checks returns ANY error:
|
|
450
|
-
→ navigate to the failing microservice
|
|
451
|
-
→ fix the error NATIVELY in source code
|
|
452
|
-
→ call run_pre_pr_checks again
|
|
453
|
-
→ repeat until ZERO errors — no exceptions
|
|
454
|
-
|
|
455
|
-
STEP 4: raise PR only when run_pre_pr_checks reports zero errors
|
|
456
|
-
|
|
457
|
-
STEP 5: call generate_ado_comments(work_item_id, layman_summary, technical_deep_dive)
|
|
445
|
+
STEP 2: call generate_ado_comments(work_item_id, layman_summary, technical_deep_dive)
|
|
458
446
|
— Use this tool to output two distinct comments for the developer to paste into the Azure DevOps (ADO) board:
|
|
459
447
|
1. **Layman Summary**: A short, fundamental, non-technical explanation of the business value and what was resolved.
|
|
460
448
|
2. **Technical Deep-Dive**: A comprehensive, highly technical breakdown of the architectural changes, core optimizations, and exact implementation details.
|
|
@@ -463,10 +451,6 @@ STEP 5: call generate_ado_comments(work_item_id, layman_summary, technical_deep_
|
|
|
463
451
|
### Hard enforcement
|
|
464
452
|
|
|
465
453
|
❌ Do NOT raise a PR while `pending_acs` is non-empty
|
|
466
|
-
❌ Do NOT suppress linter warnings to pass the gate — fix them natively
|
|
467
|
-
❌ Do NOT skip `run_pre_pr_checks` and assume the workspace is clean
|
|
468
|
-
❌ Do NOT raise a PR if tenant isolation violations are present — security breach
|
|
469
|
-
❌ Do NOT raise a PR if Flyway coverage is missing for a modified `@Entity`
|
|
470
454
|
|
|
471
455
|
## Phase 5 — Retrospective
|
|
472
456
|
(TRIGGER: after manage_task action=complete is called
|
package/index.js
CHANGED
|
@@ -112,207 +112,6 @@ function appendTelemetry(response, inputChars) {
|
|
|
112
112
|
return response;
|
|
113
113
|
}
|
|
114
114
|
// ─────────────────────────────────────────────
|
|
115
|
-
// run_pre_pr_checks helpers
|
|
116
|
-
// ─────────────────────────────────────────────
|
|
117
|
-
const CONSOLE_LOG_REGEX = [
|
|
118
|
-
{ regex: /console\.log\s*\(/g, label: "console.log()", ext: [".ts", ".tsx", ".js", ".jsx"] },
|
|
119
|
-
{ regex: /System\.out\.println\s*\(/g, label: "System.out.println()", ext: [".java"] },
|
|
120
|
-
];
|
|
121
|
-
const HARDCODED_URL_PATTERN = /(?:https?:\/\/)?(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?|(?:uat|prod|staging|preprod)\.[a-z0-9.-]+\.[a-z]{2,}/gi;
|
|
122
|
-
function walkDir(dir, exts) {
|
|
123
|
-
let results = [];
|
|
124
|
-
try {
|
|
125
|
-
const list = fs.readdirSync(dir);
|
|
126
|
-
for (const file of list) {
|
|
127
|
-
if (file === "node_modules" || file === ".git" || file === "dist" || file === "target")
|
|
128
|
-
continue;
|
|
129
|
-
const filePath = path.join(dir, file);
|
|
130
|
-
const stat = fs.statSync(filePath);
|
|
131
|
-
if (stat && stat.isDirectory()) {
|
|
132
|
-
results = results.concat(walkDir(filePath, exts));
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
if (exts.some(ext => file.endsWith(ext))) {
|
|
136
|
-
results.push(filePath);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (e) {
|
|
142
|
-
/* ignore */
|
|
143
|
-
}
|
|
144
|
-
return results;
|
|
145
|
-
}
|
|
146
|
-
// ── AST-level linting via ESLint (preferred over regex for JS/TS) ─────────────
|
|
147
|
-
function runEslint(rootDir) {
|
|
148
|
-
// Check if ESLint is configured in the project
|
|
149
|
-
const eslintConfigs = [".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml",
|
|
150
|
-
".eslintrc.yaml", "eslint.config.js", "eslint.config.mjs"];
|
|
151
|
-
const hasEslintConfig = eslintConfigs.some((f) => fs.existsSync(path.join(rootDir, f)));
|
|
152
|
-
const hasPkgEslint = (() => {
|
|
153
|
-
const pkg = readFileSafe(path.join(rootDir, "package.json"));
|
|
154
|
-
if (!pkg)
|
|
155
|
-
return false;
|
|
156
|
-
try {
|
|
157
|
-
return !!(JSON.parse(pkg).eslintConfig);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
})();
|
|
163
|
-
if (!hasEslintConfig && !hasPkgEslint)
|
|
164
|
-
return { violations: [], available: false };
|
|
165
|
-
try {
|
|
166
|
-
execSync("npx eslint . --format json --max-warnings=0 --no-error-on-unmatched-pattern", { cwd: rootDir, stdio: "pipe" });
|
|
167
|
-
return { violations: [], available: true };
|
|
168
|
-
}
|
|
169
|
-
catch (e) {
|
|
170
|
-
const raw = e.stdout?.toString() ?? "";
|
|
171
|
-
try {
|
|
172
|
-
const results = JSON.parse(raw);
|
|
173
|
-
const violations = [];
|
|
174
|
-
for (const file of results) {
|
|
175
|
-
for (const msg of file.messages) {
|
|
176
|
-
if (msg.severity > 0) {
|
|
177
|
-
violations.push(`[ESLint] ${path.relative(rootDir, file.filePath)}:${msg.line} — ${msg.message}${msg.ruleId ? ` (${msg.ruleId})` : ""}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return { violations, available: true };
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
// ESLint output not JSON — return raw stderr summary
|
|
185
|
-
const stderr = e.stderr?.toString() ?? "";
|
|
186
|
-
return { violations: stderr ? [`[ESLint] ${stderr.slice(0, 400)}`] : [], available: true };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// ── Maven + Checkstyle for Java/Spring projects ───────────────────────────────
|
|
191
|
-
function runCheckstyle(rootDir) {
|
|
192
|
-
const hasPom = fs.existsSync(path.join(rootDir, "pom.xml"));
|
|
193
|
-
if (!hasPom)
|
|
194
|
-
return { violations: [], available: false };
|
|
195
|
-
try {
|
|
196
|
-
execSync("mvn checkstyle:check -q --no-transfer-progress", { cwd: rootDir, stdio: "pipe" });
|
|
197
|
-
return { violations: [], available: true };
|
|
198
|
-
}
|
|
199
|
-
catch (e) {
|
|
200
|
-
const output = ((e.stdout?.toString() ?? "") + (e.stderr?.toString() ?? "")).slice(0, 1200);
|
|
201
|
-
return {
|
|
202
|
-
violations: [`[Checkstyle/Maven] Build failed:\n${output}`],
|
|
203
|
-
available: true,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// ── Regex fallback — used only when no linter is configured ──────────────────
|
|
208
|
-
function checkConsoleLogsRegex(rootDir) {
|
|
209
|
-
const violations = [];
|
|
210
|
-
for (const { regex, label, ext } of CONSOLE_LOG_REGEX) {
|
|
211
|
-
const files = walkDir(rootDir, ext);
|
|
212
|
-
for (const file of files) {
|
|
213
|
-
const content = readFileSafe(file);
|
|
214
|
-
if (!content)
|
|
215
|
-
continue;
|
|
216
|
-
const lines = content.split("\n");
|
|
217
|
-
lines.forEach((line, idx) => {
|
|
218
|
-
if (regex.test(line)) {
|
|
219
|
-
violations.push(`${label} found at ${path.relative(rootDir, file)}:${idx + 1}`);
|
|
220
|
-
}
|
|
221
|
-
regex.lastIndex = 0;
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return violations;
|
|
226
|
-
}
|
|
227
|
-
function checkHardcodedUrls(rootDir) {
|
|
228
|
-
const violations = [];
|
|
229
|
-
const exts = [".ts", ".tsx", ".js", ".jsx", ".java", ".properties", ".yml", ".yaml"];
|
|
230
|
-
const files = walkDir(rootDir, exts);
|
|
231
|
-
for (const file of files) {
|
|
232
|
-
const content = readFileSafe(file);
|
|
233
|
-
if (!content)
|
|
234
|
-
continue;
|
|
235
|
-
const matches = content.match(HARDCODED_URL_PATTERN);
|
|
236
|
-
if (matches) {
|
|
237
|
-
violations.push(`Hardcoded URL/IP in ${path.relative(rootDir, file)}: ${[...new Set(matches)].join(", ")}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return violations;
|
|
241
|
-
}
|
|
242
|
-
// ── Workspace / Microservice Discovery ───────────────────────────────────────
|
|
243
|
-
function findProjectRoots(dir, depth = 0) {
|
|
244
|
-
if (depth > 3)
|
|
245
|
-
return []; // Limit depth to avoid scanning massive trees
|
|
246
|
-
if (!fs.existsSync(dir))
|
|
247
|
-
return [];
|
|
248
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
249
|
-
const hasPackageJson = entries.some(e => e.name === "package.json");
|
|
250
|
-
const hasPomXml = entries.some(e => e.name === "pom.xml");
|
|
251
|
-
const roots = [];
|
|
252
|
-
if (hasPackageJson || hasPomXml) {
|
|
253
|
-
roots.push(dir);
|
|
254
|
-
}
|
|
255
|
-
for (const entry of entries) {
|
|
256
|
-
if (entry.isDirectory()) {
|
|
257
|
-
if (["node_modules", ".git", "dist", "build", "target", ".idea"].includes(entry.name))
|
|
258
|
-
continue;
|
|
259
|
-
roots.push(...findProjectRoots(path.join(dir, entry.name), depth + 1));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return [...new Set(roots)];
|
|
263
|
-
}
|
|
264
|
-
// ── Automated Tenant Isolation Scanner ────────────────────────────────────────
|
|
265
|
-
function checkTenantIsolation(rootDir) {
|
|
266
|
-
const violations = [];
|
|
267
|
-
const files = walkDir(rootDir, [".java"]).filter(f => f.endsWith("Repository.java"));
|
|
268
|
-
const queryMethodRegex = /\b(?:find|get|read|query|search|stream|count|exists|delete|remove)(?:All)?By[A-Z0-9]/;
|
|
269
|
-
const atQueryRegex = /@Query\s*\(/;
|
|
270
|
-
for (const file of files) {
|
|
271
|
-
const content = readFileSafe(file);
|
|
272
|
-
if (!content)
|
|
273
|
-
continue;
|
|
274
|
-
const lines = content.split("\n");
|
|
275
|
-
for (let i = 0; i < lines.length; i++) {
|
|
276
|
-
const line = lines[i];
|
|
277
|
-
if (queryMethodRegex.test(line) || atQueryRegex.test(line)) {
|
|
278
|
-
// Look at current line and the next to handle simple wrapping
|
|
279
|
-
const combinedContext = line + (lines[i + 1] || "");
|
|
280
|
-
if (!/tenant/i.test(combinedContext)) {
|
|
281
|
-
violations.push(`Missing tenant isolation in ${path.relative(rootDir, file)}:${i + 1} — query lacks 'tenantId':\n` +
|
|
282
|
-
` ${line.trim()}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return violations;
|
|
288
|
-
}
|
|
289
|
-
function checkFlywayMigrations(rootDir) {
|
|
290
|
-
const warnings = [];
|
|
291
|
-
const javaFiles = walkDir(rootDir, [".java"]);
|
|
292
|
-
const entityFiles = javaFiles.filter((f) => {
|
|
293
|
-
const content = readFileSafe(f);
|
|
294
|
-
return content && /@Entity\b/.test(content);
|
|
295
|
-
});
|
|
296
|
-
if (entityFiles.length === 0)
|
|
297
|
-
return warnings;
|
|
298
|
-
const flywayDirs = [
|
|
299
|
-
path.join(rootDir, "src", "main", "resources", "db", "migration"),
|
|
300
|
-
path.join(rootDir, "src", "main", "resources", "db", "migrations"),
|
|
301
|
-
path.join(rootDir, "resources", "db", "migration"),
|
|
302
|
-
];
|
|
303
|
-
const flywayDir = flywayDirs.find((d) => fs.existsSync(d));
|
|
304
|
-
if (!flywayDir) {
|
|
305
|
-
warnings.push(`Found ${entityFiles.length} JPA @Entity file(s) but no Flyway migration directory detected. ` +
|
|
306
|
-
`Expected at: src/main/resources/db/migration/`);
|
|
307
|
-
return warnings;
|
|
308
|
-
}
|
|
309
|
-
const migrationFiles = fs.readdirSync(flywayDir).filter((f) => /^V\d+__.*\.sql$/i.test(f));
|
|
310
|
-
if (migrationFiles.length === 0) {
|
|
311
|
-
warnings.push(`Found ${entityFiles.length} JPA @Entity file(s) but no Flyway SQL migrations exist in ${flywayDir}`);
|
|
312
|
-
}
|
|
313
|
-
return warnings;
|
|
314
|
-
}
|
|
315
|
-
// ─────────────────────────────────────────────
|
|
316
115
|
// MCP Server bootstrap
|
|
317
116
|
// ─────────────────────────────────────────────
|
|
318
117
|
const server = new McpServer({
|
|
@@ -500,201 +299,6 @@ server.tool("log_rejected_pattern", "Record a coding pattern that was rejected b
|
|
|
500
299
|
],
|
|
501
300
|
}, inputChars);
|
|
502
301
|
});
|
|
503
|
-
// ─── Tool 3: run_pre_pr_checks ───────────────────────────────────────────────
|
|
504
|
-
server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks across the workspace. " +
|
|
505
|
-
"Automatically discovers modified microservices and runs native linters, database script checks, and strict tenant-isolation scanners. " +
|
|
506
|
-
"Validates: spec checkboxes, hardcoded IPs/URLs, tenantId in Repositories, and Flyway migration coverage for JPA entities. " +
|
|
507
|
-
"Linter/isolation errors must be fixed natively — do not suppress warnings to pass the gate. " +
|
|
508
|
-
"MUST pass with zero errors before a PR is raised.", {
|
|
509
|
-
work_item_id: z
|
|
510
|
-
.string()
|
|
511
|
-
.describe("Azure DevOps work item ID to include in the PR summary."),
|
|
512
|
-
root_dir: z
|
|
513
|
-
.string()
|
|
514
|
-
.optional()
|
|
515
|
-
.describe("Root directory to scan (defaults to cwd). Use when the source repo is in a sub-folder."),
|
|
516
|
-
skip_checks: z
|
|
517
|
-
.array(z.enum([
|
|
518
|
-
"spec_boxes",
|
|
519
|
-
"console_logs",
|
|
520
|
-
"tenant_isolation",
|
|
521
|
-
"hardcoded_urls",
|
|
522
|
-
"flyway_migrations",
|
|
523
|
-
]))
|
|
524
|
-
.optional()
|
|
525
|
-
.describe("Explicitly skip specific checks. Use sparingly — document the reason in your PR description."),
|
|
526
|
-
}, async ({ work_item_id, root_dir, skip_checks = [] }) => {
|
|
527
|
-
const inputChars = JSON.stringify({ work_item_id, root_dir, skip_checks }).length;
|
|
528
|
-
const scanRoot = root_dir ? path.resolve(root_dir) : getWorkspaceRoot();
|
|
529
|
-
const errors = [];
|
|
530
|
-
const warnings = [];
|
|
531
|
-
const passed = [];
|
|
532
|
-
// ── 1. Branch state check ─────────────────────────────────────────────
|
|
533
|
-
if (!skip_checks.includes("spec_boxes")) {
|
|
534
|
-
const branch = getCurrentBranch(scanRoot);
|
|
535
|
-
const statePath = path.resolve(scanRoot, STATE_FILE);
|
|
536
|
-
let branchState = null;
|
|
537
|
-
if (fs.existsSync(statePath)) {
|
|
538
|
-
try {
|
|
539
|
-
const stateData = JSON.parse(readFileSafe(statePath) || "{}");
|
|
540
|
-
branchState = stateData[branch];
|
|
541
|
-
}
|
|
542
|
-
catch { }
|
|
543
|
-
}
|
|
544
|
-
if (!branchState) {
|
|
545
|
-
errors.push(`❌ [STATE] No branch state found for '${branch}' in \`${STATE_FILE}\`. ` +
|
|
546
|
-
`Call \`manage_branch_state\` with action='initialize' before raising a PR.`);
|
|
547
|
-
}
|
|
548
|
-
else if (branchState.pending_acs && branchState.pending_acs.length > 0) {
|
|
549
|
-
errors.push(`❌ [STATE] ${branchState.pending_acs.length} pending AC(s) remain for branch '${branch}':\n` +
|
|
550
|
-
branchState.pending_acs.map((u) => ` • ${u}`).join("\n"));
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
passed.push(`✅ [STATE] Branch state for '${branch}' is clear (no pending ACs).`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
warnings.push("⚠️ [STATE] Branch state check SKIPPED (manually bypassed).");
|
|
558
|
-
}
|
|
559
|
-
// ── 2. AST-level linting (Workspace Discovery → ESLint/Checkstyle → regex fallback)
|
|
560
|
-
if (!skip_checks.includes("console_logs")) {
|
|
561
|
-
let projectRoots = findProjectRoots(scanRoot);
|
|
562
|
-
if (projectRoots.length === 0)
|
|
563
|
-
projectRoots.push(scanRoot);
|
|
564
|
-
let anyLinterAvailable = false;
|
|
565
|
-
const allLintViolations = [];
|
|
566
|
-
const usedLinters = new Set();
|
|
567
|
-
for (const pRoot of projectRoots) {
|
|
568
|
-
const eslint = runEslint(pRoot);
|
|
569
|
-
const checkstyle = runCheckstyle(pRoot);
|
|
570
|
-
if (eslint.available) {
|
|
571
|
-
anyLinterAvailable = true;
|
|
572
|
-
usedLinters.add("ESLint");
|
|
573
|
-
allLintViolations.push(...eslint.violations);
|
|
574
|
-
}
|
|
575
|
-
if (checkstyle.available) {
|
|
576
|
-
anyLinterAvailable = true;
|
|
577
|
-
usedLinters.add("Checkstyle");
|
|
578
|
-
allLintViolations.push(...checkstyle.violations);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
if (anyLinterAvailable) {
|
|
582
|
-
// AST linters found and ran — they are the source of truth
|
|
583
|
-
if (allLintViolations.length > 0) {
|
|
584
|
-
errors.push(`❌ [LINTING] ${allLintViolations.length} linter error(s) across workspace — fix natively in their respective microservices:\n` +
|
|
585
|
-
allLintViolations.map((v) => ` • ${v}`).join("\n"));
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
passed.push(`✅ [LINTING] AST-level linting passed across workspace (${Array.from(usedLinters).join(" + ")}).`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
// No linters configured — fall back to regex scan with a warning
|
|
593
|
-
const regexViolations = checkConsoleLogsRegex(scanRoot);
|
|
594
|
-
if (regexViolations.length > 0) {
|
|
595
|
-
errors.push(`❌ [LOGGING] ${regexViolations.length} prohibited log statement(s) found via regex fallback\n` +
|
|
596
|
-
` (No ESLint/Checkstyle config detected in any workspace project — configure a linter for AST-level accuracy):\n` +
|
|
597
|
-
regexViolations.map((v) => ` • ${v}`).join("\n"));
|
|
598
|
-
}
|
|
599
|
-
else {
|
|
600
|
-
warnings.push("⚠️ [LOGGING] No ESLint/Checkstyle config found in workspace — used regex fallback. " +
|
|
601
|
-
"Consider adding a linter for AST-level hygiene.");
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
warnings.push("⚠️ [LINTING] Linter check SKIPPED (manually bypassed).");
|
|
607
|
-
}
|
|
608
|
-
// ── 3. Tenant Isolation Scanner ──────────────────────────────────────────
|
|
609
|
-
if (!skip_checks.includes("tenant_isolation")) {
|
|
610
|
-
const tenantViolations = checkTenantIsolation(scanRoot);
|
|
611
|
-
if (tenantViolations.length > 0) {
|
|
612
|
-
errors.push(`❌ [SECURITY] Tenant Isolation Failed: Found ${tenantViolations.length} query method(s) in *Repository.java without tenant scoping:\n` +
|
|
613
|
-
tenantViolations.map((v) => ` • ${v}`).join("\n"));
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
passed.push("✅ [SECURITY] Tenant isolation active: All database queries correctly scoped with tenantId.");
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
warnings.push("⚠️ [SECURITY] Tenant isolation check SKIPPED (manually bypassed).");
|
|
621
|
-
}
|
|
622
|
-
// ── 4. Hardcoded URL check (regex) ───────────────────────────────────────
|
|
623
|
-
if (!skip_checks.includes("hardcoded_urls")) {
|
|
624
|
-
const urlViolations = checkHardcodedUrls(scanRoot);
|
|
625
|
-
if (urlViolations.length > 0) {
|
|
626
|
-
errors.push(`❌ [SECURITY] Found ${urlViolations.length} hardcoded URL/IP(s):\n` +
|
|
627
|
-
urlViolations.map((v) => ` • ${v}`).join("\n"));
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
passed.push("✅ [SECURITY] No hardcoded UAT/Prod IPs or environment URLs found.");
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
warnings.push("⚠️ [SECURITY] Hardcoded URL check SKIPPED (manually bypassed).");
|
|
635
|
-
}
|
|
636
|
-
// ── 4. Flyway migration check ──────────────────────────────────────────
|
|
637
|
-
if (!skip_checks.includes("flyway_migrations")) {
|
|
638
|
-
const flywayWarnings = checkFlywayMigrations(scanRoot);
|
|
639
|
-
if (flywayWarnings.length > 0) {
|
|
640
|
-
errors.push(...flywayWarnings.map((w) => `❌ [FLYWAY] ${w}`));
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
passed.push("✅ [FLYWAY] Flyway migration coverage looks good.");
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
warnings.push("⚠️ [FLYWAY] Flyway migration check SKIPPED (manually bypassed).");
|
|
648
|
-
}
|
|
649
|
-
// ── Surface rejected patterns as a reminder ───────────────────────────
|
|
650
|
-
const rejectedContent = readFileSafe(resolve(REJECTED_FILE));
|
|
651
|
-
let rejectedReminder = "";
|
|
652
|
-
if (rejectedContent) {
|
|
653
|
-
try {
|
|
654
|
-
const rejected = JSON.parse(rejectedContent);
|
|
655
|
-
if (rejected.length > 0) {
|
|
656
|
-
rejectedReminder =
|
|
657
|
-
`\n\n---\n### 🚫 Rejected Patterns Reminder (${rejected.length} on record)\n` +
|
|
658
|
-
rejected.map((r) => `- [${r.category}] ${r.pattern}`).join("\n");
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
catch {
|
|
662
|
-
/* ignore parse errors */
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
// ── Build final report ─────────────────────────────────────────────────
|
|
666
|
-
const hasErrors = errors.length > 0;
|
|
667
|
-
const timestamp = new Date().toISOString();
|
|
668
|
-
let report;
|
|
669
|
-
if (hasErrors) {
|
|
670
|
-
report =
|
|
671
|
-
`# ❌ Pre-PR Checks FAILED — Work Item #${work_item_id}\n\n` +
|
|
672
|
-
`**Scanned directory:** \`${scanRoot}\`\n` +
|
|
673
|
-
`**Timestamp:** ${timestamp}\n\n` +
|
|
674
|
-
`## Errors (must fix before PR)\n\n${errors.join("\n\n")}\n\n` +
|
|
675
|
-
(warnings.length > 0 ? `## Warnings\n\n${warnings.join("\n")}\n\n` : "") +
|
|
676
|
-
(passed.length > 0 ? `## Passed\n\n${passed.join("\n")}\n` : "") +
|
|
677
|
-
rejectedReminder;
|
|
678
|
-
}
|
|
679
|
-
else {
|
|
680
|
-
report =
|
|
681
|
-
`# ✅ Pre-PR Checks PASSED — Work Item #${work_item_id}\n\n` +
|
|
682
|
-
`**Scanned directory:** \`${scanRoot}\`\n` +
|
|
683
|
-
`**Timestamp:** ${timestamp}\n\n` +
|
|
684
|
-
`## All Checks Passed\n\n${passed.join("\n")}\n\n` +
|
|
685
|
-
(warnings.length > 0 ? `## Warnings\n\n${warnings.join("\n")}\n\n` : "") +
|
|
686
|
-
`## PR Summary\n\n` +
|
|
687
|
-
`- **Work Item:** [#${work_item_id}](https://dev.azure.com/secufusion/_workitems/edit/${work_item_id})\n` +
|
|
688
|
-
`- **Spec:** All acceptance criteria verified ✅\n` +
|
|
689
|
-
`- **Guardrails:** tenantId scoping, no debug logs, no hardcoded URLs, Flyway covered ✅\n` +
|
|
690
|
-
`- **Ready to raise PR** 🚀\n` +
|
|
691
|
-
rejectedReminder;
|
|
692
|
-
}
|
|
693
|
-
return appendTelemetry({
|
|
694
|
-
content: [{ type: "text", text: report }],
|
|
695
|
-
isError: hasErrors,
|
|
696
|
-
}, inputChars);
|
|
697
|
-
});
|
|
698
302
|
// ─── Tool 4: manage_project_spec ─────────────────────────────────────────────
|
|
699
303
|
server.tool("manage_project_spec", "Manages the .secufusion-project-spec.json file — the permanent project memory containing all service ports, repos, domains, infrastructure config, coding patterns, auth flow, and golden rules. Every session must read this before writing any code.", {
|
|
700
304
|
action: z
|