@kaddo/mcp 3.23.1 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +262 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2102,6 +2102,8 @@ function hasSection(body, re) {
2102
2102
  return body.split(/\r?\n/).some((l) => /^#{1,6}\s+/.test(l) && re.test(l));
2103
2103
  }
2104
2104
  function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2105
+ const resolvedScope = opts.scope ?? "all";
2106
+ const scopeSource = opts.scopeSource ?? (opts.scope ? "explicit" : "default");
2105
2107
  const exp = buildProjectExplanation(dir);
2106
2108
  const wis = discoverWorkItems(dir);
2107
2109
  const total = wis.length;
@@ -2172,19 +2174,13 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2172
2174
  }
2173
2175
  const ownershipOverlaps = gaps.ownership_overlaps.length;
2174
2176
  let g;
2175
- if (opts.scope) {
2176
- const config = loadConfig(dir);
2177
- if (config) {
2178
- const graph = buildGraph(dir, config, { scope: opts.scope }, now);
2179
- const hints = buildGraphHints(dir, graph, now);
2180
- g = { available: true, scope: graph.scope, scopeReason: graph.scope_reason, nodes: graph.nodes.length, edges: graph.edges.length, quality: hints.quality, hints: hints.summary.hints, generatedAt: graph.generated_at };
2181
- } else {
2182
- g = { available: false, scope: opts.scope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
2183
- }
2184
- } else if (exp.graph) {
2185
- g = { available: true, scope: exp.graph.scope, scopeReason: exp.graph.scopeReason, nodes: exp.graph.nodes, edges: exp.graph.edges, quality: exp.graphHints?.quality ?? "unknown", hints: exp.graphHints?.totalHints ?? 0, generatedAt: exp.graph.generatedAt };
2177
+ const config = loadConfig(dir);
2178
+ if (config) {
2179
+ const graph = buildGraph(dir, config, { scope: resolvedScope }, now);
2180
+ const hints = buildGraphHints(dir, graph, now);
2181
+ g = { available: true, scope: graph.scope, scopeReason: graph.scope_reason, nodes: graph.nodes.length, edges: graph.edges.length, quality: hints.quality, hints: hints.summary.hints, generatedAt: graph.generated_at };
2186
2182
  } else {
2187
- g = { available: false, scope: "n/a", scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
2183
+ g = { available: false, scope: resolvedScope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
2188
2184
  }
2189
2185
  const layerStatus = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
2190
2186
  const byState = exp.workItems.byState;
@@ -2282,7 +2278,7 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2282
2278
  };
2283
2279
  const actions = [];
2284
2280
  if (!g.available) actions.push("Run `kaddo graph export --scope all` to inspect full traceability.");
2285
- else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo graph export --scope all` to include completed Work Items.");
2281
+ else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo impact --scope all` to measure accumulated knowledge impact.");
2286
2282
  if (exp.roadmap.remaining > 0) actions.push(`Materialize the ${exp.roadmap.remaining} remaining roadmap candidate(s) with \`kaddo create --from roadmap\`.`);
2287
2283
  for (const a of [
2288
2284
  groupedAction(gaps.missing_code_ownership, "Add `code:` ownership to"),
@@ -2307,6 +2303,8 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2307
2303
  generated_at: now.toISOString(),
2308
2304
  project: exp.project.name,
2309
2305
  scope: g.scope,
2306
+ default_scope: "all",
2307
+ scope_source: scopeSource,
2310
2308
  executive_summary: summary,
2311
2309
  knowledge_health,
2312
2310
  knowledge_coverage,
@@ -2361,6 +2359,11 @@ function renderImpactMarkdown(r) {
2361
2359
  L.push(`Scope: ${r.scope}`);
2362
2360
  if (r.score !== null) L.push(`Knowledge Impact Score: ${r.score}/100`);
2363
2361
  else L.push("Knowledge Impact Score: not available");
2362
+ if (r.scope_source === "default") {
2363
+ L.push("Scope note: Impact reports use `all` by default to measure accumulated knowledge impact.");
2364
+ } else if (r.scope === "active" && r.graph_quality.available === false) {
2365
+ L.push("Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
2366
+ }
2364
2367
  L.push("");
2365
2368
  L.push("## Executive Summary", "");
2366
2369
  for (const s2 of r.executive_summary) L.push(`- ${s2}`);
@@ -2417,6 +2420,9 @@ function renderImpactMarkdown(r) {
2417
2420
  L.push(`- Hints: ${gq.hints}`);
2418
2421
  if (gq.reason) L.push(`- Reason: ${gq.reason}`);
2419
2422
  if (gq.last_exported) L.push(`- Last exported: ${gq.last_exported}`);
2423
+ if (gq.scope === "active" && gq.quality === "empty") {
2424
+ L.push("- Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
2425
+ }
2420
2426
  } else {
2421
2427
  L.push("- Graph data not available.");
2422
2428
  L.push(`- Tip: ${r.graph_quality.suggestion}`);
@@ -2496,6 +2502,200 @@ function serializeImpactJson(r) {
2496
2502
  return JSON.stringify(r, null, 2) + "\n";
2497
2503
  }
2498
2504
 
2505
+ // ../cli/src/core/savings.ts
2506
+ import { parse as parseYaml6 } from "yaml";
2507
+ var DEFAULT_ASSUMPTIONS = {
2508
+ currency: "USD",
2509
+ hourly_cost: 40,
2510
+ context_preparation_minutes_saved_per_work_item: 30,
2511
+ rework_hours_avoided_per_resolved_drift: 2,
2512
+ onboarding_hours_saved_per_new_contributor: 4,
2513
+ review_minutes_saved_per_work_item_with_ownership: 20,
2514
+ clarification_minutes_saved_per_ready_work_item: 25,
2515
+ architecture_discovery_hours_saved_when_graph_good: 3,
2516
+ expected_new_contributors_per_month: 1,
2517
+ expected_work_items_per_month: 8
2518
+ };
2519
+ var SAVINGS_PATH = ".kaddo/savings.yml";
2520
+ function loadAssumptions(dir) {
2521
+ const p = join(dir, SAVINGS_PATH);
2522
+ if (!exists(p)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2523
+ try {
2524
+ const raw = parseYaml6(readFile(p));
2525
+ const a = raw?.assumptions ?? {};
2526
+ const t = raw?.team ?? {};
2527
+ const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
2528
+ return {
2529
+ source: "file",
2530
+ assumptions: {
2531
+ currency: raw?.currency ?? DEFAULT_ASSUMPTIONS.currency,
2532
+ hourly_cost: num(raw?.hourly_cost, DEFAULT_ASSUMPTIONS.hourly_cost),
2533
+ context_preparation_minutes_saved_per_work_item: num(a.context_preparation_minutes_saved_per_work_item, DEFAULT_ASSUMPTIONS.context_preparation_minutes_saved_per_work_item),
2534
+ rework_hours_avoided_per_resolved_drift: num(a.rework_hours_avoided_per_resolved_drift, DEFAULT_ASSUMPTIONS.rework_hours_avoided_per_resolved_drift),
2535
+ onboarding_hours_saved_per_new_contributor: num(a.onboarding_hours_saved_per_new_contributor, DEFAULT_ASSUMPTIONS.onboarding_hours_saved_per_new_contributor),
2536
+ review_minutes_saved_per_work_item_with_ownership: num(a.review_minutes_saved_per_work_item_with_ownership, DEFAULT_ASSUMPTIONS.review_minutes_saved_per_work_item_with_ownership),
2537
+ clarification_minutes_saved_per_ready_work_item: num(a.clarification_minutes_saved_per_ready_work_item, DEFAULT_ASSUMPTIONS.clarification_minutes_saved_per_ready_work_item),
2538
+ architecture_discovery_hours_saved_when_graph_good: num(a.architecture_discovery_hours_saved_when_graph_good, DEFAULT_ASSUMPTIONS.architecture_discovery_hours_saved_when_graph_good),
2539
+ expected_new_contributors_per_month: num(t.expected_new_contributors_per_month, DEFAULT_ASSUMPTIONS.expected_new_contributors_per_month),
2540
+ expected_work_items_per_month: num(t.expected_work_items_per_month, DEFAULT_ASSUMPTIONS.expected_work_items_per_month)
2541
+ }
2542
+ };
2543
+ } catch {
2544
+ return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2545
+ }
2546
+ }
2547
+ var READINESS_MULT = { Low: 0.25, Medium: 0.5, High: 0.75, "Very High": 1 };
2548
+ var GRAPH_MULT = { empty: 0, sparse: 0.25, partial: 0.6, good: 1, unknown: 0 };
2549
+ var round2 = (n) => Math.round(n * 100) / 100;
2550
+ function parseRatioNumerator(s) {
2551
+ const m = s.match(/^(\d+)/);
2552
+ return m ? Number(m[1]) : 0;
2553
+ }
2554
+ var SAVINGS_DISCLAIMER = "These are evidence-based estimates, not exact ROI. Adjust assumptions with real team data.";
2555
+ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2556
+ const { assumptions: a, source } = loadAssumptions(dir);
2557
+ const impact = buildImpactReport(dir, { scope: opts.scope, scopeSource: opts.scopeSource }, now);
2558
+ const completed = impact.traceability.completed_work_items;
2559
+ const wiWithOwnership = parseRatioNumerator(impact.traceability.work_items_connected_to_code);
2560
+ const acceptanceCov = impact.knowledge_coverage.find((c) => c.label.toLowerCase().includes("acceptance"));
2561
+ const wiWithAcceptance = acceptanceCov?.have ?? 0;
2562
+ const graphQuality = impact.graph_quality.available ? impact.graph_quality.quality : "unknown";
2563
+ const readiness = impact.context_readiness.level;
2564
+ const ctxPrepH = round2(completed * a.context_preparation_minutes_saved_per_work_item / 60);
2565
+ const reviewH = round2(wiWithOwnership * a.review_minutes_saved_per_work_item_with_ownership / 60);
2566
+ const clarH = round2(wiWithAcceptance * a.clarification_minutes_saved_per_ready_work_item / 60);
2567
+ const readinessMult = READINESS_MULT[readiness] ?? 0.5;
2568
+ const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
2569
+ const graphMult = GRAPH_MULT[graphQuality] ?? 0;
2570
+ const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
2571
+ const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH);
2572
+ const totalValue = Math.round(totalHours * a.hourly_cost);
2573
+ const reasons = [];
2574
+ let level;
2575
+ if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
2576
+ level = "Low";
2577
+ reasons.push("Impact score is low or the knowledge graph is empty.");
2578
+ } else {
2579
+ level = "Medium";
2580
+ reasons.push("Strong knowledge and graph evidence.");
2581
+ }
2582
+ reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
2583
+ reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
2584
+ const actions = [];
2585
+ if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
2586
+ actions.push("Calibrate hourly cost and team assumptions with real team data.");
2587
+ actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
2588
+ actions.push("Use this report as directional evidence, not accounting data.");
2589
+ actions.push("Future: persist Guard history to estimate avoided rework.");
2590
+ return {
2591
+ generated_at: now.toISOString(),
2592
+ project: impact.project,
2593
+ scope: impact.scope,
2594
+ currency: a.currency,
2595
+ disclaimer: SAVINGS_DISCLAIMER,
2596
+ assumptions_source: source,
2597
+ assumptions: {
2598
+ hourly_cost: a.hourly_cost,
2599
+ context_preparation_minutes_saved_per_work_item: a.context_preparation_minutes_saved_per_work_item,
2600
+ review_minutes_saved_per_work_item_with_ownership: a.review_minutes_saved_per_work_item_with_ownership,
2601
+ clarification_minutes_saved_per_ready_work_item: a.clarification_minutes_saved_per_ready_work_item,
2602
+ onboarding_hours_saved_per_new_contributor: a.onboarding_hours_saved_per_new_contributor,
2603
+ expected_new_contributors_per_month: a.expected_new_contributors_per_month,
2604
+ architecture_discovery_hours_saved_when_graph_good: a.architecture_discovery_hours_saved_when_graph_good
2605
+ },
2606
+ evidence: {
2607
+ knowledge_impact_score: impact.score,
2608
+ completed_work_items: completed,
2609
+ ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
2610
+ work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
2611
+ graph_quality: graphQuality,
2612
+ graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
2613
+ graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
2614
+ context_readiness: readiness,
2615
+ guard_history_available: false
2616
+ },
2617
+ estimated_savings: {
2618
+ context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
2619
+ review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
2620
+ clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
2621
+ 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
+ architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
2623
+ drift_prevention: { available: false, hours: 0, reason: "Guard history is not persisted yet." }
2624
+ },
2625
+ total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
2626
+ confidence: { level, reasons },
2627
+ suggested_actions: actions
2628
+ };
2629
+ }
2630
+ function renderSavingsMarkdown(r) {
2631
+ const L = [];
2632
+ L.push("# Kaddo Estimated Savings Report", "");
2633
+ L.push(`Generated at: ${r.generated_at}`);
2634
+ L.push(`Project: ${r.project}`);
2635
+ L.push(`Scope: ${r.scope}`);
2636
+ L.push(`Currency: ${r.currency}`);
2637
+ L.push("");
2638
+ L.push("## Disclaimer", "");
2639
+ L.push(r.disclaimer, "");
2640
+ L.push("## Executive Summary", "");
2641
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
2642
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2643
+ L.push(`- Confidence: ${r.confidence.level}`);
2644
+ if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
2645
+ L.push("");
2646
+ L.push("## Assumptions", "");
2647
+ L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
2648
+ L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
2649
+ L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
2650
+ L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
2651
+ L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
2652
+ L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
2653
+ L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
2654
+ L.push("");
2655
+ L.push("## Evidence Used", "");
2656
+ const e = r.evidence;
2657
+ L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
2658
+ L.push(`- Completed Work Items: ${e.completed_work_items}`);
2659
+ L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
2660
+ L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
2661
+ L.push(`- Graph quality: ${e.graph_quality}`);
2662
+ if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
2663
+ if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
2664
+ L.push(`- Context readiness: ${e.context_readiness}`);
2665
+ L.push("- Guard history: not available");
2666
+ L.push("");
2667
+ L.push("## Estimated Savings", "");
2668
+ const s = r.estimated_savings;
2669
+ const line = (title, l) => {
2670
+ L.push(`### ${title}`, "");
2671
+ L.push(`- Formula: ${l.formula}`);
2672
+ L.push(`- Estimated: ${l.hours} h`, "");
2673
+ };
2674
+ line("Context Preparation", s.context_preparation);
2675
+ line("Review Effort", s.review_effort);
2676
+ line("Clarification Reduction", s.clarification_reduction);
2677
+ line("Onboarding", s.onboarding);
2678
+ line("Architecture Discovery", s.architecture_discovery);
2679
+ L.push("### Drift Prevention", "");
2680
+ L.push("- Not available yet.");
2681
+ L.push(`- Reason: ${s.drift_prevention.reason}`, "");
2682
+ L.push("## Total", "");
2683
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
2684
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2685
+ L.push("");
2686
+ L.push("## Confidence", "");
2687
+ L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
2688
+ for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
2689
+ L.push("");
2690
+ L.push("## Suggested Actions", "");
2691
+ r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
2692
+ L.push("");
2693
+ return L.join("\n");
2694
+ }
2695
+ function serializeSavingsJson(r) {
2696
+ return JSON.stringify(r, null, 2) + "\n";
2697
+ }
2698
+
2499
2699
  // src/workitems.ts
2500
2700
  import matter7 from "gray-matter";
2501
2701
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
@@ -2706,7 +2906,21 @@ var RESOURCES = [
2706
2906
  if (!hasKnowledge(root)) {
2707
2907
  return text("kaddo://impact-report", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
2708
2908
  }
2709
- return [{ uri: "kaddo://impact-report", text: renderImpactMarkdown(buildImpactReport(root, { scope: "all" })), mimeType: "text/markdown" }];
2909
+ return [{ uri: "kaddo://impact-report", text: renderImpactMarkdown(buildImpactReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
2910
+ }
2911
+ },
2912
+ {
2913
+ uri: "kaddo://savings-report",
2914
+ name: "Kaddo savings report",
2915
+ description: "Estimated Savings Report \u2014 the last written report, or generated in memory (read-only).",
2916
+ mimeType: "text/markdown",
2917
+ read: (root) => {
2918
+ const saved = readText(root, ".kaddo/reports/savings-report.md");
2919
+ if (saved) return [{ uri: "kaddo://savings-report", text: saved, mimeType: "text/markdown" }];
2920
+ if (!hasKnowledge(root)) {
2921
+ return text("kaddo://savings-report", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
2922
+ }
2923
+ return [{ uri: "kaddo://savings-report", text: renderSavingsMarkdown(buildSavingsReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
2710
2924
  }
2711
2925
  }
2712
2926
  ];
@@ -3474,7 +3688,7 @@ function generateImpactReport(root, opts = {}) {
3474
3688
  requireConfig(root);
3475
3689
  requireKnowledge(root);
3476
3690
  const format = opts.format ?? "markdown";
3477
- const report = buildImpactReport(root, { scope: opts.scope ?? "all" });
3691
+ const report = buildImpactReport(root, { scope: opts.scope ?? "all", scopeSource: opts.scope ? "explicit" : "default" });
3478
3692
  const content = format === "json" ? serializeImpactJson(report) : renderImpactMarkdown(report);
3479
3693
  const out = opts.output ?? `.kaddo/reports/impact-report.${format === "json" ? "json" : "md"}`;
3480
3694
  writeDerived(root, out, content);
@@ -3486,6 +3700,22 @@ function generateImpactReport(root, opts = {}) {
3486
3700
  next_suggested_resources: ["kaddo://impact-report"]
3487
3701
  };
3488
3702
  }
3703
+ function generateSavingsReport(root, opts = {}) {
3704
+ requireConfig(root);
3705
+ requireKnowledge(root);
3706
+ const format = opts.format ?? "markdown";
3707
+ const report = buildSavingsReport(root, { scope: opts.scope ?? "all", scopeSource: opts.scope ? "explicit" : "default" });
3708
+ const content = format === "json" ? serializeSavingsJson(report) : renderSavingsMarkdown(report);
3709
+ const out = opts.output ?? `.kaddo/reports/savings-report.${format === "json" ? "json" : "md"}`;
3710
+ writeDerived(root, out, content);
3711
+ return {
3712
+ status: "ok",
3713
+ files_written: [out.replace(/\\/g, "/")],
3714
+ summary: `Savings report generated (${format}, ${report.scope} scope, ~${report.total.estimated_hours_saved}h).`,
3715
+ warnings: report.assumptions_source === "default" ? ["Using default assumptions \u2014 run `kaddo savings init` to calibrate."] : [],
3716
+ next_suggested_resources: ["kaddo://savings-report"]
3717
+ };
3718
+ }
3489
3719
  function generateCapsuleDraft(root) {
3490
3720
  const config = requireConfig(root);
3491
3721
  requireKnowledge(root);
@@ -3686,6 +3916,23 @@ function createServer(root) {
3686
3916
  () => generateImpactReport(root, { format: args.format, scope: args.scope, output: args.output })
3687
3917
  )
3688
3918
  );
3919
+ server.registerTool(
3920
+ "kaddo_generate_savings_report",
3921
+ {
3922
+ title: "Generate savings report",
3923
+ description: `Write the Estimated Savings Report under .kaddo/reports/. format: markdown (default) or json; scope: active or all (default all). Evidence-based estimates, not exact ROI. ${DERIVED_NOTE}`,
3924
+ inputSchema: {
3925
+ format: z2.enum(["markdown", "json"]).optional(),
3926
+ scope: z2.enum(["active", "all"]).optional(),
3927
+ output: z2.string().optional()
3928
+ }
3929
+ },
3930
+ async (args) => generated(
3931
+ root,
3932
+ "generate savings report",
3933
+ () => generateSavingsReport(root, { format: args.format, scope: args.scope, output: args.output })
3934
+ )
3935
+ );
3689
3936
  server.registerTool(
3690
3937
  "kaddo_generate_capsule_draft",
3691
3938
  { title: "Generate capsule draft", description: `Write a capsule DRAFT under .kaddo/exports/ (never registers it). ${DERIVED_NOTE}`, inputSchema: {} },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/mcp",
3
- "version": "3.23.1",
3
+ "version": "3.24.0",
4
4
  "description": "MCP server for Kaddo project knowledge: read-only resources/tools/prompts plus safe derived generation under .kaddo/.",
5
5
  "license": "MIT",
6
6
  "repository": {