@kaddo/mcp 3.24.0 → 3.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +363 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2086,6 +2086,129 @@ function renderExplanationAgent(exp) {
|
|
|
2086
2086
|
return JSON.stringify({ ...rest, mapped_modules: mappedModules }, null, 2) + "\n";
|
|
2087
2087
|
}
|
|
2088
2088
|
|
|
2089
|
+
// ../cli/src/core/guard-history.ts
|
|
2090
|
+
var HISTORY_DIR = ".kaddo/history";
|
|
2091
|
+
var RUNS_PATH = `${HISTORY_DIR}/guard-runs.jsonl`;
|
|
2092
|
+
var SUMMARY_PATH = `${HISTORY_DIR}/guard-summary.json`;
|
|
2093
|
+
function toPosix5(p) {
|
|
2094
|
+
return p.replace(/\\/g, "/");
|
|
2095
|
+
}
|
|
2096
|
+
function loadGuardRuns(dir) {
|
|
2097
|
+
const p = join(dir, RUNS_PATH);
|
|
2098
|
+
if (!exists(p)) return [];
|
|
2099
|
+
const out = [];
|
|
2100
|
+
for (const line of readFile(p).split(/\r?\n/)) {
|
|
2101
|
+
const t = line.trim();
|
|
2102
|
+
if (!t) continue;
|
|
2103
|
+
try {
|
|
2104
|
+
out.push(JSON.parse(t));
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return out;
|
|
2109
|
+
}
|
|
2110
|
+
function resolveDriftThreads(runs) {
|
|
2111
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
2112
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2113
|
+
for (const r of sorted) for (const w of r.warnings) paths.add(w.code_path);
|
|
2114
|
+
const threads = [];
|
|
2115
|
+
for (const codePath of paths) {
|
|
2116
|
+
const related = /* @__PURE__ */ new Set();
|
|
2117
|
+
let firstDetected = "";
|
|
2118
|
+
let lastWarn = "";
|
|
2119
|
+
for (const r of sorted) {
|
|
2120
|
+
const w = r.warnings.find((x) => x.code_path === codePath);
|
|
2121
|
+
if (w) {
|
|
2122
|
+
if (!firstDetected) firstDetected = r.generated_at;
|
|
2123
|
+
lastWarn = r.generated_at;
|
|
2124
|
+
for (const a of w.related_artifacts) related.add(a);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
let resolvedAt;
|
|
2128
|
+
for (const r of sorted) {
|
|
2129
|
+
if (r.generated_at <= lastWarn) continue;
|
|
2130
|
+
const touched = r.touched_files.some((f) => toPosix5(f) === codePath);
|
|
2131
|
+
const warnedHere = r.warnings.some((x) => x.code_path === codePath);
|
|
2132
|
+
const updatedRelated = r.updated_artifacts.some((a) => related.has(a));
|
|
2133
|
+
if (touched && updatedRelated && !warnedHere) {
|
|
2134
|
+
resolvedAt = r.generated_at;
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
threads.push({
|
|
2139
|
+
code_path: codePath,
|
|
2140
|
+
related_artifacts: [...related],
|
|
2141
|
+
first_detected: firstDetected,
|
|
2142
|
+
last_seen: lastWarn,
|
|
2143
|
+
status: resolvedAt ? "resolved" : "open",
|
|
2144
|
+
...resolvedAt ? { resolved_at: resolvedAt } : {}
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
return threads;
|
|
2148
|
+
}
|
|
2149
|
+
function hotspotDir(codePath) {
|
|
2150
|
+
const i = codePath.lastIndexOf("/");
|
|
2151
|
+
return i >= 0 ? codePath.slice(0, i + 1) : codePath;
|
|
2152
|
+
}
|
|
2153
|
+
function computeTrend(runs) {
|
|
2154
|
+
if (runs.length < 2) return { direction: "unknown", reason: "Not enough recorded runs to detect a trend." };
|
|
2155
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
2156
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2157
|
+
const avg = (arr) => arr.length ? arr.reduce((s, r) => s + r.warnings.length, 0) / arr.length : 0;
|
|
2158
|
+
const earlier = avg(sorted.slice(0, mid));
|
|
2159
|
+
const recent = avg(sorted.slice(mid));
|
|
2160
|
+
if (recent < earlier) return { direction: "improving", reason: "Fewer warnings in the last recorded runs." };
|
|
2161
|
+
if (recent > earlier) return { direction: "worsening", reason: "More warnings in the last recorded runs." };
|
|
2162
|
+
return { direction: "stable", reason: "Warning volume is roughly stable across recorded runs." };
|
|
2163
|
+
}
|
|
2164
|
+
function buildGuardHistory(dir) {
|
|
2165
|
+
const runs = loadGuardRuns(dir);
|
|
2166
|
+
if (runs.length === 0) {
|
|
2167
|
+
return {
|
|
2168
|
+
available: false,
|
|
2169
|
+
total_runs: 0,
|
|
2170
|
+
first_run: null,
|
|
2171
|
+
last_run: null,
|
|
2172
|
+
runs_with_warnings: 0,
|
|
2173
|
+
runs_clean: 0,
|
|
2174
|
+
detected: 0,
|
|
2175
|
+
open: 0,
|
|
2176
|
+
resolved: 0,
|
|
2177
|
+
resolution_rate: 0,
|
|
2178
|
+
threads: [],
|
|
2179
|
+
hotspots: [],
|
|
2180
|
+
trend: { direction: "unknown", reason: "No guard history recorded yet." }
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
2184
|
+
const threads = resolveDriftThreads(sorted);
|
|
2185
|
+
const open = threads.filter((t) => t.status === "open").length;
|
|
2186
|
+
const resolved = threads.filter((t) => t.status === "resolved").length;
|
|
2187
|
+
const detected = threads.length;
|
|
2188
|
+
const runsWithWarnings = sorted.filter((r) => r.warnings.length > 0).length;
|
|
2189
|
+
const hotspotMap = /* @__PURE__ */ new Map();
|
|
2190
|
+
for (const t of threads) {
|
|
2191
|
+
const d = hotspotDir(t.code_path);
|
|
2192
|
+
hotspotMap.set(d, (hotspotMap.get(d) ?? 0) + 1);
|
|
2193
|
+
}
|
|
2194
|
+
const hotspots = [...hotspotMap.entries()].map(([path3, warnings]) => ({ path: path3, warnings })).sort((a, b) => b.warnings - a.warnings);
|
|
2195
|
+
return {
|
|
2196
|
+
available: true,
|
|
2197
|
+
total_runs: sorted.length,
|
|
2198
|
+
first_run: sorted[0].generated_at,
|
|
2199
|
+
last_run: sorted[sorted.length - 1].generated_at,
|
|
2200
|
+
runs_with_warnings: runsWithWarnings,
|
|
2201
|
+
runs_clean: sorted.length - runsWithWarnings,
|
|
2202
|
+
detected,
|
|
2203
|
+
open,
|
|
2204
|
+
resolved,
|
|
2205
|
+
resolution_rate: detected > 0 ? Math.round(resolved / detected * 1e3) / 10 : 0,
|
|
2206
|
+
threads,
|
|
2207
|
+
hotspots,
|
|
2208
|
+
trend: computeTrend(sorted)
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2089
2212
|
// ../cli/src/core/impact-report.ts
|
|
2090
2213
|
function isBroadGlob(glob) {
|
|
2091
2214
|
if (!glob.endsWith("/**")) return false;
|
|
@@ -2337,10 +2460,10 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
2337
2460
|
ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
|
|
2338
2461
|
},
|
|
2339
2462
|
graph_quality: g.available ? { available: true, scope: g.scope, quality: g.quality, nodes: g.nodes, edges: g.edges, hints: g.hints, reason: g.scopeReason, last_exported: g.generatedAt } : { available: false, suggestion: "Run `kaddo graph export --scope all`." },
|
|
2340
|
-
guard_activity: {
|
|
2341
|
-
|
|
2342
|
-
note: "Guard history is not persisted. Run `kaddo guard` to
|
|
2343
|
-
},
|
|
2463
|
+
guard_activity: (() => {
|
|
2464
|
+
const gh = buildGuardHistory(dir);
|
|
2465
|
+
return gh.available ? { available: true, runs_recorded: gh.total_runs, detected: gh.detected, open: gh.open, resolved: gh.resolved, resolution_rate: gh.resolution_rate } : { available: false, note: "Guard history is not persisted. Run `kaddo guard --record` to record runs for trend analysis." };
|
|
2466
|
+
})(),
|
|
2344
2467
|
impact_signals,
|
|
2345
2468
|
actionable_gaps: gaps,
|
|
2346
2469
|
score,
|
|
@@ -2429,8 +2552,18 @@ function renderImpactMarkdown(r) {
|
|
|
2429
2552
|
}
|
|
2430
2553
|
L.push("");
|
|
2431
2554
|
L.push("## Guard Activity", "");
|
|
2432
|
-
|
|
2433
|
-
|
|
2555
|
+
if (r.guard_activity.available) {
|
|
2556
|
+
const ga = r.guard_activity;
|
|
2557
|
+
L.push("- Guard history: available");
|
|
2558
|
+
L.push(`- Guard runs recorded: ${ga.runs_recorded}`);
|
|
2559
|
+
L.push(`- Drift warnings detected: ${ga.detected}`);
|
|
2560
|
+
L.push(`- Open warnings: ${ga.open}`);
|
|
2561
|
+
L.push(`- Resolved warnings: ${ga.resolved}`);
|
|
2562
|
+
L.push(`- Resolution rate: ${ga.resolution_rate}%`);
|
|
2563
|
+
} else {
|
|
2564
|
+
L.push("- Guard history: not available");
|
|
2565
|
+
L.push(`- Note: ${r.guard_activity.note}`);
|
|
2566
|
+
}
|
|
2434
2567
|
L.push("");
|
|
2435
2568
|
L.push("## Impact Signals", "");
|
|
2436
2569
|
const s = r.impact_signals;
|
|
@@ -2568,25 +2701,59 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
2568
2701
|
const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
|
|
2569
2702
|
const graphMult = GRAPH_MULT[graphQuality] ?? 0;
|
|
2570
2703
|
const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
|
|
2571
|
-
const
|
|
2704
|
+
const guard = buildGuardHistory(dir);
|
|
2705
|
+
let driftSavings;
|
|
2706
|
+
if (!guard.available) {
|
|
2707
|
+
driftSavings = { available: false, hours: 0, reason: "Guard history is not persisted yet." };
|
|
2708
|
+
} else if (guard.resolved > 0) {
|
|
2709
|
+
driftSavings = {
|
|
2710
|
+
available: true,
|
|
2711
|
+
hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
|
|
2712
|
+
formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
|
|
2713
|
+
};
|
|
2714
|
+
} else {
|
|
2715
|
+
driftSavings = {
|
|
2716
|
+
available: true,
|
|
2717
|
+
hours: 0,
|
|
2718
|
+
formula: `0 resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`,
|
|
2719
|
+
reason: "Guard history exists, but no resolved drift warnings have been recorded yet."
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
|
|
2572
2723
|
const totalValue = Math.round(totalHours * a.hourly_cost);
|
|
2573
2724
|
const reasons = [];
|
|
2574
2725
|
let level;
|
|
2575
2726
|
if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
|
|
2576
2727
|
level = "Low";
|
|
2577
2728
|
reasons.push("Impact score is low or the knowledge graph is empty.");
|
|
2729
|
+
} else if (impact.score >= 85 && graphQuality === "good" && source === "file" && guard.available && guard.resolved > 0) {
|
|
2730
|
+
level = "High";
|
|
2731
|
+
reasons.push("Strong evidence, custom assumptions and recorded drift resolution.");
|
|
2578
2732
|
} else {
|
|
2579
2733
|
level = "Medium";
|
|
2580
2734
|
reasons.push("Strong knowledge and graph evidence.");
|
|
2581
2735
|
}
|
|
2582
|
-
|
|
2736
|
+
if (!guard.available) {
|
|
2737
|
+
reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
|
|
2738
|
+
} else if (guard.resolved > 0) {
|
|
2739
|
+
reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
|
|
2740
|
+
} else {
|
|
2741
|
+
reasons.push("Guard history available, but no resolved drift warnings have been recorded yet.");
|
|
2742
|
+
}
|
|
2583
2743
|
reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
|
|
2584
2744
|
const actions = [];
|
|
2585
2745
|
if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
|
|
2586
2746
|
actions.push("Calibrate hourly cost and team assumptions with real team data.");
|
|
2587
2747
|
actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
|
|
2588
2748
|
actions.push("Use this report as directional evidence, not accounting data.");
|
|
2589
|
-
|
|
2749
|
+
if (!guard.available) {
|
|
2750
|
+
actions.push("Future: persist Guard history to estimate avoided rework.");
|
|
2751
|
+
} else if (guard.resolved === 0) {
|
|
2752
|
+
actions.push("Continue running `kaddo guard --record` before important commits to capture resolved drift warnings.");
|
|
2753
|
+
if (guard.open > 0) actions.push("Review open drift warnings with `kaddo drift`.");
|
|
2754
|
+
} else {
|
|
2755
|
+
actions.push("Use drift history to calibrate `rework_hours_avoided_per_resolved_drift`.");
|
|
2756
|
+
}
|
|
2590
2757
|
return {
|
|
2591
2758
|
generated_at: now.toISOString(),
|
|
2592
2759
|
project: impact.project,
|
|
@@ -2612,7 +2779,9 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
2612
2779
|
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
2613
2780
|
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
2614
2781
|
context_readiness: readiness,
|
|
2615
|
-
guard_history_available:
|
|
2782
|
+
guard_history_available: guard.available,
|
|
2783
|
+
guard_runs_recorded: guard.total_runs,
|
|
2784
|
+
resolved_drift_warnings: guard.resolved
|
|
2616
2785
|
},
|
|
2617
2786
|
estimated_savings: {
|
|
2618
2787
|
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
@@ -2620,7 +2789,7 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
2620
2789
|
clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
|
|
2621
2790
|
onboarding: { hours: onboardH, formula: `${a.expected_new_contributors_per_month} contributor \xD7 ${a.onboarding_hours_saved_per_new_contributor} h \xD7 ${readinessMult.toFixed(2)} readiness multiplier` },
|
|
2622
2791
|
architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
|
|
2623
|
-
drift_prevention:
|
|
2792
|
+
drift_prevention: driftSavings
|
|
2624
2793
|
},
|
|
2625
2794
|
total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
|
|
2626
2795
|
confidence: { level, reasons },
|
|
@@ -2662,7 +2831,13 @@ function renderSavingsMarkdown(r) {
|
|
|
2662
2831
|
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
2663
2832
|
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
2664
2833
|
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
2665
|
-
|
|
2834
|
+
if (e.guard_history_available) {
|
|
2835
|
+
L.push("- Guard history: available");
|
|
2836
|
+
L.push(`- Guard runs recorded: ${e.guard_runs_recorded}`);
|
|
2837
|
+
L.push(`- Resolved drift warnings: ${e.resolved_drift_warnings}`);
|
|
2838
|
+
} else {
|
|
2839
|
+
L.push("- Guard history: not available");
|
|
2840
|
+
}
|
|
2666
2841
|
L.push("");
|
|
2667
2842
|
L.push("## Estimated Savings", "");
|
|
2668
2843
|
const s = r.estimated_savings;
|
|
@@ -2677,8 +2852,15 @@ function renderSavingsMarkdown(r) {
|
|
|
2677
2852
|
line("Onboarding", s.onboarding);
|
|
2678
2853
|
line("Architecture Discovery", s.architecture_discovery);
|
|
2679
2854
|
L.push("### Drift Prevention", "");
|
|
2680
|
-
|
|
2681
|
-
|
|
2855
|
+
if (s.drift_prevention.available) {
|
|
2856
|
+
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
2857
|
+
L.push(`- Estimated: ${s.drift_prevention.hours} h`);
|
|
2858
|
+
if (s.drift_prevention.reason) L.push(`- Reason: ${s.drift_prevention.reason}`);
|
|
2859
|
+
L.push("");
|
|
2860
|
+
} else {
|
|
2861
|
+
L.push("- Not available yet.");
|
|
2862
|
+
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
2863
|
+
}
|
|
2682
2864
|
L.push("## Total", "");
|
|
2683
2865
|
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
|
|
2684
2866
|
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
@@ -2696,6 +2878,118 @@ function serializeSavingsJson(r) {
|
|
|
2696
2878
|
return JSON.stringify(r, null, 2) + "\n";
|
|
2697
2879
|
}
|
|
2698
2880
|
|
|
2881
|
+
// ../cli/src/core/drift-report.ts
|
|
2882
|
+
function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
2883
|
+
const h = buildGuardHistory(dir);
|
|
2884
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
2885
|
+
const actions = [];
|
|
2886
|
+
if (!h.available) {
|
|
2887
|
+
actions.push("Run `kaddo guard --record` to start recording guard history.");
|
|
2888
|
+
} else {
|
|
2889
|
+
if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
|
|
2890
|
+
actions.push("Run `kaddo guard --record` before important commits.");
|
|
2891
|
+
actions.push("Use this report to calibrate estimated drift-prevention savings.");
|
|
2892
|
+
}
|
|
2893
|
+
return {
|
|
2894
|
+
generated_at: now.toISOString(),
|
|
2895
|
+
project,
|
|
2896
|
+
guard_history: {
|
|
2897
|
+
available: h.available,
|
|
2898
|
+
total_runs: h.total_runs,
|
|
2899
|
+
first_run: h.first_run,
|
|
2900
|
+
last_run: h.last_run,
|
|
2901
|
+
runs_with_warnings: h.runs_with_warnings,
|
|
2902
|
+
runs_clean: h.runs_clean
|
|
2903
|
+
},
|
|
2904
|
+
drift_warnings: { detected: h.detected, open: h.open, resolved: h.resolved, resolution_rate: h.resolution_rate },
|
|
2905
|
+
hotspots: h.hotspots,
|
|
2906
|
+
trend: h.trend,
|
|
2907
|
+
threads: h.threads,
|
|
2908
|
+
suggested_actions: actions
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
function renderDriftMarkdown(r) {
|
|
2912
|
+
const L = [];
|
|
2913
|
+
L.push("# Kaddo Drift Trend Report", "");
|
|
2914
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
2915
|
+
L.push(`Project: ${r.project}`);
|
|
2916
|
+
L.push("");
|
|
2917
|
+
if (!r.guard_history.available) {
|
|
2918
|
+
L.push("## Guard History", "");
|
|
2919
|
+
L.push("No guard history recorded yet.");
|
|
2920
|
+
L.push("");
|
|
2921
|
+
L.push("Run `kaddo guard --record` to start recording drift evidence.");
|
|
2922
|
+
L.push("");
|
|
2923
|
+
L.push("## Suggested Actions", "");
|
|
2924
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
2925
|
+
L.push("");
|
|
2926
|
+
return L.join("\n");
|
|
2927
|
+
}
|
|
2928
|
+
const w = r.drift_warnings;
|
|
2929
|
+
L.push("## Executive Summary", "");
|
|
2930
|
+
L.push(`- Guard runs recorded: ${r.guard_history.total_runs}`);
|
|
2931
|
+
L.push(`- Drift warnings detected: ${w.detected}`);
|
|
2932
|
+
L.push(`- Open warnings: ${w.open}`);
|
|
2933
|
+
L.push(`- Resolved warnings: ${w.resolved}`);
|
|
2934
|
+
L.push(`- Resolution rate: ${w.resolution_rate}%`);
|
|
2935
|
+
if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
|
|
2936
|
+
L.push("");
|
|
2937
|
+
L.push("## Guard History", "");
|
|
2938
|
+
L.push(`- First run: ${r.guard_history.first_run}`);
|
|
2939
|
+
L.push(`- Last run: ${r.guard_history.last_run}`);
|
|
2940
|
+
L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
|
|
2941
|
+
L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
|
|
2942
|
+
L.push("");
|
|
2943
|
+
L.push("## Drift Warnings", "");
|
|
2944
|
+
const open = r.threads.filter((t) => t.status === "open");
|
|
2945
|
+
const resolved = r.threads.filter((t) => t.status === "resolved");
|
|
2946
|
+
L.push("### Open", "");
|
|
2947
|
+
if (open.length === 0) L.push("_None._", "");
|
|
2948
|
+
for (const t of open) {
|
|
2949
|
+
L.push(`- ${t.code_path}`);
|
|
2950
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
2951
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
2952
|
+
L.push(` - Last seen: ${t.last_seen}`);
|
|
2953
|
+
L.push(" - Status: open");
|
|
2954
|
+
}
|
|
2955
|
+
L.push("");
|
|
2956
|
+
L.push("### Resolved", "");
|
|
2957
|
+
if (resolved.length === 0) L.push("_None._", "");
|
|
2958
|
+
for (const t of resolved) {
|
|
2959
|
+
L.push(`- ${t.code_path}`);
|
|
2960
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
2961
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
2962
|
+
L.push(` - Resolved at: ${t.resolved_at}`);
|
|
2963
|
+
L.push(" - Status: resolved");
|
|
2964
|
+
}
|
|
2965
|
+
L.push("");
|
|
2966
|
+
L.push("## Hotspots", "");
|
|
2967
|
+
if (r.hotspots.length === 0) L.push("_None._");
|
|
2968
|
+
for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
|
|
2969
|
+
L.push("");
|
|
2970
|
+
L.push("## Trend", "");
|
|
2971
|
+
L.push(`- Recent direction: ${r.trend.direction}`);
|
|
2972
|
+
L.push(`- Reason: ${r.trend.reason}`);
|
|
2973
|
+
L.push("");
|
|
2974
|
+
L.push("## Suggested Actions", "");
|
|
2975
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
2976
|
+
L.push("");
|
|
2977
|
+
return L.join("\n");
|
|
2978
|
+
}
|
|
2979
|
+
function serializeDriftJson(r) {
|
|
2980
|
+
const out = {
|
|
2981
|
+
generated_at: r.generated_at,
|
|
2982
|
+
project: r.project,
|
|
2983
|
+
guard_history: r.guard_history,
|
|
2984
|
+
drift_warnings: r.drift_warnings,
|
|
2985
|
+
hotspots: r.hotspots,
|
|
2986
|
+
trend: r.trend,
|
|
2987
|
+
threads: r.threads,
|
|
2988
|
+
suggested_actions: r.suggested_actions
|
|
2989
|
+
};
|
|
2990
|
+
return JSON.stringify(out, null, 2) + "\n";
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2699
2993
|
// src/workitems.ts
|
|
2700
2994
|
import matter7 from "gray-matter";
|
|
2701
2995
|
var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
|
|
@@ -2922,6 +3216,30 @@ var RESOURCES = [
|
|
|
2922
3216
|
}
|
|
2923
3217
|
return [{ uri: "kaddo://savings-report", text: renderSavingsMarkdown(buildSavingsReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
|
|
2924
3218
|
}
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
uri: "kaddo://drift-report",
|
|
3222
|
+
name: "Kaddo drift report",
|
|
3223
|
+
description: "Drift Trend Report \u2014 the last written report, or generated in memory (read-only).",
|
|
3224
|
+
mimeType: "text/markdown",
|
|
3225
|
+
read: (root) => {
|
|
3226
|
+
const saved = readText(root, ".kaddo/reports/drift-report.md");
|
|
3227
|
+
if (saved) return [{ uri: "kaddo://drift-report", text: saved, mimeType: "text/markdown" }];
|
|
3228
|
+
return [{ uri: "kaddo://drift-report", text: renderDriftMarkdown(buildDriftReport(root)), mimeType: "text/markdown" }];
|
|
3229
|
+
}
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
uri: "kaddo://guard-history",
|
|
3233
|
+
name: "Kaddo guard history",
|
|
3234
|
+
description: "Recorded guard runs (.kaddo/history/guard-runs.jsonl) \u2014 read-only.",
|
|
3235
|
+
mimeType: "application/jsonl",
|
|
3236
|
+
read: (root) => {
|
|
3237
|
+
const runs = readText(root, ".kaddo/history/guard-runs.jsonl");
|
|
3238
|
+
if (runs === null) {
|
|
3239
|
+
return text("kaddo://guard-history", "No guard history recorded yet. Run `kaddo guard --record`.", "text/plain");
|
|
3240
|
+
}
|
|
3241
|
+
return [{ uri: "kaddo://guard-history", text: runs, mimeType: "application/jsonl" }];
|
|
3242
|
+
}
|
|
2925
3243
|
}
|
|
2926
3244
|
];
|
|
2927
3245
|
|
|
@@ -3716,6 +4034,21 @@ function generateSavingsReport(root, opts = {}) {
|
|
|
3716
4034
|
next_suggested_resources: ["kaddo://savings-report"]
|
|
3717
4035
|
};
|
|
3718
4036
|
}
|
|
4037
|
+
function generateDriftReport(root, opts = {}) {
|
|
4038
|
+
requireConfig(root);
|
|
4039
|
+
const format = opts.format ?? "markdown";
|
|
4040
|
+
const report = buildDriftReport(root);
|
|
4041
|
+
const content = format === "json" ? serializeDriftJson(report) : renderDriftMarkdown(report);
|
|
4042
|
+
const out = opts.output ?? `.kaddo/reports/drift-report.${format === "json" ? "json" : "md"}`;
|
|
4043
|
+
writeDerived(root, out, content);
|
|
4044
|
+
return {
|
|
4045
|
+
status: "ok",
|
|
4046
|
+
files_written: [out.replace(/\\/g, "/")],
|
|
4047
|
+
summary: report.guard_history.available ? `Drift report generated (${report.drift_warnings.open} open, ${report.drift_warnings.resolved} resolved).` : "Drift report generated (no guard history yet \u2014 run `kaddo guard --record`).",
|
|
4048
|
+
warnings: report.guard_history.available ? [] : ["No guard history recorded yet."],
|
|
4049
|
+
next_suggested_resources: ["kaddo://drift-report"]
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
3719
4052
|
function generateCapsuleDraft(root) {
|
|
3720
4053
|
const config = requireConfig(root);
|
|
3721
4054
|
requireKnowledge(root);
|
|
@@ -3933,6 +4266,22 @@ function createServer(root) {
|
|
|
3933
4266
|
() => generateSavingsReport(root, { format: args.format, scope: args.scope, output: args.output })
|
|
3934
4267
|
)
|
|
3935
4268
|
);
|
|
4269
|
+
server.registerTool(
|
|
4270
|
+
"kaddo_generate_drift_report",
|
|
4271
|
+
{
|
|
4272
|
+
title: "Generate drift report",
|
|
4273
|
+
description: `Write the Drift Trend Report under .kaddo/reports/ from recorded guard history. format: markdown (default) or json. Does NOT record guard runs. ${DERIVED_NOTE}`,
|
|
4274
|
+
inputSchema: {
|
|
4275
|
+
format: z2.enum(["markdown", "json"]).optional(),
|
|
4276
|
+
output: z2.string().optional()
|
|
4277
|
+
}
|
|
4278
|
+
},
|
|
4279
|
+
async (args) => generated(
|
|
4280
|
+
root,
|
|
4281
|
+
"generate drift report",
|
|
4282
|
+
() => generateDriftReport(root, { format: args.format, output: args.output })
|
|
4283
|
+
)
|
|
4284
|
+
);
|
|
3936
4285
|
server.registerTool(
|
|
3937
4286
|
"kaddo_generate_capsule_draft",
|
|
3938
4287
|
{ title: "Generate capsule draft", description: `Write a capsule DRAFT under .kaddo/exports/ (never registers it). ${DERIVED_NOTE}`, inputSchema: {} },
|
package/package.json
CHANGED