secufusion-mcp 2.4.1 → 2.5.1
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/.claude-plugin/plugin.json +2 -2
- package/agents/AGENTS.md +7 -7
- package/mcp/dist/server.js +15 -355
- package/package.json +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "secufusion-mcp",
|
|
3
3
|
"description": "SecuFusion MSSP platform dev agent — project-level spec driven, tenant-isolation enforced, zero-trust guardrails. 5 Core Slash Commands: /sfn-init (dynamic DNA + architecture diagram + watcher), /sfn-plan (philosophy check + intent WHY + classify + plan), /sfn-code (spec-driven implementation), /sfn-review (zero-tolerance 3-tier PR gate), /sfn-explore (on-demand service graph or blast-radius analysis).",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Motivity Labs"
|
|
7
7
|
},
|
|
8
8
|
"homepage": "https://github.com/MLMCPS/secufusion-mcp",
|
|
9
9
|
"license": "ISC"
|
|
10
|
-
}
|
|
10
|
+
}
|
package/agents/AGENTS.md
CHANGED
|
@@ -45,15 +45,15 @@ Call `philosophy_check` AFTER DNA is loaded — never before. The engine us
|
|
|
45
45
|
- Which Kafka topics fan out to how many consumers
|
|
46
46
|
- Whether your description violates any golden rules
|
|
47
47
|
|
|
48
|
-
**This check is completely invisible to the user.** You do not announce it. You do not show
|
|
48
|
+
**This check is completely invisible to the user.** You do not announce it. You do not show verdicts or "philosophy" terminology. You surface a concern to the user only if the architectural risk is high or the intent is missing — and you translate it into plain developer language.
|
|
49
49
|
|
|
50
|
-
For `bug` and `hotfix`: philosophy check **fully runs** — no skip. The
|
|
50
|
+
For `bug` and `hotfix`: philosophy check **fully runs** — no skip. The agent acts as the reasoning engine against the Project DNA. Bugs inherently have a "WHY" (something is broken) and an implicit authority (the reporter), but the engine evaluates the **blast radius and architectural risk**. A high-risk bug fix (auth regression, data-loss, multi-tenant blast) will still be challenged if the rollback plan or scope isn't clear.
|
|
51
51
|
|
|
52
52
|
**Verdicts:**
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
53
|
+
- **Silent Pass** → Bug is isolated or safe. Proceed immediately.
|
|
54
|
+
- **Advisory** → Note concerns in `decisions.json`. Tell the user briefly what to watch out for. Continue.
|
|
55
|
+
- **Fail** → STOP. Ask the user what is missing (no jargon). Wait for answers. Re-run philosophy_check.
|
|
56
|
+
- **Blocked** → STOP completely. Tell the user the risk and who must sign off. Do not plan. Do not code.
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
@@ -323,4 +323,4 @@ Java Microservices (all behind gateway):
|
|
|
323
323
|
Services WITHOUT migrations: `sfn-auth-api`, `sfn-eureka-api`, `sfn-gateway-api`, `sfn-policy-api`, `sfn-web-ui`, `snf-browser-extn`
|
|
324
324
|
|
|
325
325
|
NEVER guess migration version numbers. Always read `.secufusion-migrations.json` first.
|
|
326
|
-
|
|
326
|
+
|
package/mcp/dist/server.js
CHANGED
|
@@ -1547,40 +1547,6 @@ server.tool("philosophy_check", "MANDATORY PHASE -1 STEP — runs BEFORE classif
|
|
|
1547
1547
|
client_confirmed: z.boolean().optional().describe("Has the decision maker explicitly confirmed this is needed?"),
|
|
1548
1548
|
}, async ({ work_item_id, title, description, task_type, reported_by, client_confirmed }) => {
|
|
1549
1549
|
const inputChars = JSON.stringify({ work_item_id, title, description, task_type }).length;
|
|
1550
|
-
// Bugs and hotfixes run through the full philosophy engine with relaxed scoring:
|
|
1551
|
-
// WHY → +20 head-start (defects have an inherent "WHY": something is broken)
|
|
1552
|
-
// WHO → +15 head-start (reporter is implicit authority for a defect)
|
|
1553
|
-
// RISK → NO_JUSTIFICATION block exempted for bug/hotfix (see Pass 5)
|
|
1554
|
-
// This ensures high-risk bug fixes (auth regressions, data-loss fixes, multi-tenant
|
|
1555
|
-
// defects) still surface ADVISORY or FAIL when the blast radius warrants it.
|
|
1556
|
-
// Check cache
|
|
1557
|
-
const philosophyDir = resolve(".secufusion/philosophy");
|
|
1558
|
-
const philosophyFile = path.join(philosophyDir, `${work_item_id}.json`);
|
|
1559
|
-
let cached = null;
|
|
1560
|
-
if (fs.existsSync(philosophyFile)) {
|
|
1561
|
-
try {
|
|
1562
|
-
cached = JSON.parse(readFileSafe(philosophyFile) || "null");
|
|
1563
|
-
if (cached && (cached.philosophy_verdict === "PHILOSOPHY_PASS" || cached.philosophy_verdict === "PHILOSOPHY_ADVISORY")) {
|
|
1564
|
-
return appendTelemetry({ content: [{ type: "text", text: `[PHILOSOPHY CACHED] ${work_item_id} — verdict: ${cached.philosophy_verdict}\n\n${cached.developer_message}` }] }, inputChars);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
catch { }
|
|
1568
|
-
}
|
|
1569
|
-
// Mandatory interview for ALL work items (features, bugs, chores, etc.)
|
|
1570
|
-
if (!cached || !cached.interview_completed) {
|
|
1571
|
-
const interviewText = `[PHILOSOPHY FAIL] ${work_item_id}\n\nBefore I help you build this, talk to me.\n\nWhat problem is this solving?\nWalk me through what the user is experiencing today\nwithout this feature. What's broken? What's painful?\nWhat happens if we don't build this at all?\n\nWho will actually use this and how often?\nIs this solving a problem for 1 client or all tenants?\n\nI see you said WHO: CONFIRMED —\nbut confirmed by whom exactly?\nIs that a verbal yes in a meeting or\na written requirement from the client?\nThose are very different things.\n\nDo NOT call classify_task. Re-run philosophy_check after resolving.`;
|
|
1572
|
-
if (!fs.existsSync(philosophyDir))
|
|
1573
|
-
fs.mkdirSync(philosophyDir, { recursive: true });
|
|
1574
|
-
try {
|
|
1575
|
-
writeFile(philosophyFile, JSON.stringify({
|
|
1576
|
-
work_item_id,
|
|
1577
|
-
philosophy_verdict: "PHILOSOPHY_FAIL",
|
|
1578
|
-
interview_completed: true
|
|
1579
|
-
}, null, 2));
|
|
1580
|
-
}
|
|
1581
|
-
catch { }
|
|
1582
|
-
return appendTelemetry({ content: [{ type: "text", text: interviewText }] }, inputChars);
|
|
1583
|
-
}
|
|
1584
1550
|
const wsRoot = getWorkspaceRoot();
|
|
1585
1551
|
let rejectedPatterns = [];
|
|
1586
1552
|
const rejRaw = readFileSafe(resolve(REJECTED_FILE));
|
|
@@ -1590,12 +1556,8 @@ server.tool("philosophy_check", "MANDATORY PHASE -1 STEP — runs BEFORE classif
|
|
|
1590
1556
|
}
|
|
1591
1557
|
catch { }
|
|
1592
1558
|
}
|
|
1593
|
-
// ── Load Architecture DNA (MANDATORY for intelligent risk scoring) ──────
|
|
1594
|
-
// philosophy_check reasons against real architecture — not just keywords.
|
|
1595
|
-
// DNA provides: DB entities, Kafka topics, services, extension, auth model.
|
|
1596
1559
|
let dna = null;
|
|
1597
1560
|
let spec = null;
|
|
1598
|
-
// Load project spec (golden rules, service map, security model)
|
|
1599
1561
|
const specCands2 = [
|
|
1600
1562
|
path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), ".secufusion-project-spec.json"),
|
|
1601
1563
|
path.resolve(wsRoot, ".secufusion-project-spec.json"),
|
|
@@ -1609,7 +1571,6 @@ server.tool("philosophy_check", "MANDATORY PHASE -1 STEP — runs BEFORE classif
|
|
|
1609
1571
|
catch { }
|
|
1610
1572
|
}
|
|
1611
1573
|
}
|
|
1612
|
-
// Load DNA — full architecture graph
|
|
1613
1574
|
const dnaCandidates = [
|
|
1614
1575
|
path.resolve(wsRoot, ".secufusion", "dna.json"),
|
|
1615
1576
|
path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), ".secufusion", "dna.json"),
|
|
@@ -1623,327 +1584,26 @@ server.tool("philosophy_check", "MANDATORY PHASE -1 STEP — runs BEFORE classif
|
|
|
1623
1584
|
catch { }
|
|
1624
1585
|
}
|
|
1625
1586
|
}
|
|
1626
|
-
// Extract architectural context
|
|
1627
1587
|
const dnaServices = dna?.services?.map((s) => (s.name || "").toLowerCase()).filter((s) => s.length > 4) || [];
|
|
1628
1588
|
const dnaEntities = dna?.db_entities?.map((e) => (e.name || e.table || "").toLowerCase()).filter((e) => e.length > 3) || [];
|
|
1629
1589
|
const dnaTopics = dna?.kafka_topics || dna?.events || [];
|
|
1630
1590
|
const dnaExtension = dna?.chrome_extension || dna?.extension || dna?.["snf-browser-extn"] || null;
|
|
1631
1591
|
const dnaSecurity = dna?.security_context || spec?.security || null;
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
if (mentionedEntities.length > 0 || /\bmigration\b/i.test(text) || /\btable\b/i.test(text) || /\bcolumn\b/i.test(text)) {
|
|
1648
|
-
const migSpec = loadMigrationSpec(wsRoot);
|
|
1649
|
-
if (migSpec && mentionedServices.length > 0) {
|
|
1650
|
-
mentionedServices.forEach((s) => {
|
|
1651
|
-
const ctx = getMigrationContextForService(migSpec, s);
|
|
1652
|
-
if (ctx && ctx.has_migrations) {
|
|
1653
|
-
dna_risk_bonus += 15;
|
|
1654
|
-
dnaRiskNotes.push(`[MIGRATION SPEC] Manual migration script required for ${s} (Next: ${ctx.next_suggested}) (+15)`);
|
|
1655
|
-
}
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
else if (migSpec && mentionedEntities.length > 0) {
|
|
1659
|
-
// If service isn't mentioned by name but entities are, we still want to warn globally
|
|
1660
|
-
dna_risk_bonus += 10;
|
|
1661
|
-
dnaRiskNotes.push(`[MIGRATION SPEC] DB touched. Ensure manual migration scripts are applied to PostgreSQL before deploy. (+10)`);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
// [DNA-2] Kafka topics — fan-out cascades to all consumers
|
|
1665
|
-
const mentionedTopics = dnaTopics.filter((t) => {
|
|
1666
|
-
const tn = (t.name || t.topic || "").toLowerCase();
|
|
1667
|
-
return tn.length > 3 && text.includes(tn);
|
|
1668
|
-
});
|
|
1669
|
-
if (mentionedTopics.length > 0) {
|
|
1670
|
-
const topicConsumers = mentionedTopics.reduce((sum, t) => sum + (t.consumers?.length || t.consumer_count || 0), 0);
|
|
1671
|
-
const topicRisk = Math.min(mentionedTopics.length * 15 + topicConsumers * 5, 35);
|
|
1672
|
-
dna_risk_bonus += topicRisk;
|
|
1673
|
-
dnaRiskNotes.push(`Kafka topics: ${mentionedTopics.map((t) => t.name || t.topic).join(", ")} (${topicConsumers} consumers) (+${topicRisk})`);
|
|
1674
|
-
}
|
|
1675
|
-
// [DNA-3] Chrome extension changes require store deployment
|
|
1676
|
-
if (dnaExtension && (text.includes("extension") || text.includes("chrome") || text.includes("background script") || text.includes("content script") || text.includes("popup"))) {
|
|
1677
|
-
dna_risk_bonus += 20;
|
|
1678
|
-
dnaRiskNotes.push("snf-browser-extn affected — manual store publish (+20)");
|
|
1679
|
-
}
|
|
1680
|
-
// [DNA-4] Shared multi-tenant auth service — changes blast all tenants
|
|
1681
|
-
const isSharedAuth = dnaSecurity && (dnaSecurity.type === "shared" || dnaSecurity.multi_tenant === true || dnaSecurity.multi_tenant === "true");
|
|
1682
|
-
if (isSharedAuth && (text.includes("auth") || text.includes("login") || text.includes("permission") || text.includes("role") || text.includes("tenant") || text.includes("token"))) {
|
|
1683
|
-
dna_risk_bonus += 25;
|
|
1684
|
-
dnaRiskNotes.push("Shared multi-tenant auth — cross-tenant blast (+25)");
|
|
1685
|
-
}
|
|
1686
|
-
// [DNA-5] Polyglot service coordination — 3+ services = integration risk
|
|
1687
|
-
if (mentionedServices.length >= 3) {
|
|
1688
|
-
dna_risk_bonus += 15;
|
|
1689
|
-
dnaRiskNotes.push(`Multi-service: ${mentionedServices.slice(0, 4).join(", ")} (+15)`);
|
|
1690
|
-
}
|
|
1691
|
-
// [DNA-6] Golden rule violation signals
|
|
1692
|
-
const antiPatterns = ["bypass", "skip migration", "no migration", "hardcode", "raw sql", "direct db", "without migration"];
|
|
1693
|
-
const hitAntipatterns = antiPatterns.filter(ap => text.includes(ap));
|
|
1694
|
-
if (hitAntipatterns.length > 0) {
|
|
1695
|
-
dna_risk_bonus += 20;
|
|
1696
|
-
dnaRiskNotes.push(`Antipattern signal: ${hitAntipatterns.join(", ")} (+20)`);
|
|
1697
|
-
}
|
|
1698
|
-
const dnaAvailable = dna !== null;
|
|
1699
|
-
const dnaWarning = dnaAvailable ? "" : "\n[NOTE] DNA not loaded — risk is keyword-only. Run /sfn-init for architecture-aware scoring.";
|
|
1700
|
-
// PASS 1 - WHY CHECK
|
|
1701
|
-
let why_score = 0;
|
|
1702
|
-
if (task_type === "bug" || task_type === "hotfix") {
|
|
1703
|
-
why_score += 20; // Relaxed WHY sensing for defects
|
|
1704
|
-
}
|
|
1705
|
-
const STRONG_WHY = ["users spend", "current process causes", "compliance requires", "audit requires", "production issue", "users are reporting", "data shows", "confirmed by", "ticket from client", "business requirement", "regulatory", "security risk", "revenue impact"];
|
|
1706
|
-
const WEAK_WHY = [
|
|
1707
|
-
"would be nice", "nice feature", "nice addition", "could be useful", "other platforms have",
|
|
1708
|
-
"other saas", "other products have", "similar products", "competitors have",
|
|
1709
|
-
"client might want", "we should probably", "nice to have", "maybe useful",
|
|
1710
|
-
"might improve", "could improve", "would improve", "would be helpful",
|
|
1711
|
-
"suggestion", "idea", "asked for it"
|
|
1712
|
-
];
|
|
1713
|
-
for (const s of STRONG_WHY)
|
|
1714
|
-
if (text.includes(s))
|
|
1715
|
-
why_score += 10;
|
|
1716
|
-
for (const s of WEAK_WHY)
|
|
1717
|
-
if (text.includes(s))
|
|
1718
|
-
why_score -= 10;
|
|
1719
|
-
if (text.includes("client asked") && !text.includes("because") && !text.includes("so that"))
|
|
1720
|
-
why_score -= 10;
|
|
1721
|
-
// Penalize descriptions under 30 words with no strong why — they can't justify themselves
|
|
1722
|
-
if (wordCount < 30 && why_score <= 0)
|
|
1723
|
-
why_score -= 10;
|
|
1724
|
-
let purpose_stated = false;
|
|
1725
|
-
let purpose_text = "";
|
|
1726
|
-
for (const m of ["so that", "in order to", "because", "which means", "this allows"]) {
|
|
1727
|
-
const idx = description.toLowerCase().indexOf(m);
|
|
1728
|
-
if (idx !== -1) {
|
|
1729
|
-
purpose_stated = true;
|
|
1730
|
-
purpose_text = description.substring(idx, idx + 120).trim();
|
|
1731
|
-
break;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
const why_verdict = why_score >= 10 ? "STRONG" : why_score >= 0 ? "ACCEPTABLE" : why_score >= -19 ? "WEAK" : "NO_JUSTIFICATION";
|
|
1735
|
-
// PASS 2 - WHO CHECK
|
|
1736
|
-
let who_score = 0;
|
|
1737
|
-
if (task_type === "bug" || task_type === "hotfix") {
|
|
1738
|
-
who_score += 15; // Relaxed WHO sensing for defects
|
|
1739
|
-
}
|
|
1740
|
-
const AUTH_SIG = ["client confirmed", "client approved", "stakeholder sign-off", "decision made by", "approved by", "confirmed in meeting", "email from client", "client requirement", "explicit request from", "signed off"];
|
|
1741
|
-
const UNCERT_SIG = [
|
|
1742
|
-
"assumed", "we think", "probably", "should want", "might need", "could need",
|
|
1743
|
-
"i think the client", "i think clients", "pm suggestion", "internal idea", "we decided",
|
|
1744
|
-
"i think", "don't think", "shouldn't need", "won't need"
|
|
1745
|
-
];
|
|
1746
|
-
for (const s of AUTH_SIG)
|
|
1747
|
-
if (text.includes(s))
|
|
1748
|
-
who_score += 10;
|
|
1749
|
-
for (const s of UNCERT_SIG)
|
|
1750
|
-
if (text.includes(s))
|
|
1751
|
-
who_score -= 10;
|
|
1752
|
-
if (reported_by) {
|
|
1753
|
-
const rb = reported_by.toLowerCase();
|
|
1754
|
-
if (rb === "client")
|
|
1755
|
-
who_score += 15;
|
|
1756
|
-
else if (rb === "pm")
|
|
1757
|
-
who_score += 5;
|
|
1758
|
-
else if (rb === "developer")
|
|
1759
|
-
who_score -= 5;
|
|
1760
|
-
}
|
|
1761
|
-
if (client_confirmed === true)
|
|
1762
|
-
who_score += 20;
|
|
1763
|
-
else if (client_confirmed === false)
|
|
1764
|
-
who_score -= 20;
|
|
1765
|
-
const who_verdict = who_score >= 15 ? "CONFIRMED" : who_score >= 0 ? "PROBABLE" : who_score >= -14 ? "UNCERTAIN" : "UNCONFIRMED";
|
|
1766
|
-
// PASS 3 - WHAT CHECK
|
|
1767
|
-
let what_score = 0;
|
|
1768
|
-
if (text.includes("acceptance criteria"))
|
|
1769
|
-
what_score += 20;
|
|
1770
|
-
const hasGWT = text.includes("given") && text.includes("when") && text.includes("then");
|
|
1771
|
-
if (hasGWT)
|
|
1772
|
-
what_score += 15;
|
|
1773
|
-
const numberedACs = (description.match(/\b(ac-?\d+|\d+\.\s+(?:given|when|then|the user|the system))/gi) || []).length;
|
|
1774
|
-
if (numberedACs >= 2)
|
|
1775
|
-
what_score += 10;
|
|
1776
|
-
if ((description.match(/\bmust\b/gi) || []).length >= 1)
|
|
1777
|
-
what_score += 5;
|
|
1778
|
-
if (text.includes("as discussed"))
|
|
1779
|
-
what_score -= 15;
|
|
1780
|
-
if (text.includes("as agreed"))
|
|
1781
|
-
what_score -= 15;
|
|
1782
|
-
if (text.includes("tbd"))
|
|
1783
|
-
what_score -= 20;
|
|
1784
|
-
if (text.includes("to be defined"))
|
|
1785
|
-
what_score -= 20;
|
|
1786
|
-
if (wordCount < 50)
|
|
1787
|
-
what_score -= 10;
|
|
1788
|
-
if (!/\b(\d+%|within \d+|less than \d+|at least \d+)\b/.test(description) && wordCount < 80)
|
|
1789
|
-
what_score -= 10;
|
|
1790
|
-
const hasOutOfScope = text.includes("out of scope") || text.includes("not included");
|
|
1791
|
-
if (hasOutOfScope)
|
|
1792
|
-
what_score += 10;
|
|
1793
|
-
if (text.includes("edge cases"))
|
|
1794
|
-
what_score += 10;
|
|
1795
|
-
if (text.includes("error handling"))
|
|
1796
|
-
what_score += 5;
|
|
1797
|
-
const what_verdict = what_score >= 20 ? "CLEAR" : what_score >= 0 ? "ACCEPTABLE" : what_score >= -19 ? "VAGUE" : "UNDEFINED";
|
|
1798
|
-
const ac_count = Math.max(numberedACs, (description.match(/\b(ac-?\d+|given\b)/gi) || []).length);
|
|
1799
|
-
// PASS 4 - RISK CHECK
|
|
1800
|
-
let risk_score = 0;
|
|
1801
|
-
const high_risk_factors = [];
|
|
1802
|
-
const riskSignals = [
|
|
1803
|
-
["db migration", 20, "DB migration"], ["database migration", 20, "DB migration"],
|
|
1804
|
-
["drop column", 30, "Drop column"], ["rename column", 30, "Rename column"],
|
|
1805
|
-
["authentication change", 25, "Auth change"], ["auth change", 25, "Auth change"],
|
|
1806
|
-
["authorization change", 20, "Authz change"], ["permission change", 20, "Permission change"],
|
|
1807
|
-
["all tenants", 20, "Affects all tenants"], ["kafka schema", 25, "Kafka schema change"],
|
|
1808
|
-
["chrome extension", 15, "Chrome extension"], ["irreversible", 30, "Irreversible"],
|
|
1809
|
-
["data migration", 20, "Data migration"], ["api contract", 25, "API contract change"],
|
|
1810
|
-
["breaking change", 25, "Breaking change"], ["affects existing", 15, "Affects existing"],
|
|
1811
|
-
];
|
|
1812
|
-
for (const [kw, score, label] of riskSignals) {
|
|
1813
|
-
if (text.includes(kw)) {
|
|
1814
|
-
risk_score += score;
|
|
1815
|
-
if (!high_risk_factors.includes(label))
|
|
1816
|
-
high_risk_factors.push(label);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
// Regex-based: catch "drop the old X columns", "Drop legacy device columns", etc.
|
|
1820
|
-
// Use 'text' (title+desc combined) since title often has key verb, description has the noun.
|
|
1821
|
-
if (/\bdrop\b.{0,60}\bcolumns?/i.test(text) && !high_risk_factors.includes("Drop column")) {
|
|
1822
|
-
risk_score += 30;
|
|
1823
|
-
high_risk_factors.push("Drop column (pattern)");
|
|
1824
|
-
}
|
|
1825
|
-
if (/\bdelete\b.{0,30}\bcolumns?/i.test(text) && !high_risk_factors.includes("Drop column")) {
|
|
1826
|
-
risk_score += 30;
|
|
1827
|
-
high_risk_factors.push("Delete column (pattern)");
|
|
1828
|
-
}
|
|
1829
|
-
if (text.includes("delete") && (text.includes(" data") || text.includes("record"))) {
|
|
1830
|
-
risk_score += 25;
|
|
1831
|
-
high_risk_factors.push("Data deletion");
|
|
1832
|
-
}
|
|
1833
|
-
if (text.includes("new endpoint"))
|
|
1834
|
-
risk_score += 10;
|
|
1835
|
-
if (text.includes("new table"))
|
|
1836
|
-
risk_score += 10;
|
|
1837
|
-
if (text.includes("new kafka topic"))
|
|
1838
|
-
risk_score += 15;
|
|
1839
|
-
if (text.includes("new service"))
|
|
1840
|
-
risk_score += 20;
|
|
1841
|
-
if (text.includes("read only") || text.includes("display only"))
|
|
1842
|
-
risk_score -= 10;
|
|
1843
|
-
if (text.includes("no db change") || text.includes("no migration"))
|
|
1844
|
-
risk_score -= 15;
|
|
1845
|
-
if (text.includes("isolated change"))
|
|
1846
|
-
risk_score -= 10;
|
|
1847
|
-
if (text.includes("config change only"))
|
|
1848
|
-
risk_score -= 10;
|
|
1849
|
-
let rejected_pattern_risk = false;
|
|
1850
|
-
for (const rp of rejectedPatterns) {
|
|
1851
|
-
const rpKeywords = rp.keywords || (rp.pattern ? [rp.pattern] : []);
|
|
1852
|
-
for (const kw of rpKeywords) {
|
|
1853
|
-
if (kw && text.includes(kw.toLowerCase())) {
|
|
1854
|
-
rejected_pattern_risk = true;
|
|
1855
|
-
risk_score += 20;
|
|
1856
|
-
high_risk_factors.push(`Rejected pattern: ${rp.id || rp.name}`);
|
|
1857
|
-
break;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
// ── Apply DNA-aware risk bonus ──────────────────────────────────────
|
|
1862
|
-
// Keyword scoring is blind to actual architecture. DNA bonus reflects real blast radius.
|
|
1863
|
-
risk_score += Math.min(dna_risk_bonus, 50); // cap at 50 to prevent double-stacking
|
|
1864
|
-
for (const note of dnaRiskNotes) {
|
|
1865
|
-
if (!high_risk_factors.includes(note))
|
|
1866
|
-
high_risk_factors.push(`[DNA] ${note}`);
|
|
1867
|
-
}
|
|
1868
|
-
const risk_verdict = risk_score <= 10 ? "LOW" : risk_score <= 30 ? "MEDIUM" : risk_score <= 50 ? "HIGH" : "CRITICAL";
|
|
1869
|
-
const reversibility = risk_score <= 20 ? "REVERSIBLE" : risk_score <= 40 ? "REVERSIBLE_WITH_EFFORT" : "DIFFICULT_TO_REVERSE";
|
|
1870
|
-
// PASS 5 - PHILOSOPHY VERDICT
|
|
1871
|
-
let philosophy_verdict = "PHILOSOPHY_PASS";
|
|
1872
|
-
const blocking_questions = [];
|
|
1873
|
-
const advisory_notes = [];
|
|
1874
|
-
if ((who_verdict === "UNCONFIRMED" && risk_verdict === "CRITICAL") ||
|
|
1875
|
-
(who_verdict === "UNCONFIRMED" && risk_verdict === "HIGH" && reversibility === "DIFFICULT_TO_REVERSE") ||
|
|
1876
|
-
(why_verdict === "NO_JUSTIFICATION" && (task_type === "feature" || task_type === "user_story")) ||
|
|
1877
|
-
(rejected_pattern_risk && risk_verdict === "CRITICAL")) {
|
|
1878
|
-
philosophy_verdict = "PHILOSOPHY_BLOCKED";
|
|
1879
|
-
}
|
|
1880
|
-
else if (who_verdict === "UNCONFIRMED" ||
|
|
1881
|
-
why_verdict === "NO_JUSTIFICATION" ||
|
|
1882
|
-
what_verdict === "UNDEFINED" ||
|
|
1883
|
-
(risk_verdict === "CRITICAL" && reversibility === "DIFFICULT_TO_REVERSE")) {
|
|
1884
|
-
philosophy_verdict = "PHILOSOPHY_FAIL";
|
|
1885
|
-
}
|
|
1886
|
-
else if (who_verdict === "UNCERTAIN" || why_verdict === "WEAK" || what_verdict === "VAGUE" || risk_verdict === "HIGH") {
|
|
1887
|
-
philosophy_verdict = "PHILOSOPHY_ADVISORY";
|
|
1888
|
-
}
|
|
1889
|
-
const allowed_next_action = philosophy_verdict === "PHILOSOPHY_PASS" ? "PROCEED" :
|
|
1890
|
-
philosophy_verdict === "PHILOSOPHY_ADVISORY" ? "ADVISORY_PROCEED" :
|
|
1891
|
-
philosophy_verdict === "PHILOSOPHY_FAIL" ? "STOP" : "ESCALATE";
|
|
1892
|
-
if (why_verdict === "NO_JUSTIFICATION" || why_verdict === "WEAK")
|
|
1893
|
-
blocking_questions.push({ category: "WHY", question: `What specific problem does "${title}" solve?`, impact: "Without a clear problem statement, the solution may be wrong.", action: "Add a problem statement with data, user pain, or regulatory requirement." });
|
|
1894
|
-
if (who_verdict === "UNCONFIRMED" || who_verdict === "UNCERTAIN")
|
|
1895
|
-
blocking_questions.push({ category: "WHO", question: "Who has explicitly confirmed this is needed?", impact: "Building unconfirmed features wastes sprint capacity.", action: "Get written confirmation from client or PM." });
|
|
1896
|
-
if (what_verdict === "UNDEFINED" || what_verdict === "VAGUE")
|
|
1897
|
-
blocking_questions.push({ category: "WHAT", question: "What are the acceptance criteria?", impact: "Vague stories produce vague software.", action: "Add Given/When/Then ACs." });
|
|
1898
|
-
if (risk_verdict === "CRITICAL" && reversibility === "DIFFICULT_TO_REVERSE")
|
|
1899
|
-
blocking_questions.push({ category: "RISK", question: `CRITICAL risk: ${high_risk_factors.join(", ")}. What is the rollback plan?`, impact: "This change may be impossible to revert.", action: "Define rollback plan and get explicit sign-off." });
|
|
1900
|
-
if (who_verdict === "UNCERTAIN")
|
|
1901
|
-
advisory_notes.push({ category: "WHO", note: "Decision maker not fully confirmed", suggestion: "Confirm with client or PM." });
|
|
1902
|
-
if (why_verdict === "WEAK")
|
|
1903
|
-
advisory_notes.push({ category: "WHY", note: "Weak justification", suggestion: "Strengthen with data or regulatory context." });
|
|
1904
|
-
if (what_verdict === "VAGUE")
|
|
1905
|
-
advisory_notes.push({ category: "WHAT", note: "ACs incomplete or vague", suggestion: "Add measurable ACs before coding." });
|
|
1906
|
-
if (risk_verdict === "HIGH")
|
|
1907
|
-
advisory_notes.push({ category: "RISK", note: `High risk: ${high_risk_factors.join(", ") || "multiple signals"}`, suggestion: "Review blast radius. Consider staged rollout." });
|
|
1908
|
-
let developer_message = "";
|
|
1909
|
-
const scoresSummary = `\nScores | WHY:${why_score} WHO:${who_score} WHAT:${what_score} RISK:${risk_score}${dna_risk_bonus > 0 ? ` (DNA+${dna_risk_bonus})` : ""}${dnaWarning}`;
|
|
1910
|
-
if (philosophy_verdict === "PHILOSOPHY_PASS") {
|
|
1911
|
-
developer_message = `[PHILOSOPHY PASS] ${work_item_id}\n\nWHY : ${why_verdict}${purpose_text ? " - " + purpose_text : ""}\nWHO : ${who_verdict}\nWHAT : ${what_verdict} — ${ac_count} AC(s)\nRISK : ${risk_verdict} (${reversibility})${high_risk_factors.length > 0 ? "\nRisk factors: " + high_risk_factors.join(", ") : ""}\n\nProceed to classify_task.${scoresSummary}`;
|
|
1912
|
-
}
|
|
1913
|
-
else if (philosophy_verdict === "PHILOSOPHY_ADVISORY") {
|
|
1914
|
-
developer_message = `[PHILOSOPHY ADVISORY] ${work_item_id}\n\nStory may proceed with caution. Note these concerns:\n\n` +
|
|
1915
|
-
advisory_notes.map((n) => ` [${n.category}] ${n.note}\n Suggestion: ${n.suggestion}`).join("\n\n") +
|
|
1916
|
-
`\n\nWHY:${why_verdict} | WHO:${who_verdict} | WHAT:${what_verdict} | RISK:${risk_verdict}\nProceed to classify_task. Record advisory notes in decisions.json.${scoresSummary}`;
|
|
1917
|
-
}
|
|
1918
|
-
else if (philosophy_verdict === "PHILOSOPHY_FAIL") {
|
|
1919
|
-
developer_message = `[PHILOSOPHY FAIL] ${work_item_id}\n\nImplementation BLOCKED. The following must be resolved before planning can begin:\n\n` +
|
|
1920
|
-
blocking_questions.map((q) => ` [${q.category}] ${q.question}\n Why: ${q.impact}\n Action: ${q.action}`).join("\n\n") +
|
|
1921
|
-
`\n\nDo NOT call classify_task. Re-run philosophy_check after resolving.${scoresSummary}`;
|
|
1922
|
-
}
|
|
1923
|
-
else {
|
|
1924
|
-
developer_message = `[PHILOSOPHY BLOCKED] ${work_item_id}\n\nESCALATION REQUIRED. ${risk_verdict} risk + ${who_verdict} authority.\n\n` +
|
|
1925
|
-
blocking_questions.map((q) => ` [${q.category}] ${q.question}\n Risk: ${q.impact}\n Required: ${q.action}`).join("\n\n") +
|
|
1926
|
-
`\n\nNO implementation or planning until explicit written sign-off from the decision maker.${scoresSummary}`;
|
|
1927
|
-
}
|
|
1928
|
-
const result = {
|
|
1929
|
-
work_item_id, title, task_type, philosophy_verdict, allowed_next_action,
|
|
1930
|
-
dna_assessment: {
|
|
1931
|
-
dna_available: dnaAvailable,
|
|
1932
|
-
dna_risk_bonus,
|
|
1933
|
-
dna_risk_notes: dnaRiskNotes,
|
|
1934
|
-
},
|
|
1935
|
-
passes: {
|
|
1936
|
-
why: { score: why_score, verdict: why_verdict, purpose_stated, purpose_text },
|
|
1937
|
-
who: { score: who_score, verdict: who_verdict },
|
|
1938
|
-
what: { score: what_score, verdict: what_verdict, ac_count, has_given_when_then: hasGWT, has_out_of_scope: hasOutOfScope, word_count: wordCount },
|
|
1939
|
-
risk: { score: risk_score, verdict: risk_verdict, reversibility, rejected_pattern_risk, high_risk_factors }
|
|
1940
|
-
},
|
|
1941
|
-
blocking_questions, advisory_notes, developer_message, scored_at: new Date().toISOString()
|
|
1942
|
-
};
|
|
1943
|
-
try {
|
|
1944
|
-
writeFile(philosophyFile, JSON.stringify(result, null, 2));
|
|
1945
|
-
}
|
|
1946
|
-
catch { }
|
|
1592
|
+
let developer_message = `[PHILOSOPHY CONTEXT GATHERED] ${work_item_id}\n\n` +
|
|
1593
|
+
`INSTRUCTIONS FOR THE AGENT:\n` +
|
|
1594
|
+
`You are now acting as the Philosophy Check Engine defined in your AGENTS.md persona.\n\n` +
|
|
1595
|
+
`1. Review the following gathered Project DNA against the task (${task_type}): "${title}".\n` +
|
|
1596
|
+
`2. Determine if the business intent is clear, if the decision maker is explicit, and if the architectural impact is fully understood.\n` +
|
|
1597
|
+
`3. If clarification is needed, STOP. Do not proceed. Ask the user the hard questions directly (What problem is this solving? Who confirmed this? What breaks if we do not build it?).\n` +
|
|
1598
|
+
`4. If everything is clear and the architectural risk is acceptable, output your "System Impact Analysis" based on this DNA, and then call classify_task.\n\n` +
|
|
1599
|
+
`── PROJECT DNA CONTEXT ──\n` +
|
|
1600
|
+
`Services: ` + (dnaServices.length > 0 ? dnaServices.join(", ") : "None detected") + `\n` +
|
|
1601
|
+
`Entities: ` + (dnaEntities.length > 0 ? dnaEntities.join(", ") : "None detected") + `\n` +
|
|
1602
|
+
`Kafka Topics: ` + (dnaTopics.length > 0 ? dnaTopics.map((t) => t.name || t.topic).join(", ") : "None detected") + `\n` +
|
|
1603
|
+
`Extension: ` + (dnaExtension ? "Present" : "None detected") + `\n` +
|
|
1604
|
+
`Security: ` + (dnaSecurity ? JSON.stringify(dnaSecurity) : "None detected") + `\n` +
|
|
1605
|
+
`Rejected Patterns: ` + (rejectedPatterns.map(p => p.id || p.name).join(", ") || "None") + `\n\n` +
|
|
1606
|
+
`Do not run the old static scoring. Use your LLM reasoning.`;
|
|
1947
1607
|
return appendTelemetry({ content: [{ type: "text", text: developer_message }] }, inputChars);
|
|
1948
1608
|
});
|
|
1949
1609
|
// --- Tool 10: classify_task --------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "secufusion-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Project-level spec driven AI workflow tooling for SecuFusion, MCP server - developer workflow tooling with guardrails",
|
|
6
6
|
"main": "mcp/dist/server.js",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"@types/node": "^22.0.0",
|
|
46
46
|
"typescript": "^5.0.0"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|