secufusion-mcp 1.0.10 → 1.0.12

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 (3) hide show
  1. package/README.md +244 -72
  2. package/index.js +386 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SecuFusion MCP Server
2
2
 
3
- > **Developer workflow tooling for the SecuFusion platform** — enforces zero-trust architecture standards, tracks task specs, and gates PRs with automated guardrail checks.
3
+ > **Project-level spec driven AI workflow tooling for the SecuFusion platform** — loads full project DNA at session start, enforces zero-trust architecture standards, tracks task specs, and gates PRs with automated guardrail checks.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/secufusion-mcp)](https://www.npmjs.com/package/secufusion-mcp)
6
6
  [![license](https://img.shields.io/npm/l/secufusion-mcp)](./LICENSE)
@@ -10,13 +10,58 @@
10
10
 
11
11
  ## What is this?
12
12
 
13
- `secufusion-mcp` is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that plugs into AI coding assistants (Claude Desktop, Cursor, Cline, etc.) and gives them three powerful tools to enforce SecuFusion's engineering standards throughout the development lifecycle:
13
+ `secufusion-mcp` is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that plugs into AI coding assistants (Claude Desktop, Cursor, Cline, etc.) and gives them **eight powerful tools** to enforce SecuFusion's engineering standards throughout the development lifecycle:
14
14
 
15
15
  | Tool | Phase | What it does |
16
16
  |---|---|---|
17
- | `manage_branch_state` | Planning & Execution | Manages a `.secufusion-state.json` tracker tied to the active Git branch |
18
- | `log_rejected_pattern` | Course Correction | Records bad patterns to `.rejected-patterns.json` so they are never repeated |
19
- | `run_pre_pr_checks` | PR Handoff | Discovers modified microservices across the workspace and gates PRs via AST-level linters + structural checks |
17
+ | `manage_project_spec` | **Phase 00** — Session Start | Loads `.secufusion-project-spec.json` — full project DNA (ports, repos, domains, coding patterns, golden rules) |
18
+ | `manage_task` | **Phase 0.7 / 1 / 2 / 4** — Task Lifecycle | Creates `.secufusion/tasks/WI-{id}/` folder with spec, progress, decisions, files-touched, scenarios, and PR summary |
19
+ | `get_task_history` | **Phase 0 / 1** — Cross-Task Intelligence | Retrieves the full history of a past task before starting similar work |
20
+ | `search_tasks` | **Phase 0 / 1** — Cross-Task Intelligence | Keyword search across all past task files — prevents re-solving solved problems |
21
+ | `get_pattern_from_task` | **Phase 1** — Cross-Task Intelligence | Extracts reusable decisions, file patterns, and test scenarios from a completed task |
22
+ | `manage_branch_state` | Legacy — Branch State | Backward-compatible branch-scoped JSON state tracker (for tasks before `manage_task`) |
23
+ | `log_rejected_pattern` | **Phase 3** — Course Correction | Records bad patterns to `.rejected-patterns.json` so they are never repeated |
24
+ | `run_pre_pr_checks` | **Phase 4** — PR Handoff | Discovers modified microservices and gates PRs via AST-level linters + structural checks |
25
+
26
+ ---
27
+
28
+ ## ⚡ The Shift: Problem-Statement Driven → Project-Level Spec Driven
29
+
30
+ This is the biggest architectural upgrade to the SecuFusion MCP.
31
+
32
+ ### Before (problem-statement driven)
33
+ The AI started every session **cold**. It had zero knowledge of the codebase and relied entirely on the developer feeding context through a work item description. Every session began with implicit questions:
34
+ - *"Which port does sfn-iam-api run on?"*
35
+ - *"How do you extract tenantId from the JWT?"*
36
+ - *"What's the coding pattern for DTO mapping?"*
37
+
38
+ The AI was **reactive** — it knew only what you told it about the current task.
39
+
40
+ ### After (project-level spec driven)
41
+ The AI starts every session by reading `.secufusion-project-spec.json` — a single file containing the **entire project's DNA**:
42
+
43
+ ```
44
+ ✅ All microservice ports, repos, Eureka names, domains
45
+ ✅ Table ownership per service
46
+ ✅ Inter-service call graph
47
+ ✅ Kafka topics produced/consumed per service
48
+ ✅ Keycloak realm + client config per service
49
+ ✅ Coding patterns (DTO mapping, @Transactional style, tenant passing)
50
+ ✅ Golden rules (tenant isolation layers, authority rules, banned patterns)
51
+ ✅ Flyway migration state per service
52
+ ```
53
+
54
+ The AI is now **proactively context-aware** — it knows your entire architecture before you say a single word about the task:
55
+
56
+ | Before | After |
57
+ |---|---|
58
+ | You explain the service every session | AI already knows all services |
59
+ | You describe the coding pattern | AI reads it from the spec |
60
+ | AI asks which port to use | AI looks it up from the spec |
61
+ | Context resets between sessions | Project knowledge is permanent |
62
+ | Spec is task-scoped | Spec is project-scoped |
63
+
64
+ > **One-time setup:** Generate `.secufusion-project-spec.json` once using the extraction prompt. From that point, every AI session starts fully informed.
20
65
 
21
66
  ---
22
67
 
@@ -246,6 +291,60 @@ The AI will call:
246
291
 
247
292
  ---
248
293
 
294
+ ### 4. `manage_project_spec`
295
+
296
+ Loads and manages `.secufusion-project-spec.json` — the permanent project memory file. **Called automatically at the start of every session (Phase 00)** before any task begins.
297
+
298
+ **Parameters:**
299
+
300
+ | Parameter | Type | Required | Description |
301
+ |---|---|---|---|
302
+ | `action` | enum | Yes | `read` \| `get_service` \| `get_golden_rules` \| `get_coding_patterns` \| `update` |
303
+ | `service_name` | string | No | **(For `get_service`)** Name of the microservice, e.g. `sfn-events-api` |
304
+ | `update_path` | string | No | **(For `update`)** Dot-notation path, e.g. `microservices.sfn-iam-api.port` |
305
+ | `update_value` | any | No | **(For `update`)** New value to set at `update_path` |
306
+
307
+ **Actions:**
308
+
309
+ | Action | Returns | Token cost |
310
+ |---|---|---|
311
+ | `read` | Full spec (all services, all patterns, all rules) | High — use once at session start |
312
+ | `get_service` | Only the block for the requested microservice | Low — use when focused on one service |
313
+ | `get_golden_rules` | `golden_rules` block + `rejected_patterns` | Low — use before any architectural decision |
314
+ | `get_coding_patterns` | `coding_patterns` block | Low — use before writing any new class |
315
+ | `update` | Confirmation of surgical dot-notation write | Low — never overwrites the full file |
316
+
317
+ **Example — Session start (reads full project context):**
318
+ ```json
319
+ { "action": "read" }
320
+ ```
321
+
322
+ **Example — Focused lookup before writing a service:**
323
+ ```json
324
+ { "action": "get_service", "service_name": "sfn-iam-api" }
325
+ ```
326
+
327
+ **Example — Check golden rules before an architectural decision:**
328
+ ```json
329
+ { "action": "get_golden_rules" }
330
+ ```
331
+
332
+ **Example — Surgical port update (never overwrites the full file):**
333
+ ```json
334
+ {
335
+ "action": "update",
336
+ "update_path": "microservices.sfn-iam-api.port",
337
+ "update_value": 9005
338
+ }
339
+ ```
340
+
341
+ **File location resolution order:**
342
+ 1. Same directory as `index.js`
343
+ 2. `process.cwd()`
344
+ 3. Walk up from `cwd` (up to 5 levels)
345
+
346
+ ---
347
+
249
348
  ## Guardrails Summary
250
349
 
251
350
  These rules are enforced automatically — the AI will never violate them:
@@ -259,38 +358,87 @@ These rules are enforced automatically — the AI will never violate them:
259
358
  ✅ Token Efficiency: Every tool response includes a 📊 Telemetry receipt tracking input/output tokens, cost, and the Session Total
260
359
  ```
261
360
 
361
+ **Performance Guardrails** (enforced whenever writing queries, Kafka consumers, or cross-service calls):
362
+ ```
363
+ ✅ All GET service methods use @Transactional(readOnly = true)
364
+ ✅ No repository method called inside a for/forEach loop — batch with findAllById/saveAll
365
+ ✅ Every new query column that is filtered/sorted has a CREATE INDEX in the Flyway migration
366
+ ✅ Kafka listeners never do synchronous DB writes or REST calls — offload to @Async
367
+ ✅ Every RestTemplate/WebClient call has an explicit timeout and fallback
368
+ ```
369
+
370
+ **Rollback Guardrails** (enforced on every Flyway migration, API contract change, Kafka schema change):
371
+ ```
372
+ ✅ SAFE migration (additive) — safe to roll back by reverting code
373
+ ⚠️ RISKY migration (NOT NULL without DEFAULT) — requires compensating migration
374
+ 🚫 DANGEROUS migration (DROP/RENAME) — requires explicit developer confirmation before writing
375
+ ✅ API hard cutover (removing /v1/ or a field) — requires confirmation; prefer /v2/ + deprecation first
376
+ ✅ Kafka schema change — coordinated deployment of producer + all consumers; flagged in rollback plan
377
+ ✅ Every manage_task initialize logs a rollback strategy starter entry in decisions.json
378
+ ```
379
+
380
+ **Breaking Change Detection** (run as Step 4 in Phase 0.5 before any plan is written):
381
+ ```
382
+ ✅ Check 1: Endpoint consumers — who calls this endpoint? Flag if response shape changes
383
+ ✅ Check 2: Entity/table consumers — @Query annotations across all repos for this column
384
+ ✅ Check 3: Kafka topic consumers — coordinated deployment required if schema changes
385
+ ✅ Check 4: Chrome extension — silent breaks invisible until users report them
386
+ ✅ Over-flag > under-flag: always present a breaking change report if in doubt
387
+ ```
388
+
262
389
  ---
263
390
 
264
391
  ## File Outputs
265
392
 
266
- | File | Description |
267
- |---|---|
268
- | `.secufusion-state.json` | Living task blueprint — tracks pending/completed ACs per branch |
269
- | `.rejected-patterns.json` | Cumulative log of all rejected patterns across sessions |
270
- | `.secufusion-tokens.json` | Persistent tracking of session-wide LLM token usage and cost |
271
-
272
- > **Tip:** Commit `.secufusion-state.json` and `.rejected-patterns.json` to your repo. Do **not** commit `.secufusion-tokens.json`.
393
+ | File | Description | Commit? |
394
+ |---|---|---|
395
+ | `.secufusion-project-spec.json` | **Project DNA** — all services, ports, patterns, golden rules. Generated once, read every session | ✅ Yes |
396
+ | `.secufusion/registry.json` | Index of all work items ever initialized — used by `search_tasks` | ✅ Yes |
397
+ | `.secufusion/tasks/WI-{id}/spec.json` | Task spec — title, description, ACs, tags, status | ✅ Yes |
398
+ | `.secufusion/tasks/WI-{id}/progress.json` | AC tracking — pending, completed, next_step | ✅ Yes |
399
+ | `.secufusion/tasks/WI-{id}/decisions.json` | Architectural decisions log (including rollback strategy) | ✅ Yes |
400
+ | `.secufusion/tasks/WI-{id}/files-touched.json` | All files modified with change summaries | ✅ Yes |
401
+ | `.secufusion/tasks/WI-{id}/scenarios.json` | Test scenarios (unit / integration / e2e / manual) | ✅ Yes |
402
+ | `.secufusion/tasks/WI-{id}/pr-summary.md` | Auto-generated PR summary on `manage_task complete` | ✅ Yes |
403
+ | `.secufusion-state.json` | **Legacy** branch-scoped state — still works via `manage_branch_state` | ✅ Yes |
404
+ | `.rejected-patterns.json` | Cumulative log of all rejected patterns across sessions | ✅ Yes |
405
+ | `.secufusion-tokens.json` | Persistent tracking of session-wide LLM token usage and cost | ❌ No |
406
+
407
+ > **Tip:** Commit the entire `.secufusion/` folder and `.rejected-patterns.json` to your repo. Do **not** commit `.secufusion-tokens.json`.
273
408
 
274
409
  ---
275
410
 
276
411
  ## Workflow Overview
277
412
 
278
413
  ```
279
- ┌─────────────────────────────────────────────────────┐
280
- │ SecuFusion MCP Workflow │
281
- ├──────────────┬──────────────────────────────────────┤
282
- │ Phase 1 │ manage_branch_state (action=init) │
283
- │ Planning │ → Initializes .secufusion-state.json │
284
- ├──────────────┼──────────────────────────────────────┤
285
- │ Phase 2 │ manage_branch_state (action=update) │
286
- │ Execution │ → Resolve ACs & define next_step │
287
- ├──────────────┼──────────────────────────────────────┤
288
- │ Phase 3 │ log_rejected_pattern │
289
- │ Correction │ → Record mistakes to avoid repeat │
290
- ├──────────────┼──────────────────────────────────────┤
291
- │ Phase 4 │ run_pre_pr_checks │
292
- │ PR Handoff │ → Must pass before raising PR │
293
- └──────────────┴──────────────────────────────────────┘
414
+ ┌──────────────┬──────────────────────────────────────────────────────────────┐
415
+ │ Phase 00 │ manage_project_spec (action=read) │
416
+ │ Session │ → Loads full project DNA from .secufusion-project-spec.json │
417
+ │ Start │ → AI knows ALL services, ports, patterns, golden rules │
418
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
419
+ │ Phase 0 │ manage_task (action=read_summary) OR search_tasks │
420
+ │ Resume │ → Token-efficient status view → resume next_step instantly │
421
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
422
+ │ Phase 0.5 │ CLASSIFY: BACKEND_ONLY / FRONTEND_ONLY / FULL_STACK │
423
+ │ Ownership │ Step 3: Performance risk scan (GREEN/AMBER/RED verdict) │
424
+ │ Check │ Step 4: Breaking change scan (endpoints/entities/Kafka/ext) │
425
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
426
+ │ Phase 0.7 │ Present full plan (scope, files, ACs, perf, rollback) │
427
+ │ Plan Gate │ → STOP and wait for "proceed" / "adjust" / "cancel" │
428
+ │ │ → manage_task (action=initialize) only after proceed │
429
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
430
+ │ Phase 1 │ search_tasks → get_task_history → manage_task initialize │
431
+ │ Planning │ → Creates .secufusion/tasks/WI-{id}/ with all 5 files │
432
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
433
+ │ Phase 2 │ manage_task: update_spec / log_file_touched / │
434
+ │ Execution │ log_decision / add_scenario │
435
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
436
+ │ Phase 3 │ log_rejected_pattern │
437
+ │ Correction │ → Record mistakes to avoid repeat │
438
+ ├──────────────┼──────────────────────────────────────────────────────────────┤
439
+ │ Phase 4 │ manage_task (action=complete) → pr-summary.md generated │
440
+ │ PR Handoff │ run_pre_pr_checks → must pass before raising PR │
441
+ └──────────────┴──────────────────────────────────────────────────────────────┘
294
442
  ```
295
443
 
296
444
  ---
@@ -337,22 +485,38 @@ Add this to your MCP client config and restart. That's it.
337
485
 
338
486
  ### Step 2 — Start a new task (Planning phase)
339
487
 
340
- Just paste your Azure DevOps work item ID and description directly into the chat. No special syntax needed.
488
+ Just paste your Azure DevOps work item ID and description directly into the chat. A good problem statement includes the raw user story and acceptance criteria. No special syntax needed.
341
489
 
342
490
  **You type:**
343
491
  ```
344
- WI-2847: Add MFA enforcement for admin users on login.
345
- Admin users must be forced through TOTP verification before
346
- accessing any dashboard route. Exempt service accounts.
492
+ WI-2847: Add MFA enforcement for admin users on login
493
+
494
+ **User Story:**
495
+ As a security administrator,
496
+ I want to enforce TOTP-based Multi-Factor Authentication (MFA) for all users with the ADMIN role,
497
+ So that sensitive dashboard actions are protected against compromised credentials.
498
+
499
+ **Acceptance Criteria:**
500
+ 1. Any user with the ADMIN role attempting to log in must be prompted for a TOTP token after password verification.
501
+ 2. The system must validate the TOTP token against the user's registered MFA device.
502
+ 3. Service accounts (role = SERVICE_ACCOUNT) are exempt from MFA, even if they possess admin privileges.
503
+ 4. If MFA fails 3 times, the account should be temporarily locked for 15 minutes.
347
504
  ```
348
505
 
349
- **The AI automatically calls `manage_branch_state` and creates/initializes `.secufusion-state.json`:**
506
+ **The AI automatically classifies the task, presents a plan, and upon approval, calls `manage_task` to initialize `.secufusion/tasks/WI-2847/`:**
350
507
  ```json
351
508
  {
352
- "task": "WI-2847: Add MFA enforcement",
353
- "pending_acs": ["AC-1: Auth flow", "AC-2: Redirects", "AC-3: Exemptions"],
354
- "completed_acs": [],
355
- "next_step": "Implement MFA logic in AuthController"
509
+ "action": "initialize",
510
+ "work_item_id": "2847",
511
+ "title": "Add MFA enforcement for admin users on login",
512
+ "description": "Enforce TOTP-based MFA for ADMIN roles, exempting service accounts, with account lockout on 3 failed attempts.",
513
+ "acceptance_criteria": [
514
+ "Prompt ADMIN users for TOTP token post-password",
515
+ "Validate TOTP token",
516
+ "Exempt SERVICE_ACCOUNT role",
517
+ "Lock account for 15m after 3 failed attempts"
518
+ ],
519
+ "tags": ["security", "auth", "mfa"]
356
520
  }
357
521
  ```
358
522
 
@@ -379,17 +543,19 @@ I've finished the tenantId scoping on all queries and written the unit tests.
379
543
  Update the state.
380
544
  ```
381
545
 
382
- The AI calls `manage_branch_state` with `action=update` and the state updates:
546
+ The AI calls `manage_task` with `action=update_spec` and the state updates:
383
547
 
384
548
  ```json
385
549
  {
386
- "task": "WI-2847: Add MFA enforcement",
387
- "pending_acs": ["AC-3: Exemptions"],
388
- "completed_acs": ["Add tenantId scoping", "Add unit tests"],
389
- "next_step": "Implement exemption logic for service accounts"
550
+ "action": "update_spec",
551
+ "work_item_id": "2847",
552
+ "pending_acs": ["Exempt SERVICE_ACCOUNT role", "Lock account for 15m after 3 failed attempts"],
553
+ "completed_acs": ["Prompt ADMIN users for TOTP token post-password", "Validate TOTP token"],
554
+ "next_step": "Implement exemption logic for service accounts in AuthController"
390
555
  }
391
556
  ```
392
557
 
558
+
393
559
  ---
394
560
 
395
561
  ### Step 5 — Course correction (if the AI does something wrong)
@@ -444,46 +610,42 @@ Scanned directory: C:\projects\secufusion-backend
444
610
 
445
611
  ---
446
612
 
447
- ## How It Works (The 2 Layers)
448
-
449
- To prevent AI amnesia or generic bad habits across your projects, the SecuFusion MCP relies on structured branch-aware JSON and native AST tooling.
613
+ ## How It Works (The 3 Layers)
450
614
 
451
- ## Phase 0 — Resume (TRIGGER: new session, switching branches, or user says "resume" or "continue")
452
- - Call `manage_branch_state` with `action: "read"`. The tool will automatically detect the current Git branch and return the structured JSON state for that specific branch. Do not re-read requirements; immediately begin executing the item explicitly listed in the `next_step` field.
615
+ The SecuFusion MCP operates across three complementary layers to prevent AI amnesia, enforce architecture standards, and gate code quality:
453
616
 
454
- ## Phase 1 — Planning (TRIGGER: user assigns a task or work item)
455
- - Call `manage_branch_state` with `action: "initialize"`. Pass the `task_description` and `reference_file_path`. The tool will generate a branch-bound JSON state file tracking the Acceptance Criteria (ACs) as a structured array of pending tasks, keyed to your active Git branch.
617
+ **Layer 1 — Project Spec (permanent, project-scoped):** `.secufusion-project-spec.json` — loaded once per session via `manage_project_spec`. The AI never needs to be told what port a service runs on, what pattern to use for DTO mapping, or how tenantId flows through the system.
456
618
 
457
- ## Phase 2 — Execution (TRIGGER: as you complete ACs, or before ending a session/response)
458
- - Call `manage_branch_state` with `action: "update"`. Move completed items from `pending_acs` to `completed_acs`.
459
- - CRITICAL: You must explicitly write a clear, actionable instruction into the `next_step` parameter (e.g., "Implement browserUsage map in DeviceActivitySummaryDTO"). This ensures your future self can resume instantly without parsing the full spec.
619
+ **Layer 2 — Task Memory (persistent, work-item-scoped):** `.secufusion/tasks/WI-{id}/` — one folder per work item, initialized via `manage_task`. Tracks spec, progress, decisions, files touched, scenarios, and generates a `pr-summary.md` on completion. Cross-task intelligence via `search_tasks`, `get_task_history`, and `get_pattern_from_task` prevents re-solving solved problems.
460
620
 
461
- ## Phase 3 — Course Correction (TRIGGER: user corrects you or rejects an approach)
462
- - Immediately call `log_rejected_pattern`. Pass the bad `pattern` and the `reason`. Always check `.rejected-patterns.json` implicitly before suggesting architectural choices.
463
-
464
- ## Phase 4 — PR Handoff (TRIGGER: user says "prepare PR" or "run checks")
465
- - Call `run_pre_pr_checks` with the Azure DevOps `workItemId`. The tool checks the structured branch state to ensure `pending_acs` is empty, then discovers modified microservices to run native linters, Flyway checks, and the Tenant-Isolation scanner.
466
- - If an error is thrown, YOU MUST navigate to that microservice, FIX THE ERROR natively, and rerun until the workspace passes.
467
-
468
- ## Guardrails (enforce always, no exceptions)
469
- - TENANT SAFETY IS AUTOMATED: The PR Gatekeeper actively scans all `*Repository.java` files. If you write a database query (derived method or `@Query`) that does not explicitly filter by `tenantId` or contain the word `tenant`, the PR will be blocked. Write secure, tenant-isolated queries on your first attempt.
470
- - Do not ignore linter errors. AST-level tools (ESLint, Checkstyle/Maven) are the source of truth for hygiene. Fix them natively.
471
- - Never hardcode UAT/Prod IPs or URLs. Use environment variables or configuration properties.
472
- - If you modify a JPA `@Entity` in any backend repo, you MUST create the corresponding Flyway `.sql` migration script before running PR checks.
473
- ```
621
+ **Layer 3 — AST Guardrails (automated, quality-gating):** ESLint, Maven Checkstyle, tenant isolation scanner, Flyway checker, performance rules, rollback classification, and breaking change detection run at PR time. Cannot be bypassed without explicit `skip_checks`.
474
622
 
475
623
  ### How it all wires together
476
624
 
477
625
  ```
478
- mcp_config.json → starts the server (tools available)
626
+ mcp_config.json → starts the server (8 tools available)
479
627
  +
480
- .agents/AGENTS.md → tells AI when to invoke each tool (the invoker)
628
+ .agents/AGENTS.md → tells AI when to invoke each tool
629
+ ↓
630
+ ★ SESSION STARTS
631
+ ↓
632
+ Phase 00: manage_project_spec → AI loads full project DNA
633
+ ↓
634
+ Phase 0: manage_task read_summary → resumes active work item
481
635
  ↓
482
636
  You say: "WI-1042: Add audit log export"
483
637
  ↓
484
- AI reads AGENTS.md → Phase 1 triggered → calls manage_branch_state
638
+ Phase 0.5: classify BACKEND_ONLY/FRONTEND_ONLY/FULL_STACK
639
+ + performance risk scan + breaking change scan
640
+ ↓
641
+ Phase 0.7: plan presented → developer approves ("proceed")
642
+ ↓
643
+ Phase 1: manage_task initialize → .secufusion/tasks/WI-1042/ created
644
+ ↓
645
+ Phase 2: code + log_file_touched + log_decision + add_scenario
485
646
  ↓
486
- .secufusion-state.json initialized for your Git branch
647
+ Phase 4: manage_task complete → pr-summary.md generated
648
+ + run_pre_pr_checks → must pass before raising PR
487
649
  ```
488
650
 
489
651
  ### Reusing across projects
@@ -502,9 +664,9 @@ mcp_config.json ← global, never changes
502
664
 
503
665
  ---
504
666
 
505
- ## Talking to the AI — 5 Things You'll Ever Say
667
+ ## Talking to the AI — What You'll Ever Say
506
668
 
507
- Once both layers are in place, you interact completely naturally:
669
+ Once all three layers are in place, you interact completely naturally:
508
670
 
509
671
  | Situation | What you say |
510
672
  |---|---|
@@ -513,10 +675,20 @@ Once both layers are in place, you interact completely naturally:
513
675
  | ❌ AI did something wrong | `Don't do X, do Y instead` |
514
676
  | 🚀 Ready for PR | `Run checks for WI-XXXX` or `Prepare PR` |
515
677
  | 🔄 Resuming after a break | `What's left?` or `Resume the current task` |
678
+ | 🔍 Starting similar work | `Has this been done before?` — AI calls `search_tasks` |
679
+ | 📋 Want the full plan first | AI automatically presents plan in Phase 0.7 — type `proceed` to start |
680
+
681
+ **Before `manage_project_spec`:** The AI started cold every session. You explained ports, coding patterns, and tenantId flow every single time.
682
+
683
+ **After `manage_project_spec`:** The AI reads `.secufusion-project-spec.json` at session start and **already knows your entire architecture**. You just describe the work.
684
+
685
+ **Before `manage_task`:** The AI used a flat branch-state file with no cross-task memory.
686
+
687
+ **After `manage_task`:** Every work item has its own structured folder. The AI tracks decisions, files, and scenarios per task. `search_tasks` finds related past work. `get_pattern_from_task` reuses proven approaches.
516
688
 
517
- **Before `AGENTS.md`:** You had to remember which tool to invoke and when.
689
+ **Before Phase 0.5 + 0.7:** The AI started coding immediately with no ownership check or explicit plan.
518
690
 
519
- **After `AGENTS.md`:** You just describe work. The AI follows the 5-phase workflow automatically — no commands, no syntax, no manual tool calls.
691
+ **After Phase 0.5 + 0.7:** The AI classifies the task (backend/frontend/full-stack), runs a performance risk and breaking change scan, presents a complete plan with rollback strategy, and **waits for your approval before writing a single line of code**.
520
692
 
521
693
  ---
522
694
 
package/index.js CHANGED
@@ -677,6 +677,392 @@ server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks acr
677
677
  isError: hasErrors,
678
678
  }, inputChars);
679
679
  });
680
+ // ─── Tool 4: manage_project_spec ─────────────────────────────────────────────
681
+ 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.", {
682
+ action: z
683
+ .enum(["read", "update", "get_service", "get_golden_rules", "get_coding_patterns"])
684
+ .describe("Action to perform: read (full spec), get_service (one microservice block), get_golden_rules (golden_rules + rejected_patterns), get_coding_patterns (coding_patterns block), update (surgical dot-notation write)."),
685
+ service_name: z
686
+ .string()
687
+ .optional()
688
+ .describe("Only for action=get_service. Name of the microservice to fetch, e.g. 'sfn-events-api'."),
689
+ update_path: z
690
+ .string()
691
+ .optional()
692
+ .describe("Only for action=update. Dot-notation path to the value to set, e.g. 'microservices.sfn-iam-api.port'."),
693
+ update_value: z
694
+ .any()
695
+ .optional()
696
+ .describe("Only for action=update. The new value to set at update_path."),
697
+ }, async ({ action, service_name, update_path, update_value }) => {
698
+ const inputChars = JSON.stringify({ action, service_name, update_path, update_value }).length;
699
+ // ── Locate .secufusion-project-spec.json (3 candidate locations) ─────────
700
+ const SPEC_FILE = ".secufusion-project-spec.json";
701
+ const candidates = [];
702
+ // 1. Same directory as index.ts/index.js
703
+ candidates.push(path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), SPEC_FILE));
704
+ // 2. process.cwd()
705
+ candidates.push(path.resolve(process.cwd(), SPEC_FILE));
706
+ // 3. Walk up from cwd until found (up to 5 levels)
707
+ let walkDir = process.cwd();
708
+ for (let i = 0; i < 5; i++) {
709
+ const parent = path.dirname(walkDir);
710
+ if (parent === walkDir)
711
+ break;
712
+ walkDir = parent;
713
+ candidates.push(path.join(walkDir, SPEC_FILE));
714
+ }
715
+ const specPath = candidates.find((p) => fs.existsSync(p)) ?? null;
716
+ if (!specPath) {
717
+ return appendTelemetry({
718
+ isError: true,
719
+ content: [{ type: "text", text: `Project spec not found. Run extraction prompt first.\n\nSearched:\n${candidates.map((p) => ` • ${p}`).join("\n")}` }],
720
+ }, inputChars);
721
+ }
722
+ // ── Parse spec ────────────────────────────────────────────────────────────
723
+ const raw = readFileSafe(specPath);
724
+ if (!raw) {
725
+ return appendTelemetry({
726
+ isError: true,
727
+ content: [{ type: "text", text: `Failed to read project spec at: ${specPath}` }],
728
+ }, inputChars);
729
+ }
730
+ let spec;
731
+ try {
732
+ spec = JSON.parse(raw);
733
+ }
734
+ catch (e) {
735
+ return appendTelemetry({
736
+ isError: true,
737
+ content: [{ type: "text", text: `Invalid JSON in project spec at: ${specPath}\n\nParse error: ${e.message}` }],
738
+ }, inputChars);
739
+ }
740
+ // ── action: read ──────────────────────────────────────────────────────────
741
+ if (action === "read") {
742
+ return appendTelemetry({
743
+ content: [{ type: "text", text: JSON.stringify(spec, null, 2) }],
744
+ }, inputChars);
745
+ }
746
+ // ── action: get_service ───────────────────────────────────────────────────
747
+ if (action === "get_service") {
748
+ if (!service_name) {
749
+ return appendTelemetry({
750
+ isError: true,
751
+ content: [{ type: "text", text: "ERROR: 'service_name' is required when action=get_service." }],
752
+ }, inputChars);
753
+ }
754
+ const microservices = spec.microservices;
755
+ const serviceBlock = microservices?.[service_name];
756
+ if (!serviceBlock) {
757
+ const available = microservices ? Object.keys(microservices).join(", ") : "none";
758
+ return appendTelemetry({
759
+ isError: true,
760
+ content: [{ type: "text", text: `Service '${service_name}' not found in spec.\n\nAvailable services: ${available}` }],
761
+ }, inputChars);
762
+ }
763
+ return appendTelemetry({
764
+ content: [{ type: "text", text: JSON.stringify({ [service_name]: serviceBlock }, null, 2) }],
765
+ }, inputChars);
766
+ }
767
+ // ── action: get_golden_rules ──────────────────────────────────────────────
768
+ if (action === "get_golden_rules") {
769
+ const goldenRules = spec.golden_rules;
770
+ const block = {
771
+ golden_rules: goldenRules ?? {},
772
+ rejected_patterns: goldenRules?.rejected_patterns ?? [],
773
+ };
774
+ return appendTelemetry({
775
+ content: [{ type: "text", text: JSON.stringify(block, null, 2) }],
776
+ }, inputChars);
777
+ }
778
+ // ── action: get_coding_patterns ───────────────────────────────────────────
779
+ if (action === "get_coding_patterns") {
780
+ const block = spec.coding_patterns ?? {};
781
+ return appendTelemetry({
782
+ content: [{ type: "text", text: JSON.stringify(block, null, 2) }],
783
+ }, inputChars);
784
+ }
785
+ // ── action: update ────────────────────────────────────────────────────────
786
+ if (action === "update") {
787
+ if (!update_path) {
788
+ return appendTelemetry({
789
+ isError: true,
790
+ content: [{ type: "text", text: "ERROR: 'update_path' is required when action=update." }],
791
+ }, inputChars);
792
+ }
793
+ if (update_value === undefined) {
794
+ return appendTelemetry({
795
+ isError: true,
796
+ content: [{ type: "text", text: "ERROR: 'update_value' is required when action=update." }],
797
+ }, inputChars);
798
+ }
799
+ // Deep set via dot-notation path
800
+ const keys = update_path.split(".");
801
+ let cursor = spec;
802
+ for (let i = 0; i < keys.length - 1; i++) {
803
+ const val = cursor[keys[i]];
804
+ if (val === undefined || val === null || typeof val !== "object") {
805
+ return appendTelemetry({
806
+ isError: true,
807
+ content: [{ type: "text", text: `ERROR: Invalid update_path — '${keys.slice(0, i + 1).join(".")}' does not exist or is not an object.\n\nAttempted path: ${update_path}` }],
808
+ }, inputChars);
809
+ }
810
+ cursor = val;
811
+ }
812
+ const finalKey = keys[keys.length - 1];
813
+ const oldValue = cursor[finalKey];
814
+ cursor[finalKey] = update_value;
815
+ writeFile(specPath, JSON.stringify(spec, null, 2));
816
+ return appendTelemetry({
817
+ content: [{
818
+ type: "text",
819
+ text: `✅ Project spec updated.\n\n**Path:** ${update_path}\n**Old value:** ${JSON.stringify(oldValue)}\n**New value:** ${JSON.stringify(update_value)}\n**File:** ${specPath}`,
820
+ }],
821
+ }, inputChars);
822
+ }
823
+ return appendTelemetry({ content: [{ type: "text", text: "Invalid action." }] }, inputChars);
824
+ });
825
+ // ─── Task management helpers ──────────────────────────────────────────────────
826
+ const TASKS_DIR = ".secufusion/tasks";
827
+ const REGISTRY_FILE = ".secufusion/registry.json";
828
+ function taskDir(workItemId) {
829
+ return resolve(`${TASKS_DIR}/WI-${workItemId}`);
830
+ }
831
+ function readJson(filePath) {
832
+ const raw = readFileSafe(filePath);
833
+ if (!raw)
834
+ return null;
835
+ try {
836
+ return JSON.parse(raw);
837
+ }
838
+ catch {
839
+ return null;
840
+ }
841
+ }
842
+ function readRegistry() {
843
+ return readJson(resolve(REGISTRY_FILE)) || [];
844
+ }
845
+ function writeRegistry(records) {
846
+ writeFile(resolve(REGISTRY_FILE), JSON.stringify(records, null, 2));
847
+ }
848
+ function upsertRegistry(entry) {
849
+ const records = readRegistry();
850
+ const idx = records.findIndex(r => r["work_item_id"] === entry["work_item_id"]);
851
+ if (idx === -1)
852
+ records.push(entry);
853
+ else
854
+ records[idx] = { ...records[idx], ...entry };
855
+ writeRegistry(records);
856
+ }
857
+ // ─── Tool 5: manage_task ─────────────────────────────────────────────────────
858
+ server.tool("manage_task", "Manages structured task records in .secufusion/tasks/WI-{id}/. Each task gets its own folder with spec, progress, decisions, files-touched, and scenarios files. Use this instead of manage_branch_state for all new work items.", {
859
+ action: z
860
+ .enum(["initialize", "read", "update_spec", "add_scenario", "log_file_touched", "log_decision", "complete", "read_summary"])
861
+ .describe("Action: initialize (create task folder + files), read (full task data), update_spec (edit spec fields), add_scenario (append test scenario), log_file_touched (record modified file), log_decision (record architectural decision), complete (mark done + generate PR summary), read_summary (token-efficient summary)."),
862
+ work_item_id: z.string().describe("Azure DevOps work item ID, e.g. '2847'. Used to name the task folder WI-{id}."),
863
+ title: z.string().optional().describe("Task title. Required for 'initialize'."),
864
+ description: z.string().optional().describe("Task description. Used with 'initialize' or 'update_spec'."),
865
+ acceptance_criteria: z.array(z.string()).optional().describe("Array of AC strings. Used with 'initialize' or 'update_spec'."),
866
+ pending_acs: z.array(z.string()).optional().describe("Pending AC list. Used with 'update_spec'."),
867
+ completed_acs: z.array(z.string()).optional().describe("Completed AC list. Used with 'update_spec'."),
868
+ next_step: z.string().optional().describe("Next actionable step. Required for 'update_spec'."),
869
+ tags: z.array(z.string()).optional().describe("Tags for searchability, e.g. ['tenant', 'iam', 'security']."),
870
+ file_path: z.string().optional().describe("File path to log. Used with 'log_file_touched'."),
871
+ change_summary: z.string().optional().describe("One-line summary of the change. Used with 'log_file_touched'."),
872
+ decision: z.string().optional().describe("Architectural decision to record. Used with 'log_decision'."),
873
+ rationale: z.string().optional().describe("Why this decision was made. Used with 'log_decision'."),
874
+ scenario: z.string().optional().describe("Test scenario description. Used with 'add_scenario'."),
875
+ scenario_type: z.enum(["unit", "integration", "e2e", "manual"]).optional().describe("Test scenario type. Used with 'add_scenario'."),
876
+ }, async ({ action, work_item_id, title, description, acceptance_criteria, pending_acs, completed_acs, next_step, tags, file_path, change_summary, decision, rationale, scenario, scenario_type }) => {
877
+ const inputChars = JSON.stringify({ action, work_item_id, title, description, acceptance_criteria, pending_acs, completed_acs, next_step, tags, file_path, change_summary, decision, rationale, scenario, scenario_type }).length;
878
+ const dir = taskDir(work_item_id);
879
+ const specPath = path.join(dir, "spec.json");
880
+ const progressPath = path.join(dir, "progress.json");
881
+ const decisionsPath = path.join(dir, "decisions.json");
882
+ const filesPath = path.join(dir, "files-touched.json");
883
+ const scenariosPath = path.join(dir, "scenarios.json");
884
+ const prSummaryPath = path.join(dir, "pr-summary.md");
885
+ if (action === "initialize") {
886
+ if (!title)
887
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'title' is required for action=initialize." }] }, inputChars);
888
+ const now = new Date().toISOString();
889
+ const spec = { work_item_id, title, description: description || "", acceptance_criteria: acceptance_criteria || [], tags: tags || [], created_at: now, status: "active" };
890
+ const progress = { pending_acs: acceptance_criteria || [], completed_acs: [], next_step: "Review spec and begin implementation.", last_updated: now };
891
+ if (!fs.existsSync(specPath)) {
892
+ writeFile(specPath, JSON.stringify(spec, null, 2));
893
+ writeFile(progressPath, JSON.stringify(progress, null, 2));
894
+ writeFile(decisionsPath, JSON.stringify([], null, 2));
895
+ writeFile(filesPath, JSON.stringify([], null, 2));
896
+ writeFile(scenariosPath, JSON.stringify([], null, 2));
897
+ }
898
+ upsertRegistry({ work_item_id, title, status: "active", created_at: now, tags: tags || [] });
899
+ return appendTelemetry({ content: [{ type: "text", text: `✅ Task WI-${work_item_id} initialized.\n\n**Folder:** ${dir}\n**Title:** ${title}\n**ACs:** ${(acceptance_criteria || []).length}\n\n${JSON.stringify(spec, null, 2)}` }] }, inputChars);
900
+ }
901
+ if (!fs.existsSync(specPath)) {
902
+ 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);
903
+ }
904
+ if (action === "read") {
905
+ const full = { spec: readJson(specPath), progress: readJson(progressPath), decisions: readJson(decisionsPath), files_touched: readJson(filesPath), scenarios: readJson(scenariosPath) };
906
+ return appendTelemetry({ content: [{ type: "text", text: JSON.stringify(full, null, 2) }] }, inputChars);
907
+ }
908
+ if (action === "read_summary") {
909
+ const spec = readJson(specPath) || {};
910
+ const progress = readJson(progressPath) || {};
911
+ const files = readJson(filesPath) || [];
912
+ const summary = { work_item_id, title: spec["title"], status: spec["status"], pending_acs: progress["pending_acs"] || [], completed_acs: progress["completed_acs"] || [], next_step: progress["next_step"], files_touched_count: files.length };
913
+ return appendTelemetry({ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }, inputChars);
914
+ }
915
+ if (action === "update_spec") {
916
+ if (!next_step)
917
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'next_step' is required for action=update_spec." }] }, inputChars);
918
+ const now = new Date().toISOString();
919
+ if (description !== undefined || acceptance_criteria !== undefined || tags !== undefined) {
920
+ const spec = readJson(specPath) || {};
921
+ if (description !== undefined)
922
+ spec["description"] = description;
923
+ if (acceptance_criteria !== undefined)
924
+ spec["acceptance_criteria"] = acceptance_criteria;
925
+ if (tags !== undefined)
926
+ spec["tags"] = tags;
927
+ writeFile(specPath, JSON.stringify(spec, null, 2));
928
+ }
929
+ const progress = readJson(progressPath) || {};
930
+ if (pending_acs !== undefined)
931
+ progress["pending_acs"] = pending_acs;
932
+ if (completed_acs !== undefined)
933
+ progress["completed_acs"] = completed_acs;
934
+ progress["next_step"] = next_step;
935
+ progress["last_updated"] = now;
936
+ writeFile(progressPath, JSON.stringify(progress, null, 2));
937
+ return appendTelemetry({ content: [{ type: "text", text: `✅ Task WI-${work_item_id} updated.\n\n> **Next step:** ${next_step}\n\n**Progress:**\n${JSON.stringify(progress, null, 2)}` }] }, inputChars);
938
+ }
939
+ if (action === "log_file_touched") {
940
+ if (!file_path)
941
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'file_path' is required for action=log_file_touched." }] }, inputChars);
942
+ const files = readJson(filesPath) || [];
943
+ const existing = files.findIndex((f) => f["file_path"] === file_path);
944
+ const entry = { file_path, change_summary: change_summary || "", logged_at: new Date().toISOString() };
945
+ if (existing === -1)
946
+ files.push(entry);
947
+ else
948
+ files[existing] = entry;
949
+ writeFile(filesPath, JSON.stringify(files, null, 2));
950
+ return appendTelemetry({ content: [{ type: "text", text: `✅ File logged for WI-${work_item_id}:\n\`${file_path}\` — ${change_summary || "(no summary)"}` }] }, inputChars);
951
+ }
952
+ if (action === "log_decision") {
953
+ if (!decision)
954
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'decision' is required for action=log_decision." }] }, inputChars);
955
+ const decisions = readJson(decisionsPath) || [];
956
+ decisions.push({ id: decisions.length + 1, decision, rationale: rationale || "", logged_at: new Date().toISOString() });
957
+ writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
958
+ return appendTelemetry({ content: [{ type: "text", text: `✅ Decision #${decisions.length} logged for WI-${work_item_id}:\n**Decision:** ${decision}\n**Rationale:** ${rationale || "(none)"}` }] }, inputChars);
959
+ }
960
+ if (action === "add_scenario") {
961
+ if (!scenario)
962
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'scenario' is required for action=add_scenario." }] }, inputChars);
963
+ const scenarios = readJson(scenariosPath) || [];
964
+ scenarios.push({ id: scenarios.length + 1, type: scenario_type || "manual", scenario, added_at: new Date().toISOString() });
965
+ writeFile(scenariosPath, JSON.stringify(scenarios, null, 2));
966
+ return appendTelemetry({ content: [{ type: "text", text: `✅ Scenario #${scenarios.length} added to WI-${work_item_id}:\n[${scenario_type || "manual"}] ${scenario}` }] }, inputChars);
967
+ }
968
+ if (action === "complete") {
969
+ const spec = readJson(specPath) || {};
970
+ const progress = readJson(progressPath) || {};
971
+ const decisions = readJson(decisionsPath) || [];
972
+ const files = readJson(filesPath) || [];
973
+ const scenarios = readJson(scenariosPath) || [];
974
+ spec["status"] = "complete";
975
+ writeFile(specPath, JSON.stringify(spec, null, 2));
976
+ progress["completed_at"] = new Date().toISOString();
977
+ writeFile(progressPath, JSON.stringify(progress, null, 2));
978
+ upsertRegistry({ work_item_id, status: "complete" });
979
+ const prSummary = [
980
+ `# PR Summary — WI-${work_item_id}: ${spec["title"] || ""}`,
981
+ ``, `**Status:** Complete `, `**Completed:** ${progress["completed_at"]}`, ``,
982
+ `## Acceptance Criteria`,
983
+ ...(progress["completed_acs"] || []).map(ac => `- ✅ ${ac}`),
984
+ ...((progress["pending_acs"] || []).length > 0 ? (progress["pending_acs"] || []).map(ac => `- ⚠️ ${ac}`) : []),
985
+ ``, `## Files Modified (${files.length})`,
986
+ ...files.map(f => `- \`${f["file_path"]}\`${f["change_summary"] ? ` — ${f["change_summary"]}` : ""}`),
987
+ ``, `## Architectural Decisions`,
988
+ ...decisions.map(d => `- **${d["decision"]}**${d["rationale"] ? `: ${d["rationale"]}` : ""}`),
989
+ ``, `## Test Scenarios (${scenarios.length})`,
990
+ ...scenarios.map(s => `- [${s["type"]}] ${s["scenario"]}`),
991
+ ].join("\n");
992
+ writeFile(prSummaryPath, prSummary);
993
+ return appendTelemetry({ content: [{ type: "text", text: `✅ Task WI-${work_item_id} marked complete.\n\n**PR summary generated:** ${prSummaryPath}\n\n${prSummary}` }] }, inputChars);
994
+ }
995
+ return appendTelemetry({ content: [{ type: "text", text: "Invalid action." }] }, inputChars);
996
+ });
997
+ // ─── Tool 6: get_task_history ─────────────────────────────────────────────────
998
+ server.tool("get_task_history", "Retrieve the full history of a past task — spec, decisions, files touched, and scenarios. Use before starting similar work to understand how it was done before.", { work_item_id: z.string().describe("Work item ID of the past task to retrieve.") }, async ({ work_item_id }) => {
999
+ const inputChars = JSON.stringify({ work_item_id }).length;
1000
+ const dir = taskDir(work_item_id);
1001
+ if (!fs.existsSync(dir)) {
1002
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found in ${resolve(TASKS_DIR)}.` }] }, inputChars);
1003
+ }
1004
+ const history = {
1005
+ spec: readJson(path.join(dir, "spec.json")),
1006
+ progress: readJson(path.join(dir, "progress.json")),
1007
+ decisions: readJson(path.join(dir, "decisions.json")),
1008
+ files_touched: readJson(path.join(dir, "files-touched.json")),
1009
+ scenarios: readJson(path.join(dir, "scenarios.json")),
1010
+ };
1011
+ return appendTelemetry({ content: [{ type: "text", text: JSON.stringify(history, null, 2) }] }, inputChars);
1012
+ });
1013
+ // ─── Tool 7: search_tasks ─────────────────────────────────────────────────────
1014
+ server.tool("search_tasks", "Search all past tasks by keyword. Scans registry and task file contents. Use before starting a new task to find related past work and avoid re-solving solved problems.", {
1015
+ keywords: z.string().describe("Space-separated search keywords, e.g. 'tenant isolation repository'."),
1016
+ status_filter: z.enum(["active", "complete", "any"]).optional().default("any").describe("Filter results by task status."),
1017
+ }, async ({ keywords, status_filter = "any" }) => {
1018
+ const inputChars = JSON.stringify({ keywords, status_filter }).length;
1019
+ const registry = readRegistry();
1020
+ if (registry.length === 0)
1021
+ return appendTelemetry({ content: [{ type: "text", text: "No tasks found in registry. Initialize tasks first." }] }, inputChars);
1022
+ const terms = keywords.toLowerCase().split(/\s+/).filter(Boolean);
1023
+ const matches = [];
1024
+ for (const entry of registry) {
1025
+ if (status_filter !== "any" && entry["status"] !== status_filter)
1026
+ continue;
1027
+ const dir = taskDir(entry["work_item_id"]);
1028
+ const blobs = [
1029
+ JSON.stringify(entry),
1030
+ readFileSafe(path.join(dir, "spec.json")) || "",
1031
+ readFileSafe(path.join(dir, "progress.json")) || "",
1032
+ readFileSafe(path.join(dir, "decisions.json")) || "",
1033
+ readFileSafe(path.join(dir, "files-touched.json")) || "",
1034
+ readFileSafe(path.join(dir, "scenarios.json")) || "",
1035
+ ].join(" ").toLowerCase();
1036
+ const hitCount = terms.filter(t => blobs.includes(t)).length;
1037
+ if (hitCount > 0) {
1038
+ const spec = readJson(path.join(dir, "spec.json")) || {};
1039
+ matches.push({ work_item_id: entry["work_item_id"], title: entry["title"], status: entry["status"], tags: entry["tags"] || [], keyword_hits: hitCount, description_excerpt: (spec["description"] || "").slice(0, 120) });
1040
+ }
1041
+ }
1042
+ matches.sort((a, b) => b["keyword_hits"] - a["keyword_hits"]);
1043
+ if (matches.length === 0)
1044
+ return appendTelemetry({ content: [{ type: "text", text: `No tasks matched keywords: "${keywords}"` }] }, inputChars);
1045
+ return appendTelemetry({ content: [{ type: "text", text: `Found ${matches.length} task(s) matching "${keywords}":\n\n${JSON.stringify(matches, null, 2)}` }] }, inputChars);
1046
+ });
1047
+ // ─── Tool 8: get_pattern_from_task ────────────────────────────────────────────
1048
+ server.tool("get_pattern_from_task", "Extract reusable implementation patterns from a completed task — decisions made, files modified, and test scenarios used. Call this before implementing something similar to a past feature.", { work_item_id: z.string().describe("Work item ID of the past task to extract patterns from.") }, async ({ work_item_id }) => {
1049
+ const inputChars = JSON.stringify({ work_item_id }).length;
1050
+ const dir = taskDir(work_item_id);
1051
+ if (!fs.existsSync(dir))
1052
+ return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found. Cannot extract patterns.` }] }, inputChars);
1053
+ const spec = readJson(path.join(dir, "spec.json")) || {};
1054
+ const decisions = readJson(path.join(dir, "decisions.json")) || [];
1055
+ const files = readJson(path.join(dir, "files-touched.json")) || [];
1056
+ const scenarios = readJson(path.join(dir, "scenarios.json")) || [];
1057
+ const patterns = {
1058
+ source_task: { work_item_id, title: spec["title"], status: spec["status"] },
1059
+ architectural_decisions: decisions.map(d => ({ decision: d["decision"], rationale: d["rationale"] })),
1060
+ files_pattern: files.map(f => f["file_path"]),
1061
+ test_patterns: scenarios.map(s => ({ type: s["type"], scenario: s["scenario"] })),
1062
+ tags: spec["tags"] || [],
1063
+ };
1064
+ return appendTelemetry({ content: [{ type: "text", text: `**Patterns from WI-${work_item_id}: ${spec["title"] || ""}**\n\n${JSON.stringify(patterns, null, 2)}` }] }, inputChars);
1065
+ });
680
1066
  // ─────────────────────────────────────────────
681
1067
  // Start transport
682
1068
  // ─────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",