secufusion-mcp 2.0.1 → 2.1.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/agents/planner.md +36 -7
- package/commands/sfn-plan.md +29 -6
- package/mcp/dist/server.js +194 -1
- package/package.json +1 -1
package/agents/planner.md
CHANGED
|
@@ -15,15 +15,23 @@ STEP 1 — ALWAYS, unconditionally:
|
|
|
15
15
|
|
|
16
16
|
STEP 2 — if you need more details about a specific service not returned by prime_session:
|
|
17
17
|
→ call manage_project_spec(action: "get_service", service_name: <that service>)
|
|
18
|
+
|
|
19
|
+
STEP 3 — ALWAYS, for any work item that has been initialized:
|
|
20
|
+
→ call spec_read_intent(work_item_id: <active_id>)
|
|
21
|
+
This loads the WHY layer: business goal, PM owner, ACs, risk, and the Polyglot
|
|
22
|
+
Service Map (which lists services in Go/Python/Rust/any language even without AST parsers).
|
|
23
|
+
If the intent file does not exist yet, skip silently — do NOT block on this.
|
|
18
24
|
```
|
|
19
25
|
|
|
20
26
|
After these calls complete, you now know:
|
|
21
|
-
- All service ports, repos, domains
|
|
22
|
-
- Table ownership per service
|
|
23
|
-
- All inter-service REST calls
|
|
24
|
-
- Kafka topics (produces/consumes per service)
|
|
25
|
-
- Keycloak config and auth flow
|
|
26
|
-
- Coding patterns and golden rules
|
|
27
|
+
- All service ports, repos, domains (from AST + DNA)
|
|
28
|
+
- Table ownership per service (from AST)
|
|
29
|
+
- All inter-service REST calls (from AST)
|
|
30
|
+
- Kafka topics (produces/consumes per service) (from AST)
|
|
31
|
+
- Keycloak config and auth flow (from DNA)
|
|
32
|
+
- Coding patterns and golden rules (from DNA)
|
|
33
|
+
- **Business goal and PM intent** (from intent file — the WHY layer)
|
|
34
|
+
- **Polyglot service map** — Go/Python/Rust/any service involved even if no AST parser exists
|
|
27
35
|
|
|
28
36
|
You are NOW allowed to reason about the task. Not before.
|
|
29
37
|
|
|
@@ -34,6 +42,8 @@ You are NOW allowed to reason about the task. Not before.
|
|
|
34
42
|
- Cannot validate task scope against architecture
|
|
35
43
|
- Cannot enforce golden rules during classification
|
|
36
44
|
- Will hallucinate service details from memory
|
|
45
|
+
- **Will miss polyglot services with no AST parser** (Go, Python, Rust, etc.)
|
|
46
|
+
- **Will lose PM/business context** when making architectural tradeoffs
|
|
37
47
|
|
|
38
48
|
There are NO circumstances under which Phase 00 can be skipped, abbreviated, or substituted.
|
|
39
49
|
|
|
@@ -48,7 +58,26 @@ There are NO circumstances under which Phase 00 can be skipped, abbreviated, or
|
|
|
48
58
|
❌ Write any code
|
|
49
59
|
❌ Proceed to Phase 0.5 or any other phase
|
|
50
60
|
|
|
51
|
-
The ONLY tool calls permitted before Phase 00 completes are `prime_session` and `
|
|
61
|
+
The ONLY tool calls permitted before Phase 00 completes are `prime_session`, `manage_project_spec`, and `spec_read_intent`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Phase 0.25 — Clarify and Capture Business Intent (The WHY Layer)
|
|
66
|
+
(TRIGGER: when a **NEW** task, feature, or bug is received that has no existing tracking)
|
|
67
|
+
|
|
68
|
+
Before thinking about *how* to implement the task, you must understand *why* it is needed.
|
|
69
|
+
1. **Analyze** the requested task.
|
|
70
|
+
2. **Ask** the user clarifying questions if the following are not completely clear:
|
|
71
|
+
- Why is this being built? What user or business problem does it solve?
|
|
72
|
+
- What is already there, and is this actually needed?
|
|
73
|
+
- What is the business risk if not delivered?
|
|
74
|
+
3. **YIELD** and wait for the user to clarify the business intent. Do not proceed until the intent is clear.
|
|
75
|
+
|
|
76
|
+
Once the intent is clarified:
|
|
77
|
+
1. Call `manage_task(action: "initialize", ...)` to initialize the task tracking (The HOW layer).
|
|
78
|
+
2. Call `spec_create_intent(...)` to capture the clarified business intent (The WHY layer).
|
|
79
|
+
|
|
80
|
+
Only after both of these are created may you proceed to Phase 0.5.
|
|
52
81
|
|
|
53
82
|
---
|
|
54
83
|
|
package/commands/sfn-plan.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Architect a solution for a task or Jira ticket using the loaded DNA.
|
|
2
|
+
description: Architect a solution for a task or Jira ticket using the loaded DNA. Clarifies business intent, creates a dedicated task workspace, classifies the work, and produces a strict implementation plan awaiting your approval.
|
|
3
3
|
argument-hint: <ticket-id or feature description> e.g. WI-123 or "add user login endpoint"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -15,21 +15,42 @@ Task / Ticket: **$ARGUMENTS**
|
|
|
15
15
|
2. If `DNA_LOADED` is `false` or `null`, **ABORT immediately** and tell the user:
|
|
16
16
|
> ❌ Project DNA is not loaded. Please run `/sfn:init` first.
|
|
17
17
|
|
|
18
|
-
## Phase 1 —
|
|
18
|
+
## Phase 1 — Clarify and Capture Business Intent (The WHY Layer)
|
|
19
19
|
|
|
20
|
+
Before thinking about *how* to implement the task, you must understand *why* it is needed.
|
|
21
|
+
1. Analyze the requested task.
|
|
22
|
+
2. Ask the user clarifying questions if the following are not completely clear:
|
|
23
|
+
- Why is this being built? What user or business problem does it solve?
|
|
24
|
+
- What is already there, and is this actually needed?
|
|
25
|
+
- What is the business risk if not delivered?
|
|
26
|
+
3. **YIELD** and wait for the user to clarify the business intent. Do not proceed to Phase 2 until the intent is clear.
|
|
27
|
+
|
|
28
|
+
Once the intent is clarified, call `spec_create_intent` with:
|
|
29
|
+
- `work_item_id`: the ticket ID or a slugified version of the description
|
|
30
|
+
- `title`: short human-readable title
|
|
31
|
+
- `business_goal`: The clarified business problem it solves.
|
|
32
|
+
- `acceptance_criteria`: Given/When/Then ACs gathered from the ticket/clarification.
|
|
33
|
+
- `services_involved`: **Polyglot service map** — list every service this touches, regardless of language (Java, Go, Python, TypeScript).
|
|
34
|
+
- `risk`: business risk if not delivered.
|
|
35
|
+
|
|
36
|
+
> 💡 This creates `.secufusion/intents/<num>-WI-<id>-<slug>.md` — a human-readable file capturing the WHY.
|
|
37
|
+
|
|
38
|
+
## Phase 2 — Initialize the Task Workspace (The HOW Layer)
|
|
39
|
+
|
|
40
|
+
Now that the *why* is captured, initialize the *how* tracking.
|
|
20
41
|
Call `manage_task` with:
|
|
21
42
|
```
|
|
22
43
|
action: "initialize"
|
|
23
|
-
work_item_id: <
|
|
44
|
+
work_item_id: <same ID as above>
|
|
24
45
|
```
|
|
25
46
|
This creates the dedicated workspace folder at `.secufusion/tasks/<id>/` and begins tracking progress. Do not proceed until this call succeeds.
|
|
26
47
|
|
|
27
|
-
## Phase
|
|
48
|
+
## Phase 3 — Load Context
|
|
28
49
|
|
|
29
50
|
1. Call `prime_session(work_item_id: <id>)` to efficiently load the project DNA state and relevant architectural context for this specific task.
|
|
30
51
|
2. Read the `.secufusion-project-spec.json` from the project root to understand the golden rules, constraints, and acceptance criteria boundaries for this project.
|
|
31
52
|
|
|
32
|
-
## Phase
|
|
53
|
+
## Phase 4 — Classify the Task
|
|
33
54
|
|
|
34
55
|
Call `classify_task` with the task description: **$ARGUMENTS**
|
|
35
56
|
|
|
@@ -39,7 +60,7 @@ This formally categorizes the work (Feature, Bug, Refactor, Security, Performanc
|
|
|
39
60
|
|
|
40
61
|
Wait for explicit approval before continuing.
|
|
41
62
|
|
|
42
|
-
## Phase
|
|
63
|
+
## Phase 5 — Write the Implementation Plan (After Approval)
|
|
43
64
|
|
|
44
65
|
Read `.agents/planner.md` for the Planner Persona instructions and produce a strict, step-by-step implementation plan that includes:
|
|
45
66
|
|
|
@@ -49,8 +70,10 @@ Read `.agents/planner.md` for the Planner Persona instructions and produce a str
|
|
|
49
70
|
- **Architectural Decisions** (with rationale grounded in the DNA)
|
|
50
71
|
- **Risk Flags** (tenant isolation, N+1 risks, missing indexes, Kafka sync calls)
|
|
51
72
|
- **Test Plan**
|
|
73
|
+
- **Polyglot Impact** — for each service in the intent's Service Map that has no AST parser, note it explicitly and list what metadata is required (ports, topics, endpoints)
|
|
52
74
|
|
|
53
75
|
Save the plan to `.secufusion/tasks/<id>/plan.md`.
|
|
54
76
|
|
|
55
77
|
Present the plan and ask:
|
|
56
78
|
> 📋 Plan ready. Review `plan.md` and approve to begin coding with `/sfn:code`.
|
|
79
|
+
|
package/mcp/dist/server.js
CHANGED
|
@@ -504,9 +504,202 @@ server.tool("manage_project_spec", "Manages the .secufusion-project-spec.json fi
|
|
|
504
504
|
}
|
|
505
505
|
return appendTelemetry({ content: [{ type: "text", text: "Invalid action." }] }, inputChars);
|
|
506
506
|
});
|
|
507
|
-
// ───
|
|
507
|
+
// ─── Business Intent Layer ────────────────────────────────────────────────────
|
|
508
|
+
// Bridges the Polyglot Flexibility & Business Intent gaps.
|
|
509
|
+
// Every work item gets BOTH:
|
|
510
|
+
// - A JSON task folder (.secufusion/tasks/WI-XXXX/) → code truth (AST-driven)
|
|
511
|
+
// - A Markdown intent file (.secufusion/intents/WI-XXXX.md) → why truth (human-driven)
|
|
512
|
+
// prime_session merges both into a single context block.
|
|
513
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
514
|
+
const INTENTS_DIR = ".secufusion/intents";
|
|
508
515
|
const TASKS_DIR = ".secufusion/tasks";
|
|
509
516
|
const REGISTRY_FILE = ".secufusion/registry.json";
|
|
517
|
+
function slugify(text) {
|
|
518
|
+
return text
|
|
519
|
+
.toLowerCase()
|
|
520
|
+
.replace(/[:\s]+/g, "-")
|
|
521
|
+
.replace(/[^a-z0-9-]+/g, "")
|
|
522
|
+
.replace(/-+/g, "-")
|
|
523
|
+
.replace(/^-|-$/g, "")
|
|
524
|
+
.slice(0, 60);
|
|
525
|
+
}
|
|
526
|
+
function nextIntentNumber() {
|
|
527
|
+
const intentsRoot = resolve(INTENTS_DIR);
|
|
528
|
+
if (!fs.existsSync(intentsRoot))
|
|
529
|
+
return "0001";
|
|
530
|
+
const files = fs.readdirSync(intentsRoot).filter(f => f.endsWith(".md"));
|
|
531
|
+
if (files.length === 0)
|
|
532
|
+
return "0001";
|
|
533
|
+
const nums = files
|
|
534
|
+
.map(f => parseInt(f.split("-")[0], 10))
|
|
535
|
+
.filter(n => !isNaN(n));
|
|
536
|
+
const max = nums.length > 0 ? Math.max(...nums) : 0;
|
|
537
|
+
return String(max + 1).padStart(4, "0");
|
|
538
|
+
}
|
|
539
|
+
function parseIntentFrontmatter(raw) {
|
|
540
|
+
const result = {};
|
|
541
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
542
|
+
if (!match)
|
|
543
|
+
return result;
|
|
544
|
+
for (const line of match[1].split("\n")) {
|
|
545
|
+
const [key, ...rest] = line.split(":");
|
|
546
|
+
if (key && rest.length)
|
|
547
|
+
result[key.trim()] = rest.join(":").trim();
|
|
548
|
+
}
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
// ─── Tool: spec_next_number ───────────────────────────────────────────────────
|
|
552
|
+
server.tool("spec_next_number", "Returns the next available 4-digit spec/intent number (e.g. 0031). Call before creating a new intent doc to ensure no collisions.", {}, async () => {
|
|
553
|
+
const inputChars = 0;
|
|
554
|
+
const next = nextIntentNumber();
|
|
555
|
+
return appendTelemetry({
|
|
556
|
+
content: [{ type: "text", text: JSON.stringify({ next_number: next, intents_dir: resolve(INTENTS_DIR) }, null, 2) }]
|
|
557
|
+
}, inputChars);
|
|
558
|
+
});
|
|
559
|
+
// ─── Tool: spec_create_intent ─────────────────────────────────────────────────
|
|
560
|
+
server.tool("spec_create_intent", "Creates a structured Markdown intent file in .secufusion/intents/ for a work item. This captures the WHY behind the code: business goal, PM sign-off, acceptance criteria, risk, and polyglot service map. Complements manage_task (which captures the HOW). Call immediately after manage_task action=initialize.", {
|
|
561
|
+
work_item_id: z.string().describe("Azure DevOps work item ID, e.g. '2847'."),
|
|
562
|
+
title: z.string().describe("Short feature/bug title."),
|
|
563
|
+
business_goal: z.string().describe("1–3 sentences: why is this being built? What user or business problem does it solve?"),
|
|
564
|
+
pm_owner: z.string().optional().describe("Product Manager name or alias responsible for this feature."),
|
|
565
|
+
ticket_url: z.string().optional().describe("Full URL to the Azure DevOps / Jira ticket."),
|
|
566
|
+
acceptance_criteria: z.array(z.string()).describe("Given/When/Then ACs. Each string is one AC."),
|
|
567
|
+
out_of_scope: z.array(z.string()).optional().describe("Things explicitly NOT covered by this work item."),
|
|
568
|
+
risk: z.string().optional().describe("Business risk if not delivered. e.g. 'SOC 2 audit failure', 'Revenue at risk', 'Low'."),
|
|
569
|
+
services_involved: z.array(z.object({
|
|
570
|
+
name: z.string().describe("Service name, e.g. 'notification-api'"),
|
|
571
|
+
language: z.string().describe("Primary language, e.g. 'TypeScript', 'Java', 'Go', 'Python'"),
|
|
572
|
+
role: z.string().describe("Role in this feature, e.g. 'produces Kafka event', 'REST consumer'")
|
|
573
|
+
})).optional().describe("Polyglot service map — lists every service involved regardless of language. This is the polyglot bridge."),
|
|
574
|
+
notes: z.string().optional().describe("Any additional context, constraints, or dependencies.")
|
|
575
|
+
}, async ({ work_item_id, title, business_goal, pm_owner, ticket_url, acceptance_criteria, out_of_scope, risk, services_involved, notes }) => {
|
|
576
|
+
const inputChars = JSON.stringify(arguments).length;
|
|
577
|
+
const num = nextIntentNumber();
|
|
578
|
+
const slug = slugify(title);
|
|
579
|
+
const filename = `${num}-WI-${work_item_id}-${slug}.md`;
|
|
580
|
+
const intentsRoot = resolve(INTENTS_DIR);
|
|
581
|
+
const filePath = path.join(intentsRoot, filename);
|
|
582
|
+
const acBlock = acceptance_criteria
|
|
583
|
+
.map((ac, i) => `- [ ] **AC${i + 1}:** ${ac}`)
|
|
584
|
+
.join("\n");
|
|
585
|
+
const oosBlock = (out_of_scope || []).length > 0
|
|
586
|
+
? (out_of_scope || []).map(s => `- ${s}`).join("\n")
|
|
587
|
+
: "_None specified._";
|
|
588
|
+
const servicesBlock = (services_involved || []).length > 0
|
|
589
|
+
? (services_involved || []).map(s => `| ${s.name} | ${s.language} | ${s.role} |`).join("\n")
|
|
590
|
+
: "| _(none specified)_ | — | — |";
|
|
591
|
+
const now = new Date().toISOString();
|
|
592
|
+
const content = `---
|
|
593
|
+
work_item_id: ${work_item_id}
|
|
594
|
+
title: ${title}
|
|
595
|
+
status: Draft
|
|
596
|
+
number: ${num}
|
|
597
|
+
pm_owner: ${pm_owner || "TBD"}
|
|
598
|
+
ticket_url: ${ticket_url || "TBD"}
|
|
599
|
+
risk: ${risk || "TBD"}
|
|
600
|
+
created_at: ${now}
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
# [WI-${work_item_id}] ${title}
|
|
604
|
+
|
|
605
|
+
## 🎯 Business Goal
|
|
606
|
+
|
|
607
|
+
${business_goal}
|
|
608
|
+
|
|
609
|
+
## 📋 Acceptance Criteria
|
|
610
|
+
|
|
611
|
+
${acBlock}
|
|
612
|
+
|
|
613
|
+
## 🚫 Out of Scope
|
|
614
|
+
|
|
615
|
+
${oosBlock}
|
|
616
|
+
|
|
617
|
+
## 🌐 Service Map (Polyglot Bridge)
|
|
618
|
+
|
|
619
|
+
| Service | Language | Role in this Feature |
|
|
620
|
+
|---------|----------|----------------------|
|
|
621
|
+
${servicesBlock}
|
|
622
|
+
|
|
623
|
+
## ⚠️ Risk
|
|
624
|
+
|
|
625
|
+
${risk || "_Not assessed._"}
|
|
626
|
+
|
|
627
|
+
## 📝 Notes
|
|
628
|
+
|
|
629
|
+
${notes || "_None._"}
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
_Intent file auto-generated by secufusion-mcp. Edit freely — this is the human layer._
|
|
633
|
+
`;
|
|
634
|
+
writeFile(filePath, content);
|
|
635
|
+
return appendTelemetry({
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `✅ Intent spec created.\n\n**File:** ${filePath}\n**Number:** ${num}\n**Slug:** ${slug}\n\nThis file captures the WHY. The JSON task folder (.secufusion/tasks/) captures the HOW.\nBoth will be merged by prime_session for a complete context.\n\n---\n${content}`
|
|
639
|
+
}]
|
|
640
|
+
}, inputChars);
|
|
641
|
+
});
|
|
642
|
+
// ─── Tool: spec_read_intent ───────────────────────────────────────────────────
|
|
643
|
+
server.tool("spec_read_intent", "Read the intent (WHY) file for a work item. Returns business goal, PM owner, acceptance criteria status, polyglot service map, and risk. Use alongside prime_session to get a complete picture of code truth + business intent.", {
|
|
644
|
+
work_item_id: z.string().describe("Work item ID to look up, e.g. '2847'."),
|
|
645
|
+
update_ac: z.object({
|
|
646
|
+
index: z.number().describe("0-based index of the AC to tick"),
|
|
647
|
+
checked: z.boolean().describe("true = mark as done (- [x]), false = uncheck (- [ ])")
|
|
648
|
+
}).optional().describe("Optionally tick or untick an acceptance criterion checkbox in the markdown.")
|
|
649
|
+
}, async ({ work_item_id, update_ac }) => {
|
|
650
|
+
const inputChars = JSON.stringify({ work_item_id, update_ac }).length;
|
|
651
|
+
const intentsRoot = resolve(INTENTS_DIR);
|
|
652
|
+
if (!fs.existsSync(intentsRoot)) {
|
|
653
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: `No intents directory found at ${intentsRoot}. Run spec_create_intent first.` }] }, inputChars);
|
|
654
|
+
}
|
|
655
|
+
const files = fs.readdirSync(intentsRoot).filter(f => f.endsWith(".md"));
|
|
656
|
+
const match = files.find(f => f.includes(`WI-${work_item_id}`));
|
|
657
|
+
if (!match) {
|
|
658
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: `No intent file found for WI-${work_item_id} in ${intentsRoot}.\nCreate one with spec_create_intent.` }] }, inputChars);
|
|
659
|
+
}
|
|
660
|
+
const filePath = path.join(intentsRoot, match);
|
|
661
|
+
let raw = fs.readFileSync(filePath, "utf-8");
|
|
662
|
+
if (update_ac !== undefined) {
|
|
663
|
+
const acLines = raw.split("\n");
|
|
664
|
+
let acCount = 0;
|
|
665
|
+
for (let i = 0; i < acLines.length; i++) {
|
|
666
|
+
if (acLines[i].match(/^- \[[ x]\] \*\*AC\d+/)) {
|
|
667
|
+
if (acCount === update_ac.index) {
|
|
668
|
+
acLines[i] = acLines[i].replace(/^- \[[ x]\]/, update_ac.checked ? "- [x]" : "- [ ]");
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
acCount++;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
raw = acLines.join("\n");
|
|
675
|
+
fs.writeFileSync(filePath, raw, "utf-8");
|
|
676
|
+
}
|
|
677
|
+
const frontmatter = parseIntentFrontmatter(raw);
|
|
678
|
+
const acMatches = [...raw.matchAll(/^- \[([ x])\] \*\*AC(\d+):\*\* (.+)$/gm)];
|
|
679
|
+
const acs = acMatches.map(m => ({
|
|
680
|
+
index: parseInt(m[2], 10) - 1,
|
|
681
|
+
done: m[1] === "x",
|
|
682
|
+
text: m[3]
|
|
683
|
+
}));
|
|
684
|
+
const done = acs.filter(a => a.done).length;
|
|
685
|
+
const summary = {
|
|
686
|
+
work_item_id,
|
|
687
|
+
file: match,
|
|
688
|
+
status: frontmatter.status || "Draft",
|
|
689
|
+
title: frontmatter.title || "",
|
|
690
|
+
pm_owner: frontmatter.pm_owner || "TBD",
|
|
691
|
+
risk: frontmatter.risk || "TBD",
|
|
692
|
+
ticket_url: frontmatter.ticket_url || "",
|
|
693
|
+
ac_progress: `${done}/${acs.length} complete`,
|
|
694
|
+
acceptance_criteria: acs,
|
|
695
|
+
};
|
|
696
|
+
return appendTelemetry({
|
|
697
|
+
content: [{
|
|
698
|
+
type: "text",
|
|
699
|
+
text: `## Intent: WI-${work_item_id}\n\n\`\`\`json\n${JSON.stringify(summary, null, 2)}\n\`\`\`\n\n---\n### Full Intent Document\n\n${raw}`
|
|
700
|
+
}]
|
|
701
|
+
}, inputChars);
|
|
702
|
+
});
|
|
510
703
|
function resolveTaskFolder(workItemId, tasksRoot, registryData) {
|
|
511
704
|
const registry = registryData || readRegistry();
|
|
512
705
|
const entry = registry.find(t => t["work_item_id"] === workItemId);
|