secufusion-mcp 1.0.22 → 1.0.24
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 +22 -6
- package/index.js +410 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -705,6 +705,21 @@ When you pass a title like `"BUG-1140: Tenant deletion reports failure"` to `man
|
|
|
705
705
|
- **Backward Compatible:** The AI only ever needs to supply the `work_item_id` (e.g. `BUG-1140`) for subsequent updates. The server instantly finds the correct folder via `registry.json` (O(1) lookup) or falls back to a prefix scan for legacy `WI-{id}` folders.
|
|
706
706
|
- **OS Safe:** Automatically trims trailing dashes and clamps lengths to prevent Windows `MAX_PATH` errors.
|
|
707
707
|
|
|
708
|
+
### Dynamic Architecture Validation (v1.0.23+)
|
|
709
|
+
|
|
710
|
+
As of version **1.0.23**, the deep analysis engine in `classify_task` is fully dynamic and driven entirely by your `.secufusion-project-spec.json`:
|
|
711
|
+
- Validation dynamically cross-references explicit microservices, frontend repos, and Chrome extensions.
|
|
712
|
+
- Explicit frontend overrides (e.g., `"pure ui"`, `"no backend changes"`) can bypass false-positive `FULL_STACK` labels.
|
|
713
|
+
- The Breaking Change Pre-Scan safely checks for exact table names and Kafka topics derived from your architecture.
|
|
714
|
+
- `coding_patterns` defined in the spec are injected seamlessly into `secufusionFlags` validation.
|
|
715
|
+
|
|
716
|
+
### Retrospective Intelligence (v1.0.24+)
|
|
717
|
+
|
|
718
|
+
As of version **1.0.24**, the MCP server introduces a fully automated **Retrospective Layer**:
|
|
719
|
+
- **Auto-Retrospective Trigger**: `manage_task complete` now automatically generates a partial retrospective and asks the developer 7 targeted questions.
|
|
720
|
+
- **record_retrospective**: A new tool that saves retrospective insights, tracking plan accuracy, classification accuracy, and pre-PR check attempts.
|
|
721
|
+
- **Dynamic Learning (Pass 0)**: `classify_task` now includes a Pass 0 that injects learned signals from past retrospectives into the active classification logic.
|
|
722
|
+
|
|
708
723
|
---
|
|
709
724
|
|
|
710
725
|
## Talking to the AI — What You'll Ever Say
|
|
@@ -760,12 +775,12 @@ The **mandatory first step** for every task without exception. Classifies a task
|
|
|
760
775
|
|
|
761
776
|
| Pass | What it does |
|
|
762
777
|
|---|---|
|
|
763
|
-
| **Pass 1 — Weighted signal tiers** | Tier 1: service names = 10pts each. Tier 2: tech constructs = 5pts. Tier 3: domain terms = 2-3pts. Tier 4: generic words = 1pt. |
|
|
764
|
-
| **Pass 2 — Negation detection** | Scans each sentence. `"not a UI issue"` → frontend penalty.
|
|
778
|
+
| **Pass 1 — Weighted signal tiers** | Tier 1: service names = 10pts each (dynamically populated from `project-spec.json`). Tier 2: tech constructs = 5pts. Tier 3: domain terms = 2-3pts. Tier 4: generic words = 1pt. |
|
|
779
|
+
| **Pass 2 — Negation detection** | Scans each sentence. `"not a UI issue"` → frontend penalty. Explicit overrides (e.g. `"pure ui"`, `"no backend changes"`) zero out backend scores to prevent `FULL_STACK` misclassifications. |
|
|
765
780
|
| **Pass 3 — Root-cause phrase extraction** | 25 backend patterns + 9 frontend patterns matched via regex. |
|
|
766
781
|
| **Pass 4 — Bug disambiguation matrix** | For `task_type: bug`: data-correctness → +15 backend, exception/crash → +15 backend, auth/permission → +12 backend. |
|
|
767
|
-
| **Pass 4.5 — Problem Statement Validation** | **NEW (v1.0.19)**: Validates Title
|
|
768
|
-
| **Pass 5 — Confidence
|
|
782
|
+
| **Pass 4.5 — Problem Statement Validation** | **NEW (v1.0.19+)**: Validates Title, Scope, and Task Type. Dynamically extracts `coding_patterns` from the project spec and injects them as active validations if relevant keywords are found. |
|
|
783
|
+
| **Pass 5 — Confidence & Breaking Change Gate** | `HIGH` only when dominant score ≥ 1.8× second-place **AND** at least one Tier 1/2 signal matched. Dynamically cross-references explicitly mentioned endpoints, Kafka topics, and DB tables against `project-spec.json` to accurately flag breaking change risks. |
|
|
769
784
|
|
|
770
785
|
**Output — `allowed_next_action`:**
|
|
771
786
|
|
|
@@ -800,12 +815,13 @@ Proceeding to plan presentation. No developer confirmation needed.
|
|
|
800
815
|
|
|
801
816
|
| Before | After |
|
|
802
817
|
|---|---|
|
|
803
|
-
| Flat keyword counting — every word scored equally | 4-tier weighted scoring — service names = 10× generic words |
|
|
818
|
+
| Flat keyword counting — every word scored equally | 4-tier weighted scoring — service names = 10× generic words (dynamically loaded from `project-spec.json`) |
|
|
804
819
|
| `"dashboard"` → scored as frontend | Root-cause phrases — `"shows wrong count on dashboard"` → backend +12 |
|
|
805
|
-
| No negation awareness | Sentence-level negation — `"not a UI issue"` removes frontend weight |
|
|
820
|
+
| No negation awareness | Sentence-level negation — `"not a UI issue"` removes frontend weight. Explicit overrides (e.g. `"pure ui"`) safely force `FRONTEND_ONLY`. |
|
|
806
821
|
| Bug heuristic: default to backend only on LOW confidence | 5-category bug disambiguation matrix (+12–15pts per category) |
|
|
807
822
|
| HIGH confidence even on equal scores | HIGH only when ratio ≥ 1.8× AND Tier 1/2 signal matched |
|
|
808
823
|
| Mixed signals → always FULL_STACK | Backend dominates at 2.5× → classified BACKEND_ONLY, frontend treated as noise |
|
|
824
|
+
| Validation / Breaking Changes hardcoded | Validation rules, endpoints, DB tables, and Kafka topics dynamically extracted from `project-spec.json` |
|
|
809
825
|
|
|
810
826
|
### `AGENTS.md` — All phases rewritten to strict enforcement
|
|
811
827
|
|
package/index.js
CHANGED
|
@@ -1044,7 +1044,98 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
1044
1044
|
...scenarios.map(s => `- [${s["type"]}] ${s["scenario"]}`),
|
|
1045
1045
|
].join("\n");
|
|
1046
1046
|
writeFile(prSummaryPath, prSummary);
|
|
1047
|
-
|
|
1047
|
+
let retroMessage = "";
|
|
1048
|
+
try {
|
|
1049
|
+
const classificationFile = path.join(resolve(CLASSIFICATIONS_DIR), `${work_item_id}.json`);
|
|
1050
|
+
const classificationData = fs.existsSync(classificationFile) ? readJson(classificationFile) : {};
|
|
1051
|
+
let migration_risk_predicted = "NONE";
|
|
1052
|
+
let breaking_changes_predicted = 0;
|
|
1053
|
+
const rollbackDecision = decisions.find(d => (d["decision"] || "").toLowerCase().includes("rollback strategy"));
|
|
1054
|
+
if (rollbackDecision) {
|
|
1055
|
+
const match = rollbackDecision["decision"].match(/\[(SAFE|RISKY|DANGEROUS)\]/i);
|
|
1056
|
+
if (match)
|
|
1057
|
+
migration_risk_predicted = match[1].toUpperCase();
|
|
1058
|
+
}
|
|
1059
|
+
const breakingDecision = decisions.find(d => (d["decision"] || "").toLowerCase().includes("breaking changes predicted"));
|
|
1060
|
+
if (breakingDecision) {
|
|
1061
|
+
const match = breakingDecision["decision"].match(/(\d+)/);
|
|
1062
|
+
if (match)
|
|
1063
|
+
breaking_changes_predicted = parseInt(match[1], 10);
|
|
1064
|
+
}
|
|
1065
|
+
const sad_paths = scenarios.filter(s => s["type"] === "e2e" || s["scenario"]?.toString().toLowerCase().includes("sad")).length;
|
|
1066
|
+
const edge_cases = scenarios.filter(s => s["scenario"]?.toString().toLowerCase().includes("edge")).length;
|
|
1067
|
+
const security_cases = scenarios.filter(s => s["scenario"]?.toString().toLowerCase().includes("security")).length;
|
|
1068
|
+
const partialRetro = {
|
|
1069
|
+
status: "partial — awaiting developer input",
|
|
1070
|
+
work_item_id,
|
|
1071
|
+
task_title: spec["title"] || "",
|
|
1072
|
+
branch: "",
|
|
1073
|
+
completed_at: progress["completed_at"],
|
|
1074
|
+
classification: {
|
|
1075
|
+
predicted: classificationData["dominant_tier"] || "",
|
|
1076
|
+
predicted_confidence: classificationData["confidence"] || "",
|
|
1077
|
+
predicted_score_backend: classificationData["backend_score"] || 0,
|
|
1078
|
+
predicted_score_frontend: classificationData["frontend_score"] || 0,
|
|
1079
|
+
was_correct: null,
|
|
1080
|
+
correction_notes: null
|
|
1081
|
+
},
|
|
1082
|
+
plan_vs_actual: {
|
|
1083
|
+
steps_planned: files.length,
|
|
1084
|
+
steps_actual: null,
|
|
1085
|
+
steps_accuracy_pct: null,
|
|
1086
|
+
files_planned: files.map(f => f["file_path"]),
|
|
1087
|
+
files_actual: [],
|
|
1088
|
+
unplanned_files: null,
|
|
1089
|
+
missed_files: null,
|
|
1090
|
+
migration_risk_predicted,
|
|
1091
|
+
migration_risk_actual: null,
|
|
1092
|
+
migration_prediction_correct: null,
|
|
1093
|
+
breaking_changes_predicted,
|
|
1094
|
+
breaking_changes_actual: null,
|
|
1095
|
+
breaking_prediction_correct: null,
|
|
1096
|
+
performance_risk_predicted: "",
|
|
1097
|
+
performance_issues_found: null,
|
|
1098
|
+
performance_prediction_correct: null
|
|
1099
|
+
},
|
|
1100
|
+
scenarios: {
|
|
1101
|
+
sad_paths_planned: sad_paths,
|
|
1102
|
+
sad_paths_covered_in_tests: null,
|
|
1103
|
+
edge_cases_planned: edge_cases,
|
|
1104
|
+
edge_cases_covered_in_tests: null,
|
|
1105
|
+
security_cases_planned: security_cases,
|
|
1106
|
+
security_cases_covered_in_tests: null,
|
|
1107
|
+
scenario_coverage_pct: null
|
|
1108
|
+
},
|
|
1109
|
+
effort: {
|
|
1110
|
+
acs_planned: (spec["acceptance_criteria"] || []).length,
|
|
1111
|
+
acs_completed: (progress["completed_acs"] || []).length,
|
|
1112
|
+
acs_added_during_work: null,
|
|
1113
|
+
acs_removed_during_work: null,
|
|
1114
|
+
scope_creep_detected: null,
|
|
1115
|
+
scope_creep_description: null
|
|
1116
|
+
},
|
|
1117
|
+
quality: {
|
|
1118
|
+
pre_pr_checks_passed_first_attempt: null,
|
|
1119
|
+
pre_pr_attempts_needed: null,
|
|
1120
|
+
checks_that_failed: null,
|
|
1121
|
+
rejected_patterns_triggered: null,
|
|
1122
|
+
linter_errors_found: null
|
|
1123
|
+
},
|
|
1124
|
+
learnings: {
|
|
1125
|
+
what_went_well: null,
|
|
1126
|
+
what_was_harder_than_expected: null,
|
|
1127
|
+
signals_that_should_have_been_caught: null,
|
|
1128
|
+
plan_sections_that_were_wrong: null
|
|
1129
|
+
},
|
|
1130
|
+
classifier_feedback: null
|
|
1131
|
+
};
|
|
1132
|
+
writeFile(path.join(dir, "retrospective.json"), JSON.stringify(partialRetro, null, 2));
|
|
1133
|
+
retroMessage = `\n─────────────────────────────────────────\n📊 RETROSPECTIVE STARTED\n\nA partial retrospective has been saved.\nTo complete it and improve future plans,\nanswer these questions:\n\nQ1: Was the classification correct?\n (predicted: ${classificationData["dominant_tier"] || "UNKNOWN"} — ${classificationData["confidence"] || "UNKNOWN"} confidence)\n Reply: retro correct / retro incorrect: {reason}\n\nQ2: How many implementation steps did it take?\n (planned: ${files.length})\n Reply: retro steps {actual_number}\n\nQ3: Any unplanned files modified?\n Reply: retro unplanned {filename1, filename2}\n or: retro unplanned none\n\nQ4: Actual migration risk?\n (predicted: ${migration_risk_predicted})\n Reply: retro migration {SAFE/RISKY/DANGEROUS/NONE}\n\nQ5: How many pre-PR attempts needed?\n Reply: retro attempts {number}\n\nQ6: Any scope creep?\n Reply: retro scope yes: {description}\n or: retro scope no\n\nQ7: Any classifier feedback?\n Reply: retro signal missing {term}\n or: retro signal noise {term}\n or: retro phrase {phrase that would help}\n or: retro skip\n\nYou can answer all at once or skip with: retro skip\n─────────────────────────────────────────`;
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
console.warn("Auto-retrospective failed:", err);
|
|
1137
|
+
}
|
|
1138
|
+
return appendTelemetry({ content: [{ type: "text", text: `✅ Task WI-${work_item_id} marked complete.\n\n**PR summary generated:** ${prSummaryPath}\n\n${prSummary}\n${retroMessage}` }] }, inputChars);
|
|
1048
1139
|
}
|
|
1049
1140
|
return appendTelemetry({ content: [{ type: "text", text: "Invalid action." }] }, inputChars);
|
|
1050
1141
|
});
|
|
@@ -1122,6 +1213,255 @@ server.tool("get_pattern_from_task", "Extract reusable implementation patterns f
|
|
|
1122
1213
|
};
|
|
1123
1214
|
return appendTelemetry({ content: [{ type: "text", text: `**Patterns from WI-${work_item_id}: ${spec["title"] || ""}**\n\n${JSON.stringify(patterns, null, 2)}` }] }, inputChars);
|
|
1124
1215
|
});
|
|
1216
|
+
// ─── Tool 8.5: record_retrospective ───────────────────────────────────────────
|
|
1217
|
+
server.tool("record_retrospective", "Records a structured retrospective after task completion. Compares planned vs actual: steps, files, migration risk, breaking changes, scenario coverage, scope creep, and pre-PR check results. Accumulates insights in .secufusion/retrospective-insights.json to make classify_task smarter over time. Called automatically by manage_task action=complete — can also be called manually at any point during a task.", {
|
|
1218
|
+
work_item_id: z.string().describe("Azure DevOps work item ID."),
|
|
1219
|
+
classification_correct: z.boolean().describe("Was the classify_task classification correct? true = matched actual work done, false = wrong or misleading"),
|
|
1220
|
+
classification_correction_notes: z.string().optional().describe("Only if classification_correct=false. What was wrong and what the correct classification should have been."),
|
|
1221
|
+
steps_actual: z.number().describe("How many implementation steps were actually needed."),
|
|
1222
|
+
unplanned_files: z.array(z.string()).optional().describe("Files that were modified but NOT in the plan."),
|
|
1223
|
+
missed_files: z.array(z.string()).optional().describe("Files that were in the plan but NOT touched."),
|
|
1224
|
+
migration_risk_actual: z.string().optional().describe("Actual Flyway migration risk encountered. 'SAFE' | 'RISKY' | 'DANGEROUS' | 'NONE'"),
|
|
1225
|
+
breaking_changes_actual: z.number().optional().describe("How many actual breaking changes were found. 0 if none."),
|
|
1226
|
+
performance_issues_found: z.boolean().optional().describe("Were any performance issues found during implementation or PR review?"),
|
|
1227
|
+
pre_pr_attempts: z.number().describe("How many times run_pre_pr_checks was called before all checks passed. 1 = passed first attempt (perfect), 2+ = needed fixes"),
|
|
1228
|
+
checks_that_failed: z.array(z.string()).optional().describe("List of check names that failed at least once."),
|
|
1229
|
+
scope_creep_detected: z.boolean().describe("Were ACs added or significantly changed during work that were not in the original plan?"),
|
|
1230
|
+
scope_creep_description: z.string().optional().describe("Only if scope_creep_detected=true. What changed and why."),
|
|
1231
|
+
what_went_well: z.array(z.string()).optional().describe("Things that worked exactly as planned."),
|
|
1232
|
+
what_was_harder: z.array(z.string()).optional().describe("Things that took more effort than expected."),
|
|
1233
|
+
classifier_feedback: z.object({
|
|
1234
|
+
signal_missing: z.string().optional(),
|
|
1235
|
+
signal_noise: z.string().optional(),
|
|
1236
|
+
root_cause_phrase: z.string().optional(),
|
|
1237
|
+
bug_category: z.string().optional(),
|
|
1238
|
+
tier_upgrade: z.string().optional()
|
|
1239
|
+
}).optional().describe("Feedback to improve classify_task for future tasks.")
|
|
1240
|
+
}, async (params) => {
|
|
1241
|
+
const inputChars = JSON.stringify(params).length;
|
|
1242
|
+
const { work_item_id, classification_correct, classification_correction_notes, steps_actual, unplanned_files = [], missed_files = [], migration_risk_actual, breaking_changes_actual, performance_issues_found, pre_pr_attempts, checks_that_failed = [], scope_creep_detected, scope_creep_description, what_went_well = [], what_was_harder = [], classifier_feedback } = params;
|
|
1243
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1244
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
1245
|
+
if (!dir)
|
|
1246
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found.` }] }, inputChars);
|
|
1247
|
+
const spec = readJson(path.join(dir, "spec.json")) || {};
|
|
1248
|
+
const progress = readJson(path.join(dir, "progress.json")) || {};
|
|
1249
|
+
const decisions = readJson(path.join(dir, "decisions.json")) || [];
|
|
1250
|
+
const files = readJson(path.join(dir, "files-touched.json")) || [];
|
|
1251
|
+
const scenarios = readJson(path.join(dir, "scenarios.json")) || [];
|
|
1252
|
+
const classificationFile = path.join(resolve(CLASSIFICATIONS_DIR), `${work_item_id}.json`);
|
|
1253
|
+
const classificationData = readJson(classificationFile) || {};
|
|
1254
|
+
const acs_planned = (spec["acceptance_criteria"] || []).length;
|
|
1255
|
+
const acs_completed = (progress["completed_acs"] || []).length;
|
|
1256
|
+
const acs_added_during_work = acs_completed - acs_planned;
|
|
1257
|
+
let migration_risk_predicted = "NONE";
|
|
1258
|
+
let breaking_changes_predicted = 0;
|
|
1259
|
+
const rollbackDecision = decisions.find(d => (d["decision"] || "").toLowerCase().includes("rollback strategy"));
|
|
1260
|
+
if (rollbackDecision) {
|
|
1261
|
+
const match = rollbackDecision["decision"].match(/\[(SAFE|RISKY|DANGEROUS)\]/i);
|
|
1262
|
+
if (match)
|
|
1263
|
+
migration_risk_predicted = match[1].toUpperCase();
|
|
1264
|
+
}
|
|
1265
|
+
const breakingDecision = decisions.find(d => (d["decision"] || "").toLowerCase().includes("breaking changes predicted"));
|
|
1266
|
+
if (breakingDecision) {
|
|
1267
|
+
const match = breakingDecision["decision"].match(/(\d+)/);
|
|
1268
|
+
if (match)
|
|
1269
|
+
breaking_changes_predicted = parseInt(match[1], 10);
|
|
1270
|
+
}
|
|
1271
|
+
const sad_paths = scenarios.filter(s => s["type"] === "e2e" || s["scenario"]?.toString().toLowerCase().includes("sad")).length;
|
|
1272
|
+
const edge_cases = scenarios.filter(s => s["scenario"]?.toString().toLowerCase().includes("edge")).length;
|
|
1273
|
+
const security_cases = scenarios.filter(s => s["scenario"]?.toString().toLowerCase().includes("security")).length;
|
|
1274
|
+
const files_planned = files.map(f => f["file_path"]);
|
|
1275
|
+
const steps_planned = files.length;
|
|
1276
|
+
const steps_accuracy_pct = steps_planned === 0 && steps_actual === 0 ? 100 : Math.round((Math.min(steps_planned, steps_actual) / Math.max(steps_planned, steps_actual)) * 100);
|
|
1277
|
+
const scenario_coverage_pct = scenarios.length === 0 ? 100 : Math.round((files.length / scenarios.length) * 100);
|
|
1278
|
+
const migration_prediction_correct = migration_risk_actual === migration_risk_predicted || (!migration_risk_actual && migration_risk_predicted === "NONE");
|
|
1279
|
+
const breaking_prediction_correct = breaking_changes_actual === breaking_changes_predicted || (!breaking_changes_actual && breaking_changes_predicted === 0);
|
|
1280
|
+
const retro = {
|
|
1281
|
+
work_item_id,
|
|
1282
|
+
task_title: spec["title"] || "",
|
|
1283
|
+
branch: "",
|
|
1284
|
+
completed_at: new Date().toISOString(),
|
|
1285
|
+
classification: {
|
|
1286
|
+
predicted: classificationData["dominant_tier"] || "",
|
|
1287
|
+
predicted_confidence: classificationData["confidence"] || "",
|
|
1288
|
+
predicted_score_backend: classificationData["backend_score"] || 0,
|
|
1289
|
+
predicted_score_frontend: classificationData["frontend_score"] || 0,
|
|
1290
|
+
was_correct: classification_correct,
|
|
1291
|
+
correction_notes: classification_correction_notes || ""
|
|
1292
|
+
},
|
|
1293
|
+
plan_vs_actual: {
|
|
1294
|
+
steps_planned,
|
|
1295
|
+
steps_actual,
|
|
1296
|
+
steps_accuracy_pct,
|
|
1297
|
+
files_planned,
|
|
1298
|
+
files_actual: files.map(f => f["file_path"]),
|
|
1299
|
+
unplanned_files,
|
|
1300
|
+
missed_files,
|
|
1301
|
+
migration_risk_predicted,
|
|
1302
|
+
migration_risk_actual: migration_risk_actual || "",
|
|
1303
|
+
migration_prediction_correct,
|
|
1304
|
+
breaking_changes_predicted,
|
|
1305
|
+
breaking_changes_actual: breaking_changes_actual || 0,
|
|
1306
|
+
breaking_prediction_correct,
|
|
1307
|
+
performance_risk_predicted: "",
|
|
1308
|
+
performance_issues_found: performance_issues_found || false,
|
|
1309
|
+
performance_prediction_correct: !performance_issues_found
|
|
1310
|
+
},
|
|
1311
|
+
scenarios: {
|
|
1312
|
+
sad_paths_planned: sad_paths,
|
|
1313
|
+
sad_paths_covered_in_tests: sad_paths,
|
|
1314
|
+
edge_cases_planned: edge_cases,
|
|
1315
|
+
edge_cases_covered_in_tests: edge_cases,
|
|
1316
|
+
security_cases_planned: security_cases,
|
|
1317
|
+
security_cases_covered_in_tests: security_cases,
|
|
1318
|
+
scenario_coverage_pct
|
|
1319
|
+
},
|
|
1320
|
+
effort: {
|
|
1321
|
+
acs_planned,
|
|
1322
|
+
acs_completed,
|
|
1323
|
+
acs_added_during_work: Math.max(0, acs_added_during_work),
|
|
1324
|
+
acs_removed_during_work: Math.max(0, -acs_added_during_work),
|
|
1325
|
+
scope_creep_detected,
|
|
1326
|
+
scope_creep_description: scope_creep_description || ""
|
|
1327
|
+
},
|
|
1328
|
+
quality: {
|
|
1329
|
+
pre_pr_checks_passed_first_attempt: pre_pr_attempts === 1,
|
|
1330
|
+
pre_pr_attempts_needed: pre_pr_attempts,
|
|
1331
|
+
checks_that_failed,
|
|
1332
|
+
rejected_patterns_triggered: [],
|
|
1333
|
+
linter_errors_found: 0
|
|
1334
|
+
},
|
|
1335
|
+
learnings: {
|
|
1336
|
+
what_went_well,
|
|
1337
|
+
what_was_harder_than_expected: what_was_harder,
|
|
1338
|
+
signals_that_should_have_been_caught: [],
|
|
1339
|
+
plan_sections_that_were_wrong: []
|
|
1340
|
+
},
|
|
1341
|
+
classifier_feedback: classifier_feedback || {}
|
|
1342
|
+
};
|
|
1343
|
+
writeFile(path.join(dir, "retrospective.json"), JSON.stringify(retro, null, 2));
|
|
1344
|
+
const insightsFile = resolve(".secufusion/retrospective-insights.json");
|
|
1345
|
+
let insights = {
|
|
1346
|
+
total_tasks_completed: 0,
|
|
1347
|
+
last_updated: "",
|
|
1348
|
+
classification_accuracy: { total_classified: 0, correct: 0, incorrect: 0, accuracy_pct: 0, most_common_misclassification: "" },
|
|
1349
|
+
plan_accuracy: { avg_steps_planned: 0, avg_steps_actual: 0, avg_step_accuracy_pct: 0, avg_files_unplanned: 0, migration_risk_accuracy_pct: 0, breaking_change_accuracy_pct: 0 },
|
|
1350
|
+
quality_trends: { avg_pre_pr_attempts: 0, most_common_failed_check: "", most_triggered_rejected_pattern: "" },
|
|
1351
|
+
scenario_coverage: { avg_sad_path_coverage_pct: 0, avg_edge_case_coverage_pct: 0, most_missed_scenario_type: "" },
|
|
1352
|
+
scope_creep: { tasks_with_scope_creep: 0, avg_acs_added_during_work: 0 },
|
|
1353
|
+
classifier_learning_queue: [],
|
|
1354
|
+
task_history: []
|
|
1355
|
+
};
|
|
1356
|
+
if (fs.existsSync(insightsFile)) {
|
|
1357
|
+
insights = readJson(insightsFile) || insights;
|
|
1358
|
+
}
|
|
1359
|
+
const n = insights.total_tasks_completed + 1;
|
|
1360
|
+
insights.total_tasks_completed = n;
|
|
1361
|
+
insights.last_updated = new Date().toISOString();
|
|
1362
|
+
insights.classification_accuracy.total_classified += 1;
|
|
1363
|
+
if (classification_correct) {
|
|
1364
|
+
insights.classification_accuracy.correct += 1;
|
|
1365
|
+
}
|
|
1366
|
+
else {
|
|
1367
|
+
insights.classification_accuracy.incorrect += 1;
|
|
1368
|
+
}
|
|
1369
|
+
insights.classification_accuracy.accuracy_pct = Math.round((insights.classification_accuracy.correct / insights.classification_accuracy.total_classified) * 100);
|
|
1370
|
+
insights.plan_accuracy.avg_steps_planned = Math.round(((insights.plan_accuracy.avg_steps_planned * (n - 1)) + steps_planned) / n);
|
|
1371
|
+
insights.plan_accuracy.avg_steps_actual = Math.round(((insights.plan_accuracy.avg_steps_actual * (n - 1)) + steps_actual) / n);
|
|
1372
|
+
insights.plan_accuracy.avg_step_accuracy_pct = Math.round(((insights.plan_accuracy.avg_step_accuracy_pct * (n - 1)) + steps_accuracy_pct) / n);
|
|
1373
|
+
insights.plan_accuracy.avg_files_unplanned = Math.round(((insights.plan_accuracy.avg_files_unplanned * (n - 1)) + unplanned_files.length) / n);
|
|
1374
|
+
const migRiskCorrect = migration_prediction_correct ? 100 : 0;
|
|
1375
|
+
insights.plan_accuracy.migration_risk_accuracy_pct = Math.round(((insights.plan_accuracy.migration_risk_accuracy_pct * (n - 1)) + migRiskCorrect) / n);
|
|
1376
|
+
const breakRiskCorrect = breaking_prediction_correct ? 100 : 0;
|
|
1377
|
+
insights.plan_accuracy.breaking_change_accuracy_pct = Math.round(((insights.plan_accuracy.breaking_change_accuracy_pct * (n - 1)) + breakRiskCorrect) / n);
|
|
1378
|
+
insights.quality_trends.avg_pre_pr_attempts = Math.round(((insights.quality_trends.avg_pre_pr_attempts * (n - 1)) + pre_pr_attempts) / n);
|
|
1379
|
+
if (scope_creep_detected)
|
|
1380
|
+
insights.scope_creep.tasks_with_scope_creep += 1;
|
|
1381
|
+
insights.scope_creep.avg_acs_added_during_work = Math.round(((insights.scope_creep.avg_acs_added_during_work * (n - 1)) + Math.max(0, acs_added_during_work)) / n);
|
|
1382
|
+
insights.task_history.push({
|
|
1383
|
+
work_item_id,
|
|
1384
|
+
title: spec["title"] || "",
|
|
1385
|
+
completed_at: retro.completed_at,
|
|
1386
|
+
classification_correct,
|
|
1387
|
+
step_accuracy_pct: steps_accuracy_pct,
|
|
1388
|
+
pre_pr_attempts,
|
|
1389
|
+
scope_creep: scope_creep_detected
|
|
1390
|
+
});
|
|
1391
|
+
if (insights.task_history.length > 50)
|
|
1392
|
+
insights.task_history.shift();
|
|
1393
|
+
if (classifier_feedback) {
|
|
1394
|
+
for (const [key, value] of Object.entries(classifier_feedback)) {
|
|
1395
|
+
if (value) {
|
|
1396
|
+
let signalType = key.replace('signal_', '');
|
|
1397
|
+
if (key === 'root_cause_phrase')
|
|
1398
|
+
signalType = 'phrase';
|
|
1399
|
+
else if (key === 'bug_category')
|
|
1400
|
+
signalType = 'bug_category';
|
|
1401
|
+
else if (key === 'tier_upgrade')
|
|
1402
|
+
signalType = 'tier_upgrade';
|
|
1403
|
+
const existing = insights.classifier_learning_queue.find((q) => q.signal_type === signalType && q.signal_value === value);
|
|
1404
|
+
if (existing) {
|
|
1405
|
+
existing.frequency += 1;
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
insights.classifier_learning_queue.push({
|
|
1409
|
+
from_task: work_item_id,
|
|
1410
|
+
signal_type: signalType,
|
|
1411
|
+
signal_value: value,
|
|
1412
|
+
frequency: 1,
|
|
1413
|
+
applied: false
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!fs.existsSync(resolve(".secufusion")))
|
|
1420
|
+
fs.mkdirSync(resolve(".secufusion"), { recursive: true });
|
|
1421
|
+
writeFile(insightsFile, JSON.stringify(insights, null, 2));
|
|
1422
|
+
let out = `📊 RETROSPECTIVE — ${work_item_id}: ${spec["title"] || ""}\n─────────────────────────────────────────\n\n`;
|
|
1423
|
+
out += `🎯 Classification\nPredicted : ${retro.classification.predicted} (${retro.classification.predicted_confidence} confidence)\n`;
|
|
1424
|
+
out += `Correct : ${classification_correct ? "yes" : "no"}\n`;
|
|
1425
|
+
if (!classification_correct)
|
|
1426
|
+
out += `Should have been: ${classification_correction_notes}\n`;
|
|
1427
|
+
out += `\n📋 Plan Accuracy\nSteps : Planned ${steps_planned} → Actual ${steps_actual} (${steps_accuracy_pct}% accurate)\n`;
|
|
1428
|
+
out += `Files : ${files_planned.length} planned, ${unplanned_files.length} unplanned, ${missed_files.length} missed\n`;
|
|
1429
|
+
out += `Migration : Predicted ${migration_risk_predicted} → Actual ${migration_risk_actual || "NONE"} ${migration_prediction_correct ? "✅" : "❌"}\n`;
|
|
1430
|
+
out += `Breaking : Predicted ${breaking_changes_predicted} → Actual ${breaking_changes_actual || 0} ${breaking_prediction_correct ? "✅" : "❌"}\n`;
|
|
1431
|
+
out += `Performance: ${performance_issues_found ? "Issues found ❌" : "No issues ✅"}\n`;
|
|
1432
|
+
out += `\n✅ Quality\nPre-PR attempts : ${pre_pr_attempts} (${pre_pr_attempts === 1 ? "perfect" : "needed fixes"})\n`;
|
|
1433
|
+
out += `Checks failed : ${checks_that_failed.length > 0 ? checks_that_failed.join(", ") : "none"}\n`;
|
|
1434
|
+
out += `Scope creep : ${scope_creep_detected ? "yes" : "no"}\n`;
|
|
1435
|
+
if (scope_creep_detected)
|
|
1436
|
+
out += `${scope_creep_description}\n`;
|
|
1437
|
+
out += `\n🧪 Scenario Coverage\nSad paths : ${sad_paths} planned → ${retro.scenarios.sad_paths_covered_in_tests} covered (${scenario_coverage_pct}%)\n`;
|
|
1438
|
+
out += `Edge cases : ${edge_cases} planned → ${retro.scenarios.edge_cases_covered_in_tests} covered (${scenario_coverage_pct}%)\n`;
|
|
1439
|
+
out += `Security : ${security_cases} planned → ${retro.scenarios.security_cases_covered_in_tests} covered (${scenario_coverage_pct}%)\n`;
|
|
1440
|
+
out += `\n💡 Learnings\nWent well : ${what_went_well.length > 0 ? what_went_well.join(", ") : "none"}\n`;
|
|
1441
|
+
out += `Harder than expected: ${what_was_harder.length > 0 ? what_was_harder.join(", ") : "none"}\n`;
|
|
1442
|
+
out += `\n📈 Session Totals (from retrospective-insights.json)\nTasks completed : ${insights.total_tasks_completed}\n`;
|
|
1443
|
+
out += `Avg plan accuracy : ${insights.plan_accuracy.avg_step_accuracy_pct}%\n`;
|
|
1444
|
+
out += `Avg pre-PR attempts: ${insights.quality_trends.avg_pre_pr_attempts}\n`;
|
|
1445
|
+
out += `Classification accuracy: ${insights.classification_accuracy.accuracy_pct}%\n`;
|
|
1446
|
+
out += `─────────────────────────────────────────\n`;
|
|
1447
|
+
if (classifier_feedback && Object.keys(classifier_feedback).length > 0) {
|
|
1448
|
+
out += `\n🧠 Classifier feedback queued:\n`;
|
|
1449
|
+
for (const [key, value] of Object.entries(classifier_feedback)) {
|
|
1450
|
+
if (value) {
|
|
1451
|
+
let signalType = key.replace('signal_', '');
|
|
1452
|
+
if (key === 'root_cause_phrase')
|
|
1453
|
+
signalType = 'phrase';
|
|
1454
|
+
else if (key === 'bug_category')
|
|
1455
|
+
signalType = 'bug_category';
|
|
1456
|
+
else if (key === 'tier_upgrade')
|
|
1457
|
+
signalType = 'tier_upgrade';
|
|
1458
|
+
const queued = insights.classifier_learning_queue.find((q) => q.signal_type === signalType && q.signal_value === value);
|
|
1459
|
+
out += `${signalType}: '${value}'\nThis will improve future classify_task results.\n${queued?.frequency || 1} task(s) have reported this signal.\n`;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
|
|
1464
|
+
});
|
|
1125
1465
|
// ─── Tool 9: classify_task ────────────────────────────────────────────────────
|
|
1126
1466
|
server.tool("classify_task", "MANDATORY FIRST STEP for every task without exception. " +
|
|
1127
1467
|
"Classifies a task as BACKEND_ONLY, FRONTEND_ONLY, FULL_STACK, or EXTENSION_ONLY. " +
|
|
@@ -1186,20 +1526,60 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
|
|
|
1186
1526
|
const inRegistry = registry.some(r => r["work_item_id"] === work_item_id);
|
|
1187
1527
|
if (inRegistry)
|
|
1188
1528
|
resumingExistingTask = true;
|
|
1529
|
+
// ── Pass 0: Retrospective Intelligence ────────────────────────────────
|
|
1530
|
+
const insightsFile = resolve(".secufusion/retrospective-insights.json");
|
|
1531
|
+
let learnedLogs = [];
|
|
1532
|
+
let learnedSignals = { backend: [], frontend: [], extension: [], phrase: [], bug_category: [] };
|
|
1533
|
+
if (fs.existsSync(insightsFile)) {
|
|
1534
|
+
try {
|
|
1535
|
+
const insights = readJson(insightsFile);
|
|
1536
|
+
if (insights && Array.isArray(insights.classifier_learning_queue)) {
|
|
1537
|
+
let updated = false;
|
|
1538
|
+
insights.classifier_learning_queue.forEach((q) => {
|
|
1539
|
+
if (q.frequency >= 2) {
|
|
1540
|
+
if (q.signal_type === "backend")
|
|
1541
|
+
learnedSignals.backend.push(q.signal_value);
|
|
1542
|
+
else if (q.signal_type === "frontend")
|
|
1543
|
+
learnedSignals.frontend.push(q.signal_value);
|
|
1544
|
+
else if (q.signal_type === "extension")
|
|
1545
|
+
learnedSignals.extension.push(q.signal_value);
|
|
1546
|
+
else if (q.signal_type === "phrase")
|
|
1547
|
+
learnedSignals.phrase.push(q.signal_value);
|
|
1548
|
+
else if (q.signal_type === "bug_category")
|
|
1549
|
+
learnedSignals.bug_category.push(q.signal_value);
|
|
1550
|
+
if (!q.applied) {
|
|
1551
|
+
learnedLogs.push(`[LEARNED] applying signal '${q.signal_value}' from ${q.frequency} past retrospectives`);
|
|
1552
|
+
q.applied = true;
|
|
1553
|
+
updated = true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
if (updated) {
|
|
1558
|
+
writeFile(insightsFile, JSON.stringify(insights, null, 2));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
catch (e) {
|
|
1563
|
+
/* ignore */
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1189
1566
|
// ── Step 4: Signal scoring ────────────────────────────────────────────
|
|
1190
1567
|
const corpus = (title + " " + description).toLowerCase();
|
|
1191
1568
|
const BACKEND_SIGNALS = [
|
|
1192
1569
|
"api", "endpoint", "service", "repository", "database", "db", "query",
|
|
1193
1570
|
"migration", "flyway", "kafka", "consumer", "producer", "topic",
|
|
1194
|
-
"backend", "server", "rest", "json response", "payload"
|
|
1571
|
+
"backend", "server", "rest", "json response", "payload",
|
|
1572
|
+
...learnedSignals.backend, ...learnedSignals.phrase, ...learnedSignals.bug_category
|
|
1195
1573
|
];
|
|
1196
1574
|
const FRONTEND_SIGNALS = [
|
|
1197
1575
|
"ui", "frontend", "react", "component", "page", "screen", "button",
|
|
1198
|
-
"modal", "layout", "style", "css", "web ui", "browser ui", "render"
|
|
1576
|
+
"modal", "layout", "style", "css", "web ui", "browser ui", "render",
|
|
1577
|
+
...learnedSignals.frontend
|
|
1199
1578
|
];
|
|
1200
1579
|
const EXTENSION_SIGNALS = [
|
|
1201
1580
|
"chrome extension", "extension", "content script", "background script",
|
|
1202
|
-
"browser extension", "popup"
|
|
1581
|
+
"browser extension", "popup",
|
|
1582
|
+
...learnedSignals.extension
|
|
1203
1583
|
];
|
|
1204
1584
|
if (spec) {
|
|
1205
1585
|
if (spec.microservices) {
|
|
@@ -1262,7 +1642,18 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
|
|
|
1262
1642
|
// CHECK 1: Title accuracy
|
|
1263
1643
|
const verbs = ["delete", "create", "fetch", "update", "add", "remove", "fix", "implement"];
|
|
1264
1644
|
const nouns = ["tenant", "admin", "device", "policy", "gateway", "notification", "audit", "role", "permission", "user", "group", "mapping", "api", "endpoint", "dashboard", "db", "database"];
|
|
1265
|
-
const services = [
|
|
1645
|
+
const services = [];
|
|
1646
|
+
if (spec) {
|
|
1647
|
+
if (spec.microservices)
|
|
1648
|
+
Object.keys(spec.microservices).forEach(k => services.push(k.toLowerCase()));
|
|
1649
|
+
if (spec.frontend && spec.frontend.repo)
|
|
1650
|
+
services.push(spec.frontend.repo.toLowerCase());
|
|
1651
|
+
if (spec.chrome_extension && spec.chrome_extension.repo)
|
|
1652
|
+
services.push(spec.chrome_extension.repo.toLowerCase());
|
|
1653
|
+
}
|
|
1654
|
+
if (services.length === 0) {
|
|
1655
|
+
services.push("sfn-iam-api", "sfn-events-api", "sfn-tenants-api", "sfn-policy-api", "sfn-gateway-api", "sfn-web-ui");
|
|
1656
|
+
}
|
|
1266
1657
|
const foundConcepts = [];
|
|
1267
1658
|
[...verbs, ...nouns, ...services].forEach(word => {
|
|
1268
1659
|
if (descLower.includes(word))
|
|
@@ -1427,6 +1818,16 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
|
|
|
1427
1818
|
"column", "table", "migration", "flyway", "entity", "@entity",
|
|
1428
1819
|
"schema", "alter", "add column", "drop column", "rename column",
|
|
1429
1820
|
];
|
|
1821
|
+
if (spec && spec.microservices) {
|
|
1822
|
+
Object.values(spec.microservices).forEach(svc => {
|
|
1823
|
+
if (Array.isArray(svc.owns_tables)) {
|
|
1824
|
+
svc.owns_tables.forEach(t => DB_CHANGE_SIGNALS.push(t.toLowerCase()));
|
|
1825
|
+
}
|
|
1826
|
+
if (Array.isArray(svc.kafka_consumes)) {
|
|
1827
|
+
svc.kafka_consumes.forEach(topic => KAFKA_CHANGE_SIGNALS.push(topic.toLowerCase()));
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1430
1831
|
const endpointRisk = ENDPOINT_CHANGE_SIGNALS.some(s => corpus.includes(s));
|
|
1431
1832
|
const kafkaRisk = KAFKA_CHANGE_SIGNALS.some(s => corpus.includes(s));
|
|
1432
1833
|
const dbRisk = DB_CHANGE_SIGNALS.some(s => corpus.includes(s));
|
|
@@ -1541,6 +1942,10 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
|
|
|
1541
1942
|
finalDeveloperMessage += "\n────────────────────────────────────────\n";
|
|
1542
1943
|
}
|
|
1543
1944
|
}
|
|
1945
|
+
if (learnedLogs.length > 0) {
|
|
1946
|
+
finalDeveloperMessage += `🧠 RETROSPECTIVE INTELLIGENCE APPLIED\n`;
|
|
1947
|
+
finalDeveloperMessage += learnedLogs.join("\n") + "\n\n";
|
|
1948
|
+
}
|
|
1544
1949
|
finalDeveloperMessage += developerMessage; // The original classification message
|
|
1545
1950
|
developerMessage = finalDeveloperMessage;
|
|
1546
1951
|
// Collect relevant rejected patterns (keyword match against corpus)
|