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.
- package/README.md +244 -72
- package/index.js +386 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SecuFusion MCP Server
|
|
2
2
|
|
|
3
|
-
> **
|
|
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
|
[](https://www.npmjs.com/package/secufusion-mcp)
|
|
6
6
|
[](./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
|
|
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
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
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-
|
|
269
|
-
| `.
|
|
270
|
-
| `.secufusion-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
│
|
|
281
|
-
|
|
282
|
-
│
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
│
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
│
|
|
289
|
-
│
|
|
290
|
-
|
|
291
|
-
│ Phase
|
|
292
|
-
│
|
|
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
|
-
|
|
346
|
-
|
|
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 `
|
|
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
|
-
"
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
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 `
|
|
546
|
+
The AI calls `manage_task` with `action=update_spec` and the state updates:
|
|
383
547
|
|
|
384
548
|
```json
|
|
385
549
|
{
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
626
|
+
mcp_config.json → starts the server (8 tools available)
|
|
479
627
|
+
|
|
480
|
-
.agents/AGENTS.md
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
667
|
+
## Talking to the AI — What You'll Ever Say
|
|
506
668
|
|
|
507
|
-
Once
|
|
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
|
|
689
|
+
**Before Phase 0.5 + 0.7:** The AI started coding immediately with no ownership check or explicit plan.
|
|
518
690
|
|
|
519
|
-
**After
|
|
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
|
// ─────────────────────────────────────────────
|